[
  {
    "path": ".azure-pipelines/release.yml",
    "content": "name: $(Date:yyyyMMdd)$(Rev:.r)\ntrigger: none\npr: none\n\nresources:\n  repositories:\n    - repository: 1ESPipelines\n      type: git\n      name: 1ESPipelineTemplates/1ESPipelineTemplates\n      ref: refs/tags/release\n\nparameters:\n  - name: 'esrp'\n    type: boolean\n    default: true\n    displayName: 'Enable ESRP code signing'\n  - name: 'github'\n    type: boolean\n    default: true\n    displayName: 'Enable GitHub release publishing'\n  - name: 'nuget'\n    type: boolean\n    default: true\n    displayName: 'Enable NuGet package publishing'\n\n#\n# 1ES Pipeline Templates do not allow using a matrix strategy so we create\n# a YAML object parameter with and foreach to create jobs for each entry.\n# Each OS has its own matrix object since their build steps differ.\n#\n  - name: windows_matrix\n    type: object\n    default:\n      - id: windows_x86\n        jobName: 'Windows (x86)'\n        runtime: win-x86\n        pool: GitClientPME-1ESHostedPool-intel-pc\n        image: win-x86_64-ado1es\n        os: windows\n      - id: windows_x64\n        jobName: 'Windows (x64)'\n        runtime: win-x64\n        pool: GitClientPME-1ESHostedPool-intel-pc\n        image: win-x86_64-ado1es\n        os: windows\n      - id: windows_arm64\n        jobName: 'Windows (ARM64)'\n        runtime: win-arm64\n        pool: GitClientPME-1ESHostedPool-arm64-pc\n        image: win-arm64-ado1es\n        os: windows\n\n  - name: macos_matrix\n    type: object\n    default:\n      - id: macos_x64\n        jobName: 'macOS (x64)'\n        runtime: osx-x64\n        pool: 'Azure Pipelines'\n        image: macOS-latest\n        os: macos\n      - id: macos_arm64\n        jobName: 'macOS (ARM64)'\n        runtime: osx-arm64\n        pool: 'Azure Pipelines'\n        image: macOS-latest\n        os: macos\n\n  - name: linux_matrix\n    type: object\n    default:\n      - id: linux_x64\n        jobName: 'Linux (x64)'\n        runtime: linux-x64\n        pool: GitClientPME-1ESHostedPool-intel-pc\n        image: ubuntu-x86_64-ado1es\n        os: linux\n      - id: linux_arm64\n        jobName: 'Linux (ARM64)'\n        runtime: linux-arm64\n        pool: GitClientPME-1ESHostedPool-arm64-pc\n        image: ubuntu-arm64-ado1es\n        os: linux\n\nvariables:\n  - name: 'esrpAppConnectionName'\n    value: '1ESGitClient-ESRP-App'\n  - name: 'esrpMIConnectionName'\n    value: '1ESGitClient-ESRP-MI'\n  - name: 'githubConnectionName'\n    value: 'GitHub-GitCredentialManager'\n  - name: 'nugetConnectionName'\n    value: '1ESGitClient-NuGet'\n  # ESRP signing variables set in the pipeline settings:\n  # - esrpEndpointUrl\n  # - esrpClientId\n  # - esrpTenantId\n  # - esrpKeyVaultName\n  # - esrpSignReqCertName\n\nextends:\n  template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelines\n  parameters:\n    sdl:\n      # SDL source analysis tasks only run on Windows images\n      sourceAnalysisPool:\n        name: GitClientPME-1ESHostedPool-intel-pc\n        image: win-x86_64-ado1es\n        os: windows\n    stages:\n      - stage: build\n        displayName: 'Build and Sign'\n        jobs:\n          #\n          # Windows build jobs\n          #\n          - ${{ each dim in parameters.windows_matrix }}:\n            - job: ${{ dim.id }}\n              displayName: ${{ dim.jobName }}\n              pool:\n                name: ${{ dim.pool }}\n                image: ${{ dim.image }}\n                os: ${{ dim.os }}\n              templateContext:\n                outputs:\n                  - output: pipelineArtifact\n                    targetPath: '$(Build.ArtifactStagingDirectory)\\_final'\n                    artifactName: '${{ dim.runtime }}'\n              steps:\n                - checkout: self\n                - task: PowerShell@2\n                  displayName: 'Read version file'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      $version = (Get-Content .\\VERSION) -replace '\\.\\d+$', ''\n                      Write-Host \"##vso[task.setvariable variable=version;isReadOnly=true]$version\"\n                - task: UseDotNet@2\n                  displayName: 'Use .NET 8 SDK'\n                  inputs:\n                    packageType: sdk\n                    version: '8.x'\n                - task: PowerShell@2\n                  displayName: 'Build payload'\n                  inputs:\n                    targetType: filePath\n                    filePath: '.\\src\\windows\\Installer.Windows\\layout.ps1'\n                    arguments: |\n                      -Configuration Release `\n                      -Output $(Build.ArtifactStagingDirectory)\\payload `\n                      -SymbolOutput $(Build.ArtifactStagingDirectory)\\symbols_raw `\n                      -RuntimeIdentifier ${{ dim.runtime }}\n                - task: ArchiveFiles@2\n                  displayName: 'Archive symbols'\n                  inputs:\n                    rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\\symbols_raw'\n                    includeRootFolder: false\n                    archiveType: zip\n                    archiveFile: '$(Build.ArtifactStagingDirectory)\\symbols\\gcm-${{ dim.runtime }}-$(version)-symbols.zip'\n                - ${{ if eq(parameters.esrp, true) }}:\n                  - task: EsrpCodeSigning@5\n                    displayName: 'Sign payload'\n                    inputs:\n                      connectedServiceName: '$(esrpAppConnectionName)'\n                      useMSIAuthentication: true\n                      appRegistrationClientId: '$(esrpClientId)'\n                      appRegistrationTenantId: '$(esrpTenantId)'\n                      authAkvName: '$(esrpKeyVaultName)'\n                      authSignCertName: '$(esrpSignReqCertName)'\n                      serviceEndpointUrl: '$(esrpEndpointUrl)'\n                      folderPath: '$(Build.ArtifactStagingDirectory)\\payload'\n                      pattern: |\n                        **/*.exe\n                        **/*.dll\n                      useMinimatch: true\n                      signConfigType: inlineSignParams\n                      inlineOperation: |\n                        [\n                          {\n                            \"KeyCode\": \"CP-230012\",\n                            \"OperationCode\": \"SigntoolSign\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {\n                              \"OpusName\": \"Microsoft\",\n                              \"OpusInfo\": \"https://www.microsoft.com\",\n                              \"FileDigest\": \"/fd SHA256\",\n                              \"PageHash\": \"/NPH\",\n                              \"TimeStamp\": \"/tr \\\"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\\\" /td sha256\"\n                            }\n                          },\n                          {\n                            \"KeyCode\": \"CP-230012\",\n                            \"OperationCode\": \"SigntoolVerify\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {}\n                          }\n                        ]\n                  - task: PowerShell@2\n                    displayName: 'Clean up code signing artifacts'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        Remove-Item \"$(Build.ArtifactStagingDirectory)\\payload\\CodeSignSummary-*.md\"\n                - task: PowerShell@2\n                  displayName: 'Build installers'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      dotnet build '.\\src\\windows\\Installer.Windows\\Installer.Windows.csproj' `\n                        --configuration Release `\n                        --no-dependencies `\n                        -p:NoLayout=true `\n                        -p:PayloadPath=\"$(Build.ArtifactStagingDirectory)\\payload\" `\n                        -p:OutputPath=\"$(Build.ArtifactStagingDirectory)\\installers\" `\n                        -p:RuntimeIdentifier=\"${{ dim.runtime }}\"\n                - ${{ if eq(parameters.esrp, true) }}:\n                  - task: EsrpCodeSigning@5\n                    condition: and(succeeded(), eq('${{ parameters.esrp }}', true))\n                    displayName: 'Sign installers'\n                    inputs:\n                      connectedServiceName: '$(esrpAppConnectionName)'\n                      useMSIAuthentication: true\n                      appRegistrationClientId: '$(esrpClientId)'\n                      appRegistrationTenantId: '$(esrpTenantId)'\n                      authAkvName: '$(esrpKeyVaultName)'\n                      authSignCertName: '$(esrpSignReqCertName)'\n                      serviceEndpointUrl: '$(esrpEndpointUrl)'\n                      folderPath: '$(Build.ArtifactStagingDirectory)\\installers'\n                      pattern: '**/*.exe'\n                      useMinimatch: true\n                      signConfigType: inlineSignParams\n                      inlineOperation: |\n                        [\n                          {\n                            \"KeyCode\": \"CP-230012\",\n                            \"OperationCode\": \"SigntoolSign\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {\n                              \"OpusName\": \"Microsoft\",\n                              \"OpusInfo\": \"https://www.microsoft.com\",\n                              \"FileDigest\": \"/fd SHA256\",\n                              \"PageHash\": \"/NPH\",\n                              \"TimeStamp\": \"/tr \\\"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\\\" /td sha256\"\n                            }\n                          },\n                          {\n                            \"KeyCode\": \"CP-230012\",\n                            \"OperationCode\": \"SigntoolVerify\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {}\n                          }\n                        ]\n                - task: ArchiveFiles@2\n                  displayName: 'Archive payload'\n                  inputs:\n                    rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\\payload'\n                    includeRootFolder: false\n                    archiveType: zip\n                    archiveFile: '$(Build.ArtifactStagingDirectory)\\installers\\gcm-${{ dim.runtime }}-$(version).zip'\n                - task: PowerShell@2\n                  displayName: 'Collect artifacts for publishing'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      New-Item -Path \"$(Build.ArtifactStagingDirectory)\\_final\" -ItemType Directory -Force\n                      Copy-Item \"$(Build.ArtifactStagingDirectory)\\installers\\*.exe\" -Destination \"$(Build.ArtifactStagingDirectory)\\_final\"\n                      Copy-Item \"$(Build.ArtifactStagingDirectory)\\installers\\*.zip\" -Destination \"$(Build.ArtifactStagingDirectory)\\_final\"\n                      Copy-Item \"$(Build.ArtifactStagingDirectory)\\symbols\\*.zip\" -Destination \"$(Build.ArtifactStagingDirectory)\\_final\"\n                      Copy-Item \"$(Build.ArtifactStagingDirectory)\\payload\" -Destination \"$(Build.ArtifactStagingDirectory)\\_final\" -Recurse\n\n          #\n          # macOS build jobs\n          #\n          - ${{ each dim in parameters.macos_matrix }}:\n            - job: ${{ dim.id }}\n              displayName: ${{ dim.jobName }}\n              pool:\n                name: ${{ dim.pool }}\n                image: ${{ dim.image }}\n                os: ${{ dim.os }}\n              templateContext:\n                outputs:\n                  - output: pipelineArtifact\n                    targetPath: '$(Build.ArtifactStagingDirectory)/_final'\n                    artifactName: '${{ dim.runtime }}'\n              steps:\n                - checkout: self\n                - task: Bash@3\n                  displayName: 'Read version file'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      echo \"##vso[task.setvariable variable=version;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')\"\n                - task: UseDotNet@2\n                  displayName: 'Use .NET 8 SDK'\n                  inputs:\n                    packageType: sdk\n                    version: '8.x'\n                - task: Bash@3\n                  displayName: 'Build payload'\n                  inputs:\n                    targetType: filePath\n                    filePath: './src/osx/Installer.Mac/layout.sh'\n                    arguments: |\n                      --runtime=\"${{ dim.runtime }}\" \\\n                      --configuration=\"Release\" \\\n                      --output=\"$(Build.ArtifactStagingDirectory)/payload\" \\\n                      --symbol-output=\"$(Build.ArtifactStagingDirectory)/symbols_raw\"\n                - task: ArchiveFiles@2\n                  displayName: 'Archive symbols'\n                  inputs:\n                    rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/symbols_raw'\n                    includeRootFolder: false\n                    archiveType: tar\n                    tarCompression: gz\n                    archiveFile: '$(Build.ArtifactStagingDirectory)/symbols/gcm-${{ dim.runtime }}-$(version)-symbols.tar.gz'\n                - ${{ if eq(parameters.esrp, true) }}:\n                  - task: AzureKeyVault@2\n                    displayName: 'Download developer certificate'\n                    inputs:\n                      azureSubscription: '$(esrpMIConnectionName)'\n                      keyVaultName: '$(esrpKeyVaultName)'\n                      secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity'\n                  - task: Bash@3\n                    displayName: 'Import developer certificate'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        # Create and unlock a keychain for the developer certificate\n                        security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain\n                        security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain\n                        security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain\n\n                        echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12\n                        echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password\n\n                        # Import the developer certificate\n                        security import $(Agent.TempDirectory)/cert.p12 \\\n                          -k $(Agent.TempDirectory)/buildagent.keychain \\\n                          -P \"$(mac-developer-certificate-password)\" \\\n                          -T /usr/bin/codesign\n\n                        # Clean up the cert file immediately after import\n                        rm $(Agent.TempDirectory)/cert.p12\n\n                        # Set ACLs to allow codesign to access the private key\n                        security set-key-partition-list \\\n                          -S apple-tool:,apple:,codesign: \\\n                          -s -k pwd \\\n                          $(Agent.TempDirectory)/buildagent.keychain\n                  - task: Bash@3\n                    displayName: 'Developer sign payload files'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload\n\n                        # Copy the files that need signing (Mach-o executables and dylibs)\n                        pushd $(Build.ArtifactStagingDirectory)/payload\n                        find . -type f -exec file --mime {} + \\\n                          | sed -n '/mach/s/: .*//p' \\\n                          | while IFS= read -r f; do\n                              rel=\"${f#./}\"\n                              tgt=\"$(Build.ArtifactStagingDirectory)/tosign/payload/$rel\"\n                              mkdir -p \"$(dirname \"$tgt\")\"\n                              cp -- \"$f\" \"$tgt\"\n                            done\n                        popd\n\n                        # Developer sign the files\n                        ./src/osx/Installer.Mac/codesign.sh \\\n                          \"$(Build.ArtifactStagingDirectory)/tosign/payload\" \\\n                          \"$(mac-developer-certificate-identity)\" \\\n                          \"$PWD/src/osx/Installer.Mac/entitlements.xml\"\n                  # ESRP code signing for macOS requires the files be packaged in a zip file for submission\n                  - task: ArchiveFiles@2\n                    displayName: 'Archive files for signing'\n                    inputs:\n                      rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload'\n                      includeRootFolder: false\n                      archiveType: zip\n                      archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip'\n                  - task: EsrpCodeSigning@5\n                    displayName: 'Sign payload'\n                    inputs:\n                      connectedServiceName: '$(esrpAppConnectionName)'\n                      useMSIAuthentication: true\n                      appRegistrationClientId: '$(esrpClientId)'\n                      appRegistrationTenantId: '$(esrpTenantId)'\n                      authAkvName: '$(esrpKeyVaultName)'\n                      authSignCertName: '$(esrpSignReqCertName)'\n                      serviceEndpointUrl: '$(esrpEndpointUrl)'\n                      folderPath: '$(Build.ArtifactStagingDirectory)/tosign'\n                      pattern: 'payload.zip'\n                      useMinimatch: true\n                      signConfigType: inlineSignParams\n                      inlineOperation: |\n                        [\n                          {\n                            \"KeyCode\": \"CP-401337-Apple\",\n                            \"OperationCode\": \"MacAppDeveloperSign\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {\n                              \"Hardening\": \"Enable\"\n                            }\n                          }\n                        ]\n                  # Extract signed files, overwriting the unsigned files, ready for packaging\n                  - task: Bash@3\n                    displayName: 'Extract signed payload files'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload\n                - task: Bash@3\n                  displayName: 'Build component package'\n                  inputs:\n                    targetType: filePath\n                    filePath: './src/osx/Installer.Mac/pack.sh'\n                    arguments: |\n                      --version=\"$(version)\" \\\n                      --payload=\"$(Build.ArtifactStagingDirectory)/payload\" \\\n                      --output=\"$(Build.ArtifactStagingDirectory)/pkg/com.microsoft.gitcredentialmanager.component.pkg\"\n                - task: Bash@3\n                  displayName: 'Build installer package'\n                  inputs:\n                    targetType: filePath\n                    filePath: './src/osx/Installer.Mac/dist.sh'\n                    arguments: |\n                      --version=\"$(version)\" \\\n                      --runtime=\"${{ dim.runtime }}\" \\\n                      --package-path=\"$(Build.ArtifactStagingDirectory)/pkg\" \\\n                      --output=\"$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).pkg\"\n                - ${{ if eq(parameters.esrp, true) }}:\n                  # ESRP code signing for macOS requires the files be packaged in a zip file first\n                  - task: Bash@3\n                    displayName: 'Prepare installer package for signing'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        mkdir -p $(Build.ArtifactStagingDirectory)/tosign\n                        cd $(Build.ArtifactStagingDirectory)/installers\n                        zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg\n                  - task: EsrpCodeSigning@5\n                    displayName: 'Sign installer package'\n                    inputs:\n                      connectedServiceName: '$(esrpAppConnectionName)'\n                      useMSIAuthentication: true\n                      appRegistrationClientId: '$(esrpClientId)'\n                      appRegistrationTenantId: '$(esrpTenantId)'\n                      authAkvName: '$(esrpKeyVaultName)'\n                      authSignCertName: '$(esrpSignReqCertName)'\n                      serviceEndpointUrl: '$(esrpEndpointUrl)'\n                      folderPath: '$(Build.ArtifactStagingDirectory)/tosign'\n                      pattern: 'installers.zip'\n                      useMinimatch: true\n                      signConfigType: inlineSignParams\n                      inlineOperation: |\n                        [\n                          {\n                            \"KeyCode\": \"CP-401337-Apple\",\n                            \"OperationCode\": \"MacAppDeveloperSign\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {\n                              \"Hardening\": \"Enable\"\n                            }\n                          }\n                        ]\n                  # Extract signed installer, overwriting the unsigned installer\n                  - task: Bash@3\n                    displayName: 'Extract signed installer package'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers\n                  - task: Bash@3\n                    displayName: 'Prepare installer package for notarization'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        mkdir -p $(Build.ArtifactStagingDirectory)/tosign\n                        cd $(Build.ArtifactStagingDirectory)/installers\n                        # Remove previous installers.zip to avoid any confusion\n                        rm -f $(Build.ArtifactStagingDirectory)/tosign/installers.zip\n                        zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg\n                  - task: EsrpCodeSigning@5\n                    displayName: 'Notarize installer package'\n                    inputs:\n                      connectedServiceName: '$(esrpAppConnectionName)'\n                      useMSIAuthentication: true\n                      appRegistrationClientId: '$(esrpClientId)'\n                      appRegistrationTenantId: '$(esrpTenantId)'\n                      authAkvName: '$(esrpKeyVaultName)'\n                      authSignCertName: '$(esrpSignReqCertName)'\n                      serviceEndpointUrl: '$(esrpEndpointUrl)'\n                      folderPath: '$(Build.ArtifactStagingDirectory)/tosign'\n                      pattern: 'installers.zip'\n                      useMinimatch: true\n                      signConfigType: inlineSignParams\n                      inlineOperation: |\n                        [\n                          {\n                            \"KeyCode\": \"CP-401337-Apple\",\n                            \"OperationCode\": \"MacAppNotarize\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {\n                              \"BundleId\": \"com.microsoft.gitcredentialmanager\"\n                            }\n                          }\n                        ]\n                  # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload\n                  - task: Bash@3\n                    displayName: 'Extract signed and notarized installer package'\n                    inputs:\n                      targetType: inline\n                      script: |\n                        unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers\n                - task: ArchiveFiles@2\n                  displayName: 'Archive payload'\n                  inputs:\n                    rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/payload'\n                    includeRootFolder: false\n                    archiveType: tar\n                    tarCompression: gz\n                    archiveFile: '$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).tar.gz'\n                - task: Bash@3\n                  displayName: 'Collect artifacts for publishing'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      mkdir -p $(Build.ArtifactStagingDirectory)/_final\n                      cp $(Build.ArtifactStagingDirectory)/installers/*.pkg $(Build.ArtifactStagingDirectory)/_final\n                      cp $(Build.ArtifactStagingDirectory)/installers/*.tar.gz $(Build.ArtifactStagingDirectory)/_final\n                      cp $(Build.ArtifactStagingDirectory)/symbols/*.tar.gz $(Build.ArtifactStagingDirectory)/_final\n                      cp -r $(Build.ArtifactStagingDirectory)/payload $(Build.ArtifactStagingDirectory)/_final\n\n          #\n          # Linux build jobs\n          #\n          - ${{ each dim in parameters.linux_matrix }}:\n            - job: ${{ dim.id }}\n              displayName: ${{ dim.jobName }}\n              pool:\n                name: ${{ dim.pool }}\n                image: ${{ dim.image }}\n                os: ${{ dim.os }}\n              templateContext:\n                outputs:\n                  - output: pipelineArtifact\n                    targetPath: '$(Build.ArtifactStagingDirectory)/_final'\n                    artifactName: '${{ dim.runtime }}'\n              steps:\n                - checkout: self\n                - task: Bash@3\n                  displayName: 'Read version file'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      echo \"##vso[task.setvariable variable=version;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')\"\n                - task: UseDotNet@2\n                  displayName: 'Use .NET 8 SDK'\n                  inputs:\n                    packageType: sdk\n                    version: '8.x'\n                - task: Bash@3\n                  displayName: 'Build payload'\n                  inputs:\n                    targetType: filePath\n                    filePath: './src/linux/Packaging.Linux/layout.sh'\n                    arguments: |\n                      --runtime=\"${{ dim.runtime }}\" \\\n                      --configuration=\"Release\" \\\n                      --output=\"$(Build.ArtifactStagingDirectory)/payload\" \\\n                      --symbol-output=\"$(Build.ArtifactStagingDirectory)/symbols_raw\"\n                - task: Bash@3\n                  displayName: 'Build packages'\n                  inputs:\n                    targetType: filePath\n                    filePath: './src/linux/Packaging.Linux/pack.sh'\n                    arguments: |\n                      --version=\"$(version)\" \\\n                      --runtime=\"${{ dim.runtime }}\" \\\n                      --payload=\"$(Build.ArtifactStagingDirectory)/payload\" \\\n                      --symbols=\"$(Build.ArtifactStagingDirectory)/symbols_raw\" \\\n                      --output=\"$(Build.ArtifactStagingDirectory)/pkg\"\n                - task: Bash@3\n                  displayName: 'Move packages'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      # Move symbols\n                      mkdir -p $(Build.ArtifactStagingDirectory)/symbols\n                      mv $(Build.ArtifactStagingDirectory)/pkg/tar/gcm-*-symbols.tar.gz $(Build.ArtifactStagingDirectory)/symbols\n\n                      # Move binary packages\n                      mkdir -p $(Build.ArtifactStagingDirectory)/installers\n                      mv $(Build.ArtifactStagingDirectory)/pkg/tar/*.tar.gz $(Build.ArtifactStagingDirectory)/installers\n                      mv $(Build.ArtifactStagingDirectory)/pkg/deb/*.deb $(Build.ArtifactStagingDirectory)/installers\n                - ${{ if eq(parameters.esrp, true) }}:\n                  - task: EsrpCodeSigning@5\n                    displayName: 'Sign Debian package'\n                    inputs:\n                      connectedServiceName: '$(esrpAppConnectionName)'\n                      useMSIAuthentication: true\n                      appRegistrationClientId: '$(esrpClientId)'\n                      appRegistrationTenantId: '$(esrpTenantId)'\n                      authAkvName: '$(esrpKeyVaultName)'\n                      authSignCertName: '$(esrpSignReqCertName)'\n                      serviceEndpointUrl: '$(esrpEndpointUrl)'\n                      folderPath: '$(Build.ArtifactStagingDirectory)/installers'\n                      pattern: |\n                        **/*.deb\n                      useMinimatch: true\n                      signConfigType: inlineSignParams\n                      inlineOperation: |\n                        [\n                          {\n                            \"KeyCode\": \"CP-453387-Pgp\",\n                            \"OperationCode\": \"LinuxSign\",\n                            \"ToolName\": \"sign\",\n                            \"ToolVersion\": \"1.0\",\n                            \"Parameters\": {}\n                          }\n                        ]\n                - task: Bash@3\n                  displayName: 'Collect artifacts for publishing'\n                  inputs:\n                    targetType: inline\n                    script: |\n                      mkdir -p $(Build.ArtifactStagingDirectory)/_final\n                      cp $(Build.ArtifactStagingDirectory)/installers/*.deb $(Build.ArtifactStagingDirectory)/_final\n                      cp $(Build.ArtifactStagingDirectory)/installers/*.tar.gz $(Build.ArtifactStagingDirectory)/_final\n                      cp $(Build.ArtifactStagingDirectory)/symbols/*.tar.gz $(Build.ArtifactStagingDirectory)/_final\n                      cp -r $(Build.ArtifactStagingDirectory)/payload $(Build.ArtifactStagingDirectory)/_final\n\n          #\n          # .NET Tool build job\n          #\n          - job: dotnet_tool\n            displayName: '.NET Tool NuGet Package'\n            pool:\n              name: GitClientPME-1ESHostedPool-intel-pc\n              image: win-x86_64-ado1es\n              os: windows\n            templateContext:\n              outputs:\n                - output: pipelineArtifact\n                  targetPath: '$(Build.ArtifactStagingDirectory)/packages'\n                  artifactName: 'dotnet-tool'\n            steps:\n              - checkout: self\n              - task: PowerShell@2\n                displayName: 'Read version file'\n                inputs:\n                  targetType: inline\n                  script: |\n                    $version = (Get-Content .\\VERSION) -replace '\\.\\d+$', ''\n                    Write-Host \"##vso[task.setvariable variable=version;isReadOnly=true]$version\"\n              - task: UseDotNet@2\n                displayName: 'Use .NET 8 SDK'\n                inputs:\n                  packageType: sdk\n                  version: '8.x'\n              - task: NuGetToolInstaller@1\n                displayName: 'Install NuGet CLI'\n                inputs:\n                  versionSpec: '>= 6.0'\n              - task: PowerShell@2\n                displayName: 'Build payload'\n                inputs:\n                  targetType: filePath\n                  filePath: './src/shared/DotnetTool/layout.ps1'\n                  arguments: |\n                    -Configuration Release `\n                    -Output \"$(Build.ArtifactStagingDirectory)/nupkg\"\n              - ${{ if eq(parameters.esrp, true) }}:\n                - task: EsrpCodeSigning@5\n                  condition: and(succeeded(), eq('${{ parameters.esrp }}', true))\n                  displayName: 'Sign payload'\n                  inputs:\n                    connectedServiceName: '$(esrpAppConnectionName)'\n                    useMSIAuthentication: true\n                    appRegistrationClientId: '$(esrpClientId)'\n                    appRegistrationTenantId: '$(esrpTenantId)'\n                    authAkvName: '$(esrpKeyVaultName)'\n                    authSignCertName: '$(esrpSignReqCertName)'\n                    serviceEndpointUrl: '$(esrpEndpointUrl)'\n                    folderPath: '$(Build.ArtifactStagingDirectory)/nupkg'\n                    pattern: |\n                      **/*.exe\n                      **/*.dll\n                    useMinimatch: true\n                    signConfigType: inlineSignParams\n                    inlineOperation: |\n                      [\n                        {\n                          \"KeyCode\": \"CP-230012\",\n                          \"OperationCode\": \"SigntoolSign\",\n                          \"ToolName\": \"sign\",\n                          \"ToolVersion\": \"1.0\",\n                          \"Parameters\": {\n                            \"OpusName\": \"Microsoft\",\n                            \"OpusInfo\": \"https://www.microsoft.com\",\n                            \"FileDigest\": \"/fd SHA256\",\n                            \"PageHash\": \"/NPH\",\n                            \"TimeStamp\": \"/tr \\\"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\\\" /td sha256\"\n                          }\n                        },\n                        {\n                          \"KeyCode\": \"CP-230012\",\n                          \"OperationCode\": \"SigntoolVerify\",\n                          \"ToolName\": \"sign\",\n                          \"ToolVersion\": \"1.0\",\n                          \"Parameters\": {}\n                        }\n                      ]\n              - task: PowerShell@2\n                displayName: 'Create NuGet packages'\n                inputs:\n                  targetType: filePath\n                  filePath: './src/shared/DotnetTool/pack.ps1'\n                  arguments: |\n                    -Configuration Release `\n                    -Version \"$(version)\" `\n                    -PackageRoot \"$(Build.ArtifactStagingDirectory)/nupkg\" `\n                    -Output \"$(Build.ArtifactStagingDirectory)/packages\"\n              - ${{ if eq(parameters.esrp, true) }}:\n                - task: EsrpCodeSigning@5\n                  condition: and(succeeded(), eq('${{ parameters.esrp }}', true))\n                  displayName: 'Sign NuGet packages'\n                  inputs:\n                    connectedServiceName: '$(esrpAppConnectionName)'\n                    useMSIAuthentication: true\n                    appRegistrationClientId: '$(esrpClientId)'\n                    appRegistrationTenantId: '$(esrpTenantId)'\n                    authAkvName: '$(esrpKeyVaultName)'\n                    authSignCertName: '$(esrpSignReqCertName)'\n                    serviceEndpointUrl: '$(esrpEndpointUrl)'\n                    folderPath: '$(Build.ArtifactStagingDirectory)/packages'\n                    pattern: |\n                      **/*.nupkg\n                      **/*.snupkg\n                    useMinimatch: true\n                    signConfigType: inlineSignParams\n                    inlineOperation: |\n                      [\n                        {\n                          \"KeyCode\": \"CP-401405\",\n                          \"OperationCode\": \"NuGetSign\",\n                          \"ToolName\": \"sign\",\n                          \"ToolVersion\": \"1.0\",\n                          \"Parameters\": {}\n                        }\n                      ]\n\n      - stage: release\n        displayName: 'Release'\n        dependsOn: [build]\n        condition: and(succeeded(), or(eq('${{ parameters.github }}', true), eq('${{ parameters.nuget }}', true)))\n        jobs:\n          - job: release_validation\n            displayName: 'Release validation'\n            pool:\n              name: GitClientPME-1ESHostedPool-intel-pc\n              image: ubuntu-x86_64-ado1es\n              os: linux\n            steps:\n              - task: Bash@3\n                displayName: 'Read version file'\n                name: version\n                inputs:\n                  targetType: inline\n                  script: |\n                    echo \"##vso[task.setvariable variable=value;isOutput=true;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')\"\n\n          - job: github\n            displayName: 'Publish GitHub release'\n            dependsOn: release_validation\n            condition: and(succeeded(), eq('${{ parameters.github }}', true))\n            pool:\n              name: GitClientPME-1ESHostedPool-intel-pc\n              image: ubuntu-x86_64-ado1es\n              os: linux\n            variables:\n              version: $[dependencies.release_validation.outputs['version.value']]\n            templateContext:\n              type: releaseJob\n              isProduction: true\n              inputs:\n                # Installers and packages\n                - input: pipelineArtifact\n                  artifactName: 'win-x86'\n                  targetPath: $(Pipeline.Workspace)/assets/win-x86\n                - input: pipelineArtifact\n                  artifactName: 'win-x64'\n                  targetPath: $(Pipeline.Workspace)/assets/win-x64\n                - input: pipelineArtifact\n                  artifactName: 'win-arm64'\n                  targetPath: $(Pipeline.Workspace)/assets/win-arm64\n                - input: pipelineArtifact\n                  artifactName: 'osx-x64'\n                  targetPath: $(Pipeline.Workspace)/assets/osx-x64\n                - input: pipelineArtifact\n                  artifactName: 'osx-arm64'\n                  targetPath: $(Pipeline.Workspace)/assets/osx-arm64\n                - input: pipelineArtifact\n                  artifactName: 'linux-x64'\n                  targetPath: $(Pipeline.Workspace)/assets/linux-x64\n                - input: pipelineArtifact\n                  artifactName: 'linux-arm64'\n                  targetPath: $(Pipeline.Workspace)/assets/linux-arm64\n                - input: pipelineArtifact\n                  artifactName: 'dotnet-tool'\n                  targetPath: $(Pipeline.Workspace)/assets/dotnet-tool\n            steps:\n              - task: GitHubRelease@1\n                displayName: 'Create Draft GitHub Release'\n                condition: and(succeeded(), eq('${{ parameters.github }}', true))\n                inputs:\n                  gitHubConnection: $(githubConnectionName)\n                  repositoryName: git-ecosystem/git-credential-manager\n                  tag: 'v$(version)'\n                  tagSource: userSpecifiedTag\n                  target: release\n                  title: 'GCM $(version)'\n                  isDraft: true\n                  addChangeLog: false\n                  assets: |\n                    $(Pipeline.Workspace)/assets/win-x86/*.exe\n                    $(Pipeline.Workspace)/assets/win-x86/*.zip\n                    $(Pipeline.Workspace)/assets/win-x64/*.exe\n                    $(Pipeline.Workspace)/assets/win-x64/*.zip\n                    $(Pipeline.Workspace)/assets/win-arm64/*.exe\n                    $(Pipeline.Workspace)/assets/win-arm64/*.zip\n                    $(Pipeline.Workspace)/assets/osx-x64/*.pkg\n                    $(Pipeline.Workspace)/assets/osx-x64/*.tar.gz\n                    $(Pipeline.Workspace)/assets/osx-arm64/*.pkg\n                    $(Pipeline.Workspace)/assets/osx-arm64/*.tar.gz\n                    $(Pipeline.Workspace)/assets/linux-x64/*.deb\n                    $(Pipeline.Workspace)/assets/linux-x64/*.tar.gz\n                    $(Pipeline.Workspace)/assets/linux-arm64/*.deb\n                    $(Pipeline.Workspace)/assets/linux-arm64/*.tar.gz\n                    $(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg\n                    $(Pipeline.Workspace)/assets/dotnet-tool/*.snupkg\n\n          - job: nuget\n            displayName: 'Publish NuGet package'\n            dependsOn: release_validation\n            condition: and(succeeded(), eq('${{ parameters.nuget }}', true))\n            pool:\n              name: GitClientPME-1ESHostedPool-intel-pc\n              image: ubuntu-x86_64-ado1es\n              os: linux\n            variables:\n              version: $[dependencies.release_validation.outputs['version.value']]\n            templateContext:\n              inputs:\n                - input: pipelineArtifact\n                  artifactName: 'dotnet-tool'\n                  targetPath: $(Pipeline.Workspace)/assets/dotnet-tool\n              outputs:\n                - output: nuget\n                  condition: and(succeeded(), eq('${{ parameters.nuget }}', true))\n                  displayName: 'Publish .NET Tool NuGet package'\n                  packagesToPush: '$(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg;$(Pipeline.Workspace)/assets/dotnet-tool/*.snupkg'\n                  packageParentPath: $(Pipeline.Workspace)/assets/dotnet-tool\n                  nuGetFeedType: external\n                  publishPackageMetadata: true\n                  publishFeedCredentials: $(nugetConnectionName)\n"
  },
  {
    "path": ".code-coverage/coverlet.settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<RunSettings>\n  <DataCollectionRunSettings>\n    <DataCollectors>\n      <DataCollector friendlyName=\"XPlat code coverage\">\n        <Configuration>\n          <Format>cobertura,lcov</Format>\n        </Configuration>\n      </DataCollector>\n    </DataCollectors>\n  </DataCollectionRunSettings>\n</RunSettings>"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/auth-issue.yml",
    "content": "name: Authentication issue\ndescription: An authentication problem occurred when running a Git command.\nlabels: [\"auth-issue\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this issue!\n\n        Please answer as many of the below questions as you can - this helps us better understand what the problem is, and consequently how to resolve your problem.\n  - type: input\n    id: version\n    attributes:\n      label: Version\n      description: |\n        What version of Git Credential Manager are you using?\n\n        Run `git credential-manager --version` from a terminal to see the current version.\n\n        If you are on an older version of GCM please try updating before creating an issue as the problem you are experiencing may have already been fixed.\n      placeholder: |\n        ex: 2.0.8-beta+e1f8492d04\n    validations:\n      required: true\n  - type: dropdown\n    id: os\n    attributes:\n      label: Operating system\n      description: What operating system are you using?\n      options:\n        - Windows\n        - macOS\n        - Linux\n        - Other - please describe below\n    validations:\n      required: true\n  - type: input\n    id: os-version\n    attributes:\n      label: OS version or distribution\n      description: Please describe the version, CPU architecture (x64, ARM, etc), or Linux distribution you are using.\n      placeholder: |\n        ex: Windows 11 Pro, Monterey 12.5, Ubuntu 22.04\n    validations:\n      required: true\n  - type: dropdown\n    id: provider\n    attributes:\n      label: Git hosting provider(s)\n      description: What Git host provider are you trying to connect to?\n      multiple: true\n      options:\n        - Azure DevOps\n        - Azure DevOps Server (TFS/on-prem)\n        - Bitbucket Cloud\n        - Bitbucket Server/DC\n        - GitHub\n        - GitHub Enterprise Server\n        - GitLab\n        - Other - please describe below\n    validations:\n      required: true\n  - type: input\n    id: provider-other\n    attributes:\n      label: Other hosting provider\n      description: If you selected \"Other\" above, please describe the Git host you are using.\n  - type: dropdown\n    id: azdo-urlformat\n    attributes:\n      label: |\n        (Azure DevOps only) What format is your remote URL?\n      description: |\n        Tip: to see your remote URL run `git remote -v` from a terminal.\n      options:\n        - https://dev.azure.com/{org}\n        - https://{org}@dev.azure.com/{org}\n        - https://{org}.visualstudio.com\n  - type: dropdown\n    id: web-access\n    attributes:\n      label: Can you access the remote repository directly in the browser?\n      description: |\n        If you are unable to access the repository via a web browser then it is likely GCM will also be unable to access the repository with your user account.\n      options:\n        - Yes, I can access the repository\n        - No, I get a permission error\n        - No, for a different reason - please describe behavior below\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: A clear and concise description of what your expectation are.\n      placeholder: |\n        ex: I am authenticated and my Git operation completes successfully.\n    validations:\n      required: true\n  - type: textarea\n    id: actual\n    attributes:\n      label: Actual behavior\n      description: |\n        A clear and concise description of what actually happens.\n        Feel free to include screenshots of dialogs or errors here, but remember to **redact any sensitive information**!\n      placeholder: |\n        ex: An exception \"FooException\" is thrown, UI freezes, etc.\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      description: |\n        To capture trace logs, set the environment variables `GCM_TRACE=1` and `GIT_TRACE=1` and re-run your Git command.\n        If you are running inside of Windows Subsystem for Linux (WSL), you must also set an additional environment variable to enable tracing: `WSLENV=$WSLENV:GCM_TRACE`. For example:\n\n        ```shell\n        WSLENV=$WSLENV:GCM_TRACE:GIT_TRACE GCM_TRACE=1 GIT_TRACE=1 git fetch\n        ```\n\n        If you are using GCM version 2.0.567 onwards you can also run `git credential-manager diagnose` to collect useful diagnostic information that can be attached here.\n\n        :warning: **Please review and redact any private information before attaching logs and files!**\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-req.yml",
    "content": "name: Feature request\ndescription: A suggestion for a new feature in Git Credential Manager.\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this feature request!\n\n        Please be as detailed as possible describing your idea; how it fixes a problem or makes something easier.\n\n        Although we cannot guarentee we will accept all requests, we will still take time to consider your ideas. Whilst we may be supportive of an idea in principal, as maintainers we may not always be able to dedicate time to implementing them. We always welcome community support and contributions however! :heart:\n  - type: textarea\n    id: description\n    attributes:\n      label: Feature description\n      description: |\n        A clear and concise description of the new feature.\n      placeholder: |\n        ex: Add spline reticulation option to widget authentication mechanism.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n\n  # Enable version updates for GitHub ecosystem\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ main, release ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'csharp' ]\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - name: Setup .NET\n      uses: actions/setup-dotnet@v5.1.0\n      with:\n        dotnet-version: 8.0.x\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n\n    - run: |\n       dotnet build\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/continuous-integration.yml",
    "content": "name: ci\r\n\r\non:\r\n  workflow_dispatch:\r\n  push:\r\n    branches: [ main ]\r\n  pull_request:\r\n    branches: [ main ]\r\n\r\njobs:\r\n# ================================\r\n#              Windows\r\n# ================================\r\n  windows:\r\n    name: Windows\r\n    runs-on: ${{ matrix.os }}\r\n    strategy:\r\n      matrix:\r\n        include:\r\n          - runtime: win-x86\r\n            os: windows-latest\r\n          - runtime: win-x64\r\n            os: windows-latest\r\n          - runtime: win-arm64\r\n            os: windows-11-arm\r\n\r\n    steps:\r\n    - uses: actions/checkout@v6\r\n\r\n    - name: Setup .NET\r\n      uses: actions/setup-dotnet@v5.1.0\r\n      with:\r\n        dotnet-version: 8.0.x\r\n\r\n    - name: Install dependencies\r\n      run: dotnet restore\r\n\r\n    - name: Build\r\n      run: |\r\n        dotnet build src/windows/Installer.Windows/Installer.Windows.csproj `\r\n         --configuration=Release `\r\n         --runtime=${{ matrix.runtime }}\r\n\r\n    - name: Test\r\n      run: |\r\n        dotnet test --verbosity normal `\r\n        --configuration=WindowsRelease `\r\n        --runtime=${{ matrix.runtime }}\r\n\r\n    - name: Prepare artifacts\r\n      shell: bash\r\n      run: |\r\n        mkdir -p artifacts/bin\r\n        mv out/windows/Installer.Windows/bin/Release/net472/gcm*.exe artifacts/\r\n        mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }} artifacts/bin/\r\n        cp out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}.sym/* artifacts/bin/${{ matrix.runtime }}/\r\n\r\n    - name: Upload artifacts\r\n      uses: actions/upload-artifact@v7\r\n      with:\r\n        name: ${{ matrix.runtime }}\r\n        path: |\r\n          artifacts\r\n\r\n# ================================\r\n#              Linux\r\n# ================================\r\n  linux:\r\n    name: Linux\r\n    runs-on: ubuntu-latest\r\n    strategy:\r\n      matrix:\r\n        runtime: [ linux-x64, linux-arm64, linux-arm ]\r\n\r\n    steps:\r\n    - uses: actions/checkout@v6\r\n\r\n    - name: Setup .NET\r\n      uses: actions/setup-dotnet@v5.1.0\r\n      with:\r\n        dotnet-version: 8.0.x\r\n\r\n    - name: Install dependencies\r\n      run: dotnet restore\r\n\r\n    - name: Build\r\n      run: |\r\n        dotnet build src/linux/Packaging.Linux/*.csproj \\\r\n         --configuration=Release --no-self-contained \\\r\n         --runtime=${{ matrix.runtime }}\r\n\r\n    - name: Test\r\n      run: |\r\n        dotnet test --verbosity normal --configuration=LinuxRelease\r\n\r\n    - name: Prepare artifacts\r\n      run: |\r\n        mkdir -p artifacts\r\n        mv out/linux/Packaging.Linux/Release/deb/*.deb artifacts/\r\n        mv out/linux/Packaging.Linux/Release/tar/*.tar.gz artifacts/\r\n\r\n    - name: Upload artifacts\r\n      uses: actions/upload-artifact@v7\r\n      with:\r\n        name: ${{ matrix.runtime }}\r\n        path: |\r\n          artifacts\r\n\r\n# ================================\r\n#              macOS\r\n# ================================\r\n  osx:\r\n    name: macOS\r\n    runs-on: macos-latest\r\n    strategy:\r\n      matrix:\r\n        runtime: [ osx-x64, osx-arm64 ]\r\n\r\n    steps:\r\n    - uses: actions/checkout@v6\r\n\r\n    - name: Setup .NET\r\n      uses: actions/setup-dotnet@v5.1.0\r\n      with:\r\n        dotnet-version: 8.0.x\r\n\r\n    - name: Install dependencies\r\n      run: dotnet restore\r\n\r\n    - name: Build\r\n      run: |\r\n        dotnet build src/osx/Installer.Mac/*.csproj \\\r\n         --configuration=Release --no-self-contained \\\r\n         --runtime=${{ matrix.runtime }}\r\n\r\n    - name: Test\r\n      run: |\r\n        dotnet test --verbosity normal --configuration=MacRelease\r\n\r\n    - name: Prepare artifacts\r\n      run: |\r\n        mkdir -p artifacts/bin\r\n        mv out/osx/Installer.Mac/pkg/Release/payload \"artifacts/bin/${{ matrix.runtime }}\"\r\n        cp out/osx/Installer.Mac/pkg/Release/payload.sym/* \"artifacts/bin/${{ matrix.runtime }}/\"\r\n        mv out/osx/Installer.Mac/pkg/Release/gcm*.pkg artifacts/\r\n\r\n    - name: Upload artifacts\r\n      uses: actions/upload-artifact@v7\r\n      with:\r\n        name: ${{ matrix.runtime }}\r\n        path: |\r\n          artifacts\r\n"
  },
  {
    "path": ".github/workflows/lint-docs.yml",
    "content": "name: \"Lint documentation\"\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ main, linux ]\n    paths:\n      - '**.md'\n      - '.github/workflows/lint-docs.yml'\n  pull_request:\n    branches: [ main, linux ]\n    paths:\n      - '**.md'\n      - '.github/workflows/lint-docs.yml'\n\njobs:\n  lint-markdown:\n    name: Lint markdown files\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101\n        with:\n          globs: |\n            \"**/*.md\"\n            \"!.github/ISSUE_TEMPLATE\"\n\n  check-links:\n    name: Check for broken links\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Run link checker\n        # For any troubleshooting, see:\n        # https://github.com/lycheeverse/lychee/blob/master/docs/TROUBLESHOOTING.md\n        uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411\n        with:\n          # user-agent: if a user agent is not specified, some websites (e.g.\n          # GitHub Docs) return HTTP errors which Lychee will interpret as\n          # a broken link.\n          # no-progress: do not show progress bar. Recommended for\n          # non-interactive shells (e.g. for CI)\n          # inputs: by default (.), this action checks files matching the\n          # patterns: './**/*.md' './**/*.html'\n          args: >-\n            --user-agent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36\"\n            --no-progress .\n          fail: true\n        env:\n          # A token is used to avoid GitHub rate limiting. A personal token with\n          # no extra permissions is enough to be able to check public repos\n          # See: https://github.com/lycheeverse/lychee#github-token\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/maintainer-absence.yml",
    "content": "name: maintainer-absence\n\non:\n  workflow_dispatch:\n    inputs:\n      startDate:\n        description: 'First day of maintainer absence [mm-dd-yyyy]'\n        required: true\n      endDate:\n        description:  'Last day of maintainer absence [mm-dd-yyyy]'\n        required: true\n\npermissions:\n  issues: write\n\njobs:\n  create-issue:\n    name: create-issue\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@v8\n        with:\n          script: |\n            const startDate = new Date('${{ github.event.inputs.startDate }}');\n            const endDate = new Date('${{ github.event.inputs.endDate }}');\n\n            if (startDate > endDate) {\n              throw 'Start date cannot be later than end date.';\n            }\n\n            // Calculate total days of absence\n            const differenceInDays = endDate.getTime() - startDate.getTime();\n            const lengthOfAbsence = differenceInDays/(1000 * 3600 * 24);\n\n            // Create issue\n            issue = await github.rest.issues.create({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              // Use the briefer input date format in title (instead of JavaScript's full date string)\n              title: `Maintainer(s) will be away from ${{ github.event.inputs.startDate }} until ${{ github.event.inputs.endDate }}`,\n              body: `The ${context.repo.repo} maintainer(s) will be away for ${lengthOfAbsence} day${lengthOfAbsence > 1 ? 's' : ''} beginning on\n              ${startDate.toDateString()} and ending on ${endDate.toDateString()}. During this time, the maintainer(s)\n              will not be actively monitoring PRs, discussions, etc. Please report any issues\n              requiring immediate attention to [@GitCredManager](https://twitter.com/GitCredManager) on Twitter.`\n            });\n\n            // Pin issue - we use GraphQL since there is no GitHub API available for this\n            const mutation = `mutation($issueId: ID!) {\n              pinIssue(input: { issueId: $issueId }) {\n                issue {\n                  repository {\n                    id\n                  }\n                }\n              }\n            }`;\n            const variables = {\n              issueId: issue.data.node_id\n            }\n            const result = await github.graphql(mutation, variables)"
  },
  {
    "path": ".github/workflows/validate-install-from-source.yml",
    "content": "name: validate-install-from-source\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n\njobs:\n  docker:\n    name: ${{matrix.vector.image}}\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        vector:\n        - image: ubuntu\n        - image: debian:bullseye\n        - image: fedora\n        # Centos no longer officially maintains images on Docker Hub. However,\n        # tgagor is a contributor who pushes updated images weekly, which should\n        # be sufficient for our validation needs.\n        - image: tgagor/centos\n        - image: redhat/ubi8\n        - image: alpine\n        - image: alpine:3.19.8\n        - image: opensuse/leap\n        - image: opensuse/tumbleweed\n        - image: registry.suse.com/suse/sle15:15.4.27.11.31\n        - image: archlinux\n        - image: mcr.microsoft.com/cbl-mariner/base/core:2.0\n        - image: mcr.microsoft.com/azurelinux/base/core:3.0\n    container: ${{matrix.vector.image}}\n    steps:\n      - run: |\n          if [[ ${{matrix.vector.image}} == *\"suse\"* ]]; then\n            zypper -n install tar gzip\n          elif [[ ${{matrix.vector.image}} == *\"centos\"* ]]; then\n            dnf install which -y\n          elif [[ ${{matrix.vector.image}} == *\"mariner\"* || ${{matrix.vector.image}} == *\"azurelinux\"* ]]; then\n            GNUPGHOME=/root/.gnupg tdnf update -y &&\n            GNUPGHOME=/root/.gnupg tdnf install tar -y # needed for `actions/checkout`\n          fi\n\n      - uses: actions/checkout@v6\n\n      - run: |\n          sh \"${GITHUB_WORKSPACE}/src/linux/Packaging.Linux/install-from-source.sh\" -y\n          git-credential-manager --help || exit 1\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n**/Properties/launchSettings.json\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk \n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush\n.cr/\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output \nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder \n.mfractor/\n\n# macOS Finder files\n.DS_Store\n\n# GCM build output\nout/\n\n# Temporary build directory\n.tmp/\n\n# dotnet local tools\n.tools/\n\n# Signing generated Files\nauth.json\ninput.json"
  },
  {
    "path": ".lycheeignore",
    "content": "godaddy\\.com[\\\\/]?$\ngitlab\\.com/-/profile/applications\nfile:[\\\\/][\\\\/][\\\\/].*\n"
  },
  {
    "path": ".markdownlint.jsonc",
    "content": "// For information on writing markdownlint configuration see:\n//  https://github.com/DavidAnson/markdownlint/blob/main/README.md#optionsconfig\n{\n    \"MD013\": {\n        \"line_length\": 80,\n        \"code_blocks\": false,\n        \"headings\": false,\n        \"tables\": false\n    },\n    \"MD024\": false   // The format for some files require repeated headings, e.g. \"Example\"\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n   // Use IntelliSense to find out which attributes exist for C# debugging\n   // Use hover for the description of the existing attributes\n   // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md\n   \"version\": \"0.2.0\",\n   \"configurations\": [\n        {\n            \"name\": \"Git Credential Manager (get)\",\n            \"type\": \"coreclr\",\n            \"request\": \"launch\",\n            \"preLaunchTask\": \"build\",\n            // If you have changed target frameworks, make sure to update the program path.\n            \"program\": \"${workspaceFolder}/out/shared/Git-Credential-Manager/bin/Debug/net8.0/git-credential-manager.dll\",\n            \"args\": [\"get\"],\n            \"cwd\": \"${workspaceFolder}/out/shared/Git-Credential-Manager\",\n            \"console\": \"integratedTerminal\",\n            \"stopAtEntry\": false,\n        },\n        {\n            \"name\": \"Git Credential Manager (store)\",\n            \"type\": \"coreclr\",\n            \"request\": \"launch\",\n            \"preLaunchTask\": \"build\",\n            // If you have changed target frameworks, make sure to update the program path.\n            \"program\": \"${workspaceFolder}/out/shared/Git-Credential-Manager/bin/Debug/net8.0/git-credential-manager.dll\",\n            \"args\": [\"store\"],\n            \"cwd\": \"${workspaceFolder}/out/shared/Git-Credential-Manager\",\n            \"console\": \"integratedTerminal\",\n            \"stopAtEntry\": false,\n        },\n        {\n            \"name\": \".NET Attach\",\n            \"type\": \"coreclr\",\n            \"request\": \"attach\",\n            \"processId\": \"${command:pickProcess}\"\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"build\",\n            \"command\": \"dotnet\",\n            \"type\": \"process\",\n            \"group\":{\n                \"kind\": \"build\",\n                \"isDefault\": true\n            },\n            \"args\": [\n                \"build\",\n                \"${workspaceFolder}/Git-Credential-Manager.sln\",\n            ],\n            \"problemMatcher\": \"$msCompile\"\n        },\n        {\n            \"label\": \"test\",\n            \"command\": \"dotnet\",\n            \"type\": \"shell\",\n            \"group\":{\n                \"kind\": \"test\",\n                \"isDefault\": true\n            },\n            \"args\": [\n                \"test\",\n                \"${workspaceFolder}/Git-Credential-Manager.sln\"\n            ],\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"dedicated\"\n            }\n        },\n        {\n            \"label\": \"test with coverage\",\n            \"command\": \"dotnet\",\n            \"type\": \"shell\",\n            \"group\": \"test\",\n            \"args\": [\n                \"test\",\n                \"${workspaceFolder}/Git-Credential-Manager.sln\",\n                \"--collect\",\n                \"'XPlat Code Coverage'\",\n                \"--settings\",\n                \"${workspaceFolder}/.code-coverage/coverlet.settings.xml\"\n            ],\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"dedicated\"\n            }\n        },\n        {\n            \"label\": \"report coverage - nix\",\n            \"command\": \"dotnet\",\n            \"type\": \"shell\",\n            \"group\": \"test\",\n            \"args\": [\n                \"~/.nuget/packages/reportgenerator/*/*/net8.0/ReportGenerator.dll\",\n                \"-reports:${workspaceFolder}/**/TestResults/**/coverage.cobertura.xml\",\n                \"-targetdir:${workspaceFolder}/out/code-coverage\"\n            ],\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"dedicated\"\n            }\n        },\n        {\n            \"label\": \"report coverage - win\",\n            \"command\": \"dotnet\",\n            \"type\": \"shell\",\n            \"group\": \"test\",\n            \"args\": [\n                \"${env:USERROFILE}/.nuget/packages/reportgenerator/*/*/net8.0/ReportGenerator.dll\",\n                \"-reports:${workspaceFolder}/**/TestResults/**/coverage.cobertura.xml\",\n                \"-targetdir:${workspaceFolder}/out/code-coverage\"\n            ],\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"dedicated\"\n            }\n        }\n    ]\n}"
  },
  {
    "path": "CODEOWNERS",
    "content": "*\t@git-ecosystem/git-client\n/src/shared/Microsoft.AzureRepos/\t@git-ecosystem/git-client @git-ecosystem/gcm-azure-maintainers\n/src/shared/Microsoft.AzureRepos.Tests/\t@git-ecosystem/git-client @git-ecosystem/gcm-azure-maintainers\n/src/shared/GitHub/\t@git-ecosystem/git-client @git-ecosystem/hubbers\n/src/shared/GitHub.Tests/\t@git-ecosystem/git-client @git-ecosystem/hubbers\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to make participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies within all project spaces, and it also applies when\nan individual is representing the project or its community in public spaces.\nExamples of representing a project or community include using an official\nproject e-mail address, posting via an official social media account, or acting\nas an appointed representative at an online or offline event. Representation of\na project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at opensource@github.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][cc-homepage],\nversion 1.4, available at [Contributor Covenant Code of Conduct][cc-coc].\n\nFor answers to common questions about this code of conduct, see the\n[Contributor Covenant FAQ][cc-faq]\n\n[cc-coc]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n[cc-faq]: https://www.contributor-covenant.org/faq\n[cc-homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nHi there! We're thrilled that you'd like to contribute to GCM :tada:. Your help\nis essential for keeping it great.\n\nContributions to GCM are [released][contribute-under-repo-license] to the public\nunder the [project's open source license][license].\n\nPlease note that this project is released with a\n[Contributor Code of Conduct][code-of-conduct]. By participating in this project\nyou agree to abide by its terms.\n\n## Start with an issue\n\n1. Open an [issue][issue] to discuss the change you want to see.\n   This helps us coordinate and reduce duplication.\n1. Once we've had some discussion, you're ready to code!\n\n## Submitting a pull request\n\n1. [Fork][fork] and clone the repository\n1. Configure and install the dependencies: `dotnet restore`\n1. Make sure the tests pass on your machine: `dotnet test`\n1. Create a new branch: `git switch -c my-branch-name`\n1. Make your change, add tests, and make sure the tests still pass\n1. For UI updates, test your changes by executing a `dotnet run` in applicable\n   UI-related project directories:\n    - `Atlassian.Bitbucket.UI.Avalonia`\n    - `GitHub.UI.Avalonia`\n    - `Atlassian.Bitbucket.UI.Windows`\n    - `GitHub.UI.Windows`\n1. Organize your changes into one or more [logical, descriptive commits][commits].\n1. Push to your fork and [submit a pull request][pr]\n1. Pat your self on the back and wait for your pull request to be reviewed and\n   merged.\n\nHere are a few things you can do that will increase the likelihood of your pull\nrequest being accepted:\n\n- Match existing code style.\n- Write tests.\n- Keep your change as focused as possible. If there are multiple changes you\n   would like to make that are not dependent upon each other, consider\n   submitting them as separate pull requests.\n\n## Resources\n\n- [How to Contribute to Open Source][how-to-contribute]\n- [Using Pull Requests][prs]\n- [GitHub Help][github-help]\n\n[code-of-conduct]: CODE_OF_CONDUCT.md\n[commits]: https://www.youtube.com/watch?v=4qLtKx9S9a8\n[contribute-under-repo-license]: https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license\n[fork]: https://github.com/git-ecosystem/git-credential-manager/fork\n[github-help]: https://help.github.com\n[how-to-contribute]: https://opensource.guide/how-to-contribute/\n[issue]: https://github.com/git-ecosystem/git-credential-manager/issues/new/choose\n[license]: LICENSE\n[pr]: https://github.com/git-ecosystem/git-credential-manager/compare\n[prs]: https://help.github.com/articles/about-pull-requests/\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <!-- This is the root Directory.Build.props file -->\n\n  <PropertyGroup>\n     <!--\n        Set helpful property for detecting the current OS platform.\n        We can't use the built-in $(OS) because it returns 'Unix' on macOS when run under Mono.\n      -->\n    <OSPlatform Condition=\"$([MSBuild]::IsOsPlatform('windows'))\">windows</OSPlatform>\n    <OSPlatform Condition=\"$([MSBuild]::IsOsPlatform('osx'))\">osx</OSPlatform>\n    <OSPlatform Condition=\"$([MSBuild]::IsOsPlatform('linux'))\">linux</OSPlatform>\n    <IsUnixLike>true</IsUnixLike>\n    <IsUnixLike Condition=\"'$(OSPlatform)'=='windows'\">false</IsUnixLike>\n\n    <!-- Define the root of the repository as a property - it's very useful! -->\n    <RepoPath>$(MSBuildThisFileDirectory)</RepoPath>\n    <RepoSrcPath>$(RepoPath)src\\</RepoSrcPath>\n    <RepoOutPath>$(RepoPath)out\\</RepoOutPath>\n    <RepoAssetsPath>$(RepoPath)assets\\</RepoAssetsPath>\n\n    <!-- Identify projects that output an executable binary (not libraries) -->\n    <_IsExeProject Condition=\"'$(OutputType)' == 'Exe' OR '$(OutputType)' == 'WinExe'\">true</_IsExeProject>\n\n    <!-- Automatically generate a Windows app manifest on Windows for exe projects -->\n    <GenerateWindowsAppManifest Condition=\"'$(GenerateWindowsAppManifest)' == '' AND '$(OSPlatform)' == 'windows' AND '$(_IsExeProject)' == 'true'\">true</GenerateWindowsAppManifest>\n  </PropertyGroup>\n\n  <ItemGroup Condition = \"'$(TargetFramework)' == 'net472'\">\n    <PackageReference Include=\"System.Text.Json\">\n      <Version>8.0.5</Version>\n    </PackageReference>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Directory.Build.targets",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <!-- This is the root Directory.Build.targets file -->\n\n  <!-- Load custom build tasks -->\n  <Import Project=\"$(RepoPath)build\\GCM.tasks\" />\n\n  <!-- Use version specified in VERSION file -->\n  <Target Name=\"GetVersion\" BeforeTargets=\"BeforeBuild;GenerateWindowsAppManifest\">\n    <GetVersion VersionFile=\"$(RepoPath)VERSION\">\n      <Output TaskParameter=\"Version\" PropertyName=\"Version\" />\n      <Output TaskParameter=\"AssemblyVersion\" PropertyName=\"AssemblyVersion\" />\n      <Output TaskParameter=\"FileVersion\" PropertyName=\"FileVersion\" />\n    </GetVersion>\n  </Target>\n\n  <!-- Windows application manifest generation -->\n  <PropertyGroup Condition=\"'$(GenerateWindowsAppManifest)' != 'false'\">\n    <ApplicationManifest>$(IntermediateOutputPath)app.manifest</ApplicationManifest>\n  </PropertyGroup>\n\n  <!-- Generate the manifest file before we set the win32 manifest properties -->\n  <Target Name=\"GenerateWindowsAppManifest\"\n\t\t  AfterTargets=\"GetVersion\"\n          BeforeTargets=\"SetWin32ManifestProperties\"\n          Condition=\"'$(GenerateWindowsAppManifest)' != 'false'\"\n          Inputs=\"$(FileVersion);$(AssemblyName)\"\n          Outputs=\"$(IntermediateOutputPath)app.manifest\">\n    <GenerateWindowsAppManifest Version=\"$(FileVersion)\"\n                                ApplicationName=\"$(AssemblyName)\"\n                                OutputFile=\"$(IntermediateOutputPath)app.manifest\"/>\n    <ItemGroup>\n      <FileWrites Include=\"$(IntermediateOutputPath)app.manifest\" />\n    </ItemGroup>\n  </Target>\n\n</Project>\n"
  },
  {
    "path": "Git-Credential-Manager.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.29927.169\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"src\", \"src\", \"{A7FC1234-95E3-4496-B5F7-4306F41E6A0E}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Git-Credential-Manager\", \"src\\shared\\Git-Credential-Manager\\Git-Credential-Manager.csproj\", \"{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Core\", \"src\\shared\\Core\\Core.csproj\", \"{31BCFC70-B767-4274-873F-1A076D422FC3}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Core.Tests\", \"src\\shared\\Core.Tests\\Core.Tests.csproj\", \"{AD41FA1E-51F5-4E4F-B7DA-32F921491313}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Microsoft.AzureRepos\", \"src\\shared\\Microsoft.AzureRepos\\Microsoft.AzureRepos.csproj\", \"{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Microsoft.AzureRepos.Tests\", \"src\\shared\\Microsoft.AzureRepos.Tests\\Microsoft.AzureRepos.Tests.csproj\", \"{97DC6241-1240-4A85-8035-F8404A983A82}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"windows\", \"windows\", \"{66722747-1B61-40E4-A89B-1AC8E6D62EA9}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"TestInfrastructure\", \"src\\shared\\TestInfrastructure\\TestInfrastructure.csproj\", \"{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"GitHub\", \"src\\shared\\GitHub\\GitHub.csproj\", \"{3C840B06-A595-4FD9-9A76-56CD45B14780}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"shared\", \"shared\", \"{D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"GitHub.Tests\", \"src\\shared\\GitHub.Tests\\GitHub.Tests.csproj\", \"{3E524EA8-D31A-4394-997C-14B522E3D6FD}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"osx\", \"osx\", \"{3D279E2D-E011-45CF-8EA8-3D71D1300443}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Installer.Mac\", \"src\\osx\\Installer.Mac\\Installer.Mac.csproj\", \"{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Installer.Windows\", \"src\\windows\\Installer.Windows\\Installer.Windows.csproj\", \"{85903170-9E52-4B53-A6E4-3F416F684FAE}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Atlassian.Bitbucket\", \"src\\shared\\Atlassian.Bitbucket\\Atlassian.Bitbucket.csproj\", \"{B49881A6-E734-490E-8EA7-FB0D9E296CFB}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Atlassian.Bitbucket.Tests\", \"src\\shared\\Atlassian.Bitbucket.Tests\\Atlassian.Bitbucket.Tests.csproj\", \"{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Packaging.Linux\", \"src\\linux\\Packaging.Linux\\Packaging.Linux.csproj\", \"{AD2A935F-3720-4802-8119-6A9B35B254DF}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"linux\", \"linux\", \"{8F9D7E67-7DD7-4E32-9134-423281AF00E9}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"GitLab\", \"src\\shared\\GitLab\\GitLab.csproj\", \"{570897DC-A85C-4598-B793-9A00CF710119}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"GitLab.Tests\", \"src\\shared\\GitLab.Tests\\GitLab.Tests.csproj\", \"{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tMacDebug|Any CPU = MacDebug|Any CPU\n\t\tMacRelease|Any CPU = MacRelease|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\t\tWindowsDebug|Any CPU = WindowsDebug|Any CPU\n\t\tWindowsRelease|Any CPU = WindowsRelease|Any CPU\n\t\tLinuxDebug|Any CPU = LinuxDebug|Any CPU\n\t\tLinuxRelease|Any CPU = LinuxRelease|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{570897DC-A85C-4598-B793-9A00CF710119}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacRelease|Any CPU.Build.0 = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(NestedProjects) = preSolution\n\t\t{28F06D44-AB25-4CF5-93F9-978C23FAA9D6} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{31BCFC70-B767-4274-873F-1A076D422FC3} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{AD41FA1E-51F5-4E4F-B7DA-32F921491313} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{714AF9EB-44E6-4058-BD3E-9039F29F4D7A} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{97DC6241-1240-4A85-8035-F8404A983A82} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{66722747-1B61-40E4-A89B-1AC8E6D62EA9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}\n\t\t{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{3C840B06-A595-4FD9-9A76-56CD45B14780} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{D5277A0E-997E-453A-8CB9-4EFCC8B16A29} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}\n\t\t{3E524EA8-D31A-4394-997C-14B522E3D6FD} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{3D279E2D-E011-45CF-8EA8-3D71D1300443} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}\n\t\t{74FA0AA4-B5C1-4F3B-B182-277FC2D50715} = {3D279E2D-E011-45CF-8EA8-3D71D1300443}\n\t\t{85903170-9E52-4B53-A6E4-3F416F684FAE} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}\n\t\t{B49881A6-E734-490E-8EA7-FB0D9E296CFB} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{025E5329-A0B1-4BA9-9203-B70B44A5F9E0} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{8F9D7E67-7DD7-4E32-9134-423281AF00E9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}\n\t\t{AD2A935F-3720-4802-8119-6A9B35B254DF} = {8F9D7E67-7DD7-4E32-9134-423281AF00E9}\n\t\t{570897DC-A85C-4598-B793-9A00CF710119} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\t\t{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {0EF9FC65-E6BA-45D4-A455-262A9EA4366B}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "Git-Credential-Manager.sln.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue\">OS</s:String>\n\t<s:String x:Key=\"/Default/Environment/Hierarchy/Build/SolBuilderDuo/UseMsbuildSolutionBuilder/@EntryValue\">No</s:String>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=PKCE/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Bitbucket/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>\n"
  },
  {
    "path": "LICENSE",
    "content": "Git Credential Manager\nCopyright © GitHub, Inc. and contributors\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE\n"
  },
  {
    "path": "NOTICE",
    "content": "NOTICES AND INFORMATION\nDo Not Translate or Localize\n\nThis repository for Git Credential Manager includes material from the\nprojects listed below.\n\n--------------------------------------------------------------------------------\n1. GitHub/VisualStudio (https://github.com/github/VisualStudio)\n\nCopyright (c) GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n--------------------------------------------------------------------------------\n2. dotnet/runtime (https://github.com/dotnet/runtime)\n\nThe MIT License (MIT)\n\nCopyright (c) .NET Foundation and Contributors\n\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Git Credential Manager\n\n[![Build Status][build-status-badge]][workflow-status]\n\n---\n\n[Git Credential Manager][gcm] (GCM) is a secure\n[Git credential helper][git-credential-helper] built on [.NET][dotnet] that runs\non Windows, macOS, and Linux. It aims to provide a consistent and secure\nauthentication experience, including multi-factor auth, to every major source\ncontrol hosting service and platform.\n\nGCM supports (in alphabetical order) [Azure DevOps][azure-devops], Azure DevOps\nServer (formerly Team Foundation Server), Bitbucket, GitHub, and GitLab.\nCompare to Git's [built-in credential helpers][git-tools-credential-storage]\n(Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring/libsecret), which\nprovide single-factor authentication support for username/password only.\n\nGCM replaces both the .NET Framework-based\n[Git Credential Manager for Windows][gcm-for-windows] and the Java-based\n[Git Credential Manager for Mac and Linux][gcm-for-mac-and-linux].\n\n## Install\n\nSee the [installation instructions][install] for the current version of GCM for\ninstall options for your operating system.\n\n## Current status\n\nGit Credential Manager is currently available for Windows, macOS, and Linux\\*.\nGCM only works with HTTP(S) remotes; you can still use Git with SSH:\n\n- [Azure DevOps SSH][azure-devops-ssh]\n- [GitHub SSH][github-ssh]\n- [Bitbucket SSH][bitbucket-ssh]\n\nFeature|Windows|macOS|Linux\\*\n-|:-:|:-:|:-:\nInstaller/uninstaller|&#10003;|&#10003;|&#10003;\nSecure platform credential storage [(see more)][gcm-credstores]|&#10003;|&#10003;|&#10003;\nMulti-factor authentication support for Azure DevOps|&#10003;|&#10003;|&#10003;\nTwo-factor authentication support for GitHub|&#10003;|&#10003;|&#10003;\nTwo-factor authentication support for Bitbucket|&#10003;|&#10003;|&#10003;\nTwo-factor authentication support for GitLab|&#10003;|&#10003;|&#10003;\nWindows Integrated Authentication (NTLM/Kerberos) support|&#10003;|_N/A_|_N/A_\nBasic HTTP authentication support|&#10003;|&#10003;|&#10003;\nProxy support|&#10003;|&#10003;|&#10003;\n`amd64` support|&#10003;|&#10003;|&#10003;\n`x86` support|&#10003;|_N/A_|&#10007;\n`arm64` support|best effort|&#10003;|&#10003;\n`armhf` support|_N/A_|_N/A_|&#10003;\n\n(\\*) GCM guarantees support only for [the Linux distributions that are officially\nsupported by dotnet][dotnet-distributions].\n\n## Supported Git versions\n\nGit Credential Manager tries to be compatible with the broadest set of Git\nversions (within reason). However there are some known problematic releases of\nGit that are not compatible.\n\n- Git 1.x\n\n  The initial major version of Git is not supported or tested with GCM.\n\n- Git 2.26.2\n\n  This version of Git introduced a breaking change with parsing credential\n  configuration that GCM relies on. This issue was fixed in commit\n  [`12294990`][gcm-commit-12294990] of the Git project, and released in Git\n  2.27.0.\n\n## How to use\n\nOnce it's installed and configured, Git Credential Manager is called implicitly\nby Git. You don't have to do anything special, and GCM isn't intended to be\ncalled directly by the user. For example, when pushing (`git push`) to\n[Azure DevOps][azure-devops], [Bitbucket][bitbucket], or [GitHub][github], a\nwindow will automatically open and walk you through the sign-in process. (This\nprocess will look slightly different for each Git host, and even in some cases,\nwhether you've connected to an on-premises or cloud-hosted Git host.) Later Git\ncommands in the same repository will re-use existing credentials or tokens that\nGCM has stored for as long as they're valid.\n\nRead full command line usage [here][gcm-usage].\n\n### Configuring a proxy\n\nSee detailed information [here][gcm-http-proxy].\n\n## Additional Resources\n\nSee the [documentation index][docs-index] for links to additional resources.\n\n## Experimental Features\n\n- [Windows broker (experimental)][gcm-windows-broker]\n\n## Future features\n\nCurious about what's coming next in the GCM project? Take a look at the [project\nroadmap][roadmap]! You can find more details about the construction of the\nroadmap and how to interpret it [here][roadmap-announcement].\n\n## Contributing\n\nThis project welcomes contributions and suggestions.\nSee the [contributing guide][gcm-contributing] to get started.\n\nThis project follows [GitHub's Open Source Code of Conduct][gcm-coc].\n\n## License\n\nWe're [MIT][gcm-license] licensed.\nWhen using GitHub logos, please be sure to follow the\n[GitHub logo guidelines][github-logos].\n\n[azure-devops]: https://azure.microsoft.com/en-us/products/devops\n[azure-devops-ssh]: https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops\n[bitbucket]: https://bitbucket.org\n[bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html\n[build-status-badge]: https://github.com/git-ecosystem/git-credential-manager/actions/workflows/continuous-integration.yml/badge.svg\n[docs-index]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/README.md\n[dotnet]: https://dotnet.microsoft.com\n[dotnet-distributions]: https://learn.microsoft.com/en-us/dotnet/core/install/linux\n[git-credential-helper]: https://git-scm.com/docs/gitcredentials\n[gcm]: https://github.com/git-ecosystem/git-credential-manager\n[gcm-coc]: CODE_OF_CONDUCT.md\n[gcm-commit-12294990]: https://github.com/git/git/commit/12294990c90e043862be9eb7eb22c3784b526340\n[gcm-contributing]: CONTRIBUTING.md\n[gcm-credstores]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/credstores.md\n[gcm-for-mac-and-linux]: https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux\n[gcm-for-windows]: https://github.com/microsoft/Git-Credential-Manager-for-Windows\n[gcm-http-proxy]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/netconfig.md#http-proxy\n[gcm-license]: LICENSE\n[gcm-usage]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/usage.md\n[gcm-windows-broker]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/windows-broker.md\n[git-tools-credential-storage]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage\n[github]: https://github.com\n[github-ssh]: https://help.github.com/en/articles/connecting-to-github-with-ssh\n[github-logos]: https://github.com/logos\n[install]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/install.md\n[ms-package-repos]: https://packages.microsoft.com/repos/\n[roadmap]: https://github.com/git-ecosystem/git-credential-manager/milestones?direction=desc&sort=due_date&state=open\n[roadmap-announcement]: https://github.com/git-ecosystem/git-credential-manager/discussions/1203\n[workflow-status]: https://github.com/git-ecosystem/git-credential-manager/actions/workflows/continuous-integration.yml\n"
  },
  {
    "path": "SECURITY.md",
    "content": "Thanks for helping make GitHub safe for everyone.\n\n## Security\n\nGitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).\n\nEven though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. \n\n## Reporting Security Issues\n\nIf you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure.\n\n**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**\n\nInstead, please send an email to opensource-security[@]github.com.\n\nPlease include as much of the information listed below as you can to help us better understand and resolve the issue:\n\n  * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)\n  * Full paths of source file(s) related to the manifestation of the issue\n  * The location of the affected source code (tag/branch/commit or direct URL)\n  * Any special configuration required to reproduce the issue\n  * Step-by-step instructions to reproduce the issue\n  * Proof-of-concept or exploit code (if possible)\n  * Impact of the issue, including how an attacker might exploit the issue\n\nThis information will help us triage your report more quickly.\n\n## Policy\n\nSee [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor)\n\n"
  },
  {
    "path": "VERSION",
    "content": "2.7.3.0\n"
  },
  {
    "path": "assets/gcm.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Viewbox xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Stretch=\"Uniform\">\n  <Canvas Name=\"svg815\" Width=\"256\" Height=\"256\">\n    <Canvas.RenderTransform>\n      <TranslateTransform X=\"0\" Y=\"0\"/>\n    </Canvas.RenderTransform>\n    <Canvas.Resources/>\n    <Path xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" Name=\"path8\" Fill=\"#FFF05033\" StrokeThickness=\"2.24708056\">\n      <Path.Data>\n        <PathGeometry Figures=\"m 127.74219 0 c -4.21881 0 -8.43843 1.6093415 -11.65821 4.828125 L 92.875 28.041016 122.31445 57.482422 c 6.84425 -2.310946 14.68947 -0.761046 20.14258 4.693359 5.48141 5.488392 7.0192 13.400136 4.65039 20.267578 l 28.37696 28.376951 c 6.86492 -2.36579 14.78398 -0.83727 20.26562 4.6543 7.66374 7.66179 7.66374 20.07642 0 27.74023 -7.66486 7.66631 -20.07893 7.66631 -27.74805 0 -5.76254 -5.76724 -7.18821 -14.23373 -4.26953 -21.33398 l -26.46289 -26.464844 -0.002 69.640624 c 1.8684 0.92496 3.63248 2.16064 5.18945 3.71094 7.66148 7.6609 7.66148 20.07236 0 27.74609 -7.66374 7.66066 -20.08459 7.66066 -27.74023 0 -7.66262 -7.67305 -7.66262 -20.08452 0 -27.74609 1.89369 -1.89039 4.08382 -3.32218 6.42187 -4.28125 V 94.199219 c -2.33912 -0.955028 -4.52734 -2.375687 -6.42383 -4.28125 -5.80409 -5.799832 -7.20125 -14.319608 -4.22461 -21.447266 L 81.466797 39.443359 4.8300781 116.08203 c -6.4393305 6.44252 -6.4393305 16.88229 0 23.32031 L 42.5 177.07227 V 162.70508 C 35.078345 157.16403 29.678851 149.02425 27.328125 140.07617 19.76286 115.647 40.902921 87.908596 66.359375 88.345703 76.747705 88.003924 87.132367 91.94485 94.875 98.837891 c 18.78493 15.372969 17.87151 47.496839 -0.888672 62.484379 l -2.566406 1.3457 0.222656 12.46503 -8.160156 8.24395 v 34.67578 l 33.119138 33.11915 c 6.43505 6.43757 16.87041 6.43757 23.31446 0 L 251.17188 139.92969 c 6.43732 -6.4407 6.43732 -16.88336 0 -23.32227 l 0.002 -0.01 L 139.39453 4.828125 C 136.1788 1.6093415 131.96099 0 127.74219 0 Z\" FillRule=\"NonZero\"/>\n      </Path.Data>\n    </Path>\n    <Path xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" Name=\"path831\" Fill=\"#FF4D4D4D\" StrokeThickness=\"4.12109041\">\n      <Path.Data>\n        <PathGeometry Figures=\"M 67.333984 94.333984 A 35.333332 35.333332 0 0 0 32 129.66602 A 35.333332 35.333332 0 0 0 48.5 159.54688 L 48.5 234.5 L 54.5 240 L 67 240 L 79 228 L 79 216.5 L 73 210 L 79 203.5 L 73 197 L 79 191 L 73 185.5 L 85.5 173 L 85.5 159.92188 A 35.333332 35.333332 0 0 0 102.66602 129.66602 A 35.333332 35.333332 0 0 0 67.333984 94.333984 z M 66.777344 109 A 9 9 0 0 1 75.777344 118 A 9 9 0 0 1 66.777344 127 A 9 9 0 0 1 57.777344 118 A 9 9 0 0 1 66.777344 109 z M 54.5 168 L 60.5 173 L 60.5 234.5 L 54.5 228 L 54.5 168 z \" FillRule=\"NonZero\"/>\n      </Path.Data>\n    </Path>\n  </Canvas>\n</Viewbox>\n"
  },
  {
    "path": "build/GCM.MSBuild.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IncludeBuildOutput>false</IncludeBuildOutput>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Build.Framework\" Version=\"16.0.461\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Microsoft.Build.Utilities.Core\" Version=\"16.0.461\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "build/GCM.tasks",
    "content": "<Project>\n    <PropertyGroup Condition=\"'$(MSBuildRuntimeType)' == 'Mono'\">\n        <_TaskAssembly>$(MSBuildToolsPath)\\Microsoft.Build.Tasks.v4.0.dll</_TaskAssembly>\n        <_TaskFactory>CodeTaskFactory</_TaskFactory>\n    </PropertyGroup>\n    <PropertyGroup Condition=\"'$(MSBuildRuntimeType)' != 'Mono'\">\n        <_TaskAssembly>$(MSBuildToolsPath)\\Microsoft.Build.Tasks.Core.dll</_TaskAssembly>\n        <_TaskFactory>RoslynCodeTaskFactory</_TaskFactory>\n    </PropertyGroup>\n\n    <UsingTask TaskName=\"GenerateWindowsAppManifest\" TaskFactory=\"$(_TaskFactory)\" AssemblyFile=\"$(_TaskAssembly)\">\n        <Task>\n            <Code Type=\"Class\" Source=\"$(MSBuildThisFileDirectory)GenerateWindowsAppManifest.cs\" />\n        </Task>\n    </UsingTask>\n\n    <UsingTask TaskName=\"GetVersion\" TaskFactory=\"$(_TaskFactory)\" AssemblyFile=\"$(_TaskAssembly)\">\n        <Task>\n            <Code Type=\"Class\" Source=\"$(MSBuildThisFileDirectory)GetVersion.cs\" />\n        </Task>\n    </UsingTask>\n</Project>\n"
  },
  {
    "path": "build/GenerateWindowsAppManifest.cs",
    "content": "using Microsoft.Build.Framework;\nusing Microsoft.Build.Utilities;\nusing System.IO;\n\nnamespace GitCredentialManager.MSBuild\n{\n    public class GenerateWindowsAppManifest : Task\n    {\n        [Required]\n        public string Version { get; set; }\n\n        [Required]\n        public string ApplicationName { get; set; }\n\n        [Required]\n        public string OutputFile { get; set; }\n\n        public override bool Execute()\n        {\n            Log.LogMessage(MessageImportance.Normal, \"Creating application manifest file for '{0}'...\", ApplicationName);\n\n            string manifestDirectory = Path.GetDirectoryName(OutputFile);\n            if (!Directory.Exists(manifestDirectory))\n            {\n                Directory.CreateDirectory(manifestDirectory);\n            }\n\n            File.WriteAllText(\n                OutputFile,\n                $@\"<?xml version=\"\"1.0\"\" encoding=\"\"utf-8\"\"?>\n<assembly manifestVersion=\"\"1.0\"\" xmlns=\"\"urn:schemas-microsoft-com:asm.v1\"\">\n  <assemblyIdentity version=\"\"{Version}\"\" name=\"\"{ApplicationName}\"\"/>\n  <compatibility xmlns=\"\"urn:schemas-microsoft-com:compatibility.v1\"\">\n    <application>\n      <!-- Windows 10 and Windows 11 -->\n      <supportedOS Id=\"\"{{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}}\"\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"\"{{1f676c76-80e1-4239-95bb-83d0f6d0da78}}\"\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"\"{{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}}\"\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"\"{{35138b9a-5d96-4fbd-8e2d-a2440225f93a}}\"\"/>\n    </application>\n  </compatibility>\n</assembly>\n\");\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "build/GetVersion.cs",
    "content": "using Microsoft.Build.Framework;\nusing Microsoft.Build.Utilities;\nusing System.IO;\n\nnamespace GitCredentialManager.MSBuild\n{\n    public class GetVersion : Task\n    {\n        [Required]\n        public string VersionFile { get; set; }\n\n        [Output]\n        public string Version { get; set; }\n\n        [Output]\n        public string AssemblyVersion { get; set; }\n\n        [Output]\n        public string FileVersion { get; set; }\n\n        public override bool Execute()\n        {\n            Log.LogMessage(MessageImportance.Normal, \"Reading VERSION file...\");\n            string textVersion = File.ReadAllText(VersionFile);\n\n            if (!System.Version.TryParse(textVersion, out System.Version fullVersion))\n            {\n                Log.LogError(\"Invalid version '{0}' specified.\", textVersion);\n                return false;\n            }\n\n            // System.Version names its version components as follows:\n            // major.minor[.build[.revision]]\n            // The main version number we use for GCM contains the first three\n            // components.\n            // The assembly and file version numbers contain all components, as\n            // omitting the revision portion from these properties causes\n            // runtime failures on Windows.\n            Version = $\"{fullVersion.Major}.{fullVersion.Minor}.{fullVersion.Build}\";\n            AssemblyVersion = FileVersion = fullVersion.ToString();\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "# User documentation\n\nThe following are links to GCM user support documentation:\n\n- [Frequently asked questions][gcm-faq]\n- [Command-line usage][gcm-usage]\n- [Configuration options][gcm-config]\n- [Environment variables][gcm-env]\n- [Enterprise configuration][gcm-enterprise-config]\n- [Network and HTTP configuration][gcm-net-config]\n- [Credential stores][gcm-credstores]\n- [Host provider specification][gcm-host-provider]\n- [Azure Repos OAuth tokens][gcm-azure-tokens]\n- [Azure Managed Identities and Service Principals][gcm-misp]\n- [GitLab support][gcm-gitlab]\n- [Generic OAuth support][gcm-oauth]\n- [NTLM and Kerberos authentication][gcm-ntlm-kerberos]\n\n[gcm-azure-tokens]: azrepos-users-and-tokens.md\n[gcm-config]: configuration.md\n[gcm-credstores]: credstores.md\n[gcm-enterprise-config]: enterprise-config.md\n[gcm-env]: environment.md\n[gcm-faq]: faq.md\n[gcm-gitlab]: gitlab.md\n[gcm-host-provider]: hostprovider.md\n[gcm-misp]: azrepos-misp.md\n[gcm-net-config]: netconfig.md\n[gcm-oauth]: generic-oauth.md\n[gcm-usage]: usage.md\n[gcm-ntlm-kerberos]: ntlm-kerberos.md\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# Architecture\n\n## Overview\n\n```text\n+------------------------------------------------------------------------------+\n|                                                                              |\n|                           Git-Credential-Manager                             |\n|                                                                              |\n+-+-------------+--------------+-----+---------------------+-----------------+-+\n  |             |              |     |                     |                 |\n  |             |              |     |             Windows |         Windows |\n  |             |              |     |                     |                 |\n  | +-----------v-----------+  |     |    +----------------v---------------+ |\n  | |                       |  |     |    |                                | |\n  | |        GitHub         <-------------+        GitHub.UI.Windows       | |\n  | |                       |  |     |    |                                | |\n  | +-+---------------------+  |     |    +-+------------------------------+ |\n  |   |                        |     |      |                                |\n  |   |  +---------------------v-+   |      | +------------------------------v-+\n  |   |  |                       |   |      | |                                |\n  |   |  |  Atlassian.Bitbucket  <------------+ Atlassian.Bitbucket.UI.Windows |\n  |   |  |                       |   |      | |                                |\n  |   |  +-+---------------------+   |      | +---------------+----------------+\n  |   |    |                         |      |                 |\n  |   |    |  +----------------------v-+    |                 |\n  |   |    |  |                        |    |                 |\n  |   |    |  |  Microsoft.AzureRepos  |    |                 |\n  |   |    |  |                        |    |                 |\n  |   |    |  +-----------+------------+    |                 |\n  |   |    |              |                 |                 |\n+-v---v----v--------------v------------+  +-v-----------------v----------------+\n|                                      |  |                                    |\n|                 Core                 <--+               Core.UI              |\n|                                      |  |                                    |\n+--------------------------------------+  +------------------------------------+\n```\n\nGit Credential Manager (GCM) is built to be Git host and platform/OS\nagnostic. Most of the shared logic (command execution, the abstract platform\nsubsystems, etc) can be found in the `Core` class\nlibrary (C#). The library targets .NET Standard as well as .NET Framework.\n\n> **Note**\n>\n> The reason for also targeting .NET Framework directly is that the\n> `Microsoft.Identity.Client` ([MSAL.NET][msal])\n> library requires a .NET Framework target to be able to show the embedded web\n> browser auth pop-up on Windows platforms.\n>\n> There are extension points that now exist in MSAL.NET meaning we can plug-in\n> our own browser pop-up handling code on .NET meaning both Windows and\n> Mac. We haven't yet gotten around to exploring this.\n>\n> See [GCM issue 113][issue-113] for more information.\n\nThe entry-point for GCM can be found in the `Git-Credential-Manager`\nproject, a console application that targets both .NET and .NET Framework.\nThis project emits the `git-credential-manager(.exe)` executable, and\ncontains very little code - registration of all supported host providers and\nrunning the `Application` object found in `Core`.\n\nProviders have their own projects/assemblies that take dependencies on the\n`Core` core assembly, and are dependents of the main\nentry point application `Git-Credential-Manager`. Code in these binaries is\nexpected to run on all supported platforms and typically (see MSAL.NET note\nabove) does not include any graphical user interface; they use terminal prompts\nonly.\n\nWhere a provider needs some platform-specific interaction or graphical user\ninterface, the recommended model is to have a separate 'helper' executable that\nthe shared, core binaries shell out to. Currently the Bitbucket and GitHub\nproviders each have a WPF (Windows only) helper executable that shows\nauthentication prompts and messages.\n\nThe `Core.UI` project is a WPF (Windows only) assembly\nthat contains common WPF components and styles that are shared between provider\nhelpers on Windows.\n\n### Cross-platform UI\n\nWe hope to be able to migrate the WPF/Windows only helpers to [Avalonia][avalonia]\nin order to gain cross-platform graphical user interface support. See\n[GCM issue 136][issue-136] for up-to-date progress on this effort.\n\n### Microsoft authentication\n\nFor authentication using Microsoft Accounts or Azure Active Directory, things\nare a little different. The `MicrosoftAuthentication` component is present in\nthe `Core` core assembly, rather than bundled with a\nspecific host provider. This was done to allow any service that may wish to in\nthe future integrate with Microsoft Accounts or Azure Active Directory can make\nuse of this reusable authentication component.\n\n## Asynchronous programming\n\nGCM makes use of the `async`/`await` model of .NET and C# in almost all\nparts of the codebase where appropriate as usually requests end up going to the\nnetwork at some point.\n\n## Command execution\n\n```text\n                             +---------------+\n                             |               |\n                             |      Git      |\n                             |               |\n                             +---+-------^---+\n                                 |       |\n                             +---v---+---+---+\n                             | stdin | stdout|\n                             +---+---+---^---+\n                                 |       |\n                            (2)  |       |  (7)\n                          Select |       | Serialize\n                         Command |       | Result\n                                 |       |\n                     (3)         |       |\n                    Select       |       |\n+---------------+  Provider  +---v-------+---+\n| Host Provider |            |               |\n|   Registry    <------------+    Command    |\n|               |            |               |\n+-------^-------+            +----+------^---+\n        |                         |      |\n        |                   (4)   |      |   (6)\n        |                Execute  |      |  Return\n        |              Operation  |      |  Result\n        |    (1)                  |      |\n        |  Register          +----v------+---+\n        |                    |               |\n        +--------------------+ Host Provider |\n                             |               |\n                             +-------^-------+\n                                     |\n                   (5) Use services  |\n                                     |\n                             +-------v-------+\n                             |    Command    |\n                             |    Context    |\n                             +---------------+\n```\n\nGit Credential Manager maintains a set of known commands including\n`Get|Store|EraseCommand`, as well as commands for install and help/usage.\n\nGCM also maintains a set of known, registered host providers that implement\nthe `IHostProvider` interface. Providers register themselves by adding an\ninstance of the provider to the `Application` object via the `RegisterProvider`\nmethod in [`Core.Program`][core-program].\nThe `GenericHostProvider` is registered last so that it can handle all other\nHTTP-based remotes as a catch-all, and provide basic username/password auth and\ndetect the presence of Windows Integrated Authentication (Kerberos, NTLM,\nNegotiate) support (1).\n\nFor each invocation of GCM, the first argument on the command-line is\nmatched against the known commands and if there is a successful match, the input\nfrom Git (over standard input) is deserialized and the command is executed (2).\n\nThe `Get|Store|EraseCommand`s consult the host provider registry for the most\nappropriate host provider. The default registry implementation select the a host\nprovider by asking each registered provider in turn if they understand the\nrequest. The provider selection can be overridden by the user via the\n[`credential.provider`][credential-provider] or [`GCM_PROVIDER`][gcm-provider]\nconfiguration and environment variable respectively (3).\n\nThe `Get|Store|EraseCommand`s call the corresponding\n`Get|Store|EraseCredentialAsync` methods on the `IHostProvider`, passing the\nrequest from Git together with an instance of the `ICommandContext` (4). The\nhost provider can then make use of various services available on the command\ncontext to complete the requested operation (5).\n\nOnce a credential has been created, retrieved, stored or erased, the host\nprovider returns the credential (for `get` operations only) to the calling\ncommand (6). The credential is then serialized and returned to Git over standard\noutput (7) and GCM terminates with a successful exit code.\n\n## Host provider\n\nHost providers implement the `IHostProvider` interface. They can choose to\ndirectly implement the interface they can also derive from the `HostProvider`\nabstract class (which itself implements the `IHostProvider` interface).\n\nThe `HostProvider` abstract class implements the\n`Get|Store|EraseCredentialAsync` methods and instead has the\n`GenerateCredentialAsync` abstract method, and the `GetServiceName` virtual\nmethod. Calls to `get`, `store`, or `erase` result in first a call to\n`GetServiceName` which should return a stable and unique value for the provider\nand request. This value forms part of the attributes associated with any stored\ncredential in the credential store. During a `get` operation the\ncredential store is queried for an existing credential with such service name.\nIf a credential is found it is returned immediately. Similarly, calls to `store`\nand `erase` are handles automatically to store credentials against, and erase\ncredentials matching the service name. Methods are implemented as `virtual`\nmeaning you can always override this behaviour, for example to clear other\ncustom caches on an `erase` request, without having to reimplement the\nlookup/store credential logic.\n\nThe default implementation of `GetServiceName` is usually sufficient for most\nproviders. It returns the computed remote URL (without a trailing slash) from\nthe input arguments from Git - `<protocol>://<host>[/<path>]` - no username is\nincluded even if present.\n\nHost providers are queried in turn, by priority (then registration order) via\nthe `IHostProvider.IsSupported(InputArguments)` method and passed the input\nreceived from Git. If the provider recognises the request, for example by a\nmatching known host name, they can return `true`. If the provider wants to\ncancel and abort an authentication request, for example if this is a HTTP (not\nHTTPS) request for a known host, they should still return `true` and later\ncancel the request.\n\nHost providers can also be queried via the `IHostProvider.IsSupported(HttpResponseMessage)`\nmethod and passed the response message from a HEAD call made to the remote URI.\nThis is useful for detecting on-premises instances based on header values. GCM\nwill only query a provider via this method overload if no other provider at the\nsame registration priority has returned `true` to the `InputArguments` overload.\n\nDepending on the request from Git, one of `GetCredentialAsync` (for `get`\nrequests), `StoreCredentialAsync` (for `store` requests) or\n`EraseCredentialAsync` (for `erase` requests) will be called. The argument\n`InputArguments` contains the request information passed over standard input\nfrom Git/the caller; the same as was passed to `IsSupported`.\n\nThe return value for the `get` operation must be an `ICredential` that Git can\nuse to complete authentication.\n\n> **Note:**\n>\n> The credential can also be an instance where both username and password are\n> the empty string, to signal to Git it should let cURL use \"any auth\"\n> detection - typically to use Windows Integrated Authentication.\n\nThere are no return values for the `store` and `erase` operations as Git ignores\nany output or exit codes for these commands. Failures for these operations are\nbest communicated via writing to the Standard Error stream via\n`ICommandContext.Streams.Error`.\n\n## Command context\n\nThe `ICommandContext` which contains numerous services which are useful for\ninteracting with various platform subsystems, such as the file system or\nenvironment variables. All services on the command context are exposed as\ninterfaces for ease of testing and portability between different operating\nsystems and platforms.\n\nComponent|Description\n-|-\nCredentialStore|A secure operating system controlled location for storing and retrieving `ICredential` objects.\nSettings|Abstraction over all GCM settings.\nStreams|Abstraction over standard input, output and error streams connected to the parent process (typically Git).\nTerminal|Provides interactions with an attached terminal, if it exists.\nSessionManager|Provides information about the current user session.\nTrace|Provides tracing information that may be useful for debugging issues in the wild. Secret information MUST be filtered out completely or via the `Write___Secret` method(s).\nFileSystem|Abstraction over file system operations.\nHttpClientFactory|Factory for creating `HttpClient` instances that are configured with the correct user agent, headers, and proxy settings.\nGit|Provides interactions with Git and Git configuration.\nEnvironment|Abstraction over the current system/user environment variables.\nSystemPrompts|Provides services for showing system/OS native credential prompts.\n\n## Error handling and tracing\n\nGCM operates a 'fail fast' approach to unrecoverable errors. This usually\nmeans throwing an `Exception` which will propagate up to the entry-point and be\ncaught, a non-zero exit code returned, and the error message printed with the\n\"fatal:\" prefix. For errors originating from interop/native code, you should\nthrow an exception of the `InteropException` type. Error messages in exceptions\nshould be human readable. When there is a known or user-fixable issue,\ninstructions on how to self-remedy the issue, or links to relevant\ndocumentation should be given.\n\nWarnings can be emitted over the standard error stream\n(`ICommandContext.Streams.Error`) when you want to alert the user to a potential\nissue with their configuration that does not necessarily stop the\noperation/authentication.\n\nThe `ITrace` component can be found on the `ICommandContext` object or passed in\ndirectly to some constructors. Verbose and diagnostic information is be written\nto the trace object in most places of GCM.\n\n[avalonia]: https://avaloniaui.net/\n[core-program]: ../src/shared/Git-Credential-Manager/Program.cs\n[credential-provider]: configuration.md#credentialprovider\n[issue-113]: https://github.com/git-ecosystem/git-credential-manager/issues/113\n[issue-136]: https://github.com/git-ecosystem/git-credential-manager/issues/136\n[gcm-provider]: environment.md#GCM_PROVIDER\n[msal]: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet\n"
  },
  {
    "path": "docs/autodetect.md",
    "content": "# Host provider auto-detection\n\nGit Credential Manager (GCM) supports authentication with multiple different Git\nhost providers including: GitHub, Bitbucket, and Azure Repos. As well as the\nhosted/cloud offerings, GCM can also work with the self-hosted or \"on-premises\"\nversions of these services: GitHub Enterprise Server, Bitbucket DC Server, and\nAzure DevOps Server (TFS).\n\nBy default, GCM will attempt to automatically detect which particular provider\nis behind the Git remote URL you're interacting with. For the cloud versions of\nthe supported providers this is done by matching the hostname of the remote URL\nto the well-known hostnames of the services. For example \"github.com\" or\n\"dev.azure.com\".\n\n## Self-hosted/on-prem detection\n\nIn order to detect which host provider to use for a self-hosted instance, each\nprovider can provide some heuristic matching of the hostname. For example any\nhostname that begins \"github.*\" will be matched to the GitHub host provider.\n\nIf a heuristic matches incorrectly, you can always\n[explicitly configure][explicit-config] GCM to use a particular provider.\n\n## Remote URL probing\n\nIn addition to heuristic matching, GCM will make a network call to the remote\nURL and inspect HTTP response headers to try and detect a self-hosted instance.\n\nThis network call is only performed if neither an exact nor fuzzy match by\nhostname can be made. Only one HTTP `HEAD` call is made per credential request\nreceived by Git. To avoid this network call, please\n[explicitly configure][explicit-config] the host provider for your self-hosted\ninstance.\n\nAfter a successful detection of the host provider, GCM will automatically set\nthe [`credential.provider`][credential-provider] configuration entry\nfor that remote to avoid needing to perform this expensive network call in\nfuture requests.\n\n### Timeout\n\nYou can control how long GCM will wait for a response to the remote network call\nby setting the [`GCM_AUTODETECT_TIMEOUT`][gcm-autodetect-timeout] environment\nvariable, or the [`credential.autoDetectTimeout`][credential-autoDetectTimeout]\nGit configuration setting to the maximum number of milliseconds to wait.\n\nThe default value is 2000 milliseconds (2 seconds). You can prevent the network\ncall altogether by setting a zero or negative value, for example -1.\n\n## Manual configuration\n\nIf the auto-detection mechanism fails to select the correct host provider, or\nif the remote probing network call is causing performance issues, you can\nconfigure GCM to always use a particular host provider, for a given remote URL.\n\nYou can either use the the [`GCM_PROVIDER`][gcm-provider] environment variable,\nor the [`credential.provider`][credential-provider] Git configuration setting\nfor this purpose.\n\nFor example to tell GCM to always use the GitHub host provider for the\n\"ghe.example.com\" hostname, you can run the following command:\n\n```shell\ngit config --global credential.ghe.example.com.provider github\n```\n\n[credential-autoDetectTimeout]: configuration.md#credentialautodetecttimeout\n[credential-provider]: configuration.md#credentialprovider\n[explicit-config]: #manual-configuration\n[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT\n[gcm-provider]: environment.md#GCM_PROVIDER\n"
  },
  {
    "path": "docs/azrepos-misp.md",
    "content": "# Azure Managed Identities and Service Principals\n\nGit Credential Manager supports Managed Identities and Service Principals for\nauthentication with Azure Repos. This document provides an overview of Managed\nIdentities and Service Principals and how to use them with GCM.\n\n## Managed Identities\n\nAzure Managed Identities can be used to authenticate and authorize applications\nand services to access Azure resources. Managed Identities are a secure way to\naccess Azure resources without needing to store credentials in code or\nconfiguration files.\n\nThere are two types of Managed Identities:\n\n**System-assigned**\n\nSystem-assigned Managed Identities are tied to a specific Azure resource, such\nas a Virtual Machine or App Service. When a system-assigned Managed Identity is\nenabled, Azure creates an identity for the resource in the Azure AD tenant\nthat's trusted by the subscription. The lifecycle of the identity is tied to the\nresource to which it's assigned.\n\n**User-assigned**\n\nUser-assigned Managed Identities are created as standalone Azure resources and\ncan be assigned to one or more Azure resources. This allows you to use the same\nManaged Identity across multiple resources.\n\nYou can read more about Managed Identities in the\n[Azure documentation][az-mi].\n\n### How to configure Managed Identities\n\nIn order to use a Managed Identity with GCM, you need to ensure that the Managed\nIdentity has the necessary permissions to access the Azure Repos repository.\n\nYou can read more about how to configure Managed Identities in the\n[Azure Repos documentation][azdo-misp].\n\nOnce you have configured the Managed Identity, you can use it with GCM by simply\nsetting one of the following environment variables or Git configuration options:\n\n**Git configuration:** [`credential.azreposManagedIdentity`][gcm-mi-config]\n\n**Environment variable:** [`GCM_AZREPOS_MANAGEDIDENTITY`][gcm-mi-env]\n\nValue|Description\n-|-\n`system`|System-Assigned Managed Identity\n`[guid]`|User-Assigned Managed Identity with the specified client ID\n`id://[guid]` **|User-Assigned Managed Identity with the specified client ID\n`resource://[guid]` **|User-Assigned Managed Identity for the associated resource\n\nYou can obtain the `[guid]` from the Azure Portal or by using the Azure CLI\nto inspect the Managed Identity or resource.\n\n** Note there is an open issue that prevents successfull authentication when\nusing these formats: https://github.com/git-ecosystem/git-credential-manager/issues/1570\n\n## Service Principals\n\nAzure Service Principals are used to authenticate and authorize applications and\nservices to access Azure resources. Service Principals are similar in many ways\nto Managed Identities (in fact Service Principals are used under the hood to\nimplement Managed Identities), but they have expliclty defined credentials that\nare not managed by Azure.\n\nThere are a number of different ways to create and configure Service Principals,\nincluding using the Azure Portal or Azure CLI. You can read more about Service\nPrincipals in the [Azure documentation][az-sp].\n\n### How to configure Service Principals\n\nMuch like with Managed Identities, to use a Service Principal with GCM you first\nneed to ensure that the principal has the necessary permissions to access the\nAzure Repos repository.\n\nYou can read more about how to configure Service Principals in the\n[Azure Repos documentation][azdo-misp].\n\nOnce you have configured the Service Principal, you can use it with GCM by\nsetting one of the following environment variables or Git configuration options:\n\n**Git configuration:** [`credential.azreposServicePrincipal`][gcm-sp-config]\n\n**Environment variable:** [`GCM_AZREPOS_SERVICE_PRINCIPAL`][gcm-sp-env]\n\nThe format of the value for these options must be in the format:\n\n```text\n{tenantId}/{clientId}\n```\n\nWhere `{tenantId}` is the Azure tenant ID and `{clientId}` is the client ID of\nthe Service Principal. These values can be found in the Azure Portal or by using\nthe Azure CLI to inspect the Service Principal.\n\n#### Authentication with Service Principals\n\nWhen using a Service Principal with GCM, you will also need to provide the\nclient secret or certificate that is associated with the Service Principal.\n\nYou can provide the client secret or certificate to GCM by setting one of the\nfollowing environment variables or Git configuration options.\n\nType|Git Configuration|Environment Variable\n-|-|-\nClient Secret|[`credential.azreposServicePrincipalSecret`][gcm-sp-secret-config]|[`GCM_AZREPOS_SP_SECRET`][gcm-sp-secret-env]\nCertificate|[`credential.azreposServicePrincipalCertificateThumbprint`][gcm-sp-cert-config]|[`GCM_AZREPOS_SP_CERT_THUMBPRINT`][gcm-sp-cert-env]\nSend X5C|[`credential.azreposServicePrincipalCertificateSendX5C`][gcm-sp-cert-x5c-config]|[`GCM_AZREPOS_SP_CERT_SEND_X5C`][gcm-sp-cert-x5c-env]\n\nThe value for these options should be the client secret or the thumbrint of the\ncertificate that is associated with the Service Principal.\n\nThe certificate itself should be installed on the machine where GCM is running\nand should be installed in personal store the certificate store for either the\ncurrent user or the local machine.\n\n[az-mi]: https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview\n[az-sp]: https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser\n[azdo-misp]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops\n[gcm-mi-config]: https://gh.io/gcm/config#credentialazreposmanagedidentity\n[gcm-mi-env]: https://gh.io/gcm/env#GCM_AZREPOS_MANAGEDIDENTITY\n[gcm-sp-config]: https://gh.io/gcm/config#credentialazreposserviceprincipal\n[gcm-sp-env]: https://gh.io/gcm/env#GCM_AZREPOS_SERVICE_PRINCIPAL\n[gcm-sp-secret-config]: https://gh.io/gcm/config#credentialazreposserviceprincipalsecret\n[gcm-sp-secret-env]: https://gh.io/gcm/env#GCM_AZREPOS_SP_SECRET\n[gcm-sp-cert-config]: https://gh.io/gcm/config#credentialazreposserviceprincipalcertificatethumbprint\n[gcm-sp-cert-x5c-config]: https://gh.io/gcm/config#credentialazreposserviceprincipalcertificatesendx5c\n[gcm-sp-cert-env]: https://gh.io/gcm/env#GCM_AZREPOS_SP_CERT_THUMBPRINT\n[gcm-sp-cert-x5c-env]: https://gh.io/gcm/env#GCM_AZREPOS_SP_CERT_SEND_X5C\n"
  },
  {
    "path": "docs/azrepos-users-and-tokens.md",
    "content": "# Azure Repos: Access tokens and Accounts\n\n## Different credential types\n\nThe Azure Repos host provider supports creating multiple types of credential:\n\n- Azure DevOps personal access tokens\n- Microsoft identity OAuth tokens\n\nTo select which type of credential the Azure Repos host provider will create\nand use, you can set the [`credential.azreposCredentialType`][credential-azreposCredentialType]\nconfiguration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`][gcm-azrepos-credential-type]\nenvironment variable).\n\n### Azure DevOps personal access tokens\n\nHistorically, the only option supported by the Azure Repos host provider was\nAzure DevOps Personal Access Tokens (PATs).\n\nThese PATs are only used by Azure DevOps, and must be [managed through the Azure\nDevOps user settings page][azure-devops-pats] or [REST API][azure-devops-api].\n\nPATs have a limited lifetime and new tokens must be created once they expire. In\nGit Credential Manager, when a PAT expired (or was manually revoked) this\nresulted in a new authentication prompt.\n\n### Microsoft identity OAuth tokens\n\n\"Microsoft identity OAuth token\" is the generic term for OAuth-based access\ntokens issued by Azure Active Directory for either Work and School Accounts\n(AAD tokens) or Personal Accounts (Microsoft Account/MSA tokens).\n\nAzure DevOps supports Git authentication using Microsoft identity OAuth tokens\nas well as PATs. Microsoft identity OAuth tokens created by Git Credential\nManager are scoped to Azure DevOps only.\n\nUnlike PATs, Microsoft identity OAuth tokens get automatically refreshed and\nrenewed as long as you are actively using them to perform Git operations.\n\nThese tokens are also securely shared with other Microsoft developer tools\nincluding the Visual Studio IDE and Azure CLI. This means that as long as you're\nusing Git or one of these tools with the same account, you'll never need to\nre-authenticate due to expired tokens!\n\n#### User accounts\n\nIn versions of Git Credential Manager that support Microsoft identity OAuth\ntokens, the user account used to authenticate for a particular Azure DevOps\norganization will now be remembered.\n\nThe first time you clone, fetch or push from/to an Azure DevOps organization you\nwill be prompted to sign-in and select a user account. Git Credential Manager\nwill remember which account you used and continue to use that for all future\nremote Git operations (clone/fetch/push). An account is said to be \"bound\" to\nan Azure DevOps organization.\n\n---\n\n**Note:** If GCM is set to use PAT credentials, this account will **NOT** be\nused and you will continue to be prompted to select a user account to renew the\ncredential. This may change in the future.\n\n---\n\nNormally you won't need to worry about managing which user accounts Git\nCredential Manager is using as this is configured automatically when you first\nauthenticate for a particular Azure DevOps organization.\n\nIn advanced scenarios (such as using multiple accounts) you can interact with\nand manage remembered user accounts using the 'azure-repos' provider command:\n\n```shell\ngit-credential-manager azure-repos [ list | bind | unbind | ... ] <options>\n```\n\n##### Listing remembered accounts\n\nYou can list all bound user accounts by Git Credential Manager for each Azure\nDevOps organization using the `list` command:\n\n```shell\n$ git-credential-manager azure-repos list\ncontoso:\n  (global) -> alice@contoso.com\nfabrikam:\n  (global) -> user42@fabrikam.com\n```\n\nIn the above example, the `contoso` Azure DevOps organization is associated with\nthe `alice@contoso.com` user account, while the `fabrikam` organization is\nassociated to the `user42@fabrikam.com` user account.\n\nGlobal \"bindings\" apply to all remote Git operations for the current computer\nuser profile and are stored in `~/.gitconfig` or `%USERPROFILE%\\.gitconfig`.\n\n##### Using different accounts within a repository\n\nIf you generally use one account for an Azure DevOps organization, the default\nglobal bindings will be sufficient. However, if you wish to use a different\nuser account for an organization in a particular repository you can use a local\nbinding.\n\nLocal account bindings only apply within a single repository and are stored in\nthe `.git/config` file. If there are local bindings in a repository you can show\nthem with the `list` command:\n\n```shell\n~/myrepo$ git-credential-manager azure-repos list\ncontoso:\n  (global) -> alice@contoso.com\n  (local)  -> alice-alt@contoso.com\n```\n\nWithin the `~/myrepo` repository, the `alice-alt@contoso.com` account will be\nused by Git and GCM for the `contoso` Azure DevOps organization.\n\nTo create a local binding, use the `bind` command with the `--local` option when\ninside a repository:\n\n```shell\n~/myrepo$ git-credential-manager azure-repos bind --local contoso alice-alt@contso.com\n```\n\n```diff\n  contoso:\n    (global) -> alice@contoso.com\n+   (local)  -> alice-alt@contoso.com\n```\n\n##### Forget an account\n\nTo have Git Credential Manager forget a user account, use the `unbind` command:\n\n```shell\ngit-credential-manager azure-repos unbind fabrikam\n```\n\n```diff\n  contoso:\n    (global) -> alice@contoso.com\n- fabrikam:\n-   (global) -> user42@fabrikam.com\n```\n\nIn the above example, and global account binding for the `fabrikam` organization\nwill be forgotten. The next time you need to renew a PAT (if using PATs) or\nperform any remote Git operation (is using Azure tokens) you will be prompted\nto authenticate again.\n\nTo forget or remove a local binding, within the repository run the `unbind`\ncommand with the `--local` option:\n\n```shell\n~/myrepo$ git-credential-manager azure-repos unbind --local contoso\n```\n\n```diff\n  contoso:\n    (global) -> alice@contoso.com\n-   (local)  -> alice-alt@contoso.com\n```\n\n##### Using different accounts for specific Git remotes\n\nAs well as global and local user account bindings, you can instruct Git\nCredential Manager to use a specific user account for an individual Git remotes\nwithin the same local repository.\n\nTo show which accounts are being used for each Git remote in a repository use\nthe `list` command with the `--show-remotes` option:\n\n```shell\n~/myrepo$ git-credential-manager azure-repos list --show-remotes\ncontoso:\n  (global) -> alice@contoso.com\n  origin:\n    (fetch) -> (inherit)\n    (push)  -> (inherit)\nfabrikam:\n  (global) -> alice@fabrikam.com\n```\n\nIn the above example, the `~/myrepo` repository has a single Git remote named\n`origin` that points to the `contoso` Azure DevOps organization. There is no\nuser account specifically associated with the `origin` remote, so the global\nuser account binding for `contoso` will be used (the global binding is\ninherited).\n\nTo associate a user account with a particular Git remote you must manually edit\nthe remote URL using `git config` commands to include the username in the\n[user information][rfc3986-s321] part of the URL.\n\n```shell\ngit config --local remote.origin.url https://alice-alt%40contoso.com@contoso.visualstudio.com/project/_git/repo\n```\n\nIn the above example the `alice-alt@contoso.com` account is being set as the\naccount to use for the `origin` Git remote.\n\n---\n\n**Note:** All special characters must be URL encoded/escaped, for example `@`\nbecomes `%40`.\n\n---\n\nThe `list --show-remotes` command will show the user account specified in the\nremote URL:\n\n```shell\n~/myrepo$ git-credential-manager azure-repos list --show-remotes\ncontoso:\n  (global) -> alice@contoso.com\n  origin:\n    (fetch) -> alice-alt@contoso.com\n    (push)  -> alice-alt@contoso.com\nfabrikam:\n  (global) -> alice@fabrikam.com\n```\n\n[azure-devops-pats]: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page\n[credential-azreposCredentialType]: configuration.md#credentialazreposcredentialtype\n[gcm-azrepos-credential-type]: environment.md#GCM_AZREPOS_CREDENTIALTYPE\n[azure-devops-api]: https://docs.microsoft.com/en-gb/rest/api/azure/devops/tokens/pats\n[rfc3986-s321]: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1\n"
  },
  {
    "path": "docs/bitbucket-authentication.md",
    "content": "# Bitbucket Authentication\n\nWhen GCM is triggered by Git, it will check the `host` parameter passed\nto it. If this parameter contains `bitbucket.org` it will trigger Bitbucket\nauthentication and prompt you for credentials. In this scenario, you have two\noptions for authentication: `OAuth` or `Password/Token`.\n\n### OAuth\n\nThe dialog GCM presents for authentication contains two tabs. The first tab\n(labeled `Browser`) will trigger OAuth Authentication. Clicking the `Sign in\nwith your browser` button opens a browser request to\n`_https://bitbucket.org/site/oauth2/authorize?response_type=code&client_id={consumerkey}&state=authenticated&scope={scopes}&redirect_uri=http://localhost:34106/_`. This triggers a flow on Bitbucket requiring you to log in\n(and potentially complete 2FA) to authorize GCM to access Bitbucket with the\nspecified scopes. GCM will then spawn a temporary local webserver, listening on\nport 34106, to handle the OAuth redirect/callback. Assuming you successfully\nlog into Bitbucket and authorize GCM, this callback will include the appropriate\ntokens for GCM to handle authencation. These tokens are then stored in your\nconfigured [credential store][credstores] and are returned to Git.\n\n### Password/Token\n\n**Note:** Bitbucket Data Center, also known as Bitbucket Server or Bitbucket On\nPremises, only supports Basic Authentication - please follow the below\ninstructions if you are using this product.\n\nThe dialog GCM presents for authentication contains two tabs. The second tab\n(labeled `Password/Token`) will trigger Basic Authentication. This tab contains\ntwo fields, one for your username and one for your password or token. If the\n`username` parameter was passed into GCM, that will pre-populate the username\nfield, although it can be overridden. Enter your username (if needed) and your\npassword or token (i.e. Bitbucket App Password) and click `Sign in`.\n\n:rotating_light: Requirements for App Passwords :rotating_light:\n\nIf you are planning to use an [App Password][app-password] for basic\nauthentication, it must at a minimum have _Account Read_ permissions (as shown\nbelow). If your App Password does not have these permissions, you will be\nre-prompted for credentials on every interaction with the server.\n\n![][app-password-example]\n\nWhen your username and password are submitted, GCM will attempt to retrieve a\nbasic authentication token for these credentials via the Bitbucket REST API. If\nthis is successful, the credentials, username, and password/token are stored in\nyour configured [credential store][credstores] and are returned to Git.\n\nIf the API request fails with a 401 return code, the entered username/password\ncombination is invalid; nothing is stored and nothing is returned to Git. In\nthis scenario, re-attempt authentication, ensuring your credentials are correct.\n\nIf the API request fails with a 403 (Forbidden) return code, the username and\npassword are valid, but 2FA is enabled on the corresponding Bitbucket Account.\nIn this scenario, you will be prompted to complete the OAuth authentication\nprocess.  If this is successful, the credentials, username, and password/token\nare stored in your configured [credential store][credstores] and are returned to\nGit.\n\n[app-password]: https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/\n[app-password-example]: img/app-password.png\n[credstores]: ./credstores.md\n"
  },
  {
    "path": "docs/bitbucket-development.md",
    "content": "# Bitbucket Authentication, 2FA and OAuth\n\nBy default for authenticating against private Git repositories Bitbucket\nsupports SSH and username/password Basic Auth over HTTPS. Username/password\nBasic Auth over HTTPS is also available for REST API access. Additionally\nBitbucket supports App-specific passwords which can be used via Basic Auth as\nusername/app-specific-password.\n\nTo enhance security Bitbucket offers optional Two-Factor Authentication (2FA).\nWhen 2FA is enabled username/password Basic Auth access to the REST APIs and to\nGit repositories is suspended. At that point users are left with the choice of\nusername/apps-specific-password Basic Auth for REST APIs and Git interactions,\nOAuth for REST APIs and Git/Hg interactions or SSH for Git/HG interactions and\none of the previous choices for REST APIs. SSH and REST API access are beyond\nthe scope of this document. Read about [Bitbucket's 2FA implementation][2fa-impl].\n\nApp-specific passwords are not particularly user friendly as once created\nBitbucket hides their value, even from the owner. They are intended for use\nwithin application that talk to Bitbucket where application can remember and use\nthe app-specific-password. [Additional information][additional-info].\n\nOAuth is the intended authentication method for user interactions with HTTPS\nremote URL for Git repositories when 2FA is active. Essentially once a client\napplication has an OAuth access token it can be used in place of a user's\npassword. Read more about information [Bitbucket's OAuth implementation][oauth-impl].\n\nBitbucket's OAuth implementation follows the standard specifications for OAuth\n2.0, which is out of scope for this document. However it implements a\ncomparatively rare part of OAuth 2.0 Refresh Tokens. Bitbucket's Access Token's\nexpire after 1 hour if not revoked, as opposed to GitHub's that expire after 1\nyear. When GitHub's Access Tokens expire the user must anticipate in the\nstandard OAuth authentication flow to get a new Access Token. Since this occurs,\nin theory, once per year this is not too onerous. Since Bitbucket's Access\nTokens expire every hour it is too much to expect a user to go through the OAuth\nauthentication flow every hour.Bitbucket implements refresh Tokens.\nRefresh Tokens are issued to the client application at the same time as Access\nTokens. They can only be used to request a new Access Token, and then only if\nthey have not been revoked. As such the support for Bitbucket and the use of its\nOAuth in the Git Credentials Manager differs significantly from how VSTS and\nGitHub are implemented. This is explained in more detail below.\n\n## Multiple User Accounts\n\nUnlike the GitHub implementation within the Git Credential Manager, the\nBitbucket implementation stores 'secrets', passwords, app-specific passwords, or\nOAuth tokens, with usernames in the [Windows Credential Manager][wincred-manager]\nvault.\n\nDepending on the circumstances this means either saving an explicit username in\nto the Windows Credential Manager/Vault or including the username in the URL\nused as the identifying key of entries in the Windows Credential Manager vault,\ni.e. using a key such as `git:https://mminns@bitbucket.org/` rather than\n`git:https://bitbucket.org`. This means that the Bitbucket implementation in the\nGCM can support multiple accounts, and usernames,  for a single user against\nBitbucket, e.g. a personal account and a work account.\n\n## On-Premise Bitbucket\n\nOn-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC,\nhas a number of differences compared to the cloud instance of Bitbucket,\n[bitbucket.org][bitbucket].\n\nIt is possible to test with Bitbucket Server by running it locally using the\nfollowing command from the Atlassian SDK:\n\n    ❯ atlas-run-standalone --product bitbucket\n\nSee the developer documentation for [atlas-run-standalone][atlas-run-standalone].\n\nThis will download and run a standalone instance of Bitbucket Server which can\nbe accessed using the credentials `admin`/`admin` at\n\n    https://localhost:7990/bitbucket\n\nAtlassian has [documentation][atlassian-sdk] on how to download and install\ntheir SDK.\n\n## OAuth2 Configuration\n\nBitbucket DC [7.20](https://confluence.atlassian.com/bitbucketserver/bitbucket-data-center-and-server-7-20-release-notes-1101934428.html)\nadded support for OAuth2 Incoming Application Links and this can be used to\nsupport OAuth2 authentication for Git. This is especially useful in environments\nwhere Bitbucket uses SSO as it removes the requirement for users to manage\n[SSH keys](https://confluence.atlassian.com/display/BITBUCKETSERVER0717/Using+SSH+keys+to+secure+Git+operations)\nor manual [HTTP access tokens](https://confluence.atlassian.com/display/BITBUCKETSERVER0717/Personal+access+tokens).\n\n### Host Configuration\n\nFor more details see\n[Bitbucket's documentation on Data Center and Server Application Links to other Applications](https://confluence.atlassian.com/bitbucketserver/link-to-other-applications-1018764620.html)\n\nCreate Incoming OAuth 2 Application Link:\n<!-- markdownlint-disable MD034 -->\n1. Navigate to Administration/Application Links\n1. Create Link\n   1. Screen 1\n      - External Application [check]\n      - Incoming Application [check]\n   1. Screen 2\n      - Name : GCM\n      - Redirect URL : `http://localhost:34106/`\n      - Application Permissions : Repositories.Read [check], Repositories.Write [check]\n   1. Save\n   <!-- markdownlint-enable MD034 -->\n   1. Copy the `ClientId` and `ClientSecret` to configure GCM\n\n### Client Configuration\n\nSet the OAuth2 configuration use the `ClientId` and `ClientSecret` copied above,\n(for details see [credential.bitbucketDataCenterOAuthClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId)\nand [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credential.bitbucketDataCenterOAuthClientSecret))\n\n    ❯ git config --global credential.bitbucketDataCenterOAuthClientId {`Copied ClientId`}\n\n    ❯ git config --global credential.bitbucketDataCenterOAuthClientSecret {`Copied ClientSecret`}\n<!-- markdownlint-disable MD034 -->\nAs described in [Configuration options](configuration.md#Configuration%20options)\nthe settings can be made more specific to apply only to a specific Bitbucket DC\nhost by specifying the host url, e.g. https://bitbucket.example.com/\n<!-- markdownlint-enable MD034 -->\n\n    ❯ git config --global credential.https://bitbucket.example.com.bitbucketDataCenterOAuthClientId {`Copied ClientId`}\n\n    ❯ git config --global credential.https://bitbucket.example.com.bitbucketDataCenterOAuthClientSecret {`Copied ClientSecret`}\n<!-- markdownlint-disable MD034 -->\nDue to the way GCM resolves hosts and determines REST API urls, if the Bitbucket\nDC instance is hosted under a relative url (e.g. https://example.com/bitbucket)\nit is necessary to configure Git to send the full path to GCM. This is done\nusing the [credential.useHttpPath](configuration.md#credential.useHttpPath)\nsetting.\n    ❯ git config --global credential.https://example.com/bitbucket.usehttppath true\n<!-- markdownlint-enable MD034 -->\n\nIf a port number is used in the url of the Bitbucket DC instance the Git\nconfiguration needs to reflect this. However, due to [Issue 608](https://github.com/git-ecosystem/git-credential-manager/issues/608)\nthe port is ignored when resolving [credential.bitbucketDataCenterOAuthClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId)\nand [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credential.bitbucketDataCenterOAuthClientSecret).\n<!-- markdownlint-disable MD034 -->\nFor example, a Bitbucket DC host at https://example.com:7990/bitbucket would\nrequire configuration in the form:\n<!-- markdownlint-enable MD034 -->\n    ❯ git config --global credential.https://example.com/bitbucket.bitbucketDataCenterOAuthClientId {`Copied ClientId`}\n\n    ❯ git config --global credential.https://example.com/bitbucket.bitbucketDataCenterOAuthClientSecret {`Copied ClientSecret`}\n\n    ❯ git config --global credential.https://example.com:7990/bitbucket.usehttppath true\n\n[additional-info]:https://confluence.atlassian.com/display/BITBUCKET/App+passwords\n[atlas-run-standalone]: https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/\n[bitbucket]: https://bitbucket.org\n[2fa-impl]: https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html\n[oauth-impl]: https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html\n[atlassian-sdk]: https://developer.atlassian.com/server/framework/atlassian-sdk/\n[wincred-manager]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa374792(v=vs.85).aspx\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "# Configuration options\n\n[Git Credential Manager][usage] works out of the box for most users.\n\nGit Credential Manager (GCM) can be configured using Git's configuration files,\nand follows all of the same rules Git does when consuming the files.\n\nGlobal configuration settings override system configuration settings, and local\nconfiguration settings override global settings; and because the configuration\ndetails exist within Git's configuration files you can use Git's `git config`\nutility to set, unset, and alter the setting values. All of GCM's configuration\nsettings begin with the term `credential`.\n\nGCM honors several levels of settings, in addition to the standard local\n\\> global \\> system tiering Git uses. URL-specific settings or overrides can be\napplied to any value in the `credential` namespace with the syntax below.\n\nAdditionally, GCM respects several GCM-specific [environment variables][envars]\n**which take precedence over configuration options**. System administrators may\nalso configure [default values][enterprise-config] for many settings used by GCM.\n\nGCM will only be used by Git if it is installed and configured. Use\n`git config --global credential.helper manager` to assign GCM as your\ncredential helper. Use `git config credential.helper` to see the current\nconfiguration.\n\n**Example:**\n\n> `credential.microsoft.visualstudio.com.namespace` is more specific than\n> `credential.visualstudio.com.namespace`, which is more specific than\n> `credential.namespace`.\n\nIn the examples above, the `credential.namespace` setting would affect any\nremote repository; the `credential.visualstudio.com.namespace` would affect any\nremote repository in the domain, and/or any subdomain (including `www.`) of,\n'visualstudio.com'; where as the\n`credential.microsoft.visualstudio.com.namespace` setting would only be applied\nto remote repositories hosted at 'microsoft.visualstudio.com'.\n\nFor the complete list of settings GCM understands, see the list below.\n\n## Available settings\n\n### credential.interactive\n\nPermit or disable GCM from interacting with the user (showing GUI or TTY\nprompts). If interaction is required but has been disabled, an error is returned.\n\nThis can be helpful when using GCM in headless and unattended environments, such\nas build servers, where it would be preferable to fail than to hang indefinitely\nwaiting for a non-existent user.\n\nTo disable interactivity set this to `false` or `0`.\n\n#### Compatibility\n\nIn previous versions of GCM this setting had a different behavior and accepted\nother values. The following table summarizes the change in behavior and the\nmapping of older values such as `never`:\n\nValue(s)|Old meaning|New meaning\n-|-|-\n`auto`|Prompt if required – use cached credentials if possible|_(unchanged)_\n`never`, `false`| Never prompt – fail if interaction is required|_(unchanged)_\n`always`, `force`, `true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)\n\n#### Example\n\n```shell\ngit config --global credential.interactive false\n```\n\nDefaults to enabled.\n\n**Also see: [GCM_INTERACTIVE][gcm-interactive]**\n\n---\n\n### credential.trace\n\nEnables trace logging of all activities.\nConfiguring Git and GCM to trace to the same location is often desirable, and\nGCM is compatible and cooperative with `GIT_TRACE`.\n\n#### Example\n\n```shell\ngit config --global credential.trace /tmp/git.log\n```\n\nIf the value of `credential.trace` is a full path to a file in an existing\ndirectory, logs are appended to the file.\n\nIf the value of `credential.trace` is `true` or `1`, logs are written to\nstandard error.\n\nDefaults to disabled.\n\n**Also see: [GCM_TRACE][gcm-trace]**\n\n---\n\n### credential.traceSecrets\n\nEnables tracing of secret and sensitive information, which is by default masked\nin trace output. Requires that `credential.trace` is also enabled.\n\n#### Example\n\n```shell\ngit config --global credential.traceSecrets true\n```\n\nIf the value of `credential.traceSecrets` is `true` or `1`, trace logs will include\nsecret information.\n\nDefaults to disabled.\n\n**Also see: [GCM_TRACE_SECRETS][gcm-trace-secrets]**\n\n---\n\n### credential.traceMsAuth\n\nEnables inclusion of Microsoft Authentication library (MSAL) logs in GCM trace\noutput. Requires that `credential.trace` is also enabled.\n\n#### Example\n\n```shell\ngit config --global credential.traceMsAuth true\n```\n\nIf the value of `credential.traceMsAuth` is `true` or `1`, trace logs will\ninclude verbose MSAL logs.\n\nDefaults to disabled.\n\n**Also see: [GCM_TRACE_MSAUTH][gcm-trace-msauth]**\n\n---\n\n### credential.debug\n\nPauses execution of GCM at launch to wait for a debugger to be attached.\n\n#### Example\n\n```shell\ngit config --global credential.debug true\n```\n\nDefaults to disabled.\n\n**Also see: [GCM_DEBUG][gcm-debug]**\n\n---\n\n### credential.provider\n\nDefine the host provider to use when authenticating.\n\nID|Provider\n-|-\n`auto` _(default)_|_\\[automatic\\]_ ([learn more][autodetect])\n`azure-repos`|Azure Repos\n`github`|GitHub\n`bitbucket`|Bitbucket\n`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_\n`generic`|Generic (any other provider not listed above)\n\nAutomatic provider selection is based on the remote URL.\n\nThis setting is typically used with a scoped URL to map a particular set of\nremote URLs to providers, for example to mark a host as a GitHub Enterprise\ninstance.\n\n#### Example\n\n```shell\ngit config --global credential.ghe.contoso.com.provider github\n```\n\n**Also see: [GCM_PROVIDER][gcm-provider]**\n\n---\n\n### credential.authority _(deprecated)_\n\n> This setting is deprecated and should be replaced by `credential.provider`\n> with the corresponding provider ID value.\n>\n> See the [migration guide][provider-migrate] for more information.\n\nSelect the host provider to use when authenticating by which authority is\nsupported by the providers.\n\nAuthority|Provider(s)\n-|-\n`auto` _(default)_|_\\[automatic\\]_\n`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|Azure Repos _(supports Microsoft Authentication)_\n`github`|GitHub _(supports GitHub Authentication)_\n`bitbucket`|Bitbucket.org _(supports Basic Authentication and OAuth)_, Bitbucket Server _(supports Basic Authentication)_\n`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_\n`basic`, `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|Generic _(supports Basic and Windows Integrated Authentication)_\n\n#### Example\n\n```shell\ngit config --global credential.ghe.contoso.com.authority github\n```\n\n**Also see: [GCM_AUTHORITY][gcm-authority]**\n\n---\n\n### credential.guiPrompt\n\nPermit or disable GCM from presenting GUI prompts. If an equivalent terminal/\ntext-based prompt is available, that will be shown instead.\n\nTo disable all interactivity see [credential.interactive][credential-interactive].\n\n#### Example\n\n```shell\ngit config --global credential.guiPrompt false\n```\n\nDefaults to enabled.\n\n**Also see: [GCM_GUI_PROMPT][gcm-gui-prompt]**\n\n---\n\n### credential.guiSoftwareRendering\n\nForce the use of software rendering for GUI prompts.\n\nThis is currently only applicable on Windows.\n\n#### Example\n\n```shell\ngit config --global credential.guiSoftwareRendering true\n```\n\nDefaults to false (use hardware acceleration where available).\n\n> [!NOTE]\n> Windows on ARM devices defaults to using software rendering to work around a\n> known Avalonia issue: <https://github.com/AvaloniaUI/Avalonia/issues/10405>\n\n**Also see: [GCM_GUI_SOFTWARE_RENDERING][gcm-gui-software-rendering]**\n\n---\n\n### credential.allowUnsafeRemotes\n\nAllow transmitting credentials to unsafe remote URLs such as unencrypted HTTP\nURLs. This setting is not recommended for general use and should only be used\nwhen necessary.\n\nDefaults false (disallow unsafe remote URLs).\n\n#### Example\n\n```shell\ngit config --global credential.allowUnsafeRemotes true\n```\n\n**Also see: [GCM_ALLOW_UNSAFE_REMOTES][gcm-allow-unsafe-remotes]**\n\n---\n\n### credential.autoDetectTimeout\n\nSet the maximum length of time, in milliseconds, that GCM should wait for a\nnetwork response during host provider auto-detection probing.\n\nSee [auto-detection][auto-detection] for more information.\n\n**Note:** Use a negative or zero value to disable probing altogether.\n\nDefaults to 2000 milliseconds (2 seconds).\n\n#### Example\n\n```shell\ngit config --global credential.autoDetectTimeout -1\n```\n\n**Also see: [GCM_AUTODETECT_TIMEOUT][gcm-autodetect-timeout]**\n\n---\n\n### credential.allowWindowsAuth\n\nAllow detection of Windows Integrated Authentication (WIA) support for generic\nhost providers. Setting this value to `false` will prevent the use of WIA and\nforce a basic authentication prompt when using the Generic host provider.\n\n**Note:** WIA is only supported on Windows.\n\n**Note:** WIA is an umbrella term for NTLM and Kerberos (and Negotiate).\n\nValue|WIA detection\n-|-\n`true` _(default)_|Permitted\n`false`|Not permitted\n\n#### Example\n\n```shell\ngit config --global credential.tfsonprem123.allowWindowsAuth false\n```\n\n**Also see: [GCM_ALLOW_WINDOWSAUTH][gcm-allow-windowsauth]**\n\n---\n\n### credential.httpProxy _(deprecated)_\n\n> This setting is deprecated and should be replaced by the\n> [standard `http.proxy` Git configuration option][git-config-http-proxy].\n>\n> See [HTTP Proxy][http-proxy] for more information.\n\nConfigure GCM to use the a proxy for network operations.\n\n**Note:** Git itself does _not_ respect this setting; this affects GCM _only_.\n\n#### Example\n\n```shell\ngit config --global credential.httpsProxy http://john.doe:password@proxy.contoso.com\n```\n\n**Also see: [GCM_HTTP_PROXY][gcm-http-proxy]**\n\n---\n\n### credential.bitbucketAuthModes\n\nOverride the available authentication modes presented during Bitbucket\nauthentication. If this option is not set, then the available authentication\nmodes will be automatically detected.\n\n**Note:** This setting only applies to Bitbucket.org, and not Server or DC\ninstances.\n\n**Note:** This setting supports multiple values separated by commas.\n\nValue|Authentication Mode\n-|-\n_(unset)_|Automatically detect modes\n`oauth`|OAuth-based authentication\n`basic`|Basic/PAT-based authentication\n\n#### Example\n\n```shell\ngit config --global credential.bitbucketAuthModes \"oauth,basic\"\n```\n\n**Also see: [GCM_BITBUCKET_AUTHMODES][gcm-bitbucket-authmodes]**\n\n---\n\n### credential.bitbucketAlwaysRefreshCredentials\n\nForces GCM to ignore any existing stored Basic Auth or OAuth access tokens and\nalways run through the process to refresh the credentials before returning them\nto Git.\n\nThis is especially relevant to OAuth credentials. Bitbucket.org access tokens\nexpire after 2 hours, after that the refresh token must be used to get a new\naccess token.\n\nEnabling this option will improve performance when using Oauth2 and interacting\nwith Bitbucket.org if, on average, commits are done less frequently than every\n2 hours.\n\nEnabling this option will decrease performance when using Basic Auth by\nrequiring the user the re-enter credentials every time.\n\nValue|Refresh Credentials Before Returning\n-|-\n`true`, `1`, `yes`, `on` |Always\n`false`, `0`, `no`, `off`_(default)_|Only when the credentials are found to be invalid\n\n#### Example\n\n```shell\ngit config --global credential.bitbucketAlwaysRefreshCredentials true\n```\n\nDefaults to false/disabled.\n\n**Also see: [GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS][gcm-bitbucket-always-refresh-credentials]**\n\n---\n\n### credential.bitbucketValidateStoredCredentials\n\nForces GCM to validate any stored credentials before returning them to Git. It\ndoes this by calling a REST API resource that requires authentication.\n\nDisabling this option reduces the HTTP traffic within GCM when it is retrieving\ncredentials. This may improve user performance, but will increase the number of\ntimes Git remote calls fail to authenticate with the host and therefore require\nthe user to re-try the Git remote call.\n\nEnabling this option helps ensure Git is always provided with valid credentials.\n\nValue|Validate credentials\n-|-\n`true`, `1`, `yes`, `on`_(default)_|Always\n`false`, `0`, `no`, `off`|Never\n\n#### Example\n\n```shell\ngit config --global credential.bitbucketValidateStoredCredentials true\n```\n\nDefaults to true/enabled.\n\n**Also see: [GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS](environment.md#GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS)**\n\n---\n\n### credential.bitbucketDataCenterOAuthClientId\n\nTo use OAuth with Bitbucket DC it is necessary to create an external, incoming\n[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html).\n\nIt is then necessary to configure the local GCM installation with both the OAuth\n[ClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId) and\n[ClientSecret](configuration.md#credential.bitbucketDataCenterOauthSecret) from\nthe AppLink.\n\n#### Example\n\n```shell\ngit config --global credential.bitbucketDataCenterOAuthClientId 1111111111111111111\n```\n\nDefaults to undefined.\n\n**Also see: [GCM_BITBUCKET_DATACENTER_CLIENTID](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTID)**\n\n---\n\n### credential.bitbucketDataCenterOAuthClientSecret\n\nTo use OAuth with Bitbucket DC it is necessary to create an external, incoming\n[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html).\n\nIt is then necessary to configure the local GCM installation with both the OAuth\n[ClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId) and\n[ClientSecret](configuration.md#credential.bitbucketDataCenterOauthSecret)\nfrom the AppLink.\n\n#### Example\n\n```shell\ngit config --global credential.bitbucketDataCenterOAuthClientSecret 222222222222222222222\n```\n\nDefaults to undefined.\n\n**Also see: [GCM_BITBUCKET_DATACENTER_CLIENTSECRET](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTSECRET)**\n\n---\n\n### credential.gitHubAccountFiltering\n\nEnable or disable automatic account filtering for GitHub based on server hints\nwhen there are multiple available accounts. This setting is only applicable to\nGitHub.com with [Enterprise Managed Users][github-emu].\n\nValue|Description\n-|-\n`true` _(default)_|Filter available accounts based on server hints.\n`false`|Show all available accounts.\n\n#### Example\n\n```shell\ngit config --global credential.gitHubAccountFiltering \"false\"\n```\n\n**Also see: [GCM_GITHUB_ACCOUNTFILTERING][gcm-github-accountfiltering]**\n\n---\n\n### credential.gitHubAuthModes\n\nOverride the available authentication modes presented during GitHub\nauthentication. If this option is not set, then the available authentication\nmodes will be automatically detected.\n\n**Note:** This setting supports multiple values separated by commas.\n\nValue|Authentication Mode\n-|-\n_(unset)_|Automatically detect modes\n`oauth`|Expands to: `browser, device`\n`browser`|OAuth authentication via a web browser _(requires a GUI)_\n`device`|OAuth authentication with a device code\n`basic`|Basic authentication using username and password\n`pat`|Personal Access Token (pat)-based authentication\n\n#### Example\n\n```shell\ngit config --global credential.gitHubAuthModes \"oauth,basic\"\n```\n\n**Also see: [GCM_GITHUB_AUTHMODES][gcm-github-authmodes]**\n\n---\n\n### credential.gitLabAuthModes\n\nOverride the available authentication modes presented during GitLab\nauthentication. If this option is not set, then the available authentication\nmodes will be automatically detected.\n\n**Note:** This setting supports multiple values separated by commas.\n\nValue|Authentication Mode\n-|-\n_(unset)_|Automatically detect modes\n`browser`|OAuth authentication via a web browser _(requires a GUI)_\n`basic`|Basic authentication using username and password\n`pat`|Personal Access Token (pat)-based authentication\n\n#### Example\n\n```shell\ngit config --global credential.gitLabAuthModes \"browser\"\n```\n\n**Also see: [GCM_GITLAB_AUTHMODES][gcm-gitlab-authmodes]**\n\n---\n\n### credential.namespace\n\nUse a custom namespace prefix for credentials read and written in the OS\ncredential store. Credentials will be stored in the format\n`{namespace}:{service}`.\n\nDefaults to the value `git`.\n\n#### Example\n\n```shell\ngit config --global credential.namespace \"my-namespace\"\n```\n\n**Also see: [GCM_NAMESPACE][gcm-namespace]**\n\n---\n\n### credential.credentialStore\n\nSelect the type of credential store to use on supported platforms.\n\nDefault value on Windows is `wincredman`, on macOS is `keychain`, and is unset\non Linux.\n\n**Note:** See more information about configuring secret stores in\n[cred-stores][cred-stores].\n\nValue|Credential Store|Platforms\n-|-|-\n_(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|-\n`wincredman`|Windows Credential Manager (not available over SSH).|Windows\n`dpapi`|DPAPI protected files. Customize the DPAPI store location with [credential.dpapiStorePath][credential-dpapistorepath]|Windows\n`keychain`|macOS Keychain.|macOS\n`secretservice`|[freedesktop.org Secret Service API][freedesktop-ss] via [libsecret][libsecret] (requires a graphical interface to unlock secret collections).|Linux\n`gpg`|Use GPG to store encrypted files that are compatible with the [pass][pass] (requires GPG and `pass` to initialize the store).|macOS, Linux\n`cache`|Git's built-in [credential cache][credential-cache].|macOS, Linux\n`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`][credential-plaintextstorepath].|Windows, macOS, Linux\n`none`|Do not store credentials via GCM.|Windows, macOS, Linux\n\n#### Example\n\n```bash\ngit config --global credential.credentialStore gpg\n```\n\n**Also see: [GCM_CREDENTIAL_STORE][gcm-credential-store]**\n\n---\n\n### credential.cacheOptions\n\nPass [options][cache-options] to the Git credential cache when\n[`credential.credentialStore`][credential-credentialstore]\nis set to `cache`. This allows you to select a different amount\nof time to cache credentials (the default is 900 seconds) by passing\n`\"--timeout <seconds>\"`. Use of other options like `--socket` is untested\nand unsupported, but there's no reason it shouldn't work.\n\nDefaults to empty.\n\n#### Example\n\n```shell\ngit config --global credential.cacheOptions \"--timeout 300\"\n```\n\n**Also see: [GCM_CREDENTIAL_CACHE_OPTIONS][gcm-credential-cache-options]**\n\n---\n\n### credential.plaintextStorePath\n\nSpecify a custom directory to store plaintext credential files in when\n[`credential.credentialStore`][credential-credentialstore] is set to `plaintext`.\n\nDefaults to the value `~/.gcm/store` or `%USERPROFILE%\\.gcm\\store`.\n\n#### Example\n\n```shell\ngit config --global credential.plaintextStorePath /mnt/external-drive/credentials\n```\n\n**Also see: [GCM_PLAINTEXT_STORE_PATH][gcm-plaintext-store-path]**\n\n---\n\n### credential.dpapiStorePath\n\nSpecify a custom directory to store DPAPI protected credential files in when\n[`credential.credentialStore`][credential-credentialstore] is set to `dpapi`.\n\nDefaults to the value `%USERPROFILE%\\.gcm\\dpapi_store`.\n\n#### Example\n\n```batch\ngit config --global credential.dpapiStorePath D:\\credentials\n```\n\n**Also see: [GCM_DPAPI_STORE_PATH][gcm-dpapi-store-path]**\n\n---\n\n### credential.gpgPassStorePath\n\nSpecify a custom directory to store GPG-encrypted [pass][pass]-compatible credential files\nin when [`credential.credentialStore`][credential-credentialstore] is set to `gpg`.\n\nDefaults to the value `~/.password-store` or `%USERPROFILE%\\.password-store`.\n\n#### Example\n\n```shell\ngit config --global credential.gpgPassStorePath /mnt/external-drive/.password-store\n```\n\n**Note:** Location of the password store used by [pass][pass] can be overridden by the\n`PASSWORD_STORE_DIR` environment variable, see the [man page][pass-man] for details.\n\n---\n\n### credential.msauthFlow\n\nSpecify which authentication flow should be used when performing Microsoft\nauthentication and an interactive flow is required.\n\nDefaults to `auto`.\n\n**Note:** If [`credential.msauthUseBroker`][credential-msauthusebroker] is set\nto `true` and the operating system authentication broker is available, all flows\nwill be delegated to the broker. If both of those things are true, then the\nvalue of `credential.msauthFlow` has no effect.\n\nValue|Authentication Flow\n-|-\n`auto` _(default)_|Select the best option depending on the current environment and platform.\n`embedded`|Show a window with embedded web view control.\n`system`|Open the user's default web browser.\n`devicecode`|Show a device code.\n\n#### Example\n\n```shell\ngit config --global credential.msauthFlow devicecode\n```\n\n**Also see: [GCM_MSAUTH_FLOW][gcm-msauth-flow]**\n\n---\n\n### credential.msauthUseBroker _(experimental)_\n\nUse the operating system account manager where available.\n\nDefaults to `false`. In certain cloud hosted environments when using a work or\nschool account, such as [Microsoft DevBox][devbox], the default is `true`.\n\nThese defaults are subject to change in the future.\n\n_**Note:** before you enable this option on Windows, please review the\n[Windows Broker][wam] details for what this means to your local Windows user\naccount._\n\nValue|Description\n-|-\n`true`|Use the operating system account manager as an authentication broker.\n`false` _(default)_|Do not use the broker.\n\n#### Example\n\n```shell\ngit config --global credential.msauthUseBroker true\n```\n\n**Also see: [GCM_MSAUTH_USEBROKER][gcm-msauth-usebroker]**\n\n---\n\n### credential.msauthUseDefaultAccount _(experimental)_\n\nUse the current operating system account by default when the broker is enabled.\n\nDefaults to `false`. In certain cloud hosted environments when using a work or\nschool account, such as [Microsoft DevBox][devbox], the default is `true`.\n\nThese defaults are subject to change in the future.\n\nValue|Description\n-|-\n`true`|Use the current operating system account by default.\n`false` _(default)_|Do not assume any account to use by default.\n\n#### Example\n\n```shell\ngit config --global credential.msauthUseDefaultAccount true\n```\n\n**Also see: [GCM_MSAUTH_USEDEFAULTACCOUNT][gcm-msauth-usedefaultaccount]**\n\n---\n\n### credential.useHttpPath\n\nTells Git to pass the entire repository URL, rather than just the hostname, when\ncalling out to a credential provider. (This setting\n[comes from Git itself][use-http-path], not GCM.)\n\nDefaults to `false`.\n\n**Note:** GCM sets this value to `true` for `dev.azure.com` (Azure Repos) hosts\nafter installation by default.\n\nThis is because `dev.azure.com` alone is not enough information to determine the\ncorrect Azure authentication authority - we require a part of the path. The\nfallout of this is that for `dev.azure.com` remote URLs we do not support\nstoring credentials against the full-path. We always store against the\n`dev.azure.com/org-name` stub.\n\nIn order to use Azure Repos and store credentials against a full-path URL, you\nmust use the `org-name.visualstudio.com` remote URL format instead.\n\nValue|Git Behavior\n-|-\n`false` _(default)_|Git will use only `user` and `hostname` to look up credentials.\n`true`|Git will use the full repository URL to look up credentials.\n\n#### Example\n\nOn Windows using GitHub, for a user whose login is `alice`, and with\n`credential.useHttpPath` set to `false` (or not set), the following remote URLs\nwill use the same credentials:\n\n```text\nCredential: \"git:https://github.com\" (user = alice)\n\n   https://github.com/foo/bar\n   https://github.com/contoso/widgets\n   https://alice@github.com/contoso/widgets\n```\n\n```text\nCredential: \"git:https://bob@github.com\" (user = bob)\n\n   https://bob@github.com/foo/bar\n   https://bob@github.com/example/myrepo\n```\n\nUnder the same user but with `credential.useHttpPath` set to `true`, these\ncredentials would be used:\n\n```text\nCredential: \"git:https://github.com/foo/bar\" (user = alice)\n\n   https://github.com/foo/bar\n```\n\n```text\nCredential: \"git:https://github.com/contoso/widgets\" (user = alice)\n\n   https://github.com/contoso/widgets\n   https://alice@github.com/contoso/widgets\n```\n\n```text\nCredential: \"git:https://bob@github.com/foo/bar\" (user = bob)\n\n   https://bob@github.com/foo/bar\n```\n\n```text\nCredential: \"git:https://bob@github.com/example/myrepo\" (user = bob)\n\n   https://bob@github.com/example/myrepo\n```\n\n---\n\n### credential.azreposCredentialType\n\nSpecify the type of credential the Azure Repos host provider should return.\n\nDefaults to the value `pat`. In certain cloud hosted environments when using a\nwork or school account, such as [Microsoft DevBox][devbox], the default value is\n`oauth`.\n\nValue|Description\n-|-\n`pat`|Azure DevOps personal access tokens\n`oauth`|Microsoft identity OAuth tokens (AAD or MSA tokens)\n\nHere is more information about [Azure Access tokens][azure-tokens].\n\n#### Example\n\n```shell\ngit config --global credential.azreposCredentialType oauth\n```\n\n**Also see: [GCM_AZREPOS_CREDENTIALTYPE][gcm-azrepos-credentialtype]**\n\n---\n\n### credential.azreposManagedIdentity\n\nUse a [Managed Identity][managed-identity] to authenticate with Azure Repos.\n\nThe value `system` will tell GCM to use the system-assigned Managed Identity.\n\nTo specify a user-assigned Managed Identity, use the format `id://{clientId}`\nwhere `{clientId}` is the client ID of the Managed Identity. Alternatively any\nGUID-like value will also be interpreted as a user-assigned Managed Identity\nclient ID.\n\nTo specify a Managed Identity associated with an Azure resource, you can use the\nformat `resource://{resourceId}` where `{resourceId}` is the ID of the resource.\n\nFor more information about managed identities, see the Azure DevOps\n[documentation][azrepos-sp-mid].\n\nValue|Description\n-|-\n`system`|System-Assigned Managed Identity\n`[guid]`|User-Assigned Managed Identity with the specified client ID\n`id://[guid]`|User-Assigned Managed Identity with the specified client ID\n`resource://[guid]`|User-Assigned Managed Identity for the associated resource\n\n```shell\ngit config --global credential.azreposManagedIdentity \"id://11111111-1111-1111-1111-111111111111\"\n```\n\n**Also see: [GCM_AZREPOS_MANAGEDIDENTITY][gcm-azrepos-credentialmanagedidentity]**\n\n---\n\n### credential.azreposServicePrincipal\n\nSpecify the client and tenant IDs of a [service principal][service-principal]\nto use when performing Microsoft authentication for Azure Repos.\n\nThe value of this setting should be in the format: `{tenantId}/{clientId}`.\n\nYou must also set at least one authentication mechanism if you set this value:\n\n- [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret]\n- [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint]\n- [credential.azreposServicePrincipalCertificateSendX5C][credential-azrepos-sp-cert-x5c]\n\nFor more information about service principals, see the Azure DevOps\n[documentation][azrepos-sp-mid].\n\n#### Example\n\n```shell\ngit config --global credential.azreposServicePrincipal \"11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222\"\n```\n\n**Also see: [GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-service-principal]**\n\n---\n\n### credential.azreposServicePrincipalSecret\n\nSpecifies the client secret for the [service principal][service-principal] when\nperforming Microsoft authentication for Azure Repos with\n[credential.azreposServicePrincipalSecret][credential-azrepos-sp] set.\n\n#### Example\n\n```shell\ngit config --global credential.azreposServicePrincipalSecret \"da39a3ee5e6b4b0d3255bfef95601890afd80709\"\n```\n\n**Also see: [GCM_AZREPOS_SP_SECRET][gcm-azrepos-sp-secret]**\n\n---\n\n### credential.azreposServicePrincipalCertificateThumbprint\n\nSpecifies the thumbprint of a certificate to use when authenticating as a\n[service principal][service-principal] for Azure Repos when\n[GCM_AZREPOS_SERVICE_PRINCIPAL][credential-azrepos-sp] is set.\n\n#### Example\n\n```shell\ngit config --global credential.azreposServicePrincipalCertificateThumbprint \"9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc\"\n```\n\n**Also see: [GCM_AZREPOS_SP_CERT_THUMBPRINT][gcm-azrepos-sp-cert-thumbprint]**\n\n---\n\n### credential.azreposServicePrincipalCertificateSendX5C\n\nWhen using a certificate for [service principal][service-principal] authentication, this configuration\nspecifies whether the X5C claim should be should be sent to the STS. Sending the x5c\nenables application developers to achieve easy certificate rollover in Azure AD:\nthis method will send the public certificate to Azure AD along with the token request,\nso that Azure AD can use it to validate the subject name based on a trusted issuer\npolicy. This saves the application admin from the need to explicitly manage the\ncertificate rollover. For details see [https://aka.ms/msal-net-sni](https://aka.ms/msal-net-sni).\n\n#### Example\n\n```shell\ngit config --global credential.azreposServicePrincipalCertificateSendX5C true\n```\n**Also see: [GCM_AZREPOS_SP_CERT_SEND_X5C][gcm-azrepos-sp-cert-x5c]**\n\n---\n\n### trace2.normalTarget\n\nTurns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format\ndocumentation][trace2-normal-docs] for more details.\n\n#### Example\n\n```shell\ngit config --global trace2.normalTarget true\n```\n\nIf the value of `trace2.normalTarget` is a full path to a file in an existing\ndirectory, logs are appended to the file.\n\nIf the value of `trace2.normalTarget` is `true` or `1`, logs are written to\nstandard error.\n\nDefaults to disabled.\n\n**Also see: [GIT_TRACE2][trace2-normal-env]**\n\n---\n\n### trace2.eventTarget\n\nTurns on Trace2 Event Format tracing - see [Git's Trace2 Event Format\ndocumentation][trace2-event-docs] for more details.\n\n#### Example\n\n```shell\ngit config --global trace2.eventTarget true\n```\n\nIf the value of `trace2.eventTarget` is a full path to a file in an existing\ndirectory, logs are appended to the file.\n\nIf the value of `trace2.eventTarget` is `true` or `1`, logs are written to\nstandard error.\n\nDefaults to disabled.\n\n**Also see: [GIT_TRACE2_EVENT][trace2-event-env]**\n\n---\n\n### trace2.perfTarget\n\nTurns on Trace2 Performance Format tracing - see [Git's Trace2 Performance\nFormat documentation][trace2-performance-docs] for more details.\n\n#### Example\n\n```shell\ngit config --global trace2.perfTarget true\n```\n\nIf the value of `trace2.perfTarget` is a full path to a file in an existing\ndirectory, logs are appended to the file.\n\nIf the value of `trace2.perfTarget` is `true` or `1`, logs are written to\nstandard error.\n\nDefaults to disabled.\n\n**Also see: [GIT_TRACE2_PERF][trace2-performance-env]**\n\n[auto-detection]: autodetect.md\n[azure-tokens]: azrepos-users-and-tokens.md\n[use-http-path]: https://git-scm.com/docs/gitcredentials/#Documentation/gitcredentials.txt-useHttpPath\n[credential-credentialstore]: #credentialcredentialstore\n[credential-dpapistorepath]: #credentialdpapistorepath\n[credential-interactive]: #credentialinteractive\n[credential-msauthusebroker]: #credentialmsauthusebroker-experimental\n[credential-plaintextstorepath]: #credentialplaintextstorepath\n[credential-cache]: https://git-scm.com/docs/git-credential-cache\n[cred-stores]: credstores.md\n[devbox]: https://azure.microsoft.com/en-us/products/dev-box\n[enterprise-config]: enterprise-config.md\n[envars]: environment.md\n[freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/\n[gcm-allow-windowsauth]: environment.md#GCM_ALLOW_WINDOWSAUTH\n[gcm-allow-unsafe-remotes]: environment.md#GCM_ALLOW_UNSAFE_REMOTES\n[gcm-authority]: environment.md#GCM_AUTHORITY-deprecated\n[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT\n[gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE\n[gcm-azrepos-credentialmanagedidentity]: environment.md#GCM_AZREPOS_MANAGEDIDENTITY\n[gcm-bitbucket-always-refresh-credentials]: environment.md#GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS\n[gcm-bitbucket-authmodes]: environment.md#GCM_BITBUCKET_AUTHMODES\n[gcm-credential-cache-options]: environment.md#GCM_CREDENTIAL_CACHE_OPTIONS\n[gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE\n[gcm-debug]: environment.md#GCM_DEBUG\n[gcm-dpapi-store-path]: environment.md#GCM_DPAPI_STORE_PATH\n[gcm-github-accountfiltering]: environment.md#GCM_GITHUB_ACCOUNTFILTERING\n[gcm-github-authmodes]: environment.md#GCM_GITHUB_AUTHMODES\n[gcm-gitlab-authmodes]:environment.md#GCM_GITLAB_AUTHMODES\n[gcm-gui-prompt]: environment.md#GCM_GUI_PROMPT\n[gcm-gui-software-rendering]: environment.md#GCM_GUI_SOFTWARE_RENDERING\n[gcm-http-proxy]: environment.md#GCM_HTTP_PROXY-deprecated\n[gcm-interactive]: environment.md#GCM_INTERACTIVE\n[gcm-msauth-flow]: environment.md#GCM_MSAUTH_FLOW\n[gcm-msauth-usebroker]: environment.md#GCM_MSAUTH_USEBROKER-experimental\n[gcm-msauth-usedefaultaccount]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental\n[gcm-namespace]: environment.md#GCM_NAMESPACE\n[gcm-plaintext-store-path]: environment.md#GCM_PLAINTEXT_STORE_PATH\n[gcm-provider]: environment.md#GCM_PROVIDER\n[gcm-trace]: environment.md#GCM_TRACE\n[gcm-trace-secrets]: environment.md#GCM_TRACE_SECRETS\n[gcm-trace-msauth]: environment.md#GCM_TRACE_MSAUTH\n[github-emu]: https://docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/using-enterprise-managed-users-for-iam/about-enterprise-managed-users\n[usage]: usage.md\n[git-config-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy\n[http-proxy]: netconfig.md#http-proxy\n[autodetect]: autodetect.md\n[libsecret]: https://wiki.gnome.org/Projects/Libsecret\n[managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview\n[provider-migrate]: migration.md#gcm_authority\n[cache-options]: https://git-scm.com/docs/git-credential-cache#_options\n[pass]: https://www.passwordstore.org/\n[pass-man]: https://git.zx2c4.com/password-store/about/\n[trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target\n[trace2-normal-env]: environment.md#GIT_TRACE2\n[trace2-event-docs]: https://git-scm.com/docs/api-trace2#_the_event_format_target\n[trace2-event-env]: environment.md#GIT_TRACE2_EVENT\n[trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target\n[trace2-performance-env]: environment.md#GIT_TRACE2_PERF\n[wam]: windows-broker.md\n[service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals\n[azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity\n[credential-azrepos-sp]: #credentialazreposserviceprincipal\n[credential-azrepos-sp-secret]: #credentialazreposserviceprincipalsecret\n[credential-azrepos-sp-cert-thumbprint]: #credentialazreposserviceprincipalcertificatethumbprint\n[credential-azrepos-sp-cert-x5c]: #credentialazreposserviceprincipalcertificatesendx5c\n[gcm-azrepos-service-principal]: environment.md#GCM_AZREPOS_SERVICE_PRINCIPAL\n[gcm-azrepos-sp-secret]: environment.md#GCM_AZREPOS_SP_SECRET\n[gcm-azrepos-sp-cert-thumbprint]: environment.md#GCM_AZREPOS_SP_CERT_THUMBPRINT\n[gcm-azrepos-sp-cert-x5c]: environment.md#GCM_AZREPOS_SP_CERT_SEND_X5C\n"
  },
  {
    "path": "docs/credstores.md",
    "content": "# Credential stores\n\nThere are several options for storing credentials that GCM supports:\n\n- Windows Credential Manager\n- DPAPI protected files\n- macOS Keychain\n- [freedesktop.org Secret Service API][freedesktop-secret-service]\n- GPG/[`pass`][passwordstore] compatible files\n- Git's built-in [credential cache][credential-cache]\n- Plaintext files\n- Passthrough/no-op (no credential store)\n\nThe default credential stores on macOS and Windows are the macOS Keychain and\nthe Windows Credential Manager, respectively.\n\nGCM comes without a default store on Linux distributions.\n\nYou can select which credential store to use by setting the [`GCM_CREDENTIAL_STORE`][gcm-credential-store]\nenvironment variable, or the [`credential.credentialStore`][credential-store]\nGit configuration setting. For example:\n\n```shell\ngit config --global credential.credentialStore gpg\n```\n\nSome credential stores have limitations, or further configuration required\ndepending on your particular setup. See more detailed information below for each\ncredential store.\n\n## Windows Credential Manager\n\n**Available on:** _Windows_\n\n**This is the default store on Windows.**\n\n**:warning: Does not work over a network/SSH session.**\n\n```batch\nSET GCM_CREDENTIAL_STORE=\"wincredman\"\n```\n\nor\n\n```shell\ngit config --global credential.credentialStore wincredman\n```\n\nThis credential store uses the Windows Credential APIs (`wincred.h`) to store\ndata securely in the Windows Credential Manager (also known as the Windows\nCredential Vault in earlier versions of Windows).\n\nYou can [access and manage data in the credential manager][access-windows-credential-manager]\nfrom the control panel, or via the [`cmdkey` command-line tool][cmdkey].\n\nWhen connecting to a Windows machine over a network session (such as SSH), GCM\nis unable to persist credentials to the Windows Credential Manager due to\nlimitations in Windows. Connecting by Remote Desktop doesn't suffer from this\nlimitation.\n\n## DPAPI protected files\n\n**Available on:** _Windows_\n\n```batch\nSET GCM_CREDENTIAL_STORE=\"dpapi\"\n```\n\nor\n\n```shell\ngit config --global credential.credentialStore dpapi\n```\n\nThis credential store uses Windows DPAPI to encrypt credentials which are stored\nas files in your file system. The file structure is the same as the\n[plaintext files credential store][plaintext-files] except the first line (the\nsecret value) is protected by DPAPI.\n\nBy default files are stored in `%USERPROFILE%\\.gcm\\dpapi_store`. This can be\nconfigured using the environment variable `GCM_DPAPI_STORE_PATH` environment\nvariable.\n\nIf the directory doesn't exist it will be created.\n\n## macOS Keychain\n\n**Available on:** _macOS_\n\n**This is the default store on macOS.**\n\n```shell\nexport GCM_CREDENTIAL_STORE=keychain\n# or\ngit config --global credential.credentialStore keychain\n```\n\nThis credential store uses the default macOS Keychain, which is typically the\n`login` keychain.\n\nYou can [manage data stored in the keychain][mac-keychain-management]\nusing the Keychain Access application.\n\n## [freedesktop.org Secret Service API][freedesktop-secret-service]\n\n**Available on:** _Linux_\n\n**:warning: Requires a graphical user interface session.**\n\n```shell\nexport GCM_CREDENTIAL_STORE=secretservice\n# or\ngit config --global credential.credentialStore secretservice\n```\n\nThis credential store uses the `libsecret` library to interact with the Secret\nService. It stores credentials securely in 'collections', which can be viewed by\ntools such as `secret-tool` and `seahorse`.\n\nA graphical user interface is required in order to show a secure prompt to\nrequest a secret collection be unlocked.\n\n## GPG/[`pass`][passwordstore] compatible files\n\n**Available on:** _macOS, Linux_\n\n**:warning: Requires `gpg`, `pass`, and a GPG key pair.**\n\n```shell\nexport GCM_CREDENTIAL_STORE=gpg\n# or\ngit config --global credential.credentialStore gpg\n```\n\nThis credential store uses GPG to encrypt files containing credentials which are\nstored in your file system. The file structure is compatible with the popular\n[`pass`][passwordstore] tool. By default files are stored in\n`~/.password-store` but this can be configured using the `pass` environment\nvariable `PASSWORD_STORE_DIR`.\n\nBefore you can use this credential store, it must be initialized by the `pass`\nutility, which in-turn requires a valid GPG key pair. To initalize the store,\nrun:\n\n```shell\npass init <gpg-id>\n```\n\n..where `<gpg-id>` is the user ID of a GPG key pair on your system. To create a\nnew GPG key pair, run:\n\n```shell\ngpg --gen-key\n```\n\n..and follow the prompts.\n\n### Headless/TTY-only sessions\n\nIf you are using the `gpg` credential store in a headless/TTY-only environment,\nyou must ensure you have configured the GPG Agent (`gpg-agent`) with a suitable\npin-entry program for the terminal such as `pinentry-tty` or `pinentry-curses`.\n\nIf you are connecting to your system via SSH, then the `SSH_TTY` variable should\nautomatically be set. GCM will pass the value of `SSH_TTY` to GPG/GPG Agent\nas the TTY device to use for prompting for a passphrase.\n\nIf you are not connecting via SSH, or otherwise do not have the `SSH_TTY`\nenvironment variable set, you must set the `GPG_TTY` environment variable before\nrunning GCM. The easiest way to do this is by adding the following to your\nprofile (`~/.bashrc`, `~/.profile` etc):\n\n```shell\nexport GPG_TTY=$(tty)\n```\n\n**Note:** Using `/dev/tty` does not appear to work here - you must use the real\nTTY device path, as returned by the `tty` utility.\n\n## Git's built-in [credential cache][credential-cache]\n\n**Available on:** _macOS, Linux_\n\n```shell\nexport GCM_CREDENTIAL_STORE=cache\n# or\ngit config --global credential.credentialStore cache\n```\n\nThis credential store uses Git's built-in ephemeral\nin-memory [credential cache][credential-cache].\nThis helps you reduce the number of times you have to authenticate but\ndoesn't require storing credentials on persistent storage. It's good for\nscenarios like [Azure Cloud Shell][azure-cloudshell]\nor [AWS CloudShell][aws-cloudshell], where you don't want to\nleave credentials on disk but also don't want to re-authenticate on every Git\noperation.\n\nBy default, `git credential-cache` stores your credentials for 900 seconds.\nThat, and any other [options it accepts][git-credential-cache-options],\nmay be altered by setting them in the environment variable\n`GCM_CREDENTIAL_CACHE_OPTIONS` or the Git config value\n`credential.cacheOptions`. (Using the `--socket` option is untested\nand unsupported, but there's no reason it shouldn't work.)\n\n```shell\nexport GCM_CREDENTIAL_CACHE_OPTIONS=\"--timeout 300\"\n# or\ngit config --global credential.cacheOptions \"--timeout 300\"\n```\n\n## Plaintext files\n\n**Available on:** _Windows, macOS, Linux_\n\n**:warning: This is not a secure method of credential storage!**\n\n```shell\nexport GCM_CREDENTIAL_STORE=plaintext\n# or\ngit config --global credential.credentialStore plaintext\n```\n\nThis credential store saves credentials to plaintext files in your file system.\nBy default files are stored in `~/.gcm/store` or `%USERPROFILE%\\.gcm\\store`.\nThis can be configured using the environment variable `GCM_PLAINTEXT_STORE_PATH`\nenvironment variable.\n\nIf the directory doesn't exist it will be created.\n\nOn POSIX platforms the newly created store directory will have permissions set\nsuch that only the owner can `r`ead/`w`rite/e`x`ecute (`700` or `drwx---`).\nPermissions on existing directories will not be modified.\n\nNB. GCM's plaintext store is distinct from [git-credential-store][git-credential-store],\nthough the formats are similar. The default paths differ.\n\n---\n\n:warning: **WARNING** :warning:\n\n**This storage mechanism is NOT secure!**\n\n**Secrets and credentials are stored in plaintext files _without any security_!**\n\nIt is **HIGHLY RECOMMENDED** to always use one of the other credential store\noptions above. This option is only provided for compatibility and use in\nenvironments where no other secure option is available.\n\nIf you chose to use this credential store, it is recommended you set the\npermissions on this directory such that no other users or applications can\naccess files within. If possible, use a path that exists on an external volume\nthat you take with you and use full-disk encryption.\n\n## Passthrough/no-op (no credential store)\n\n**Available on:** _Windows, macOS, Linux_\n\n**:warning: .**\n\n```batch\nSET GCM_CREDENTIAL_STORE=\"none\"\n```\n\nor\n\n```shell\ngit config --global credential.credentialStore none\n```\n\nThis option disables the internal credential store. All operations to store or\nretrieve credentials will do nothing, and will return success. This is useful if\nyou want to use a different credential store, chained in sequence via Git\nconfiguration, and don't want GCM to store credentials.\n\nNote that you'll want to ensure that another credential helper is placed before\nGCM in the `credential.helper` Git configuration or else you will be prompted to\nenter your credentials every time you interact with a remote repository.\n\n[access-windows-credential-manager]: https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0\n[aws-cloudshell]: https://aws.amazon.com/cloudshell/\n[azure-cloudshell]: https://docs.microsoft.com/azure/cloud-shell/overview\n[cmdkey]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmdkey\n[credential-store]: configuration.md#credentialcredentialstore\n[credential-cache]: https://git-scm.com/docs/git-credential-cache\n[freedesktop-secret-service]: https://specifications.freedesktop.org/secret-service/\n[gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE\n[git-credential-store]: https://git-scm.com/docs/git-credential-store\n[mac-keychain-management]: https://support.apple.com/en-gb/guide/mac-help/mchlf375f392/mac\n[git-credential-cache-options]: https://git-scm.com/docs/git-credential-cache#_options\n[passwordstore]: https://www.passwordstore.org/\n[plaintext-files]: #plaintext-files\n"
  },
  {
    "path": "docs/development.md",
    "content": "# Development and debugging\n\nStart by cloning this repository:\n\n```shell\ngit clone https://github.com/git-ecosystem/git-credential-manager\n```\n\nYou also need the latest version of the .NET SDK which can be downloaded and\ninstalled from the [.NET website][dotnet-web].\n\n## Building\n\nThe `Git-Credential-Manager.sln` solution can be opened and built in Visual\nStudio, Visual Studio for Mac, Visual Studio Code, or JetBrains Rider.\n\n### macOS\n\nTo build from inside an IDE, make sure to select the `MacDebug` or `MacRelease`\nsolution configurations.\n\nTo build from the command line, run:\n\n```shell\ndotnet build -c MacDebug\n```\n\nYou can find a copy of the installer .pkg file in `out/osx/Installer.Mac/pkg/Debug`.\n\nThe flat binaries can also be found in `out/osx/Installer.Mac/pkg/Debug/payload`.\n\n### Windows\n\nTo build from inside an IDE, make sure to select the `WindowsDebug` or\n`WindowsRelease` solution configurations.\n\nTo build from the command line, run:\n\n```powershell\ndotnet build -c WindowsDebug\n```\n\nYou can find a copy of the installer .exe file in `out\\windows\\Installer.Windows\\bin\\Debug\\net472`.\n\nThe flat binaries can also be found in `out\\windows\\Payload.Windows\\bin\\Debug\\net472\\win-x86`.\n\n### Linux\n\nThe two available solution configurations are `LinuxDebug` and `LinuxRelease`.\n\nTo build from the command line, run:\n\n```shell\ndotnet build -c LinuxDebug\n```\n\nIf you want to build for a specific architecture, you can provide `linux-x64` or `linux-arm64` or `linux-arm` as the runtime:\n\n```shell\ndotnet build -c LinuxDebug -r linux-arm64\n```\n\nYou can find a copy of the Debian package (.deb) file in `out/linux/Packaging.Linux/deb/Debug`.\n\nThe flat binaries can also be found in `out/linux/Packaging.Linux/payload/Debug`.\n\n## Debugging\n\nTo debug from inside an IDE you'll want to set `Git-Credential-Manager` as the\nstartup project, and specify one of `get`, `store`, or `erase` as a program\nargument.\n\nTo simulate Git interacting with GCM, when you start from your IDE of choice,\nyou'll need to enter the following [information over standard input][ioformat]:\n\n```text\nprotocol=http<LF>\nhost=<HOSTNAME><LF>\n<LF>\n<LF>\n```\n\n..where `<HOSTNAME>` is a supported hostname such as `github.com`, and `<LF>` is\na line feed (or CRLF, we support both!).\n\nYou may also include the following optional fields, depending on your scenario:\n\n```text\nusername=<USERNAME><LF>\npassword=<PASSWORD><LF>\n```\n\nFor more information about how Git interacts with credential helpers, please\nread Git's documentation on [custom helpers][custom-helpers].\n\n### Attaching to a running process\n\nIf you want to debug an already running GCM process, set the `GCM_DEBUG`\nenvironment variable to `1` or `true`. The process will wait on launch for a\ndebugger to attach before continuing.\n\nThis is useful when debugging interactions between GCM and Git, and you want\nGit to be the one launching us.\n\n### Collect trace output\n\nGCM has two tracing systems - one that is distinctly GCM's and one that\nimplements certain features of [Git's Trace2 API][trace2]. Below are\ninstructions for how to use each.\n\n#### `GCM_TRACE`\n\nIf you want to debug a release build or installation of GCM, you can set the\n`GCM_TRACE` environment variable to `1` to print trace information to standard\nerror, or to an absolute file path to write trace information to a file.\n\nFor example:\n\n```shell\n$ GCM_TRACE=1 git-credential-manager version\n> 18:47:56.526712 ...er/Application.cs:69 trace: [RunInternalAsync] Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0) 'version'\n> Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0)\n```\n\n#### Git's Trace2 API\n\nThis API can also be used to print debug, performance, and telemetry information\nto stderr or a file in various formats.\n\n##### Supported format targets\n\n1. The Normal Format Target: Similar to `GCM_TRACE`, this target writes\nhuman-readable output and is best suited for debugging. It can be enabled via\nenvironment variable or config, for example:\n\n    ```shell\n    export GIT_TRACE2=1\n    ```\n\n    or\n\n    ```shell\n    git config --global trace2.normalTarget ~/log.normal\n    ```\n\n0. The Performance Format Target: This format is column-based and geared toward\nanalyzing performance during development and testing. It can be enabled via\nenvironment variable or config, for example:\n\n    ```shell\n    export GIT_TRACE2_PERF=1\n    ```\n\n    or\n\n    ```shell\n    git config --global trace2.perfTarget ~/log.perf\n    ```\n\n0. The Event Format Target: This format is json-based and is geared toward\ncollection of large quantities of data for advanced analysis. It can be enabled\nvia environment variable or config, for example:\n\n    ```shell\n    export GIT_TRACE2_EVENT=1\n    ```\n\n    or\n\n    ```shell\n    git config --global trace2.eventTarget ~/log.event\n    ```\n\nYou can read more about each of these format targets in the [corresponding\nsection][trace2-targets] of Git's Trace2 API documentation.\n\n##### Supported events\n\nThe below describes, at a high level, the Trace2 API events that are currently\nsupported in GCM and the information they provide:\n\n1. `version`: contains the version of the current executable (e.g. GCM or a\nhelper exe)\n0. `start`: contains the complete argv received by current executable's `Main()`\nmethod\n0. `exit`: contains current executable's exit code\n0. `child_start`: describes a child process that is about to be spawned\n0. `child_exit`: describes a child process at exit\n0. `region_enter`: describes a region (e.g. a timer for a section of code that\nis interesting) on entry\n0. `region_leave`: describes a region on leaving\n\nYou can read more about each of these format targets in the [corresponding\nsection][trace2-events] of Git's Trace2 API documentation.\n\nWant to see more events? Consider contributing! We'd :love: to see your\nawesome work in support of building out this API.\n\n### Code coverage metrics\n\nIf you want code coverage metrics these can be generated either from the command\nline:\n\n```shell\ndotnet test --collect:\"XPlat Code Coverage\" --settings=./.code-coverage/coverlet.settings.xml\n```\n\nOr via the VSCode Terminal/Run Task:\n\n```console\ntest with coverage\n```\n\nHTML reports can be generated using ReportGenerator, this should be installed\nduring the build process, from the command line:\n\n```shell\ndotnet ~/.nuget/packages/reportgenerator/*/*/net8.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage\n```\n\nor\n\n```shell\ndotnet {$env:USERPROFILE}/.nuget/packages/reportgenerator/*/*/net8.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage\n```\n\nOr via VSCode Terminal/Run Task:\n\n```console\nreport coverage - nix\n```\n\nor\n\n```console\nreport coverage - win\n```\n\n## Linting Documentation\n\nDocuments are linted using [markdownlint][markdownlint] which can be installed\nas a CLI tool via NPM or as an [extension in VSCode][vscode-markdownlint]. See\nthe [documentation on GitHub][markdownlint]. The configuration used for\nmarkdownlint is in [.markdownlint.jsonc][markdownlint-config].\n\nDocuments are checked for link validity using [lychee][lychee]. Lychee can be\ninstalled in a variety of ways depending on your platform, see the [docs on GitHub][lychee-docs].\nSome URLs are ignored by lychee, per the [lycheeignore][lycheeignore].\n\n[dotnet-web]: https://dotnet.microsoft.com/\n[custom-helpers]: https://git-scm.com/docs/gitcredentials#_custom_helpers\n[ioformat]: https://git-scm.com/docs/git-credential#IOFMT\n[lychee]: https://lychee.cli.rs/\n[lychee-docs]: https://github.com/lycheeverse/lychee\n[lycheeignore]: ../.lycheeignore\n[markdownlint]: https://github.com/DavidAnson/markdownlint-cli2\n[markdownlint-config]: ../.markdownlint.jsonc\n[trace2]: https://git-scm.com/docs/api-trace2\n[trace2-events]: https://git-scm.com/docs/api-trace2#_event_specific_keyvalue_pairs\n[trace2-targets]: https://git-scm.com/docs/api-trace2#_trace2_targets\n[vscode-markdownlint]: https://github.com/DavidAnson/vscode-markdownlint\n"
  },
  {
    "path": "docs/enterprise-config.md",
    "content": "# Enterprise configuration defaults\n\nGit Credential Manager (GCM) can be configured using multiple\ndifferent mechanisms. In order of preference, those mechanisms are:\n\n1. [Environment variables][environment]\n1. Standard [Git configuration][config] files\n   1. Repository/local configuration (`.git/config`)\n   1. User/global configuration (`$HOME/.gitconfig` or `%HOME%\\.gitconfig`)\n   1. Installation/system configuration (`etc/gitconfig`)\n1. Enterprise system administrator defaults\n1. Compiled default values\n\nThis model largely matches what Git itself supports, namely environment\nvariables that take precedence over Git configuration files.\n\nThe addition of the enterprise system administrator defaults enables those\nadministrators to configure many GCM settings using familiar MDM tooling, rather\nthan having to modify the Git installation configuration files.\n\n## User Freedom\n\nWe believe the user should _always_ be at liberty to configure\nGit and GCM exactly as they wish. By preferring environment variables and Git\nconfiguration files over system admin values, these only act as _default values_\nthat can always be overridden by the user in the usual ways.\n\n## Windows\n\nDefault setting values come from the Windows Registry, specifically the\nfollowing keys:\n\n```text\nHKEY_LOCAL_MACHINE\\SOFTWARE\\GitCredentialManager\\Configuration\n```\n\nBy using the Windows Registry, system administrators can use Group Policy to\neasily set defaults for GCM's settings.\n\nThe names and possible values of all settings under this key are the same as\nthose of the [Git configuration][config] settings.\n\nThe type of each registry key can be either `REG_SZ` (string) or `REG_DWORD`\n(integer).\n\n### 32-bit / x86\n\nWhen running the 32-bit (x86) version of GCM on a 64-bit (x64 or ARM64)\ninstallation of Windows, the registry access is transparently redirected to\nthe `WOW6432Node` node.\n\n```text\nHKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GitCredentialManager\\Configuration\n```\n\n## macOS\n\nDefault settings values come from macOS's preferences system. Configuration\nprofiles can be deployed to devices using a compatible Mobile Device Management\n(MDM) solution.\n\nConfiguration for Git Credential Manager must take the form of a dictionary, set\nfor the domain `git-credential-manager` under the key `configuration`. For\nexample:\n\n```shell\ndefaults write git-credential-manager configuration -dict-add <key> <value>\n```\n\n..where `<key>` is the name of the settings from the [Git configuration][config]\nreference, and `<value>` is the desired value.\n\nAll values in the `configuration` dictionary must be strings. For boolean values\nuse `true` or `false`, and for integer values use the number in string form.\n\nTo read the current configuration:\n\n```console\n$ defaults read git-credential-manager configuration\n{\n    <key1> = <value1>;\n    ...\n    <keyN> = <valueN>;\n}\n```\n\n## Linux\n\nDefault settings values come from the `/etc/git-credential-manager/config.d`\ndirectory. Each file in this directory represents a single settings dictionary.\n\nAll files in this directory are read at runtime and merged into a single\ncollection of settings, in the order they are read from the file system.\nTo provide a stable ordering, it is recommended to prefix each filename with a\nnumber, e.g. `42-my-settings`.\n\nThe format of each file is a simple set of key/value pairs, separated by an\n`=` sign, and each line separated by a line-feed (`\\n`, LF) character.\nComments are identified by a `#` character at the beginning of a line.\n\nFor example:\n\n```text\n# /etc/git-credential-manager/config.d/00-example1\ncredential.noguiprompt=0\n```\n\n```text\n# /etc/git-credential-manager/config.d/01-example2\ncredential.trace=true\ncredential.traceMsAuth=true\n```\n\nAll settings names and values are the same as the [Git configuration][config]\nreference.\n\n> Note: These files are read once at startup. If changes are made to these files\nthey will not be reflected in an already running process.\n\n[environment]: environment.md\n[config]: configuration.md\n"
  },
  {
    "path": "docs/environment.md",
    "content": "# Environment variables\n\n[Git Credential Manager][gcm] works out of the box for most users. Configuration\noptions are available to customize or tweak behavior.\n\nGit Credential Manager (GCM) can be configured using environment variables.\n**Environment variables take precedence over [configuration][configuration]\noptions and enterprise system administrator [default values][default-values]**.\n\nFor the complete list of environment variables GCM understands, see the list\nbelow.\n\n## Available settings\n\n### GCM_TRACE\n\nEnables trace logging of all activities.\nConfiguring Git and GCM to trace to the same location is often desirable, and\nGCM is compatible and cooperative with `GIT_TRACE`.\n\n#### Example\n\n##### Windows\n\n```batch\nSET GIT_TRACE=%UserProfile%\\git.log\nSET GCM_TRACE=%UserProfile%\\git.log\n```\n\n##### macOS/Linux\n\n```bash\nexport GIT_TRACE=$HOME/git.log\nexport GCM_TRACE=$HOME/git.log\n```\n\nIf the value of `GCM_TRACE` is a full path to a file in an existing directory,\nlogs are appended to the file.\n\nIf the value of `GCM_TRACE` is `true` or `1`, logs are written to standard error.\n\nDefaults to disabled.\n\n**Also see: [credential.trace][credential-trace]**\n\n---\n\n### GCM_TRACE_SECRETS\n\nEnables tracing of secret and sensitive information, which is by default masked\nin trace output. Requires that `GCM_TRACE` is also enabled.\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_TRACE=%UserProfile%\\gcm.log\nSET GCM_TRACE_SECRETS=1\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_TRACE=$HOME/gcm.log\nexport GCM_TRACE_SECRETS=1\n```\n\nIf the value of `GCM_TRACE_SECRETS` is `true` or `1`, trace logs will include\nsecret information.\n\nDefaults to disabled.\n\n**Also see: [credential.traceSecrets][credential-trace-secrets]**\n\n---\n\n### GCM_TRACE_MSAUTH\n\nEnables inclusion of Microsoft Authentication library (MSAL) logs in GCM trace\noutput. Requires that `GCM_TRACE` is also enabled.\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_TRACE=%UserProfile%\\gcm.log\nSET GCM_TRACE_MSAUTH=1\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_TRACE=$HOME/gcm.log\nexport GCM_TRACE_MSAUTH=1\n```\n\nIf the value of `GCM_TRACE_MSAUTH` is `true` or `1`, trace logs will include\nverbose MSAL logs.\n\nDefaults to disabled.\n\n**Also see: [credential.traceMsAuth][credential-trace-msauth]**\n\n---\n\n### GCM_DEBUG\n\nPauses execution of GCM at launch to wait for a debugger to be attached.\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_DEBUG=1\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_DEBUG=1\n```\n\nDefaults to disabled.\n\n**Also see: [credential.debug][credential-debug]**\n\n---\n\n### GCM_INTERACTIVE\n\nPermit or disable GCM from interacting with the user (showing GUI or TTY\nprompts). If interaction is required but has been disabled, an error is\nreturned.\n\nThis can be helpful when using GCM in headless and unattended environments, such\nas build servers, where it would be preferable to fail than to hang indefinitely\nwaiting for a non-existent user.\n\nTo disable interactivity set this to `false` or `0`.\n\n#### Compatibility\n\nIn previous versions of GCM this setting had a different behavior and accepted\nother values. The following table summarizes the change in behavior and the\nmapping of older values such as `never`:\n\nValue(s)|Old meaning|New meaning\n-|-|-\n`auto`|Prompt if required – use cached credentials if possible|_(unchanged)_\n`never`, `false`| Never prompt – fail if interaction is required|_(unchanged)_\n`always`, `force`, `true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_INTERACTIVE=0\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_INTERACTIVE=0\n```\n\nDefaults to enabled.\n\n**Also see: [credential.interactive][credential-interactive]**\n\n---\n\n### GCM_PROVIDER\n\nDefine the host provider to use when authenticating.\n\nID|Provider\n-|-\n`auto` _(default)_|_\\[automatic\\]_ ([learn more][autodetect])\n`azure-repos`|Azure Repos\n`github`|GitHub\n`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_\n`generic`|Generic (any other provider not listed above)\n\nAutomatic provider selection is based on the remote URL.\n\nThis setting is typically used with a scoped URL to map a particular set of\nremote URLs to providers, for example to mark a host as a GitHub Enterprise\ninstance.\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_PROVIDER=github\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_PROVIDER=github\n```\n\n**Also see: [credential.provider][credential-provider]**\n\n---\n\n### GCM_AUTHORITY _(deprecated)_\n\n> This setting is deprecated and should be replaced by `GCM_PROVIDER` with the\n> corresponding provider ID value.\n>\n> See the [migration guide][migration-guide] for more information.\n\nSelect the host provider to use when authenticating by which authority is\nsupported by the providers.\n\nAuthority|Provider(s)\n-|-\n`auto` _(default)_|_\\[automatic\\]_\n`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|Azure Repos _(supports Microsoft Authentication)_\n`github`|GitHub _(supports GitHub Authentication)_\n`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_\n`basic`, `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|Generic _(supports Basic and Windows Integrated Authentication)_\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_AUTHORITY=github\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_AUTHORITY=github\n```\n\n**Also see: [credential.authority][credential-authority]**\n\n---\n\n### GCM_GUI_PROMPT\n\nPermit or disable GCM from presenting GUI prompts. If an equivalent terminal/\ntext-based prompt is available, that will be shown instead.\n\nTo disable all interactivity see [GCM_INTERACTIVE][gcm-interactive].\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_GUI_PROMPT=0\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_GUI_PROMPT=0\n```\n\nDefaults to enabled.\n\n**Also see: [credential.guiPrompt][credential-guiprompt]**\n\n---\n\n### GCM_GUI_SOFTWARE_RENDERING\n\nForce the use of software rendering for GUI prompts.\n\nThis is currently only applicable on Windows.\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_GUI_SOFTWARE_RENDERING=1\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_GUI_SOFTWARE_RENDERING=1\n```\n\nDefaults to false (use hardware acceleration where available).\n\n> [!NOTE]\n> Windows on ARM devices defaults to using software rendering to work around a\n> known Avalonia issue: <https://github.com/AvaloniaUI/Avalonia/issues/10405>\n\n**Also see: [credential.guiSoftwareRendering][credential-guisoftwarerendering]**\n\n---\n\n### GCM_ALLOW_UNSAFE_REMOTES\n\nAllow transmitting credentials to unsafe remote URLs such as unencrypted HTTP\nURLs. This setting is not recommended for general use and should only be used\nwhen necessary.\n\nDefaults false (disallow unsafe remote URLs).\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_ALLOW_UNSAFE_REMOTES=true\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_ALLOW_UNSAFE_REMOTES=true\n```\n\n**Also see: [credential.allowUnsafeRemotes][credential-allowunsaferemotes]**\n\n---\n\n### GCM_AUTODETECT_TIMEOUT\n\nSet the maximum length of time, in milliseconds, that GCM should wait for a\nnetwork response during host provider auto-detection probing.\n\nSee [autodetection][autodetect] for more information.\n\n**Note:** Use a negative or zero value to disable probing altogether.\n\nDefaults to 2000 milliseconds (2 seconds).\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_AUTODETECT_TIMEOUT=-1\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_AUTODETECT_TIMEOUT=-1\n```\n\n**Also see: [credential.autoDetectTimeout][credential-autodetecttimeout]**\n\n---\n\n### GCM_ALLOW_WINDOWSAUTH\n\nAllow detection of Windows Integrated Authentication (WIA) support for generic\nhost providers. Setting this value to `false` will prevent the use of WIA and\nforce a basic authentication prompt when using the Generic host provider.\n\n**Note:** WIA is only supported on Windows.\n\n**Note:** WIA is an umbrella term for NTLM and Kerberos (and Negotiate).\n\nValue|WIA detection\n-|-\n`true`, `1`, `yes`, `on` _(default)_|Permitted\n`false`, `0`, `no`, `off`|Not permitted\n\n#### Example\n\n##### Windows\n\n```batch\nSET GCM_ALLOW_WINDOWSAUTH=0\n```\n\n##### macOS/Linux\n\n```bash\nexport GCM_ALLOW_WINDOWSAUTH=0\n```\n\n**Also see: [credential.allowWindowsAuth][credential-allowwindowsauth]**\n\n---\n\n### GCM_HTTP_PROXY _(deprecated)_\n\n> This setting is deprecated and should be replaced by the [standard `http.proxy`\n> Git configuration option][git-httpproxy].\n>\n> See the [HTTP proxy configuration][network-http-proxy] for more information.\n\nConfigure GCM to use the a proxy for network operations.\n\n**Note:** Git itself does _not_ respect this setting; this affects GCM _only_.\n\n#### Windows\n\n```batch\nSET GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com\n```\n\n**Also see: [credential.httpProxy][credential-httpproxy]**\n\n---\n\n### GCM_BITBUCKET_AUTHMODES\n\nOverride the available authentication modes presented during Bitbucket\nauthentication. If this option is not set, then the available authentication\nmodes will be automatically detected.\n\n**Note:** This setting only applies to Bitbucket.org, and not Server or DC\ninstances.\n\n**Note:** This setting supports multiple values separated by commas.\n\nValue|Authentication Mode\n-|-\n_(unset)_|Automatically detect modes\n`oauth`|OAuth-based authentication\n`basic`|Basic/PAT-based authentication\n\n#### Windows\n\n```batch\nSET GCM_BITBUCKET_AUTHMODES=\"oauth,basic\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_BITBUCKET_AUTHMODES=\"oauth,basic\"\n```\n\n**Also see: [credential.bitbucketAuthModes][credential-bitbucketauthmodes]**\n\n---\n\n### GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS\n\nForces GCM to ignore any existing stored Basic Auth or OAuth access tokens and\nalways run through the process to refresh the credentials before returning them\nto Git.\n\nThis is especially relevant to OAuth credentials. Bitbucket.org access tokens\nexpire after 2 hours, after that the refresh token must be used to get a new\naccess token.\n\nEnabling this option will improve performance when using Oauth2 and interacting\nwith Bitbucket.org if, on average, commits are done less frequently than every 2\nhours.\n\nEnabling this option will decrease performance when using Basic Auth by\nrequiring the user the re-enter credentials every time.\n\nValue|Refresh Credentials Before Returning\n-|-\n`true`, `1`, `yes`, `on` |Always\n`false`, `0`, `no`, `off`_(default)_|Only when the credentials are found to be invalid\n\n#### Windows\n\n```batch\nSET GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1\n```\n\nDefaults to false/disabled.\n\n**Also see: [credential.bitbucketAlwaysRefreshCredentials](configuration.md#credentialbitbucketAlwaysRefreshCredentials)**\n\n---\n\n### GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS\n\nForces GCM to validate any stored credentials before returning them to Git. It\ndoes this by calling a REST API resource that requires authentication.\n\nDisabling this option reduces the HTTP traffic within GCM when it is retrieving\ncredentials. This may improve user performance, but will increase the number of\ntimes Git remote calls fail to authenticate with the host and therefore require\nthe user to re-try the Git remote call.\n\nEnabling this option helps ensure Git is always provided with valid credentials.\n\nValue|Validate credentials\n-|-\n`true`, `1`, `yes`, `on`_(default)_|Always\n`false`, `0`, `no`, `off`|Never\n\n#### Windows\n\n```batch\nSET GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS=1\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS=1\n```\n\nDefaults to true/enabled.\n\n**Also see: [credential.bitbucketValidateStoredCredentials](configuration.md#credentialbitbucketValidateStoredCredentials)**\n\n---\n\n### GCM_BITBUCKET_DATACENTER_CLIENTID\n\nTo use OAuth with Bitbucket DC it is necessary to create an external, incoming\n[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html).\n\nIt is then necessary to configure the local GCM installation with the OAuth\n[ClientId](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTID) and\n[ClientSecret](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTSECRET)\nfrom the AppLink.\n\n#### Windows\n\n```batch\nSET GCM_BITBUCKET_DATACENTER_CLIENTID=1111111111111111111\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_BITBUCKET_DATACENTER_CLIENTID=1111111111111111111\n```\n\nDefaults to undefined.\n\n**Also see: [credential.bitbucketDataCenterOAuthClientId](configuration.md#credentialbitbucketDataCenterOAuthClientId)**\n\n---\n\n### GCM_BITBUCKET_DATACENTER_CLIENTSECRET\n\nTo use OAuth with Bitbucket DC it is necessary to create an external, incoming\n[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html).\n\nIt is then necessary to configure the local GCM installation with the OAuth\n[ClientId](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTID) and\n[ClientSecret](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTSECRET)\nfrom the AppLink.\n\n#### Windows\n\n```batch\nSET GCM_BITBUCKET_DATACENTER_CLIENTSECRET=222222222222222222222\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_BITBUCKET_DATACENTER_CLIENTSECRET=222222222222222222222\n```\n\nDefaults to undefined.\n\n**Also see: [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credentialbitbucketDataCenterOAuthClientSecret)**\n\n---\n\n### GCM_GITHUB_ACCOUNTFILTERING\n\nEnable or disable automatic account filtering for GitHub based on server hints\nwhen there are multiple available accounts. This setting is only applicable to\nGitHub.com with [Enterprise Managed Users][github-emu].\n\nValue|Description\n-|-\n`true` _(default)_|Filter available accounts based on server hints.\n`false`|Show all available accounts.\n\n#### Windows\n\n```batch\nSET GCM_GITHUB_ACCOUNTFILTERING=false\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_GITHUB_ACCOUNTFILTERING=false\n```\n\n**Also see: [credential.gitHubAccountFiltering][credential-githubaccountfiltering]**\n\n---\n\n### GCM_GITHUB_AUTHMODES\n\nOverride the available authentication modes presented during GitHub\nauthentication. If this option is not set, then the available authentication\nmodes will be automatically detected.\n\n**Note:** This setting supports multiple values separated by commas.\n\nValue|Authentication Mode\n-|-\n_(unset)_|Automatically detect modes\n`oauth`|Expands to: `browser, device`\n`browser`|OAuth authentication via a web browser _(requires a GUI)_\n`device`|OAuth authentication with a device code\n`basic`|Basic authentication using username and password\n`pat`|Personal Access Token (pat)-based authentication\n\n#### Windows\n\n```batch\nSET GCM_GITHUB_AUTHMODES=\"oauth,basic\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_GITHUB_AUTHMODES=\"oauth,basic\"\n```\n\n**Also see: [credential.gitHubAuthModes][credential-githubauthmodes]**\n\n---\n\n### GCM_GITLAB_AUTHMODES\n\nOverride the available authentication modes presented during GitLab\nauthentication. If this option is not set, then the available authentication\nmodes will be automatically detected.\n\n**Note:** This setting supports multiple values separated by commas.\n\nValue|Authentication Mode\n-|-\n_(unset)_|Automatically detect modes\n`browser`|OAuth authentication via a web browser _(requires a GUI)_\n`basic`|Basic authentication using username and password\n`pat`|Personal Access Token (pat)-based authentication\n\n#### Windows\n\n```batch\nSET GCM_GITLAB_AUTHMODES=\"browser\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_GITLAB_AUTHMODES=\"browser\"\n```\n\n**Also see: [credential.gitLabAuthModes][credential-gitlabauthmodes]**\n\n---\n\n### GCM_NAMESPACE\n\nUse a custom namespace prefix for credentials read and written in the OS\ncredential store. Credentials will be stored in the format\n`{namespace}:{service}`.\n\nDefaults to the value `git`.\n\n#### Windows\n\n```batch\nSET GCM_NAMESPACE=\"my-namespace\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_NAMESPACE=\"my-namespace\"\n```\n\n**Also see: [credential.namespace][credential-namespace]**\n\n---\n\n### GCM_CREDENTIAL_STORE\n\nSelect the type of credential store to use on supported platforms.\n\nDefault value on Windows is `wincredman`, on macOS is `keychain`, and is unset\non Linux.\n\n**Note:** For more information about configuring secret stores see the\n[credential stores documentation][credential-stores].\n\nValue|Credential Store|Platforms\n-|-|-\n_(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|-\n`wincredman`|Windows Credential Manager (not available over SSH).|Windows\n`dpapi`|DPAPI protected files. Customize the DPAPI store location with [`GCM_DPAPI_STORE_PATH`][gcm-dpapi-store-path]|Windows\n`keychain`|macOS Keychain.|macOS\n`secretservice`|[freedesktop.org Secret Service API][freedesktop-ss] via [libsecret][libsecret] (requires a graphical interface to unlock secret collections).|Linux\n`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility][passwordstore] (requires GPG and `pass` to initialize the store).|macOS, Linux\n`cache`|Git's built-in [credential cache][git-credential-cache].|Windows, macOS, Linux\n`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`][gcm-plaintext-store-path].|Windows, macOS, Linux\n`none`|Do not store credentials via GCM.|Windows, macOS, Linux\n\n#### Windows\n\n```batch\nSET GCM_CREDENTIAL_STORE=\"gpg\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_CREDENTIAL_STORE=\"gpg\"\n```\n\n**Also see: [credential.credentialStore][credential-credentialstore]**\n\n---\n\n### GCM_CREDENTIAL_CACHE_OPTIONS\n\nPass [options][git-cache-options]\nto the Git credential cache when [`GCM_CREDENTIAL_STORE`][gcm-credential-store]\nis set to `cache`. This allows you to select a different amount\nof time to cache credentials (the default is 900 seconds) by passing\n`\"--timeout <seconds>\"`. Use of other options like `--socket` is untested\nand unsupported, but there's no reason it shouldn't work.\n\nDefaults to empty.\n\n#### Windows\n\n```batch\nSET GCM_CREDENTIAL_CACHE_OPTIONS=\"--timeout 300\"\n```\n\n#### macOS/Linux\n\n```shell\nexport GCM_CREDENTIAL_CACHE_OPTIONS=\"--timeout 300\"\n```\n\n**Also see: [credential.cacheOptions][credential-cacheoptions]**\n\n---\n\n### GCM_PLAINTEXT_STORE_PATH\n\nSpecify a custom directory to store plaintext credential files in when\n[`GCM_CREDENTIAL_STORE`][gcm-credential-store] is set to `plaintext`.\n\nDefaults to the value `~/.gcm/store` or `%USERPROFILE%\\.gcm\\store`.\n\n#### Windows\n\n```batch\nSETX GCM_PLAINTEXT_STORE_PATH=D:\\credentials\n```\n\n#### macOS/Linux\n\n```shell\nexport GCM_PLAINTEXT_STORE_PATH=/mnt/external-drive/credentials\n```\n\n**Also see: [credential.plaintextStorePath][credential-plain-text-store]**\n\n---\n\n### GCM_DPAPI_STORE_PATH\n\nSpecify a custom directory to store DPAPI protected credential files in when\n[`GCM_CREDENTIAL_STORE`][gcm-credential-store] is set to `dpapi`.\n\nDefaults to the value `%USERPROFILE%\\.gcm\\dpapi_store`.\n\n#### Windows\n\n```batch\nSETX GCM_DPAPI_STORE_PATH=D:\\credentials\n```\n\n**Also see: [credential.dpapiStorePath][credential-dpapi-store-path]**\n\n---\n\n### GCM_GPG_PATH\n\nSpecify the path (_including_ the executable name) to the version of `gpg` used\nby `pass` (`gpg2` if present, otherwise `gpg`). This is primarily meant to allow\nmanual resolution of the conflict that occurs on legacy Linux systems with\nparallel installs of `gpg` and `gpg2`.\n\nIf not specified, GCM defaults to using the version of `gpg2` on the `$PATH`,\nfalling back on `gpg` if `gpg2` is not found.\n\n#### macOS/Linux\n\n```bash\nexport GCM_GPG_PATH=\"/usr/local/bin/gpg2\"\n```\n\n_No configuration equivalent._\n\n---\n\n### GCM_MSAUTH_FLOW\n\nSpecify which authentication flow should be used when performing Microsoft\nauthentication and an interactive flow is required.\n\nDefaults to `auto`.\n\n**Note:** If [`GCM_MSAUTH_USEBROKER`][gcm-msauth-usebroker] is set to `true`\nand the operating system authentication broker is available, all flows will be\ndelegated to the broker. If both of those things are true, then the value of\n`GCM_MSAUTH_FLOW` has no effect.\n\nValue|Authentication Flow\n-|-\n`auto` _(default)_|Select the best option depending on the current environment and platform.\n`embedded`|Show a window with embedded web view control.\n`system`|Open the user's default web browser.\n`devicecode`|Show a device code.\n\n#### Windows\n\n```batch\nSET GCM_MSAUTH_FLOW=\"devicecode\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_MSAUTH_FLOW=\"devicecode\"\n```\n\n**Also see: [credential.msauthFlow][credential-msauth-flow]**\n\n---\n\n### GCM_MSAUTH_USEBROKER _(experimental)_\n\nUse the operating system account manager where available.\n\nDefaults to `false`. In certain cloud hosted environments when using a work or\nschool account, such as [Microsoft DevBox][devbox], the default is `true`.\n\nThese defaults are subject to change in the future.\n\n_**Note:** before you enable this option on Windows, please\n[review the details][windows-broker] about what this means to your local Windows\nuser account._\n\nValue|Description\n-|-\n`true`|Use the operating system account manager as an authentication broker.\n`false` _(default)_|Do not use the broker.\n\n#### Windows\n\n```batch\nSET GCM_MSAUTH_USEBROKER=\"true\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_MSAUTH_USEBROKER=\"false\"\n```\n\n**Also see: [credential.msauthUseBroker][credential-msauth-usebroker]**\n\n---\n\n### GCM_MSAUTH_USEDEFAULTACCOUNT _(experimental)_\n\nUse the current operating system account by default when the broker is enabled.\n\nDefaults to `false`. In certain cloud hosted environments when using a work or\nschool account, such as [Microsoft DevBox][devbox], the default is `true`.\n\nThese defaults are subject to change in the future.\n\nValue|Description\n-|-\n`true`|Use the current operating system account by default.\n`false` _(default)_|Do not assume any account to use by default.\n\n#### Windows\n\n```batch\nSET GCM_MSAUTH_USEDEFAULTACCOUNT=\"true\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_MSAUTH_USEDEFAULTACCOUNT=\"false\"\n```\n\n**Also see: [credential.msauthUseDefaultAccount][credential-msauth-usedefaultaccount]**\n\n---\n\n### GCM_AZREPOS_CREDENTIALTYPE\n\nSpecify the type of credential the Azure Repos host provider should return.\n\nDefaults to the value `pat`. In certain cloud hosted environments when using a\nwork or school account, such as [Microsoft DevBox][devbox], the default value is\n`oauth`.\n\nValue|Description\n-|-\n`pat`|Azure DevOps personal access tokens\n`oauth`|Microsoft identity OAuth tokens (AAD or MSA tokens)\n\nMore information about Azure Access tokens can be found [here][azure-access-tokens].\n\n#### Windows\n\n```batch\nSET GCM_AZREPOS_CREDENTIALTYPE=\"oauth\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_AZREPOS_CREDENTIALTYPE=\"oauth\"\n```\n\n**Also see: [credential.azreposCredentialType][credential-azrepos-credential-type]**\n\n---\n\n### GCM_AZREPOS_MANAGEDIDENTITY\n\nUse a [Managed Identity][managed-identity] to authenticate with Azure Repos.\n\nThe value `system` will tell GCM to use the system-assigned Managed Identity.\n\nTo specify a user-assigned Managed Identity, use the format `id://{clientId}`\nwhere `{clientId}` is the client ID of the Managed Identity. Alternatively any\nGUID-like value will also be interpreted as a user-assigned Managed Identity\nclient ID.\n\nTo specify a Managed Identity associated with an Azure resource, you can use the\nformat `resource://{resourceId}` where `{resourceId}` is the ID of the resource.\n\nFor more information about managed identities, see the Azure DevOps\n[documentation][azrepos-sp-mid].\n\nValue|Description\n-|-\n`system`|System-Assigned Managed Identity\n`[guid]`|User-Assigned Managed Identity with the specified client ID\n`id://[guid]`|User-Assigned Managed Identity with the specified client ID\n`resource://[guid]`|User-Assigned Managed Identity for the associated resource\n\n#### Windows\n\n```batch\nSET GCM_AZREPOS_MANAGEDIDENTITY=\"id://11111111-1111-1111-1111-111111111111\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_AZREPOS_MANAGEDIDENTITY=\"id://11111111-1111-1111-1111-111111111111\"\n```\n\n**Also see: [credential.azreposManagedIdentity][credential-azrepos-managedidentity]**\n\n---\n\n### GCM_AZREPOS_SERVICE_PRINCIPAL\n\nSpecify the client and tenant IDs of a [service principal][service-principal]\nto use when performing Microsoft authentication for Azure Repos.\n\nThe value of this setting should be in the format: `{tenantId}/{clientId}`.\n\nYou must also set at least one authentication mechanism if you set this value:\n\n- [GCM_AZREPOS_SP_SECRET][gcm-azrepos-sp-secret]\n- [GCM_AZREPOS_SP_CERT_THUMBPRINT][gcm-azrepos-sp-cert-thumbprint]\n\nFor more information about service principals, see the Azure DevOps\n[documentation][azrepos-sp-mid].\n\n#### Windows\n\n```batch\nSET GCM_AZREPOS_SERVICE_PRINCIPAL=\"11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_AZREPOS_SERVICE_PRINCIPAL=\"11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222\"\n```\n\n**Also see: [credential.azreposServicePrincipal][credential-azrepos-sp]**\n\n---\n\n### GCM_AZREPOS_SP_SECRET\n\nSpecifies the client secret for the [service principal][service-principal] when\nperforming Microsoft authentication for Azure Repos with\n[GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-sp] set.\n\n#### Windows\n\n```batch\nSET GCM_AZREPOS_SP_SECRET=\"da39a3ee5e6b4b0d3255bfef95601890afd80709\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_AZREPOS_SP_SECRET=\"da39a3ee5e6b4b0d3255bfef95601890afd80709\"\n```\n\n**Also see: [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret]**\n\n---\n\n### GCM_AZREPOS_SP_CERT_THUMBPRINT\n\nSpecifies the thumbprint of a certificate to use when authenticating as a\n[service principal][service-principal] for Azure Repos when\n[GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-sp] is set.\n\n#### Windows\n\n```batch\nSET GCM_AZREPOS_SP_CERT_THUMBPRINT=\"9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_AZREPOS_SP_CERT_THUMBPRINT=\"9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc\"\n```\n\n**Also see: [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint]**\n\n---\n\n### GCM_AZREPOS_SP_CERT_SEND_X5C\n\nWhen using a certificate for service principal authentication, this configuration\nspecifies whether the X5C claim should be should be sent to the STS. Sending the x5c\nenables application developers to achieve easy certificate rollover in Azure AD:\nthis method will send the public certificate to Azure AD along with the token request,\nso that Azure AD can use it to validate the subject name based on a trusted issuer\npolicy. This saves the application admin from the need to explicitly manage the\ncertificate rollover. For details see [https://aka.ms/msal-net-sni](https://aka.ms/msal-net-sni).\n\n#### Windows\n\n```batch\nSET GCM_AZREPOS_SP_CERT_SEND_X5C=\"true\"\n```\n\n#### macOS/Linux\n\n```bash\nexport GCM_AZREPOS_SP_CERT_SEND_X5C=\"true\"\n```\n\n**Also see: [credential.azreposServicePrincipalCertificateSendX5C][credential-azrepos-sp-cert-x5c]**\n\n---\n\n### GIT_TRACE2\n\nTurns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format\ndocumentation][trace2-normal-docs] for more details.\n\n#### Windows\n\n```batch\nSET GIT_TRACE2=%UserProfile%\\log.normal\n```\n\n#### macOS/Linux\n\n```bash\nexport GIT_TRACE2=~/log.normal\n```\n\nIf the value of `GIT_TRACE2` is a full path to a file in an existing directory,\nlogs are appended to the file.\n\nIf the value of `GIT_TRACE2` is `true` or `1`, logs are written to standard\nerror.\n\nDefaults to disabled.\n\n**Also see: [trace2.normalFormat][trace2-normal-config]**\n\n---\n\n### GIT_TRACE2_EVENT\n\nTurns on Trace2 Event Format tracing - see [Git's Trace2 Event Format\ndocumentation][trace2-event-docs] for more details.\n\n#### Windows\n\n```batch\nSET GIT_TRACE2_EVENT=%UserProfile%\\log.event\n```\n\n#### macOS/Linux\n\n```bash\nexport GIT_TRACE2_EVENT=~/log.event\n```\n\nIf the value of `GIT_TRACE2_EVENT` is a full path to a file in an existing\ndirectory, logs are appended to the file.\n\nIf the value of `GIT_TRACE2_EVENT` is `true` or `1`, logs are written to\nstandard error.\n\nDefaults to disabled.\n\n**Also see: [trace2.eventFormat][trace2-event-config]**\n\n---\n\n### GIT_TRACE2_PERF\n\nTurns on Trace2 Performance Format tracing - see [Git's Trace2 Performance\nFormat documentation][trace2-performance-docs] for more details.\n\n#### Windows\n\n```batch\nSET GIT_TRACE2_PERF=%UserProfile%\\log.perf\n```\n\n#### macOS/Linux\n\n```bash\nexport GIT_TRACE2_PERF=~/log.perf\n```\n\nIf the value of `GIT_TRACE2_PERF` is a full path to a file in an existing\ndirectory, logs are appended to the file.\n\nIf the value of `GIT_TRACE2_PERF` is `true` or `1`, logs are written to\nstandard error.\n\nDefaults to disabled.\n\n**Also see: [trace2.perfFormat][trace2-performance-config]**\n\n[autodetect]: autodetect.md\n[azure-access-tokens]: azrepos-users-and-tokens.md\n[configuration]: configuration.md\n[credential-allowwindowsauth]: configuration.md#credentialallowwindowsauth\n[credential-allowunsaferemotes]: configuration.md#credentialallowunsaferemotes\n[credential-authority]: configuration.md#credentialauthority-deprecated\n[credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout\n[credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype\n[credential-azrepos-managedidentity]: configuration.md#credentialazreposmanagedidentity\n[credential-bitbucketauthmodes]: configuration.md#credentialbitbucketAuthModes\n[credential-cacheoptions]: configuration.md#credentialcacheoptions\n[credential-credentialstore]: configuration.md#credentialcredentialstore\n[credential-debug]: configuration.md#credentialdebug\n[credential-dpapi-store-path]: configuration.md#credentialdpapistorepath\n[credential-githubaccountfiltering]: configuration.md#credentialgitHubAccountFiltering\n[credential-githubauthmodes]: configuration.md#credentialgitHubAuthModes\n[credential-gitlabauthmodes]: configuration.md#credentialgitLabAuthModes\n[credential-guiprompt]: configuration.md#credentialguiprompt\n[credential-guisoftwarerendering]: configuration.md#credentialguisoftwarerendering\n[credential-httpproxy]: configuration.md#credentialhttpProxy-deprecated\n[credential-interactive]: configuration.md#credentialinteractive\n[credential-namespace]: configuration.md#credentialnamespace\n[credential-msauth-flow]: configuration.md#credentialmsauthflow\n[credential-msauth-usebroker]: configuration.md#credentialmsauthusebroker-experimental\n[credential-msauth-usedefaultaccount]: configuration.md#credentialmsauthusedefaultaccount-experimental\n[credential-plain-text-store]: configuration.md#credentialplaintextstorepath\n[credential-provider]: configuration.md#credentialprovider\n[credential-stores]: credstores.md\n[credential-trace]: configuration.md#credentialtrace\n[credential-trace-secrets]: configuration.md#credentialtracesecrets\n[credential-trace-msauth]: configuration.md#credentialtracemsauth\n[default-values]: enterprise-config.md\n[devbox]: https://azure.microsoft.com/en-us/products/dev-box\n[freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/\n[gcm]: usage.md\n[gcm-interactive]: #gcm_interactive\n[gcm-credential-store]: #gcm_credential_store\n[gcm-dpapi-store-path]: #gcm_dpapi_store_path\n[gcm-plaintext-store-path]: #gcm_plaintext_store_path\n[gcm-msauth-usebroker]: #gcm_msauth_usebroker-experimental\n[git-cache-options]: https://git-scm.com/docs/git-credential-cache#_options\n[git-credential-cache]: https://git-scm.com/docs/git-credential-cache\n[git-httpproxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy\n[github-emu]: https://docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/using-enterprise-managed-users-for-iam/about-enterprise-managed-users\n[network-http-proxy]: netconfig.md#http-proxy\n[libsecret]: https://wiki.gnome.org/Projects/Libsecret\n[managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview\n[migration-guide]: migration.md#gcm_authority\n[passwordstore]: https://www.passwordstore.org/\n[trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target\n[trace2-normal-config]: configuration.md#trace2normalTarget\n[trace2-event-docs]: https://git-scm.com/docs/api-trace2#_the_event_format_target\n[trace2-event-config]: configuration.md#trace2eventTarget\n[trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target\n[trace2-performance-config]: configuration.md#trace2perfTarget\n[windows-broker]: windows-broker.md\n[service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals\n[azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity\n[gcm-azrepos-sp]: #gcm_azrepos_service_principal\n[gcm-azrepos-sp-secret]: #gcm_azrepos_sp_secret\n[gcm-azrepos-sp-cert-thumbprint]: #gcm_azrepos_sp_cert_thumbprint\n[gcm-azrepos-sp-cert-x5c]: #gcm_azrepos_sp_cert_send_x5c\n[credential-azrepos-sp]: configuration.md#credentialazreposserviceprincipal\n[credential-azrepos-sp-secret]: configuration.md#credentialazreposserviceprincipalsecret\n[credential-azrepos-sp-cert-thumbprint]: configuration.md#credentialazreposserviceprincipalcertificatethumbprint\n[credential-azrepos-sp-cert-x5c]: configuration.md#credentialazreposserviceprincipalcertificatesendx5c\n"
  },
  {
    "path": "docs/faq.md",
    "content": "# Frequently asked questions\n\n## Authentication problems\n\n### Q: I got an error trying to push/pull/clone. What do I do now?\n\nPlease follow these steps to diagnose or resolve the problem:\n\n1. Check if you can access the remote repository in a web browser. If you\ncannot, this is probably a permission problem and you should follow up with the\nrepository administrator for access. Execute `git remote -v` from a terminal to\nshow the remote URL.\n\n1. If you are experiencing a Git authentication problem using an editor, IDE or\nother tool, try performing the same operation from the terminal. Does this still\nfail? If the operation succeeds from the terminal please include details of the\nspecific tool and version in any issue reports.\n\n1. Set the environment variable `GCM_TRACE` and run the Git operation again.\nFind instructions in the [environment doc][env-trace].\n\n1. If all else fails, create an issue [here][create-issue], making sure to\ninclude the trace log.\n\n### Q: I got an error saying unsecure HTTP is not supported\n\nTo keep your data secure, Git Credential Manager will not send credentials for\nAzure Repos, Azure DevOps Server (TFS), GitHub, and Bitbucket, over HTTP\nconnections that are not secured using TLS (HTTPS).\n\nPlease make sure your remote URLs use \"https://\" rather than \"http://\".\n\n### Q: I got an authentication error and I am behind a network proxy\n\nYou probably need to configure Git and GCM to use a proxy. Please see detailed\ninformation in the [network config doc][netconfig-http-proxy].\n\n### Q: I'm getting errors about picking a credential store on Linux\n\nOn Linux you must [select and configure a credential store][credstores], as due\nto the varied nature of distributions and installations, we cannot guarantee a\nsuitable storage solution is available.\n\n## About the project\n\n### Q: How does this project relate to [Git Credential Manager for Windows][gcm-windows] and [Git Credential Manager for Mac and Linux][gcm-linux]?\n\nGit Credential Manager for Windows (GCM Windows) is a .NET Framework-based Git\ncredential helper which runs on Windows. Likewise the Git Credential Manager for\nMac and Linux (Java GCM) is a Java-based Git credential helper that runs only on\nmacOS and Linux. Although both of these projects aim to solve the same problem\n(providing seamless multi-factor HTTPS authentication with Git), they are based\non different codebases and languages which is becoming hard to manage to ensure\nfeature parity.\n\nGit Credential Manager (GCM; this project) aims to replace both GCM Windows and\nJava GCM with a unified codebase which should be easier to maintain and enhance\nin the future.\n\n### Q: Does this mean GCM for Windows (.NET Framework-based) is deprecated?\n\nYes. Git Credential Manager for Windows (GCM Windows) is no longer receiving\nupdates and fixes. All development effort has now been directed to GCM. GCM is\navailable as an credential helper option in Git for Windows 2.28, and will be\nmade the default helper in 2.29.\n\n### Q: Does this mean the Java-based GCM for Mac/Linux is deprecated?\n\nYes. Usage of Git Credential Manager for Mac and Linux (Java GCM) should be\nreplaced with GCM or SSH keys. If you wish to install GCM on macOS or Linux,\nplease follow the [download and installation instructions][download-and-install].\n\n### Q: I want to use SSH\n\nGCM is only useful for HTTP(S)-based remotes. Git supports SSH out-of-the box so\nyou shouldn't need to install anything else.\n\nTo use SSH please follow the below links:\n\n- [Azure DevOps][azure-ssh]\n- [GitHub][github-ssh]\n- [Bitbucket][bitbucket-ssh]\n\n### Q: Are HTTP(S) remotes preferred over SSH?\n\nNo, neither are \"preferred\". SSH isn't going away, and is supported \"natively\"\nin Git.\n\n### Q: Why did you not just port the existing GCM Windows codebase from .NET Framework to .NET Core?\n\nGCM Windows was not designed with a cross-platform architecture.\n\n### What level of support does GCM have?\n\nSupport will be best-effort. We would really appreciate your feedback to make\nthis a great experience across each platform we support.\n\n### Q: Why does GCM not support operating system/distribution 'X', or Git hosting provider 'Y'?\n\nThe likely answer is we haven't gotten around to that yet! 🙂\n\nWe are working on ensuring support for the Windows, macOS, and Ubuntu operating\nsystem, as well as the following Git hosting providers: Azure Repos, Azure\nDevOps Server (TFS), GitHub, and Bitbucket.\n\nWe are happy to accept proposals and/or contributions to enable GCM to run on\nother platforms and Git host providers. Thank you!\n\n## Technical\n\n### Why is the `credential.useHttpPath` setting required for `dev.azure.com`?\n\nDue to the design of Git and credential helpers such as GCM, we need this\nsetting to make Git use the full remote URL (including the path component) when\ncommunicating with GCM. The new `dev.azure.com` format of Azure DevOps URLs\nmeans the account name is now part of the path component (for example:\n`https://dev.azure.com/contoso/...`). The Azure DevOps account name is required\nin order to resolve the correct authority for authentication (which Azure AD\ntenant backs this account, or if it is backed by Microsoft personal accounts).\n\nIn the older GCM for Windows product, the solution to the same problem was a\n\"hack\". GCM for Windows would walk the process tree looking for the\n`git-remote-https.exe` process, and attempt to read/parse the process\nenvironment block looking for the command line arguments (that contained the\nfull remote URL). This is fragile and not a cross-platform solution, hence the\nneed for the `credential.useHttpPath` setting with GCM.\n\n### Why does GCM take so long at startup the first time?\n\nGCM will [autodetect][autodetect] what kind of Git host it's talking to. GitHub,\nBitbucket, and Azure DevOps each have their own form(s) of authentication, plus\nthere's a \"generic\" username and password option.\n\nFor the hosted versions of these services, GCM can guess from the URL which\nservice to use. But for on-premises versions which would have unique URLs, GCM\nwill probe with a network call. GCM caches the results of the probe, so it\nshould be faster on the second and later invocations.\n\nIf you know which provider you're talking to and want to avoid the probe, that's\npossible. You can explicitly tell GCM which provider to use for a URL\n\"example.com\" like this:\n\nProvider|Command\n-|-\nGitHub|`git config --global credential.https://example.com.provider github`\nBitbucket|`git config --global credential.https://example.com.provider bitbucket`\nAzure DevOps|`git config --global credential.https://example.com.provider azure-repos`\nGeneric|`git config --global credential.https://example.com.provider generic`\n\n### How do I fix \"Could not create SSL/TLS secure channel\" errors on Windows 7?\n\nThis likely indicates that you don't have newer TLS versions available. Please\n[follow Microsoft's guide][enable-windows-ssh] for enabling TLS 1.1 and 1.2 on\nyour machine, specifically the **SChannel** instructions. You'll need to be on\nat least Windows 7 SP1, and in the end you should have a `TLS 1.2` key with\n`DisabledByDefault` set to `0`. You can also read\n[more from Microsoft][windows-server-tls] on this change.\n\n### How do I use GCM with Windows Subsystem for Linux (WSL)?\n\nFollow the instructions in [our WSL guide][wsl] carefully. Especially note the\nneed to run `git config --global credential.https://dev.azure.com.useHttpPath true`\n_within_ WSL if you're using Azure DevOps.\n\n### Does GCM work with multiple users? If so, how?\n\nThat's a fairly complicated question to answer, but in short, yes. See\n[our document on multiple users][multiple-users] for details.\n\n### How can I disable GUI dialogs and prompts?\n\nThere are various environment variables and configuration options available to\ncustomize how GCM will prompt you (or not) for input. Please see the following:\n\n- [`GCM_INTERACTIVE`][env-interactive] / [`credential.interactive`][config-interactive]\n- [`GCM_GUI_PROMPT`][env-gui-prompt] / [`credential.guiPrompt`][config-gui-prompt]\n- [`GIT_TERMINAL_PROMPT`][git-term-prompt] (note this is a _Git setting_ that\nwill affect Git as well as GCM)\n\n### How can I extend GUI prompts/integrate prompts with my application?\n\nApplication developers who use Git - think Visual Studio, GitKraken, etc. - may\nwant to replace the GCM default UI with prompts styled to look like their\napplication. This isn't complicated (though it is a bit of work).\n\nYou can replace the GUI prompts of the Bitbucket and GitHub host providers\nspecifically by using the `credential.gitHubHelper`/`credential.bitbucketHelper`\nsettings or `GCM_GITHUB_HELPER`/`GCM_BITBUCKET_HELPER` environment variables.\n\nSet these variables to the path of an external helper executable that responds\nto the requests as the bundled UI helpers do. See the current `--help` documents\nfor the bundled UI helpers (`GitHub.UI`/`Atlassian.Bitbucket.UI`) for more\ninformation.\n\nYou may also set these variables to the empty string `\"\"` to force terminal/\ntext-based prompts instead.\n\n### How do I revoke consent for GCM for GitHub.com?\n\nIn your GitHub user settings, navigate to\n[Integrations > Applications > Authorized OAuth Apps > Git Credential Manager][github-connected-apps]\nand pick \"Revoke access\".\n\n![Revoke GCM OAuth app access][github-oauthapp-revoke]\n\nAfter revoking access, any tokens created by GCM will be invalidated and can no\nlonger be used to access your repositories. The next time GCM attempts to access\nGitHub.com you will be prompted to consent again.\n\n### I used the install from source script to install GCM on my Linux distribution. Now how can I uninstall GCM and its dependencies?\n\nPlease see full instructions [here][linux-uninstall-from-src].\n\n### How do I revoke access for a GitLab OAuth application?\n\nThere are some scenarios (e.g. updated scopes) for which you will need to\nmanually revoke and re-authorize access for a GitLab OAuth application. You can\ndo so by:\n\n1. Navigating to [the **Applications** page within your **User Settings**][gitlab-apps].\n2. Scrolling to **Authorized applications**.\n3. Clicking the **Revoke** button next to the name of the application for which\nyou would like to revoke access (Git Credential Manager is used here for\ndemonstration purposes).\n\n   ![Button to revoke GitLab OAuth Application access][gitlab-oauthapp-revoke]\n4. Waiting for a notification stating **The application was revoked access**.\n\n   ![Notifaction of successful revocation][gitlab-oauthapp-revoked]\n5. Re-authorizing the application with the new scope (GCM should automatically\ninitiate this flow for you next time access is requested).\n\n### Q: What do the `configure` and `unconfigure` commands do?\n\n#### `configure`\n\nThe `configure` command will set up Git to use GCM exclusively as the credential\nhelper. The `configure` command is automatically called by the installers for\nWindows and macOS, but you can also run it manually.\n\nIt will also set Git to provide the full remote URL (including path) to\ncredential helpers for Azure Repos remotes using the `dev.azure.com` URL format.\nThis is required in order to be to able to correctly identify the correct\nauthority for that Azure DevOps organization.\n\nSpecifically, the `configure` command will modify your user Git configuration to\ninclude the following lines:\n\n```ini\n[credential]\n    helper =\n    helper = <path-to-gcm>\n[credential \"https://dev.azure.com\"]\n    useHttpPath = true\n```\n\n..where `<path-to-gcm>` is the absolute path to the GCM executable.\n\nThe empty `helper =` line makes sure that existing credential helpers that may\nbe set in the system Git configuration are not used. For more details see the\n[credential.helper][helper-config-docs].\n\nIf you pass the `--system` option, the `configure` command will instead modify\nthe system Git configuration. This is useful if you want to set up GCM for all\nusers on a machine.\n\n#### `unconfigure`\n\nThis command essentially undoes what the `configure` command does. It will check\nyour Git configuration for the lines added by the `configure` command and remove\nthem. The `unconfigure` command is run by the uninstaller for Windows and the\nuninstall script on macOS.\n\nOn Windows, if run with the `--system` option, the `unconfigure` command will\nalso ensure that the `credential.helper` setting in the system Git configuration\nis not removed and is left as `manager`, the default set by Git for Windows.\n\n[autodetect]: autodetect.md\n[azure-ssh]: https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops\n[bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html\n[config-gui-prompt]: configuration.md#credentialguiprompt\n[config-interactive]: configuration.md#credentialinteractive\n[create-issue]: https://github.com/git-ecosystem/git-credential-manager/issues/create\n[credstores]: credstores.md\n[download-and-install]: ../README.md#download-and-install\n[enable-windows-ssh]: https://support.microsoft.com/topic/update-to-enable-tls-1-1-and-tls-1-2-as-default-secure-protocols-in-winhttp-in-windows-c4bd73d2-31d7-761e-0178-11268bb10392\n[env-gui-prompt]: environment.md#GCM_GUI_PROMPT\n[env-interactive]: environment.md#GCM_INTERACTIVE\n[env-trace]: environment.md#GCM_TRACE\n[gcm-linux]: https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux\n[gcm-windows]: https://github.com/Microsoft/Git-Credential-Manager-for-Windows\n[git-term-prompt]: https://git-scm.com/docs/git#Documentation/git.txt-codeGITTERMINALPROMPTcode\n[github-connected-apps]: https://github.com/settings/connections/applications/0120e057bd645470c1ed\n[github-oauthapp-revoke]: img/github-oauthapp-revoke.png\n[github-ssh]: https://help.github.com/en/articles/connecting-to-github-with-ssh\n[gitlab-apps]: https://gitlab.com/-/profile/applications\n[gitlab-oauthapp-revoke]: ./img/gitlab-oauthapp-revoke.png\n[gitlab-oauthapp-revoked]: ./img/gitlab-oauthapp-revoked.png\n[helper-config-docs]: https://git-scm.com/docs/gitcredentials#Documentation/gitcredentials.txt-helper\n[multiple-users]: multiple-users.md\n[netconfig-http-proxy]: netconfig.md#http-proxy\n[linux-uninstall-from-src]: ./linux-fromsrc-uninstall.md\n[windows-server-tls]: https://docs.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn786418(v=ws.11)#tls-12\n[wsl]: wsl.md\n"
  },
  {
    "path": "docs/generic-oauth.md",
    "content": "# Generic Host Provider OAuth\n\nMany Git hosts use the popular standard OAuth2 or OpenID Connect (OIDC)\nauthentication mechanisms to secure repositories they host.\nGit Credential Manager supports any generic OAuth2-based Git host by simply\nsetting some configuration.\n\n## Registering an OAuth application\n\nIn order to use GCM with a Git host that supports OAuth you must first have\nregistered an OAuth application with your host. The instructions on how to do\nthis can be found with your Git host provider's documentation.\n\nWhen registering a new application, you should make sure to set an HTTP-based\nredirect URL that points to `localhost`; for example:\n\n```text\nhttp://localhost\nhttp://localhost:<port>\nhttp://127.0.0.1\nhttp://127.0.0.1:<port>\n```\n\nNote that you cannot use an HTTPS redirect URL. GCM does not require a specific\nport number be used; if your Git host requires you to specify a port number in\nthe redirect URL then GCM will use that. Otherwise an available port will be\nselected at the point authentication starts.\n\nYou must ensure that all scopes required to read and write to Git repositories\nhave been granted for the application or else credentials that are generated\nwill cause errors when pushing or fetching using Git.\n\nAs part of the registration process you should also be given a Client ID and,\noptionally, a Client Secret. You will need both of these to configure GCM.\n\n## Configure GCM\n\nIn order to configure GCM to use OAuth with your Git host you need to set the\nfollowing values in your Git configuration:\n\n- Client ID\n- Client Secret (optional)\n- Redirect URL (optional, defaults to `http://127.0.0.1`)\n- Scopes (optional)\n- OAuth Endpoints\n  - Authorization Endpoint\n  - Token Endpoint\n  - Device Code Authorization Endpoint (optional)\n\nOAuth endpoints can be found by consulting your Git host's OAuth app development\ndocumentation. The URLs can be either absolute or relative to the host name;\nfor example: `https://example.com/oauth/authorize` or `/oauth/authorize`.\n\nIn order to set these values, you can run the following commands, where `<HOST>`\nis the hostname of your Git host:\n\n```shell\ngit config --global credential.<HOST>.oauthClientId <ClientID>\ngit config --global credential.<HOST>.oauthClientSecret <ClientSecret>\ngit config --global credential.<HOST>.oauthRedirectUri <RedirectURL>\ngit config --global credential.<HOST>.oauthAuthorizeEndpoint <AuthEndpoint>\ngit config --global credential.<HOST>.oauthTokenEndpoint <TokenEndpoint>\ngit config --global credential.<HOST>.oauthScopes <Scopes>\ngit config --global credential.<HOST>.oauthDeviceEndpoint <DeviceEndpoint>\n```\n\n**Example commands:**\n\n- `git config --global credential.https://example.com.oauthClientId C33F2751FB76`\n\n- `git config --global credential.https://example.com.oauthScopes \"code:write profile:read\"`\n\n**Example Git configuration**\n\n```ini\n[credential \"https://example.com\"]\n    oauthClientId = 9d886e36-5771-4f2b-8c8b-420c68ad5baa\n    oauthClientSecret = 4BC5BD4704EAE28FD832\n    oauthRedirectUri = \"http://127.0.0.1\"\n    oauthAuthorizeEndpoint = \"/login/oauth/authorize\"\n    oauthTokenEndpoint = \"/login/oauth/token\"\n    oauthDeviceEndpoint = \"/login/oauth/device\"\n    oauthScopes = \"code:write profile:read\"\n    oauthDefaultUserName = \"OAUTH\"\n    oauthUseClientAuthHeader = false\n```\n\n### Additional configuration\n\nDepending on the specific implementation of OAuth with your Git host you may\nalso need to specify additional behavior.\n\n#### Token user name\n\nIf your Git host requires that you specify a username to use with OAuth tokens\nyou can either include the username in the Git remote URL, or specify a default\noption via Git configuration.\n\nExample Git remote with username: `https://username@example.com/repo.git`.\nIn order to use special characters you need to URL encode the values; for\nexample `@` becomes `%40`.\n\nBy default GCM uses the value `OAUTH-USER` unless specified in the remote URL,\nor overridden using the `credential.<HOST>.oauthDefaultUserName` configuration.\n\n#### Include client authentication in headers\n\nIf your Git host's OAuth implementation has specific requirements about whether\nthe client ID and secret should or should not be included in an `Authorization`\nheader during OAuth requests, you can control this using the following setting:\n\n```shell\ngit config --global credential.<HOST>.oauthUseClientAuthHeader <true|false>\n```\n\nThe default behavior is to include these values; i.e., `true`.\n"
  },
  {
    "path": "docs/github-apideprecation.md",
    "content": "# GitHub Authentication Deprecation\n\n## What's going on?\n\nGitHub now [requires token-based authentication][token-auth] to\ncall their APIs, and in the future, use Git itself.\n\nThis means Git credential helpers such as [Git Credential Manager (GCM) for\nWindows][gcm-windows], and old versions of [GCM][gcm] that offer\nusername/password flows **will not be able to create new access tokens** for\naccessing Git repositories.\n\nIf you already have tokens generated by Git credential helpers like GCM for\nWindows, they will continue to work until they expire or are revoked/deleted.\n\n## What should I do now?\n\n### Windows command-line users\n\nThe best thing to do right now is upgrade to the latest Git for Windows (at\nleast version 2.29), which includes a version of Git Credential Manager that\nuses supported OAuth token-based authentication.\n\n[Download the latest Git for Windows ⬇️][git-windows]\n\n### Visual Studio users\n\nPlease update to the latest supported release of Visual Studio, that includes\nGCM and support for OAuth token-based authentication.\n\n- [Visual Studio 2019 ⬇️][vs-2019]\n- [Visual Studio 2017 ⬇️][vs-2017]\n\n### SSH, macOS, and Linux users\n\nIf you are using SSH this change does **not** affect you.\n\nIf you are using an older version of Git Credential Manager (before\n2.0.124-beta) please upgrade to the latest version following [these\ninstructions][gcm-install].\n\n## What if I cannot upgrade Git for Windows?\n\nIf you are unable to upgrade Git for Windows, you can manually install Git\nCredential Manager as a standalone install. This will override the older,\nGCM for Windows bundled with the Git for Windows installation.\n\n[Download Git Credential Manager standalone ⬇️][gcm-latest]\n\n## What if I cannot use Git Credential Manager?\n\nIf you are unable to use Git Credential Manager due to a bug or\ncompatibility issue we'd [like to know why][gcm-new-issue]!\n\n## Help! I cannot make any changes to my Windows machine without an Administrator\n\nIf you do not have permission to change your installation (for example in a\ncorporate environment) you can use the per-user installer. Check out the [latest\nrelease][gcm-latest] and download the `gcmcoreuser-win-*.exe`\nexecutable.\n\n### Help! I still cannot or don't want to install anything\n\nThere is a workaround which should work and doesn't require installing anything.\n\n1. Tell your system administrator they should start planning to upgrade the\n   installed version of Git for Windows to at least 2.29! 😁\n\n1. [Create a new personal access token][github-pat] (see official\n   [documentation][github-pat-docs])\n\n1. Enter a name (\"note\") for the token and ensure the `repo`, `gist`, and\n   `workflow` scopes are selected:\n   ![image][github-pat-note-image]\n   ...\n   ![image][github-pat-repo-scope-image]\n   ...\n   ![image][github-pat-gist-scope-image]\n   ...\n   ![image][github-pat-workflow-scope-image]\n\n1. Click \"Generate Token\"\n\n   ![image][github-generate-pat-image]\n\n1. **[IMPORTANT]** Keep the resulting page open as this contains your new token\n   (this will only be displayed once!)\n\n   ![image][github-display-pat-image]\n\n1. Save the generated PAT in the Windows Credential Manager:\n\n   1. If you prefer to use the command-line, open a command prompt (cmd.exe) and\n      type the following:\n\n      ```bash\n      cmdkey /generic:git:https://github.com /user:PersonalAccessToken /pass\n      ```\n\n      You will be prompted to enter a password – copy the newly generated PAT in\n      step 4 and paste it here, and press the `Enter` key\n\n      ![image][windows-cli-save-pat-image]\n\n   1. If you do not wish to use the command-line, [open the Credential Manager\n      via Control Panel][windows-credential-manager]\n      and select the \"Windows Credentials\" tab.\n\n      ![image][windows-gui-credentials-image]\n\n      Click \"Add a generic credential\", and enter the following details:\n\n      - Internet or network address: `git:https://github.com`\n      - Username: `PersonalAccessToken`\n      - Password: _(copy and paste the PAT generated in step 4 here)_\n\n      ![image][windows-gui-add-pat-image]\n\n## What about GitHub Enterprise Server (GHES)?\n\nAs mentioned in [the blog post][github-token-authentication-requirements],\nthe new token-based authentication requirements **DO NOT** apply to GHES:\n\n> We have not announced any changes to GitHub Enterprise Server, which remains\n> unaffected at this time.\n\n[token-auth]: https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/\n[gcm]: https://aka.ms/gcm\n[gcm-install]: ../README.md#download-and-install\n[gcm-latest]: https://aka.ms/gcm/latest\n[gcm-new-issue]: https://github.com/git-ecosystem/git-credential-manager/issues/new/choose\n[gcm-windows]: https://github.com/microsoft/Git-Credential-Manager-for-Windows\n[git-windows]: https://git-scm.com/download/win\n[github-display-pat-image]: img/github-display-pat.png\n[github-generate-pat-image]: img/github-generate-pat.png\n[github-pat]: https://github.com/settings/tokens/new?scopes=repo,gist,workflow\n[github-pat-docs]: https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token\n[github-pat-gist-scope-image]: img/github-pat-gist-scope.png\n[github-pat-note-image]: img/github-pat-note.png\n[github-pat-repo-scope-image]: img/github-pat-repo-scope.png\n[github-pat-workflow-scope-image]: img/github-pat-workflow-scope.png\n[github-token-authentication-requirements]: https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/\n[windows-cli-save-pat-image]: img/windows-cli-save-pat.png\n[vs-2019]: https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2019\n[vs-2017]: https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2017\n[windows-credential-manager]: https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0\n[windows-gui-add-pat-image]: img/windows-gui-add-pat.png\n[windows-gui-credentials-image]: img/windows-gui-credentials.png\n"
  },
  {
    "path": "docs/gitlab.md",
    "content": "# GitLab support\n\nGit Credential Manager supports [gitlab.com][gitlab] out the box.\n\n## Using on another instance\n\nTo use on another instance, eg. `https://gitlab.example.com` requires setup and\nconfiguration:\n\n1. [Create an OAuth application][gitlab-oauth]. This can be at the user, group\nor instance level. Specify a name and use a redirect URI of `http://127.0.0.1/`.\n_Unselect_ the 'Confidential' option. Set the 'read_repository' and\n'write_repository' scopes.\n1. Copy the application ID and configure\n`git config --global credential.https://gitlab.example.com.gitLabDevClientId <APPLICATION_ID>`\n1. Copy the application secret and configure\n`git config --global credential.https://gitlab.example.com.gitLabDevClientSecret\n<APPLICATION_SECRET>`\n1. Optional if you want to force browser auth:\n`git config --global credential.https://gitlab.example.com.gitLabAuthModes browser`\n1. For good measure, configure\n`git config --global credential.https://gitlab.example.com.provider gitlab`.\nThis may be necessary to recognise the domain as a GitLab instance.\n1. Verify the config is as expected\n`git config --global --get-urlmatch credential https://gitlab.example.com`\n\n### Clearing config\n\n```console\ngit config --global --unset-all credential.https://gitlab.example.com.gitLabDevClientId\ngit config --global --unset-all credential.https://gitlab.example.com.gitLabDevClientSecret\ngit config --global --unset-all credential.https://gitlab.example.com.provider\n```\n\n### Config for popular instances\n\nFor convenience, here are the config commands for several popular GitLab\ninstances, provided by community member [hickford](https://github.com/hickford/):\n\n```console\n# https://gitlab.freedesktop.org/\ngit config --global credential.https://gitlab.freedesktop.org.gitLabDevClientId 6503d8c5a27187628440d44e0352833a2b49bce540c546c22a3378c8f5b74d45\ngit config --global credential.https://gitlab.freedesktop.org.gitLabDevClientSecret 2ae9343a034ff1baadaef1e7ce3197776b00746a02ddf0323bb34aca8bff6dc1\n# https://gitlab.gnome.org/\ngit config --global credential.https://gitlab.gnome.org.gitLabDevClientId adf21361d32eddc87bf6baf8366f242dfe07a7d4335b46e8e101303364ccc470\ngit config --global credential.https://gitlab.gnome.org.gitLabDevClientSecret cdca4678f64e5b0be9febc0d5e7aab0d81d27696d7adb1cf8022ccefd0a58fc0\n# https://invent.kde.org/\ngit config --global credential.https://invent.kde.org.gitLabDevClientId cd7cb4342c7cd83d8c2fcc22c87320f88d0bde14984432ffca07ee24d0bf0699\ngit config --global credential.https://invent.kde.org.gitLabDevClientSecret 9cc8440b280c792ac429b3615ae1c8e0702e6b2479056f899d314f05afd94211\n# https://salsa.debian.org/\ngit config --global credential.https://salsa.debian.org.gitLabDevClientId 213f5fd32c6a14a0328048c0a77cc12c19138cc165ab957fb83d0add74656f89\ngit config --global credential.https://salsa.debian.org.gitLabDevClientSecret 3616b974b59451ecf553f951cb7b8e6e3c91c6d84dd3247dcb0183dac93c2a26\n# https://gitlab.haskell.org/\ngit config --global credential.https://gitlab.haskell.org.gitLabDevClientId 57de5eaab72b3dc447fca8c19cea39527a08e82da5377c2d10a8ebb30b08fa5f\ngit config --global credential.https://gitlab.haskell.org.gitLabDevClientSecret 5170a480da8fb7341e0daac94223d4fff549c702efb2f8873d950bb2b88e434f\n# https://code.videolan.org/\ngit config --global credential.https://code.videolan.org.gitLabDevClientId f35c379241cc20bf9dffecb47990491b62757db4fb96080cddf2461eacb40375\ngit config --global credential.https://code.videolan.org.gitLabDevClientSecret 631558ec973c5ef65b78db9f41103f8247dc68d979c86f051c0fe4389e1995e8\n```\n\nSee also [issue #677](https://github.com/git-ecosystem/git-credential-manager/issues/677).\n\n## Preferences\n\n```console\nSelect an authentication method for 'https://gitlab.com/':\n  1. Web browser (default)\n  2. Personal access token\n  3. Username/password\noption (enter for default):\n```\n\nIf you have a preferred authentication mode, you can specify\n[credential.gitLabAuthModes][config-gitlab-auth-modes]:\n\n```console\ngit config --global credential.gitLabAuthModes browser\n```\n\n## Caveats\n\nImproved support requires changes in GitLab. Please vote for these issues if\nthey affect you:\n\n1. No support for OAuth device authorization (necessary for machines without web\nbrowser): [GitLab issue 332682][gitlab-issue-332682]\n1. Preconfigure Git Credential Manager as instance-wide OAuth application:\n[GitLab issue 374172](gitlab-issue-374172)\n1. Username/password authentication is suggested even if disabled on server:\n[GitLab issue 349463][gitlab-issue-349463]\n\n[config-gitlab-auth-modes]: configuration.md#credential.gitLabAuthModes\n[gitlab]: https://gitlab.com\n[gitlab-issue-332682]: https://gitlab.com/gitlab-org/gitlab/-/issues/332682\n[gitlab-issue-374172]: https://gitlab.com/gitlab-org/gitlab/-/issues/374172\n[gitlab-issue-349463]: https://gitlab.com/gitlab-org/gitlab/-/issues/349463\n[gitlab-oauth]: https://docs.gitlab.com/ee/integration/oauth_provider.html\n"
  },
  {
    "path": "docs/hostprovider.md",
    "content": "# Git Credential Manager Host Provider\n\n## Abstract\n\nGit Credential Manger, the cross-platform and cross-host Git credential\nhelper, can be extended to support any Git hosting service allowing seamless\nauthentication to secured Git repositories by implementing and registering a\n\"host provider\".\n\n## 1. Introduction\n\nGit Credential Manager (GCM) is a host and platform agnostic Git\ncredential helper application. Support for authenticating to any Git hosting\nservice can be added to GCM by creating a custom \"host provider\" and\nregistering it within the product. Host providers can be submitted via a pull\nrequest on [the Git Credential Manager repository on GitHub][gcm].\n\nThis document outlines the required and expected behaviour of a host provider,\nand what is required to implement and register one.\n\n### 1.1. Notational Conventions\n\nThe key words \"MUST\", \"MUST NOT\", \"REQUIRED\", \"SHALL\", \"SHALL NOT\",\n\"SHOULD\", \"SHOULD NOT\", \"RECOMMENDED\", \"MAY\", and \"OPTIONAL\" in this\nspecification are to be interpreted as described in\n[[RFC2119][rfc-2119]].\n\n### 1.2. Abbreviations\n\nThroughout this document you may see multiple abbreviations of product names and\nsecurity or credential objects.\n\n\"Git Credential Manager\" is abbreviated to \"GCM\". \"Git Credential\nManager for Windows\" is abbreviated to \"GCM for Windows\" or \"GCM Windows\".\n\"Git Credential Manager for Mac & Linux\" is abbreviated to \"GCM for\nMac/Linux\" or \"GCM Mac/Linux\".\n\nOAuth2 [[RFC6749][rfc-6749]] \"access tokens\" are\nabbreviated to \"ATs\" and \"refresh tokens\" to \"RTs\". \"Personal Access Tokens\" are\nabbreviated to \"PATs\".\n\n## 2. Implementation\n\nWriting and adding a host provider to GCM requires two main actions:\nimplementing the `IHostProvider` interface, and registering an instance of the\nprovider with the application via the host provider registry.\n\nHost providers MUST implement the `IHostProvider` interface. They can choose to\ndirectly implement the interface they MAY derive from the `HostProvider`\nabstract class (which itself implements the `IHostProvider` interface) - see\n[2.6][hostprovider-base-class].\n\nImplementors MUST implement all interface properties and abstract methods.\n\nThe `Id` and `Name` properties MUST be implemented and MUST NOT return default\nor empty values.\n\nThe `Id` field MUST be unique over the set of all providers, or\nelse an error will be thrown at registration time. The `Id` field MAY be a\nunique random string of characters and digits such as a UUID, but it is\nRECOMMENDED to use a human-readable value consisting of letter characters in the\nrange \\[a-z\\] only.\n\nThe `Name` property MUST be a human readable string and MUST identify the Git\nhosting service this provider supports.\n\nThe `SupportedAuthorityIds` property MUST return an instance of an object and\nNOT a `null` reference. Populating this collection with values is OPTIONAL but\nhighly RECOMMENDED. You should return a set of stable identifiers of all\nauthorities that the provider supports authentication against.\n\n### 2.1. Registration\n\nHost providers must provide an instance of their `IHostProvider` type to the\nGCM application host provider registry to be considered for handling\nrequests.\n\nThe main GCM `Application` object has one principal registry which you can\nregister providers with by calling the `RegisterProvider` method.\n\n#### 2.1.2. Ordering\n\nThe default host provider registry in GCM has multiple priority levels that\nhost providers can be registered at: High, Normal, and Low.\n\nFor each priority level (starting with High, then Normal, then Low), the\nregistry will call each host provider in the order they were registered in,\nunless the user has overridden the provider selection process.\n\nThere are no rules or restrictions on the ordering of host providers, except\nthat the `GenericHostProvider` MUST be registered last and at the Low priority.\nThe generic provider is a catch-all provider implementation that will handle any\nrequest in a standard way.\n\n### 2.2. Handling Requests\n\nThe `IsSupported(InputArguments)` method will be called on all registered host\nproviders in-turn on the invocation of a `get`, `store`, or `erase` request. The\nfirst host provider to return `true` will be called upon to handle the specific\nrequest. If the user has overridden the host provider selection process, a\nspecific host provider may be selected instead, and the\n`IsSupported(InputArguments)` method will NOT be called.\n\nThis method MUST return `true` if and only if the provider understands the\nrequest and can serve or handle the request. If the provider does not know how\nto handle the request it MUST return `false` instead.\n\nIf no host provider returns `true` to a call to the `IsSupported(InputArguments)`\nmethod for a each host provider priority level, then a HTTP HEAD request will be\nmade to the remote URL and each host provider will be be called via the\n`IsSupported(HttpResponseMessage)` method. A host provider SHOULD use this call\nto check for recognised on-premises instances (for example, by inspecting\nresponse headers) and return `true` if it wishes to be called upon to handle the\ncredential request, otherwise it MUST return `false`.\n\nHost providers SHOULD NOT make further network calls if possible during any of\nthe `IsSupported` method overloads to avoid degrading the performance of the\noverall application.\n\n#### 2.2.1. Rejecting Requests\n\nThe `IsSupported` methods MUST return `true` if the host provider would like to\ncancel the authentication operation based on the current context or input.\nFor example, if provider requires a secure protocol but the requested protocol\nfor a supported hostname is `http` and not `https`.\n\nHost providers MUST instead cancel the request from the `GetCredentialAsync`\nmethod by throwing an `Exception`. Implementors MUST provide detailed\ninformation regarding the reason why the authentication cannot continue, for\nexample \"HTTP is not secure, please use HTTPS\".\n\n### 2.3. Retrieving Credentials\n\nThe `GetCredentialAsync` method will be called when a `get` request is made.\nThe method MUST return an instance of an `ICredential` capable of fulfilling the\nspecific access request. The argument passed to `GetCredentialAsync` contains\nproperties indicating the required `protocol` and `host` for this request. The\n`username` and `path` properties are OPTIONAL, however if they are present, they\nMUST be considered and used to direct the authentication.\n\nThe host provider MAY attempt to locate any existing credential, stored by the\n`StoreCredentialAsync` method, before resorting to the creation a new one.\n\nThe host provider MAY choose to check if a stored credential is still valid\nby inspecting any stored metadata associated with the value. A host provider MAY\nalso choose to further validate a retrieved stored credential by making a web\nrequest. However, it is NOT RECOMMENDED to make any request that is known to be\nslow or that typically produces inconclusive validation results.\n\nIf a provider chooses to make a validation web request and that request fails or\nis inconclusive, it SHOULD assume the credential is still valid and return it\nanyway, letting Git (the caller) attempt to use it and validate it itself.\n\nThe returned `ICredential` MAY leave both the username and password values as\nthe empty string or `null`. This signals to Git (or rather cURL) that it should\nnegotiate the authentication mechanism with the remote itself. This is typically\nused for Windows Integrated Authentication.\n\n#### 2.3.1 Authentication Prompts\n\nWhen it is not possible to locate an existing credential suitable for the\ncurrent request, a host provider SHOULD prompt the user to complete an\nauthentication flow.\n\nThe method, modes, and interactions for performing authentication will vary\nwidely between Git hosting services and their supported authentication\nauthorities. A host provider SHOULD attempt to detect the best authentication\nexperience given the current environment or context, and select that one to\nattempt first.\n\nHost providers are RECOMMENDED to attempt authentication mechanisms that do not\nrequire user interaction if possible. If there are multiple authentication\nmechanisms that could be equally considered \"best\" they MAY prompt the user\nto make a selection. Host providers MAY wish to remember such a selection for\nfuture use, however they MUST make it clear how to clear this stored selection\nto the user.\n\nIf interaction is required to complete authentication a host provider MUST first\ncheck if interaction has been disabled (`ISettings.IsInteractionAllowed`), and\nan exception MUST be thrown if interaction has been disallowed.\n\nAuthentication prompts that display a graphical user interface such as a window\nare MUST be preferred when an interactive \"desktop\" session is available.\n\nIf an authentication prompt is required when an interactive session is not\navailable and a terminal/TTY is attached then a provider MUST first check if\nterminal prompts are enabled (`ISettings.IsTerminalPromptsEnabled`), and an\nexception MUST be thrown if interaction has been disallowed.\n\n### 2.4. Storing Credentials\n\nHost providers MAY store credentials at various stages of a typical\nauthentication flow, or when explicitly requested to do so in a call to\n`StoreCredentialAsync`.\n\nProviders SHOULD use the credential store (exposed as `ICredentialStore`) to\npersist secret values and credential entities such as passwords, PATs and OAuth\ntokens.\n\nThe typical Git credential helper call pattern is one call to `get`, followed by\neither a `store` request in case of a HTTP 200 (OK) response, or `erase` in case\nof HTTP 401 (Unauthorized) response. In some cases there is additional context\nthat is present as part of the `get` request or during the generation of a new\ncredential that is not present during the subsequent call to `store` (or\n`erase`). In these cases providers MAY store the credential during the `get`\nrather than, or as well as during the `store`.\n\nHost providers MAY store multiple credentials or tokens in the same request if\nit is required. One example where multiple credential storage is needed is with\nOAuth2 access tokens (AT) and refresh tokens (RT). Both the AT and RT SHOULD be\nstored in the same location using the credential store with complementary\ncredential service names.\n\n### 2.5. Erasing Credentials\n\nIf host providers have stored credentials in the credential store, they MUST\nrespond to requests to erase them in calls to `EraseCredentialAsync`.\n\nIf a host provider cannot locate a credential to erase it MUST NOT raise an\nerror and MUST exit successfully. A warning message MAY be emitted to the\ntracing system.\n\nHost providers MUST NOT perform their own repeated validation of credentials\nfor the purposes of ignoring the request to erase them. The ultimate authority\non the validity of a credential is the caller (Git).\n\nProviders MAY validate any additional or ancillary credentials (such as OAuth\nRTs) are still valid when a request to erase the primary credential (such as an\nOAuth AT) is made, and choose not to delete those additional credentials. The\nprimary credential MUST still always be erased in all cases.\n\n### 2.6 `HostProvider` base class\n\nThe `HostProvider` abstract base class is provided for the convenience of host\nprovider implementors. This base class implements most required methods of the\n`IHostProvider` interface with common credential recall and storage behaviour.\n\nThe `GetCredentialAsync`, `StoreCredentialAsync`, and `EraseCredentialAsync`\nmethods are implemented as `virtual` meaning they MAY be overridden by derived\nclasses to customise the behaviour of those operations. It is NOT RECOMMENDED\nto derive from the `HostProvider` base class if the implementor must override\nmost of the methods as implemented - implementors SHOULD implement the\n`IHostProvider` interface directly instead.\n\nImplementors that choose to derive from this base class MUST implement all\nabstract methods and properties. The primary abstract method to implement\nis `GenerateCredentialAsync`.\n\nThere is also an additional `virtual` method named `GetServiceName` that is used\nby the default implementations of the `Get|Store|EraseCredentialAsync` methods\nto locate and store credentials.\n\n#### 2.6.1 `GetServiceName`\n\nThe `GetServiceName` virtual method, if overriden, MUST return a string that\nidentifies the service/provider for this request, and is used for storing\ncredentials. The value returned MUST be stable - i.e, it MUST return the same\nvalue given the same or equivalent input arguments.\n\nBy default this method returns the full remote URI, without a trailing slash,\nincluding protocol/scheme, hostname, and path if present in the input arguments.\nAny username in the input arguments is never included in the URI.\n\n#### 2.6.2 `GenerateCredentialAsync`\n\nThe `GenerateCredentialAsync` method will be called if an existing credential\nwith a matching service (from `GetServiceName`) and account is not found in the\ncredential store.\n\nThis method MUST return a freshly created/generated credential and not any\nexisting or stored one. It MAY use existing or stored ancillary data or tokens,\nsuch as OAuth refresh tokens, to generate the new token (such as an OAuth AT).\n\n### 2.7. External Metadata\n\nHost providers MAY wish to store extra data about authentications or users\ncollected or produced during authentication operations. These SHOULD be stored\nin a per-user, local location such as the user's home or profile directory.\n\nSecrets, credentials or other sensitive data SHOULD be stored in the credential\nstore, or otherwise protected by some form of per-user, local encryption.\n\nIn the case of stored data caches, providers SHOULD invalidate relevant parts\nof, or the entire cache, when a call to `EraseCredentialAsync` is made.\n\n## 3. Helpers\n\nHost providers MAY wish to make use of platform or operating system specific\nfeatures such as native APIs and native graphical user interfaces, in order to\noffer a better authentication experience.\n\nHost providers MUST function without the presence of a helper, even if that\nfunction is to fail gracefully with a user-friendly error message, including\na remedy to correct their installation. Host providers SHOULD always offer a\nterminal/TTY or text-based authentication mechanism alongside any graphical\ninterface provided by a helper.\n\nIn order to achieve this host providers MUST introduce an out-of-process\n\"helper\" executable that can be invoked from the main GCM process. This\nallows the \"helper\" executable full implementation freedom of runtime, language,\netc.\n\nCommunications between the main and helper processes MAY use any IPC mechanism\navailable. It is RECOMMENDED implementors use standard input/output streams or\nfile descriptors to send and receive data as this is consistent with how Git and\nGCM communicate. UNIX sockets or Windows Named Pipes MAY also be used when\nan ongoing back-and-forth communication is required.\n\n### 3.1. Discovery\n\nIt is RECOMMENDED that helper discovery is achieved by simply checking for the\npresence of the expected executable file. The name and path of the helper\nexecutable SHOULD be configurable by the user via Git's configuration files.\n\n## 4. Error Handling\n\nIf an unrecoverable error occurs a host provider MUST throw an exception and\nMUST include detailed failure information in the error message. If the reason\nfor failure can be fixed by the user the error message MUST include instructions\nto fix the problem, or a link to online documentation.\n\nIn the case of a recoverable error, host providers SHOULD print a warning\nmessage to the standard error stream, and MUST include the error information and\nthe recovery steps take in the trace log.\n\nIn the case of an authentication error, providers SHOULD attempt to prompt the\nuser again with a message indicating the incorrect authentication details have\nbeen entered.\n\n## 5. Custom Commands\n\nIf a host provider wishes to surface custom commands the SHOULD implement the\n`ICommandProvider` interface.\n\nEach provider is given the opportunity to create a single `ProviderCommand`\ninstance to which further sub-commands can be parented to. Commanding is\nprovided by the `System.CommandLine` API library [[1][references]].\n\nThere are no limitations on what format sub-commands, arguments, or options must\ntake, but implementors SHOULD attempt to follow existing practices and styles.\n\n## References\n\n1. [`System.CommandLine` API][github-dotnet-cli]\n\n[gcm]: https://github.com/git-ecosystem/git-credential-manager\n[github-dotnet-cli]: https://github.com/dotnet/command-line-api\n[hostprovider-base-class]: #26-hostprovider-base-class\n[references]: #references\n[rfc-2119]: https://www.rfc-editor.org/rfc/rfc2119\n[rfc-6749]: https://www.rfc-editor.org/rfc/rfc6749\n"
  },
  {
    "path": "docs/install.md",
    "content": "# Install instructions\n\nThere are multiple ways to install GCM on macOS, Windows, and Linux. Preferred\ninstallation methods for each OS are designated with a :star:.\n\n## macOS\n\n### Homebrew :star:\n\n**Note:** If you have an existing installation of the 'Java GCM' on macOS and\nyou have installed this using Homebrew, this installation will be unlinked\n(`brew unlink git-credential-manager`) when GCM is installed.\n\n#### Install\n\n```shell\nbrew install --cask git-credential-manager\n```\n\nAfter installing you can stay up-to-date with new releases by running:\n\n```shell\nbrew upgrade --cask git-credential-manager\n```\n\n#### Uninstall\n\nTo uninstall, run the following:\n\n```shell\nbrew uninstall --cask git-credential-manager\n```\n\n---\n\n### macOS Package\n\n#### Install\n\nDownload and double-click the [installation package][latest-release] and follow\nthe instructions presented.\n\n#### Uninstall\n\nTo uninstall, run the following:\n\n```shell\nsudo /usr/local/share/gcm-core/uninstall.sh\n```\n\n---\n\n<!-- this explicit anchor should stay stable so that external docs can link here -->\n<!-- markdownlint-disable-next-line no-inline-html -->\n<a name=\"linux-install-instructions\"></a>\n\n## Linux\n\n**Note:** all Linux distributions\n[require additional configuration][gcm-credstores] to use GCM.\n\n---\n\n### .NET tool :star:\n\nSee the [.NET tool](#net-tool) section below for instructions on this\ninstallation method.\n\n---\n\n### Debian package\n\n#### Install\n\nDownload the latest [.deb package][latest-release]*, and run the following:\n\n```shell\nsudo dpkg -i <path-to-package>\ngit-credential-manager configure\n```\n\n#### Uninstall\n\n```shell\ngit-credential-manager unconfigure\nsudo dpkg -r gcm\n```\n\n*If you'd like to validate the package's signature after downloading, check out\nthe instructions [here][linux-validate-gpg-debian].\n\n---\n\n### Tarball\n\n#### Install\n\nDownload the latest [tarball][latest-release]*, and run the following:\n\n```shell\ntar -xvf <path-to-tarball> -C /usr/local/bin\ngit-credential-manager configure\n```\n\n#### Uninstall\n\n```shell\ngit-credential-manager unconfigure\nrm $(command -v git-credential-manager)\n```\n\n*If you would like to validate the tarball's signature after downloading, check\nout the instructions [here][linux-validate-gpg-tarball].\n\n---\n\n### Install from source helper script\n\n#### Install\n\nEnsure `curl` is installed:\n\n```shell\ncurl --version\n```\n\nIf `curl` is not installed, please use your distribution's package manager\nto install it.\n\nDownload and run the script:\n\n```shell\ncurl -L https://aka.ms/gcm/linux-install-source.sh | sh\ngit-credential-manager configure\n```\n\n**Note:** You will be prompted to enter your credentials so that the script\ncan download GCM's dependencies using your distribution's package\nmanager.\n\n#### Uninstall\n\n[Follow these instructions][linux-uninstall] for your distribution.\n\n---\n\n## Windows\n\n### Git for Windows :star:\n\nGCM is included with [Git for Windows][git-for-windows]. During installation\nyou will be asked to select a credential helper, with GCM listed as the default.\n\n![image][git-for-windows-gcm-screenshot]\n\n---\n\n### Standalone installation\n\nYou can also download the [latest installer][latest-release] for Windows to\ninstall GCM standalone.\n\n**:warning: Important :warning:**\n\nInstalling GCM as a standalone package on Windows will forcibly override the\nversion of GCM that is bundled with Git for Windows, **even if the version\nbundled with Git for Windows is a later version**.\n\nThere are two flavors of standalone installation on Windows:\n\n- User (`gcmuser-win*`):\n\n  Does not require administrator rights. Will install only for the current user\n  and updates only the current user's Git configuration.\n\n- System (`gcm-win*`):\n\n  Requires administrator rights. Will install for all users on the system and\n  update the system-wide Git configuration.\n\nTo install, double-click the desired installation package and follow the\ninstructions presented.\n\n### Uninstall (Windows 10)\n\nTo uninstall, open the Settings app and navigate to the Apps section. Select\n\"Git Credential Manager\" and click \"Uninstall\".\n\n### Uninstall (Windows 7-8.1)\n\nTo uninstall, open Control Panel and navigate to the Programs and Features\nscreen. Select \"Git Credential Manager\" and click \"Remove\".\n\n### Windows Subsystem for Linux (WSL)\n\nGit Credential Manager can be used with the [Windows Subsystem for Linux\n(WSL)][ms-wsl] to enable secure authentication of your remote Git\nrepositories from inside of WSL.\n\n[Please see the GCM on WSL docs][gcm-wsl] for more information.\n\n---\n\n## .NET tool\n\nGCM is available to install as a cross-platform [.NET\ntool][dotnet-tool]. This is\nthe preferred install method for Linux because you can use it to install on any\n[.NET-supported\ndistribution][dotnet-supported-distributions]. You\ncan also use this method on macOS if you so choose.\n\n**Note:** Make sure you have installed [version 8.0 of the .NET\nSDK][dotnet-install] before attempting to run the following `dotnet tool`\ncommands. After installing, you will also need to follow the output instructions\nto add the tools directory to your `PATH`.\n\n#### Install\n\n```shell\ndotnet tool install -g git-credential-manager\ngit-credential-manager configure\n```\n\n#### Update\n\n```shell\ndotnet tool update -g git-credential-manager\n```\n\n#### Uninstall\n\n```shell\ngit-credential-manager unconfigure\ndotnet tool uninstall -g git-credential-manager\n```\n\n[dotnet-install]: https://learn.microsoft.com/en-us/dotnet/core/install/linux#packages\n[dotnet-supported-distributions]: https://learn.microsoft.com/en-us/dotnet/core/install/linux\n[dotnet-tool]: https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools\n[gcm-credstores]: credstores.md\n[gcm-wsl]: wsl.md\n[git-for-windows]: https://gitforwindows.org/\n[git-for-windows-gcm-screenshot]: img/git-for-windows-gcm-screenshot.png\n[latest-release]: https://github.com/git-ecosystem/git-credential-manager/releases/latest\n[linux-uninstall]: linux-fromsrc-uninstall.md\n[linux-validate-gpg-debian]: ./linux-validate-gpg.md#debian-package\n[linux-validate-gpg-tarball]: ./linux-validate-gpg.md#tarball\n[ms-wsl]: https://aka.ms/wsl#\n"
  },
  {
    "path": "docs/linux-fromsrc-uninstall.md",
    "content": "# Uninstalling after installing from source\n\nThese instructions will guide you in removing GCM after running the\n[install from source script][install-from-source] on your Linux distribution.\n\n:rotating_light: PROCEED WITH CAUTION :rotating_light:\n\nFor completeness, we provide uninstall instructions for _the GCM application,\nthe GCM repo, and the maximum number of dependencies*_ for all distributions.\nThis repo and these dependencies may or may not have already been present on\nyour system when you ran the install from source script, and uninstalling them\ncould impact other programs and/or your normal workflows. Please keep this in\nmind when following the instructions below.\n\n*Certain distributions require some dependencies of the script to function as\nexpected, so we only include instructions to remove the non-required\ndependencies.\n\n## All distributions\n\n**Note:** If you ran the install from source script from a pre-existing clone of\nthe `git-credential-manager` repo or outside of your `$HOME` directory, you will\nneed to modify the final two commands below to point to the location of your\npre-existing clone or the directory from which you ran the install from source\nscript.\n\n```console\ngit-credential-manager unconfigure &&\nsudo rm $(command -v git-credential-manager) &&\nsudo rm -rf /usr/local/share/gcm-core &&\nsudo rm -rf ~/git-credential-manager &&\nsudo rm ~/install-from-source.sh\n```\n\n## Debian/Ubuntu\n\n**Note:** If you had a pre-existing installation of dotnet that was not\ninstalled via `apt` or `apt-get` when you ran the install from source script,\nyou will need to remove it using [these instructions][uninstall-dotnet] and\nremove `dotnet-*` from the below command.\n\n```console\nsudo apt remove dotnet-* dpkg-dev apt-transport-https git curl wget\n```\n\n## Linux Mint\n\n**Note:** If you had a pre-existing installation of dotnet when you ran the\ninstall from source script that was not located at `~/.dotnet`, you will need to\nmodify the first command below to point to the custom install location. If you\nwould like to remove the specific version of dotnet that the script installed\nand keep other versions, you can do so with [these instructions][uninstall-dotnet].\n\n```console\nsudo rm -rf ~/.dotnet &&\nsudo apt remove git curl\n```\n\n## Fedora/CentOS/RHEL\n\n**Note:** If you had a pre-existing installation of dotnet when you ran the\ninstall from source script that was not located at `~/.dotnet`, you will need to\nmodify the first command below to point to the custom install location. If you\nwould like to remove the specific version of dotnet that the script installed\nand keep other versions, you can do so with [these instructions][uninstall-dotnet].\n\n```console\nsudo rm -rf ~/.dotnet\n```\n\n## Alpine\n\n**Note:** If you had a pre-existing installation of dotnet when you ran the\ninstall from source script that was not located at `~/.dotnet`, you will need to\nmodify the first command below to point to the custom install location. If you\nwould like to remove the specific version of dotnet that the script installed\nand keep other versions, you can do so with [these instructions][uninstall-dotnet].\n\n```console\nsudo rm -rf ~/.dotnet &&\nsudo apk del icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which\nbash coreutils gcompat git curl\n```\n\n[install-from-source]: ../src/linux/Packaging.Linux/install-from-source.sh\n[uninstall-dotnet]: https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net\n"
  },
  {
    "path": "docs/linux-validate-gpg.md",
    "content": "# Validating GCM's GPG signature\n\nFollow the below instructions to import GCM's public key and use it to validate\nthe latest Debian package and/or tarball signature.\n\n## Debian package\n\n```shell\n# Install needed packages\napt-get install -y curl debsig-verify\n\n# Download public key signature file\ncurl -s https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest \\\n| grep -E 'browser_download_url.*gcm-public.asc' \\\n| cut -d : -f 2,3 \\\n| tr -d \\\" \\\n| xargs -I 'url' curl -L -o gcm-public.asc 'url'\n\n# De-armor public key signature file\ngpg --output gcm-public.gpg --dearmor gcm-public.asc\n\n# Note that the fingerprint of this key is \"3C853823978B07FA\", which you can\n# determine by running:\ngpg --show-keys gcm-public.asc | head -n 2 | tail -n 1 | tail -c 17\n\n# Copy de-armored public key to debsig keyring folder\nmkdir /usr/share/debsig/keyrings/3C853823978B07FA\nmv gcm-public.gpg /usr/share/debsig/keyrings/3C853823978B07FA/\n\n# Create an appropriate policy file\nmkdir /etc/debsig/policies/3C853823978B07FA\ncat > /etc/debsig/policies/3C853823978B07FA/generic.pol << EOL\n<?xml version=\"1.0\"?>\n<!DOCTYPE Policy SYSTEM \"https://www.debian.org/debsig/1.0/policy.dtd\">\n<Policy xmlns=\"https://www.debian.org/debsig/1.0/\">\n\n  <Origin Name=\"Git Credential Manager\" id=\"3C853823978B07FA\" Description=\"Git Credential Manager public key\"/>\n\n  <Selection>\n    <Required Type=\"origin\" File=\"gcm-public.gpg\" id=\"3C853823978B07FA\"/>\n  </Selection>\n\n  <Verification MinOptional=\"0\">\n    <Required Type=\"origin\" File=\"gcm-public.gpg\" id=\"3C853823978B07FA\"/>\n  </Verification>\n\n</Policy>\nEOL\n\n# Download Debian package\ncurl -s https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest \\\n| grep \"browser_download_url.*deb\" \\\n| cut -d : -f 2,3 \\\n| tr -d \\\" \\\n| xargs -I 'url' curl -L -o gcm.deb 'url'\n\n# Verify\ndebsig-verify gcm.deb\n```\n\n## Tarball\n```shell\n# Download the public key signature file\ncurl -s https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest \\\n| grep -E 'browser_download_url.*gcm-public.asc' \\\n| cut -d : -f 2,3 \\\n| tr -d \\\" \\\n| xargs -I 'url' curl -L -o gcm-public.asc 'url'\n\n# Import the public key\ngpg --import gcm-public.asc\n\n# Download the tarball and its signature file\ncurl -s https://api.github.com/repos/ldennington/git-credential-manager/releases/latest \\\n| grep -E 'browser_download_url.*gcm-linux.*[0-9].[0-9].[0-9].tar.gz' \\\n| cut -d : -f 2,3 \\\n| tr -d \\\" \\\n| xargs -I 'url' curl -LO 'url'\n\n# Trust the public key\necho -e \"5\\ny\\n\" |  gpg --command-fd 0 --expert --edit-key 3C853823978B07FA trust\n\n# Verify the signature\ngpg --verify gcm-linux_amd64*.tar.gz.asc gcm-linux*.tar.gz\n```\n"
  },
  {
    "path": "docs/migration.md",
    "content": "# Migration Guide\n\n## Migrating from Git Credential Manager for Windows\n\n### GCM_AUTHORITY\n\nThis setting (and the corresponding `credential.authority` configuration) is\ndeprecated and should be replaced with the `GCM_PROVIDER` (or corresponding\n`credential.authority` configuration) setting.\n\nBecause both Basic HTTP authentication and Windows Integrated Authentication\n(WIA) are now handled by one provider, if you specified `basic` as your\nauthority you also need to disable WIA using `GCM_ALLOW_WINDOWSAUTH` /\n`credential.allowWindowsAuth`.\n\nThe following table shows the correct replacement for all legacy authorities\nvalues:\n\nGCM_AUTHORITY (credential.authority)|&rarr;|GCM_PROVIDER (credential.provider)|GCM_ALLOW_WINDOWSAUTH (credential.allowWindowsAuth)\n-|-|-|-\n`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|&rarr;|`azure-repos`|_N/A_\n`github`|&rarr;|`github`|_N/A_\n`basic`|&rarr;|`generic`|`false`\n`integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|&rarr;|`generic`|`true` _(default)_\n\nFor example if you had previous set the authority for the `example.com` host to\n`basic`..\n\n```shell\ngit config --global credential.example.com.authority basic\n```\n\n..then you can replace this with the following..\n\n```shell\ngit config --global --unset credential.example.com.authority\ngit config --global credential.example.com.provider generic\ngit config --global credential.example.com.allowWindowsAuth false\n```\n"
  },
  {
    "path": "docs/multiple-users.md",
    "content": "# Multiple users\n\nIf you work with multiple different identities on a single Git hosting service,\nyou may be wondering if Git Credential Manager (GCM) supports this workflow. The\nanswer is yes, with a bit of complexity due to how it interoperates with Git.\n\n---\n\n**Prompted to select an account?**\n\nRead the [**TL;DR** section][tldr] below for a quick summary of how to make GCM\nremember which account to use for which repository.\n\n---\n\n## Foundations: Git and Git hosts\n\nGit itself doesn't have a single, strong concept of \"user\". There's the\n`user.name` and `user.email` which get embedded into commit headers/trailers,\nbut these are arbitrary strings. GCM doesn't interact with this notion of a user\nat all. You can put whatever you want into your `user.*` config, and nothing in\nGCM will change at all.\n\nSeparate from the user strings in commits, Git recognizes the \"user\" part of a\nremote URL or a credential. These are not often used, at least by default, in\nthe web UI of major Git hosts.\n\nGit hosting providers (like GitHub or Bitbucket) _do_ have a concept of \"user\".\nTypically it's an identity like a username or email address, plus a password or\nother credential to perform actions as that user. You may have guessed by now\nthat GCM (the Git **Credential** Manager) does work with this notion of a user.\n\n## People, identities, credentials, oh my\n\nYou (a physical person) may have one or more user accounts (identities) with one\nor more Git hosting providers. Since most Git hosts don't put a \"user\" part in\ntheir URLs, by default, Git will treat the user part for a remote as the empty\nstring. If you have multiple identities on one domain, you'll need to insert a\nunique user part per-identity yourself.\n\nThere are good reasons for having multiple identities on one domain. You might\nuse one GitHub identity for your personal work, another for your open source\nwork, and a third for your employer's work. You can ask Git to assign a\ndifferent credential to different repositories hosted on the same provider.\nHTTPS URLs include an optional \"name\" part before an `@` sign in the domain\nname, and you can use this to force Git to distinguish multiple users. This\nshould likely be your username on the Git hosting service, since there are\ncases where GCM will use it like a username.\n\n## Setting it up\n\nAs an example, let's say you're working on multiple repositories hosted at the\nsame domain name.\n\n| Repo URL | Identity |\n|----------|----------|\n| `https://example.com/open-source/library.git` | `contrib123` |\n| `https://example.com/more-open-source/app.git` | `contrib123` |\n| `https://example.com/big-company/secret-repo.git` | `employee9999` |\n\nWhen you clone these repos, include the identity and an `@` before the domain\nname in order to force Git and GCM to use different identities. If you've\nalready cloned the repos, you can update the remote URL to include the identity.\n\n### Example: fresh clones\n\n```shell\n# instead of `git clone https://example.com/open-source/library.git`, run:\ngit clone https://contrib123@example.com/open-source/library.git\n\n# instead of `git clone https://example.com/big-company/secret-repo.git`, run:\ngit clone https://employee9999@example.com/big-company/secret-repo.git\n```\n\n### Example: existing clones\n\n```shell\n# in the `library` repo, run:\ngit remote set-url origin https://contrib123@example.com/open-source/library.git\n\n# in the `secret-repo` repo, run:\ngit remote set-url origin https://employee9999@example.com/big-company/secret-repo.git\n```\n\n## Azure DevOps\n\n[Azure DevOps has some additional, optional complexity][azure-access-tokens]\nwhich you should also be aware of if you're using it.\n\n[azure-access-tokens]: azrepos-users-and-tokens.md\n\n## GitHub\n\nYou can use the `github [list | login | logout]` commands to manage your GitHub\naccounts. These commands are documented in the [command-line usage][cli-usage]\nor by running `git credential-manager github --help`.\n\n## TL;DR: Tell GCM to remember which account to use\n\nTo set a default account for a particular remote you can simply set the\nfollowing Git configuration:\n\n```shell\ngit config --global credential.<URL>.username <USERNAME>\n```\n\n..where `<URL>` is the remote URL and `<USERNAME>` is the account you wish to\nhave as the default. For example, for `github.com` and the user `alice`:\n\n```shell\ngit config --global credential.https://github.com.username alice\n```\n\nIf you wish to set a user for a specific repository or remote URL, you can\ninclude the account name in the remote URL. If you're using HTTPS remotes, you\ncan include the account name in the URL by inserting it before the `@` sign\nin the domain name.\n\nFor example, if you want to always use the `alice` account for the `mona/test`\nGitHub repository, you can clone it using the `alice` account by running:\n\n```shell\ngit clone https://alice@github.com/mona/test\n```\n\nTo update an existing clone, you can run `git remote set-url` to update the URL:\n\n```shell\ngit remote set-url origin https://alice@github.com/mona/test\n```\n\nIf your account name includes an `@` then remember to escape this character\nusing `%40`: `https://alice%40contoso.com@example.com/test`.\n\n[tldr]: #tldr-tell-gcm-to-remember-which-account-to-use\n[cli-usage]: usage.md\n"
  },
  {
    "path": "docs/netconfig.md",
    "content": "# Network and HTTP configuration\n\nGit Credential Manager's network and HTTP(S) behavior can be configured in a few\ndifferent ways via [environment variables][environment] and\n[configuration options][configuration].\n\n## HTTP Proxy\n\nIf your computer sits behind a network firewall that requires the use of a\nproxy server to reach repository remotes or the wider Internet, there are\nvarious methods for configuring GCM to use a proxy.\n\nThe simplest way to configure a proxy for _all_ HTTP(S) remotes is to\n[use the standard Git HTTP(S) proxy setting `http.proxy`][git-http-proxy].\n\nFor example to configure a proxy for all remotes for the current user:\n\n```shell\ngit config --global http.proxy http://proxy.example.com\n```\n\nTo specify a proxy for a particular remote you can\n[use the `remote.<name>.proxy` repository-level setting][git-remote-name-proxy],\nfor example:\n\n```shell\ngit config --local remote.origin.proxy http://proxy.example.com\n```\n\nThe advantage to using these standard configuration options is that in addition\nto GCM being configured to use the proxy, Git itself will be configured at the\nsame time. This is probably the most commonly desired case in environments\nbehind an Internet-blocking firewall.\n\n### Authenticated proxies\n\nSome proxy servers do not accept anonymous connections and require\nauthentication. In order to specify the credentials to be used with a proxy,\nyou can specify the username and password as part of the proxy URL setting.\n\nThe format follows [RFC 3986 section 3.2.1][rfc-3986-321] by including the\ncredentials in the 'user information' part of the URI. The password is optional.\n\n```text\nprotocol://username[:password]@hostname\n```\n\nFor example, to specify the username `john.doe` and the password `letmein123`\nfor the proxy server `proxy.example.com`:\n\n```text\nhttps://john.doe:letmein123@proxy.example.com\n```\n\nIf you have special characters (as defined by\n[RFC 3986 section 2.2][rfc-3986-22]) in your username or password such as `:`,\n`@`, or any other non-URL friendly character you can URL-encode them\n([section 2.1][rfc-3986-21]).\n\nFor example, a space character would be encoded with `%20`.\n\n### Other proxy options\n\nGCM supports other ways of configuring a proxy for convenience and compatibility.\n\n1. GCM-specific configuration options (_**only** respected by GCM; **deprecated**_):\n   - `credential.httpProxy`\n   - `credential.httpsProxy`\n1. cURL environment variables (_also respected by Git_):\n   - `http_proxy`\n   - `https_proxy`/`HTTPS_PROXY`\n   - `all_proxy`/`ALL_PROXY`\n1. `GCM_HTTP_PROXY` environment variable (_**only** respected by GCM;\n**deprecated**_)\n\nNote that with the cURL environment variables there are both lowercase and\nuppercase variants.\n\n**_Lowercase variants take precedence over the uppercase form._** This is\nconsistent with how libcurl (and therefore Git) operates.\n\nThe `http_proxy` variable exists only in the lowercase variant and libcurl does\n_not_ consider any uppercase form. _GCM also reflects this behavior._\n\nSee [the curl docs][curl-proxy-env-vars] for more information.\n\n### Bypassing addresses\n\nIn some circumstances you may wish to bypass a configured proxy for specific\naddresses. GCM supports the cURL environment variable `no_proxy` (and\n`NO_PROXY`) for this scenario, as does Git itself.\n\nLike with the [other cURL proxy environment variables][other-proxy-options],\nthe lowercase variant will take precedence over the uppercase form.\n\nThis environment variable should contain a comma-separated or space-separated\nlist of host names that should not be proxied (should connect directly).\n\nGCM attempts to match [libcurl's behaviour][curlopt-noproxy],\nwhich is briefly summarized here:\n\n- a value of `*` disables proxying for all hosts;\n- other wildcard use is **not** supported;\n- each name in the list is matched as a domain which contains the hostname,\n  or the hostname itself\n- a leading period/dot `.` matches against the provided hostname\n\nFor example, setting `NO_PROXY` to `example.com` results in the following:\n\nHostname|Matches?\n-|-\n`example.com`|:white_check_mark:\n`example.com:80`|:white_check_mark:\n`www.example.com`|:white_check_mark:\n`notanexample.com`|:x:\n`www.notanexample.com`|:x:\n`example.com.othertld`|:x:\n\n**Example:**\n\n```text\nno_proxy=\"contoso.com,www.fabrikam.com\"\n```\n\n## TLS Verification\n\nIf you are using self-signed TLS (SSL) certificates with a self-hosted host\nprovider such as GitHub Enterprise Server or Azure DevOps Server (previously\nTFS), you may see the following error message when attempting to connect using\nGit and/or GCM:\n\n```shell\n$ git clone https://ghe.example.com/john.doe/myrepo\nfatal: The remote certificate is invalid according to the validation procedure.\n```\n\nThe **recommended and safest option** is to acquire a TLS certificate signed by\na public trusted certificate authority (CA). There are multiple public CAs; here\nis a non-exhaustive list to consider: [Let's Encrypt][lets-encrypt],\n[Comodo][comodo], [Digicert][digicert], [GoDaddy][godaddy],\n[GlobalSign][globalsign].\n\nIf it is not possible to **obtain a TLS certificate from a trusted 3rd party**\nthen you should try to add the _specific_ self-signed certificate or one of the\nCA certificates in the verification chain to your operating system's trusted\ncertificate store ([macOS][mac-keychain-access], [Windows][install-cert-vista]).\n\nIf you are _unable_ to either **obtain a trusted certificate**, or trust the\nself-signed certificate you can disable certificate verification in Git and GCM.\n\n---\n**Security Warning** :warning:\n\nDisabling verification of TLS (SSL) certificates removes protection against a\n[man-in-the-middle (MITM) attack][mitm-attack].\n\nOnly disable certificate verification if you are sure you need to, are aware of\nall the risks, and are unable to trust specific self-signed certificates\n(as described above).\n\n---\n\nThe [environment variable `GIT_SSL_NO_VERIFY`][git-ssl-no-verify] and\n[Git configuration option `http.sslVerify`][git-http-ssl-verify] can be used to\ncontrol TLS (SSL) certificate verification.\n\nTo disable verification for a specific remote (for example `https://example.com`):\n\n```shell\ngit config --global http.https://example.com.sslVerify false\n```\n\nTo disable verification for the current user for **_all remotes_** (**not\nrecommended**):\n\n```shell\n# Environment variable (Windows)\nSET GIT_SSL_NO_VERIFY=1\n\n# Environment variable (macOS/Linux)\nexport GIT_SSL_NO_VERIFY=1\n\n# Git configuration (Windows/macOS/Linux)\ngit config --global http.sslVerify false\n```\n\n---\n\n**Note:** You may also experience similar verification errors if you are using a\nnetwork traffic inspection tool such as [Telerik Fiddler][telerik-fiddler]. If\nyou are using such tools please consult their documentation for trusting the\nproxy root certificates.\n\n---\n\n## Unsafe Remote URLs\n\nIf you are using a remote URL that is not considered safe, such as unencrypted\nHTTP (remote URLs that start with `http://`), host providers may prevent you\nfrom authenticating with your credentials.\n\nIn this case, you should consider using a HTTPS (starting with `https://`)\nremote URL to ensure your credentials are transmitted securely.\n\nIf you accept the risks associated with using an unsafe remote URL, you can\nconfigure GCM to allow the use of unsafe remote URLS by setting the environment\nvariable [`GCM_ALLOW_UNSAFE_REMOTES`][unsafe-envar], or by using the Git\nconfiguration option [`credential.allowUnsafeRemotes`][unsafe-config] to `true`.\n\n[environment]: environment.md\n[configuration]: configuration.md\n[git-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy\n[git-remote-name-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remoteltnamegtproxy\n[rfc-3986-321]: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1\n[rfc-3986-22]: https://www.rfc-editor.org/rfc/rfc3986#section-2.2\n[rfc-3986-21]: https://www.rfc-editor.org/rfc/rfc3986#section-2.1\n[curl-proxy-env-vars]: https://everything.curl.dev/usingcurl/proxies#proxy-environment-variables\n[other-proxy-options]: #other-proxy-options\n[curlopt-noproxy]: https://curl.se/libcurl/c/CURLOPT_NOPROXY.html\n[lets-encrypt]: https://letsencrypt.org/\n[comodo]: https://www.comodoca.com/\n[digicert]: https://www.digicert.com/\n[godaddy]: https://www.godaddy.com/\n[globalsign]: https://www.globalsign.com\n[mac-keychain-access]: https://support.apple.com/en-gb/guide/keychain-access/kyca2431/mac\n[install-cert-vista]: https://blogs.technet.microsoft.com/sbs/2008/05/08/installing-a-self-signed-certificate-as-a-trusted-root-ca-in-windows-vista/\n[mitm-attack]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack\n[git-ssl-no-verify]: https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking\n[git-http-ssl-verify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify\n[telerik-fiddler]: https://www.telerik.com/fiddler\n[unsafe-envar]: environment.md#gcm_allow_unsafe_remotes\n[unsafe-config]: configuration.md#credentialallowunsaferemotes\n"
  },
  {
    "path": "docs/ntlm-kerberos.md",
    "content": "# NTLM and Kerberos Authentication\n\n## Background\n\nNTLM and Kerberos are two authentication protocols that are commonly used in\nWindows environments.\n\nIn Git Credential Manager (GCM), we refer to these protocols under the umbrella\nterm \"Windows Integrated Authentication\".\n\n### NTLM\n\n[NTLM (NT LAN Manager)][ntlm-wiki] is a challenge-response authentication\nprotocol used in various Microsoft network protocols, such as\n[SMB file sharing][smb-docs].\n\n> [!CAUTION]\n> NTLM is now considered _**insecure**_ due to weak cryptographic algorithms and\n> vulnerabilities to various attacks, such as pass-the-hash and relay attacks.\n> As such, it is not recommended for use in modern applications.\n>\n> There are several versions of NTLM, with NTLMv2 being the latest, however\n> **all versions** are considered weak by modern security standards.\n>\n> Microsoft lists [NTLM as a deprecated protocol][ntlm-deprecated] and has\n> removed NTLMv1 from Windows as of Windows 11 build 24H2 / Server 2025.\n\nNTLM is advertised by HTTP servers using the `WWW-Authenticate: NTLM` header.\nWhen a client receives this header, it can respond with an NTLM authentication\nmessage to prove its identity.\n\n### Kerberos\n\n[Kerberos][kerberos-wiki], on the other hand, is a more secure and robust\nauthentication protocol that uses tickets to authenticate users and services.\nIt is the recommended authentication protocol for Windows domains and is widely\nused in enterprise environments.\n\nUnlike NTLM, Kerberos is typically not directly advertised by HTTP servers, but\nis instead advertised using \"SPNEGO\" and the `WWW-Authenticate: Negotiate`\nheader.\n\n#### GSS-API Negotiate and SPNEGO\n\nKerberos (or NTLM) authentication is typically initially established using the\n[GSS-API][gssapi-wiki] ([RFC 2743][gssapi-rfc]) negotiation mechanism\n[\"SPNEGO\"][spnego-wiki] ([RFC 4178][spnego-rfc]). SPNEGO allows the client and\nserver to agree on which authentication protocol to use (Kerberos or NTLM) based\non their capabilities. Typically Kerberos is preferred if both the client and\nserver support it, with NTLM acting as a fallback.\n\n## Built-in Support in Git\n\nGit provides built-in support for NTLM and Kerberos authentication through the\nuse of [libcurl][libcurl], which is the underlying library used by Git for HTTP\nand HTTPS communications. When Git is compiled with libcurl support, it can\nleverage the authentication mechanisms provided by libcurl, including NTLM and\nKerberos.\n\nOn Windows, Git can use the native Windows [SSPI][sspi-wiki] (Security Support\nProvider Interface) to perform NTLM and Kerberos authentication. This allows Git\nto integrate seamlessly with the Windows authentication infrastructure.\n\n> [!NOTE]\n> As of Git for Windows version 2.XX.X, **NTLM support is disabled by default**.\n> Kerberos support _remains enabled_.\n\n### Re-enabling NTLM Support\n\nYou can re-enable NTLM support in Git for Windows for a particular remote by\nsetting Git config option [`http.<url>.allowNTLMAuth`][ntlm-config] to `true`.\nFor example, to enable NTLM authentication for `https://example.com`, you would\nrun the following command:\n\n```shell\ngit config --global http.https://example.com.allowNTLMAuth true\n```\n\n> [!WARNING]\n> Enabling NTLM authentication may expose you to security risks, as NTLM is\n> considered insecure. It is recommended to use Kerberos authentication where\n> possible, and to only use NTLM with trusted servers in secure environments.\n\n> [!WARNING]\n> Only ever use NTLM authentication over secure connections (i.e., HTTPS) to\n> protect against eavesdropping and man-in-the-middle attacks.\n\nWhen using GCM with a remote that supports NTLM authentication, GCM will warn\nyou if NTLM authentication is not enabled in Git but the remote server\nadvertises NTLM support.\n\n![GCM warning prompt that NTLM is disabled inside of Git][ntlm-warning-image]\n\n* Selecting \"Just this time\" will continue with NTLM authentication, but only\n  for the current operation. The next time you interact with that remote, you\n  will be prompted again.\n\n* Selecting \"Always for this remote\" will set the `http.<url>.allowNTLMAuth`\n  configuration option to `true` for that remote, and continue with NTLM\n  authentication.\n\n* Selecting \"No\" will prompt for a basic username/password credential, and Git's\n  NTLM authentication support will remain disabled. If the remote server only\n  supports NTLM then authentication will fail.\n\n### Seamless Authentication\n\nWhen using NTLM or Kerberos authentication with Git on Windows, it is possible\nto achieve seamless authentication without prompting for credentials. This is\nbecause Git can leverage the existing Windows user credentials to authenticate\nwith the server.\n\nThis means that if you are logged into your Windows account, Git can use those\ncredentials to authenticate with the remote server automatically, without\nprompting you for a username or password.\n\nThis feature is enabled by default in Git. To disable this behavior, you can set\nthe [`http.<url>.emptyAuth`][emptyauth] configuration option to `false`. For\nexample, to disable seamless authentication for `https://example.com`, you would\nrun the following command:\n\n```shell\ngit config --global http.https://example.com.emptyAuth false\n```\n\nIf you disable seamless authentication, Git will prompt you for credentials\nwhen accessing a remote that advertises NTLM or Kerberos support rather than\nusing the current Windows user's credentials.\n\n[ntlm-wiki]: https://en.wikipedia.org/wiki/NTLM\n[kerberos-wiki]: https://en.wikipedia.org/wiki/Kerberos_(protocol)\n[smb-docs]: https://learn.microsoft.com/en-gb/windows/win32/fileio/microsoft-smb-protocol-and-cifs-protocol-overview\n[ntlm-deprecated]: https://learn.microsoft.com/en-us/windows/whats-new/deprecated-features\n[ntlm-config]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpallowNTLMAuth\n[gssapi-rfc]: https://datatracker.ietf.org/doc/html/rfc2743\n[gssapi-wiki]: https://en.wikipedia.org/wiki/GSSAPI\n[spnego-rfc]: https://datatracker.ietf.org/doc/html/rfc4178\n[spnego-wiki]: https://en.wikipedia.org/wiki/SPNEGO\n[libcurl]: https://curl.se/libcurl/\n[sspi-wiki]: https://en.wikipedia.org/wiki/Security_Support_Provider_Interface\n[emptyauth]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpemptyAuth\n[ntlm-warning-image]: img/ntlm-warning.png\n"
  },
  {
    "path": "docs/rename.md",
    "content": "# Git Credential Manager Rename\n\nIn November 2021, _\"Git Credential Manager Core\"_ was [renamed][rename-pr] to\nsimply _\"Git Credential Manager\"_, dropping the \"Core\" moniker. We announced the\nnew name in a [GitHub blog post][rename-blog], along with the new home for the\nproject in its own [organization][gcm-org].\n\n![Git Credential Manager Core renamed](img/gcmcore-rename.png)\n\nAt the time, the actual exectuable name was not updated and continued to be\n`git-credential-manager-core`. As of [2.0.877][rename-ver], the executable has\nbeen renamed to `git-credential-manager`, matching the new project name.\n\n---\n\n:warning: **Update:** :warning:\n\nAs of [2.3.0][no-symlink-ver] the `git-credential-manager-core` symlinks have been\nremoved.\n\nIf you have not updated your configuration you will see error messages similar to:\n\n```console\ngit: 'credential-manager-core' is not a git command. See 'git --help'.\n```\n\nTo fix your configuration, please follow the [instructions][instructions] below.\n\n---\n\n## Rename transition\n\nIf you continue to use the `git-credential-manager-core` executable name you may\nsee warning messages like below:\n\n```console\nwarning: git-credential-manager-core was renamed to git-credential-manager\nwarning: see https://aka.ms/gcm/rename for more information\n```\n\nSince the executable was renamed in 2.0.877, GCM has also included symlinks\nusing the old name in order to ensure no one's setups would immediately break.\n\nThese links will remain until _two_ major Git versions are released after GCM\n2.0.877, _**at which point the symlinks will no longer be included**_.\n\nIt is recommended to update your Git configuration to use the new executable\nname as soon as possible to prevent any issues in the future.\n\n## How to update\n\n### Git for Windows\n\nIf you are using GCM bundled with Git for Windows (recommended), you should make\nsure you have updated to the latest version.\n\n[Download the latest Git for Windows ⬇️][git-windows]\n\n### Windows standalone installer\n\nIf you are using GCM installed either by the user (`gcmuser-*.exe`) or system\n(`gcm-*.exe`) installers on Windows, you should uninstall the current version\nfirst and then download and install the [latest version][gcm-latest].\n\nUninstall instructions for your Windows version can be found\n[here][win-standalone-instr].\n\n### macOS Homebrew\n\n> **Note:** As of October 2022 the old `git-credential-manager-core` cask name\n> is still used. In the future we plan to rename the package to drop the `-core`\n> suffix.\n\nIf you use Homebrew to install GCM on macOS you should use `brew upgrade` to\ninstall the latest version.\n\n```sh\nbrew upgrade git-credential-manager-core\n```\n\n### macOS package\n\nIf you use the .pkg file to install GCM on macOS, you should first uninstall the\ncurrent version, and then install the [latest package][gcm-latest].\n\n```sh\nsudo /usr/local/share/gcm-core/uninstall.sh\ninstaller -pkg <path-to-new-package> -target /\n```\n\n### Linux Debian package\n\nIf you use the .deb Debian package to install GCM on Linux, you should first\n`unconfigure` the current version, uninstall the package, and then install and\n`configure` the [latest version][gcm-latest].\n\n```sh\ngit-credential-manager-core unconfigure\nsudo dpkg -r gcmcore\nsudo dpkg -i <path-to-new-package>\ngit-credential-manager configure\n```\n\n### Linux tarball\n\nIf you are using the pre-built GCM binaries on Linux from our tarball, you\nshould first `unconfigure` the current version before extracting the [latest\nbinaries][gcm-latest].\n\n```sh\ngit-credential-manager-core unconfigure\nrm $(command -v git-credential-manager-core)\ntar -xvf <path-to-new-tarball> -C /usr/local/bin\ngit-credential-manager configure\n```\n\n### Troubleshooting\n\nIf after updating your GCM installations if you are still seeing the\n[warning][warnings] messages you can try manually editing your Git configuration\nto point to the correct GCM executable name.\n\nStart by listing all Git configuration for `credential.helper`, including which\nfiles the particular config entries are located in, using the following command:\n\n```sh\ngit config --show-origin --get-all credential.helper\n```\n\nOn Mac or Linux you should see something like this:\n\n<!-- markdownlint-disable MD010 -->\n```shell-session\n$ git config --show-origin --get-all credential.helper\nfile:/opt/homebrew/etc/gitconfig\tcredential.helper=osxkeychain\nfile:/Users/jdoe/.gitconfig\tcredential.helper=\nfile:/Users/jdoe/.gitconfig\tcredential.helper=/usr/local/share/gcm-core/git-credential-manager-core\n```\n\nOn Windows you should see something like this:\n\n```shell-session\n> git config --show-origin --get-all credential.helper\nfile:C:/Program Files/Git/etc/gitconfig\tcredential.helper=manager-core\n```\n<!-- markdownlint-enable MD010 -->\n\nLook out for entries that include `git-credential-manager-core` or\n`manager-core`; these should be replaced and updated to `git-credential-manager`\nor `manager` respectively.\n\n> **Note:** When updating the Git configuration file in your home directory\n> (`$HOME/.gitconfig` or `%USERPROFILE%\\.gitconfig`) you should ensure there are\n> is an additional blank entry for `credential.helper` before the GCM entry.\n>\n> **Mac/Linux**\n>\n> ```ini\n> [credential]\n>     helper =\n>     helper = /usr/local/share/gcm-core/git-credential-manager\n> ```\n>\n> **Windows**\n>\n> ```ini\n> [credential]\n>     helper =\n>     helper = C:/Program\\\\ Files\\\\ \\\\(x86\\\\)/Git\\\\ Credential\\\\ Manager/git-credential-manager.exe\n> ```\n>\n> The blank entry is important as it makes sure GCM is the only credential\n> helper that is configured, and overrides any helpers configured at the system/\n> machine-wide level.\n\n[rename-pr]: https://github.com/git-ecosystem/git-credential-manager/pull/541\n[rename-blog]: https://github.blog/2022-04-07-git-credential-manager-authentication-for-everyone/#universal-git-authentication\n[gcm-org]: https://github.com/git-ecosystem\n[rename-ver]: https://github.com/git-ecosystem/git-credential-manager/releases/tag/v2.0.877\n[git-windows]: https://git-scm.com/download/win\n[gcm-latest]: https://aka.ms/gcm/latest\n[warnings]: #rename-transition\n[win-standalone-instr]: ../README.md#standalone-installation\n[instructions]: #how-to-update\n[no-symlink-ver]: https://github.com/git-ecosystem/git-credential-manager/releases/tag/v2.3.0\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Command-line usage\n\nAfter installation, Git will use Git Credential Manager and you will only need\nto interact with any authentication dialogs asking for credentials.\nGCM stays invisible as much as possible, so ideally you’ll forget that you’re\ndepending on GCM at all.\n\nAssuming GCM has been installed, use your favorite terminal to execute the\nfollowing commands to interact directly with GCM.\n\n```shell\ngit credential-manager [<command> [<args>]]\n```\n\n## Commands\n\n### --help / -h / -?\n\nDisplays a list of available commands.\n\n### --version\n\nDisplays the current version.\n\n### get / store / erase\n\nCommands for interaction with Git. You shouldn't need to run these manually.\n\nRead the [Git manual][git-credentials-custom-helpers] about custom helpers for\nmore information.\n\n### configure/unconfigure\n\nSet your user-level Git configuration (`~/.gitconfig`) to use GCM. If you pass\n`--system` to these commands, they act on the system-level Git configuration\n(`/etc/gitconfig`) instead.\n\n### azure-repos\n\nInteract with the Azure Repos host provider to bind/unbind user accounts to\nAzure DevOps organizations or specific remote URLs, and manage the\nauthentication authority cache.\n\nFor more information about managing user account bindings see\n[here][azure-access-tokens-ua].\n\n[azure-access-tokens-ua]: azrepos-users-and-tokens.md#useraccounts\n[git-credentials-custom-helpers]: https://git-scm.com/docs/gitcredentials#_custom_helpers\n\n### github\n\nInteract with the GitHub host provider to manage your accounts on GitHub.com and\nGitHub Enterprise Server instances.\n"
  },
  {
    "path": "docs/windows-broker.md",
    "content": "# Web Account Manager integration\n\nGit Credential Manager (GCM) knows how to integrate with the\n[Web Account Manager (WAM)][azure-refresh-token-terms] feature of Windows. GCM\nuses WAM to store credentials for Azure DevOps. Authentication requests are said\nto be \"brokered\" to the operating system. Currently, GCM will share\nauthentication state with a few other Microsoft developer tools like Visual\nStudio and the Azure CLI, meaning fewer authentication prompts. Enabling WAM\nintegration may also be required with certain\n[Conditional Access policies][azure-conditional-access], which enterprises use\nto help protect their assets, including source code.\n\nIntegration with the WAM broker offers convenience and other benefits, but may\nalso make unexpected other changes on your device. On a device owned and managed\nby your institution or employer, WAM is probably the right choice. On a personal\ndevice or a device owned by a different institution (e.g. if you're a contractor\nworking for Company A with access to resources at Company B), there are\nsurprising behaviors that you should be aware of before enabling WAM integration.\n\nNote that this only affects [Azure DevOps][azure-devops].\nIt doesn't impact authentication with GitHub, Bitbucket, or any other Git host.\n\n## How to enable\n\nYou can opt-in to WAM support by setting the environment variable\n[`GCM_MSAUTH_USEBROKER`][GCM_MSAUTH_USEBROKER] or setting the Git configuration\nvalue [`credential.msauthUseBroker`][credential.msauthUseBroker].\n\n## Features\n\nWhen you turn on WAM support, GCM can cooperate with Windows and with other\nWAM-enabled software on your machine. This means a more seamless experience,\nfewer multi-factor authentication prompts, and the ability to use additional\nauthentication technologies like smart cards and Windows Hello. These\nconvenience and security features make a good case for enabling WAM.\n\n## Using the current OS account by default\n\nEnabling WAM does not currently automatically use the current Windows account\nfor authentication. In order to opt-in to this behavior you can set the\n[`GCM_MSAUTH_USEDEFAULTACCOUNT`][GCM_MSAUTH_USEDEFAULTACCOUNT] environment\nvariable or set the\n[`credential.msauthUseDefaultAccount`][credential.msauthUseDefaultAccount] Git\nconfiguration value to `true`.\n\nIn certain cloud hosted environments when using a work or school account, such\nas [Microsoft Dev Box][devbox], this setting is **_automatically enabled_**.\n\nTo disable this behavior, set the environment variable\n[`GCM_MSAUTH_USEDEFAULTACCOUNT`][GCM_MSAUTH_USEDEFAULTACCOUNT] or the\n[`credential.msauthUseDefaultAccount`][credential.msauthUseDefaultAccount] Git\nconfiguration value explicitly to `false`.\n\n## Surprising behaviors\n\nThe WAM and Windows identity systems are complex, addressing a very broad range\nof customer use cases. What works for a solo home user may not be adequate for a\ncorporate-managed fleet of 100,000 devices and vice versa. The GCM team isn't\nresponsible for the user experience or choices made by WAM, but by integrating\nwith WAM, we inherit some of those choices. Therefore, we want you to be aware\nof some defaults and experiences if you choose to use WAM integration.\n\n### For work or school accounts (Azure AD-backed identities)\n\nWhen you sign into an Azure DevOps organization backed by Azure AD (often your\ncompany or school email), if your machine is already joined to Azure AD matching\nthat Azure DevOps organization, you'll get a seamless and easy-to-use experience.\n\nIf your machine isn't Azure AD-joined, or is Azure AD-joined to a different\ntenant, WAM will present you with a dialog box suggesting you stay signed in and\nallow the organization to manage your device. The dialog box has changed a bit\nin various versions of Windows; here are two examples from 2021:\n\n![Consent dialog pre-21H1][aad-questions]\n\n![Consent dialog post-21H1][aad-questions-21h1]\n\nDepending on what you click, one of three things can happen:\n\n- If you leave \"allow my organization to manage my device\" checked and click\n\"OK\", your computer will be registered with the Azure AD tenant backing the\norganization.\nIt may also be MDM-enrolled (\"Mobile Device Management\" -- think Intune,\nAirWatch, MobileIron, etc.), meaning an administrator can deploy policies to\nyour machine: requiring certain kinds of sign-in, turning on antivirus and\nfirewall software, and enabling BitLocker.\nYour identity will also be available to other apps on the computer for signing\nin, some of which may do so automatically.\n\n![Example of policies pushed to an Intune-enrolled device][aad-bitlocker]\n\n- If you uncheck \"allow my organization to manage my device\" and click \"OK\",\nyour computer will be registered with Azure AD but will not be MDM-enrolled.\nYour identity will be available to other apps on the computer for signing in.\nOther apps may log you in automatically or prompt you again to allow your\norganization to manage your device. Despite joining Azure AD, your\norganization's Conditional Access policies may still prevent you from accessing\nAzure DevOps.\nIf so, you'll be prompted with instructions on how to enroll in MDM.\n\n- If you instead click \"No, sign in to this app only\", your machine will not be\njoined to Azure AD or MDM-enrolled, so no policies can be enforced, and your\nidentity won't be made available to other apps on the computer.\nSimilar to the above, your organization's Conditional Access policies may\nprevent you from proceeding.\n\nIf Conditional Access is required to access your organization's Git repositories,\nyou can [enable WAM integration][GCM_MSAUTH_USEBROKER] (or follow other\ninstructions your organization provides).\n\n#### Removing device management\n\nIf you've allowed your computer to be managed and want to undo it, you can go\ninto **Settings**, **Accounts**, **Access work or school**.\nIn the section where you see your email address and organization name, click\n**Disconnect**.\n\n![Finding your work or school account][aad-work-school]\n\n![Disconnecting from Azure AD][aad-disconnect]\n\n### For Microsoft accounts\n\nWhen you sign into an Azure DevOps organization backed by Microsoft account\n(MSA) identities (email addresses like `@outlook.com` or `@gmail.com` fall into\nthis category), you may be prompted to select an existing \"work or school\naccount\" or use a different one.\n\nIn order to sign in with an MSA you should continue and select \"Use a different\n[work or school] account\", but enter your MSA credentials when prompted. This is\ndue to a configuration outside of our control. We expect this experience to\nimprove over time and a \"personal account\" option to be presented in the future.\n\n![Initial dialog to choose an existing or different account][ms-sign-in]\n\nIf you've connected your MSA to Windows or signed-in to other Microsoft\napplications such as Office, then you may see this account listed in the\nauthentication prompts when using GCM.\n\n---\n\n⚠️ **Important** ⚠️\n\nWhen adding a new MSA to Windows, you'll be asked to select whether to use this\naccount across all of your device (**option 1**), or only permit Microsoft-apps\nto access your identity (**option 2**). If you opt to use the account everywhere,\nthen your local Windows user account will be connected to that MSA.\nThis means you'll need to use your MSA credentials to sign in to Windows going\nforward.\n\nSelecting \"just this app\" or \"Microsoft apps only\" will still allow you to use\nthis MSA across apps in Windows, but will not require you to use your MSA\ncredentials to sign in to Windows.\n\n![Confirmation to connect your MSA to Windows][msa-confirm]\n\nTo disconnect an MSA added using option 1, you can go into **Settings**,\n**Accounts**, **Your info** and click **Stop signing in to all Microsoft apps\nautomatically**.\n\n![Remove your Microsoft account from Windows][msa-remove]\n\nFor MSAs added for \"Microsoft apps only\", you can modify whether or not these\naccounts are available to other applications, and also remove the accounts from\n **Settings**, **Accounts**, **Emails & accounts**:\n\n![Allow all Microsoft apps to access your identity][all-ms-apps]\n\n![Microsoft apps must ask to access your identity][apps-must-ask]\n\n## Running as administrator\n\n### GCM 2.1 and later\n\nFrom version 2.1 onwards, GCM uses a version of the [Microsoft Authentication\nLibrary (MSAL)][msal-dotnet] that supports use of the Windows\nbroker from an elevated process.\n\n### Previous versions\n\nThe Windows broker (\"WAM\") makes heavy use of [COM][ms-com], a remote procedure\ncall (RPC) technology built into Windows. In order to integrate with WAM, Git\nCredential Manager and the underlying\n[Microsoft Authentication Library (MSAL)][msal-dotnet] must use COM interfaces\nand RPCs. When you run Git Credential Manager as an elevated process, some of\nthe calls made between GCM and WAM may fail due to differing process security\nlevels. This can happen when you run `git` from an Administrator command-prompt\nor perform Git operations from Visual Studio running as Administrator.\n\nIf you've enabled using the broker, GCM will check whether it's running in an\nelevated process. If it is, GCM will automatically attempt to modify the COM\nsecurity settings for the running process so that GCM and WAM can work together.\nHowever, this automatic process security change is not guaranteed to succeed.\nVarious external factors like registry or system-wide COM settings may cause it\nto fail. If GCM can't modify the process's COM security settings, GCM prints a\nwarning message and won't be able to use the broker.\n\n```text\nwarning: broker initialization failed\nFailed to set COM process security to allow Windows broker from an elevated process (0x80010119).\nSee https://aka.ms/gcm/wamadmin for more information.\n```\n\n### Possible solutions\n\nIn order to fix the problem, there are a few options:\n\n1. Update to the [latest Git for Windows][git-for-windows-latest]\n   **(recommended)**.\n2. Run Git or Git Credential Manager from non-elevated processes.\n3. Disable the broker by setting the\n   [`GCM_MSAUTH_USEBROKER`][GCM_MSAUTH_USEBROKER]\n   environment variable or the\n   [`credential.msauthUseBroker`][credential.msauthUseBroker]\n   Git configuration setting to `false`.\n\n[azure-refresh-token-terms]: https://docs.microsoft.com/azure/active-directory/devices/concept-primary-refresh-token#key-terminology-and-components\n[azure-conditional-access]: https://docs.microsoft.com/azure/active-directory/conditional-access/overview\n[azure-devops]: https://azure.microsoft.com/en-us/products/devops\n[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER-experimental\n[GCM_MSAUTH_USEDEFAULTACCOUNT]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental\n[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker-experimental\n[credential.msauthUseDefaultAccount]: configuration.md#credentialmsauthusedefaultaccount-experimental\n[aad-questions]: img/aad-questions.png\n[aad-questions-21h1]: img/aad-questions-21H1.png\n[aad-bitlocker]: img/aad-bitlocker.png\n[aad-work-school]: img/aad-work-school.png\n[aad-disconnect]: img/aad-disconnect.png\n[ms-sign-in]: img/get-signed-in.png\n[all-ms-apps]: img/all-microsoft.png\n[apps-must-ask]: img/apps-must-ask.png\n[ms-com]: https://docs.microsoft.com/en-us/windows/win32/com/the-component-object-model\n[msa-confirm]: img/msa-confirm.png\n[msa-remove]: img/msa-remove.png\n[msal-dotnet]: https://aka.ms/msal-net\n[devbox]: https://azure.microsoft.com/en-us/products/dev-box\n[git-for-windows-latest]: https://git-scm.com/download/win\n"
  },
  {
    "path": "docs/wsl.md",
    "content": "# Windows Subsystem for Linux (WSL)\n\nGCM can be used with the\n[Windows Subsystem for Linux (WSL)][wsl], both WSL1 and WSL2, by\nfollowing these instructions.\n\nIn order to use GCM with WSL you must be on Windows 10 Version 1903 or later.\nThis is the first version of Windows that includes the required `wsl.exe` tool\nthat GCM uses to interoperate with Git in your WSL distributions.\n\nIt is highly recommended that you install Git for Windows to both install GCM\nand enable the best experience sharing credentials & settings between WSL and\nthe Windows host. Alternatively, you must be using GCM version 2.0.XXX or later\nand configure the `WSLENV` environment variable as\n[described below][configuring-wsl-without-git-for-windows].\n\n## Configuring WSL with Git for Windows (recommended)\n\nStart by installing the [latest Git for Windows ⬇️][latest-git-for-windows]\n\n_Inside your WSL installation_, run the following command to set GCM as the Git\ncredential helper:\n\n```shell\ngit config --global credential.helper \"/mnt/c/Program\\ Files/Git/mingw64/bin/git-credential-manager.exe\"\n```\n\n> **Note:** the location of git-credential-manager.exe may be different in your\ninstallation of Git for Windows.\n\nIf you intend to use Azure DevOps you must _also_ set the following Git\nconfiguration _inside of your WSL installation_.\n\n```shell\ngit config --global credential.https://dev.azure.com.useHttpPath true\n```\n\n## Configuring WSL without Git for Windows\n\nIf you wish to use GCM inside of WSL _without installing Git for Windows_\nyou must complete additional configuration so that GCM can callback to Git\ninside of your WSL installation.\n\nStart by installing the [latest GCM for Windows⬇️][latest-gcm]\n\n_Inside your WSL installation_, run the following command to set GCM as the Git\ncredential helper:\n\n```shell\ngit config --global credential.helper \"/mnt/c/Program\\ Files\\ \\(x86\\)/Git\\ Credential\\ Manager/git-credential-manager.exe\"\n\n# For Azure DevOps support only\ngit config --global credential.https://dev.azure.com.useHttpPath true\n```\n\nIn **_Windows_** you need to update the `WSLENV` environment variable to include\nthe value `GIT_EXEC_PATH/wp`. From an _Administrator_ Command Prompt run the\nfollowing:\n\n```batch\nSETX WSLENV %WSLENV%:GIT_EXEC_PATH/wp\n```\n\nAfter updating the `WSLENV` environment variable, restart your WSL installation.\n\n### Using the user-only GCM installer?\n\nIf you have installed GCM using the user-only installer (i.e, the `gcmuser-*.exe`\ninstaller and not the system-wide/admin required installer), you need to modify\nthe above instructions to point to\n`/mnt/c/Users/<USERNAME>/AppData/Local/Programs/Git\\ Credential\\ Manager/git-credential-manager.exe`\ninstead.\n\n## How it works\n\nGCM leverages the built-in interoperability between Windows and WSL, provided by\nMicrosoft. You can read more about Windows/WSL interop [here][wsl-interop].\n\nGit inside of a WSL installation can launch the GCM _Windows_ application\ntransparently to acquire credentials. Running GCM as a Windows application\nallows it to take full advantage of the host operating system for storing\ncredentials securely, and presenting GUI prompts for authentication.\n\nUsing the host operating system (Windows) to store credentials also means that\nyour Windows applications and WSL distributions can all share those credentials,\nremoving the need to sign-in multiple times.\n\n## Shared configuration\n\nUsing GCM as a credential helper for a WSL Git installation means that any\nconfiguration set in WSL Git is NOT respected by GCM (by default). This is\nbecause GCM is running as a Windows application, and therefore will use the Git\nfor Windows installation to query configuration.\n\nThis means things like proxy settings for GCM need to be set in Git for Windows\nas well as WSL Git as they are stored in different files\n(`%USERPROFILE%\\.gitconfig` vs `\\\\wsl$\\distro\\home\\$USER\\.gitconfig`).\n\nYou can configure WSL such that GCM will use the WSL Git configuration following\nthe [instructions above][configuring-wsl-without-git-for-windows]. However,\nthis then means that things like proxy settings are unique to the specific WSL\ninstallation, and not shared with others or the Windows host.\n\n## Can I install Git Credential Manager directly inside of WSL?\n\nYes. Rather than install GCM as a Windows application (and have WSL Git invoke\nthe Windows GCM), can you install GCM as a Linux application instead.\n\nTo do this, simply follow the\n[GCM installation instructions for Linux][linux-installation].\n\n**Note:** In this scenario, because GCM is running as a Linux application\nit cannot utilize authentication or credential storage features of the host\nWindows operating system.\n\n[wsl]: https://aka.ms/wsl\n[configuring-wsl-without-git-for-windows]: #configuring-wsl-without-git-for-windows\n[latest-git-for-windows]: https://github.com/git-for-windows/git/releases/latest\n[latest-gcm]: https://aka.ms/gcm/latest\n[wsl-interop]: https://docs.microsoft.com/en-us/windows/wsl/interop\n[linux-installation]: ../README.md#linux\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"rollForward\": \"latestMajor\",\n    \"version\": \"8.0\"\n  }\n}\n\n"
  },
  {
    "path": "nuget.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<configuration>\r\n  <packageSources>\r\n    <clear />\r\n    <add key=\"nuget\" value=\"https://api.nuget.org/v3/index.json\" />\r\n  </packageSources>\r\n</configuration>\r\n"
  },
  {
    "path": "src/linux/Directory.Build.props",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <!-- Import parent Directory.Build.props file -->\n  <Import Project=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))\" />\n\n  <!-- Set binary and intermediate output directories -->\n  <PropertyGroup>\n    <PlatformOutPath>$(RepoOutPath)linux\\</PlatformOutPath>\n    <ProjectOutPath>$(PlatformOutPath)$(MSBuildProjectName)\\</ProjectOutPath>\n    <BaseOutputPath>$(ProjectOutPath)bin\\</BaseOutputPath>\n    <BaseIntermediateOutputPath>$(ProjectOutPath)obj\\</BaseIntermediateOutputPath>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/linux/Packaging.Linux/Packaging.Linux.csproj",
    "content": "<Project>\n  <!-- Implicit SDK props import -->\n  <Import Project=\"Sdk.props\" Sdk=\"Microsoft.NET.Sdk\" />\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <InstallFromSource>false</InstallFromSource>\n    <InstallPrefix>/usr/local</InstallPrefix>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"**/*\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../../shared/Git-Credential-Manager/Git-Credential-Manager.csproj\" ReferenceOutputAssembly=\"false\" />\n  </ItemGroup>\n\n  <!-- Implicit SDK targets import (so we can override the default targets below) -->\n  <Import Project=\"Sdk.targets\" Sdk=\"Microsoft.NET.Sdk\" />\n\n  <Target Name=\"CoreCompile\" Condition=\"'$(OSPlatform)'=='linux'\">\n    <Message Text=\"$(MSBuildProjectDirectory)\\build.sh --install-from-source=$(InstallFromSource) --configuration='$(Configuration)' --version='$(Version)' --runtime='$(RuntimeIdentifier)' --install-prefix='$(InstallPrefix)'\" Importance=\"High\" />\n    <Exec Command=\"$(MSBuildProjectDirectory)\\build.sh --install-from-source=$(InstallFromSource) --configuration='$(Configuration)' --version='$(Version)' --runtime='$(RuntimeIdentifier)' --install-prefix='$(InstallPrefix)'\" />\n  </Target>\n\n  <Target Name=\"CoreClean\">\n    <RemoveDir Directories=\"$(ProjectOutPath)\" />\n  </Target>\n\n  <Target Name=\"PrepareForRun\" />\n\n</Project>\n"
  },
  {
    "path": "src/linux/Packaging.Linux/build.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\n\necho \"Building Packaging.Linux...\"\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nINSTALLER_SRC=\"$SRC/linux/Packaging.Linux\"\nINSTALLER_OUT=\"$OUT/linux/Packaging.Linux\"\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --configuration=*)\n    CONFIGURATION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --version=*)\n    VERSION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --install-from-source=*)\n    INSTALL_FROM_SOURCE=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --runtime=*)\n    RUNTIME=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --install-prefix=*)\n    INSTALL_PREFIX=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Ensure install prefix exists\nif [ ! -d \"$INSTALL_PREFIX\" ]; then\n    mkdir -p \"$INSTALL_PREFIX\"\nfi\n\nif [ ! -z \"$RUNTIME\" ]; then\n    echo \"Building for runtime ${RUNTIME}\"\nfi\n\n# Perform pre-execution checks\nCONFIGURATION=\"${CONFIGURATION:=Debug}\"\nif [ -z \"$VERSION\" ]; then\n    die \"--version was not set\"\nfi\n\nOUTDIR=\"$INSTALLER_OUT/$CONFIGURATION\"\nPAYLOAD=\"$OUTDIR/payload\"\nSYMBOLS=\"$OUTDIR/payload.sym\"\n\n# Lay out payload\n\"$INSTALLER_SRC/layout.sh\" --configuration=\"$CONFIGURATION\" --runtime=\"$RUNTIME\" --output=\"$PAYLOAD\" --symbol-output=\"$SYMBOLS\" || exit 1\n\nif [ $INSTALL_FROM_SOURCE = true ]; then\n    echo \"Installing to $INSTALL_PREFIX\"\n\n    # Install directories\n    INSTALL_TO=\"$INSTALL_PREFIX/share/gcm-core/\"\n    LINK_TO=\"$INSTALL_PREFIX/bin/\"\n\n    mkdir -p \"$INSTALL_TO\" \"$LINK_TO\"\n\n    # Copy all binaries and shared libraries to target installation location\n    cp -R \"$PAYLOAD\"/* \"$INSTALL_TO\" || exit 1\n\n    # Create symlink\n    if [ ! -f \"$LINK_TO/git-credential-manager\" ]; then\n        ln -s -r \"$INSTALL_TO/git-credential-manager\" \\\n            \"$LINK_TO/git-credential-manager\" || exit 1\n    fi\n\n    echo \"Install complete.\"\nelse\n    # Pack\n    \"$INSTALLER_SRC/pack.sh\" --configuration=\"$CONFIGURATION\" --runtime=\"$RUNTIME\" --payload=\"$PAYLOAD\" --symbols=\"$SYMBOLS\" --version=\"$VERSION\" || exit 1\nfi\n\necho \"Build of Packaging.Linux complete.\"\n"
  },
  {
    "path": "src/linux/Packaging.Linux/install-from-source.sh",
    "content": "#!/bin/sh\n\n# Halt execution immediately on failure.\n# Note there are some scenarios in which this will not exit; see\n# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html\n# for additional details.\nset -e\n\nis_ci=\nfor i in \"$@\"; do\n    case \"$i\" in\n        -y)\n        is_ci=true\n        shift # Past argument=value\n        ;;\n        --install-prefix=*)\n        installPrefix=\"${i#*=}\"\n        shift # past argument=value\n        ;;\n    esac\ndone\n\n# If install-prefix is not passed, use default value\nif [ -z \"$installPrefix\" ]; then\n    installPrefix=/usr/local\nfi\n\n# Ensure install directory exists\nif [ ! -d \"$installPrefix\" ]; then\n    echo \"The folder $installPrefix does not exist\"\n    exit\nfi\n\n# In non-ci scenarios, advertise what we will be doing and\n# give user the option to exit.\nif [ -z $is_ci ]; then\n    echo \"This script will download, compile, and install Git Credential Manager to:\n\n    $installPrefix/bin\n\nGit Credential Manager is licensed under the MIT License: https://aka.ms/gcm/license\"\n\n\twhile true; do\n        # Display prompt once before reading input\n        printf \"Do you want to continue? [Y/n] \"\n\n        # Prefer reading from the controlling terminal (TTY) when available,\n        # so that input works even if the script is piped (e.g. curl URL | sh)\n        if [ -r /dev/tty ]; then\n            read yn < /dev/tty\n        # If no TTY is available, attempt to read from standard input (stdin)\n        elif ! read yn; then\n            # If input is not possible via TTY or stdin, assume a non-interactive environment\n            # and abort with guidance for automated usage\n            echo \"Interactive prompt unavailable in this environment. Use 'sh -s -- -y' for automated install.\"\n            exit 1\n        fi\n\n        case \"$yn\" in\n            [Yy]*|\"\") break ;;\n            [Nn]*) exit ;;\n            *) echo \"Please answer yes or no.\" ;;\n        esac\n    done\nfi\n\ninstall_packages() {\n    pkg_manager=$1\n    install_verb=$2\n    packages=$3\n\n    for package in $packages; do\n        # Ensure we don't stomp on existing installations.\n        if type $package >/dev/null 2>&1; then\n            continue\n        fi\n\n        if [ $pkg_manager = apk ]; then\n            $sudo_cmd $pkg_manager $install_verb $package\n        elif [ $pkg_manager = zypper ]; then\n            $sudo_cmd $pkg_manager -n $install_verb $package\n        elif [ $pkg_manager = pacman ]; then\n            $sudo_cmd $pkg_manager --noconfirm $install_verb $package\n        else\n            $sudo_cmd $pkg_manager $install_verb $package -y\n        fi\n    done\n}\n\nensure_dotnet_installed() {\n    if [ -z \"$(verify_existing_dotnet_installation)\" ]; then\n        curl -LO https://dot.net/v1/dotnet-install.sh\n        chmod +x ./dotnet-install.sh\n        bash -c \"./dotnet-install.sh --channel 8.0\"\n\n        # Since we have to run the dotnet install script with bash, dotnet isn't\n        # added to the process PATH, so we manually add it here.\n        cd ~\n        export DOTNET_ROOT=$(pwd)/.dotnet\n        add_to_PATH $DOTNET_ROOT\n    fi\n}\n\nverify_existing_dotnet_installation() {\n    # Get initial pieces of installed sdk version(s).\n    sdks=$(dotnet --list-sdks | cut -c 1-3)\n\n    # If we have a supported version installed, return.\n    supported_dotnet_versions=\"8.0\"\n    for v in $supported_dotnet_versions; do\n        if [ $(echo $sdks | grep \"$v\") ]; then\n            echo $sdks\n        fi\n    done\n}\n\nadd_to_PATH () {\n  for directory; do\n    if [ ! -d \"$directory\" ]; then\n        continue; # Skip nonexistent directory.\n    fi\n    case \":$PATH:\" in\n        *\":$directory:\"*)\n            break\n        ;;\n        *)\n            export PATH=$PATH:$directory\n        ;;\n    esac\n  done\n}\n\napt_install() {\n    pkg_name=$1\n\n    $sudo_cmd apt update\n    $sudo_cmd apt install $pkg_name -y 2>/dev/null\n}\n\nprint_unsupported_distro() {\n    prefix=$1\n    distro=$2\n\n    echo \"$prefix: $distro is not officially supported by the GCM project.\"\n    echo \"See https://gh.io/gcm/linux for details.\"\n}\n\nversion_at_least() {\n\t[ \"$(printf '%s\\n' \"$1\" \"$2\" | sort -V | head -n1)\" = \"$1\" ]\n}\n\nsudo_cmd=\n\n# If the user isn't root, we need to use `sudo` for certain commands\n# (e.g. installing packages).\nif [ -z \"$sudo_cmd\" ]; then\n    if [ `id -u` != 0 ]; then\n        sudo_cmd=sudo\n    fi\nfi\n\neval \"$(sed -n 's/^ID=/distribution=/p' /etc/os-release)\"\neval \"$(sed -n 's/^VERSION_ID=/version=/p' /etc/os-release | tr -d '\"')\"\ncase \"$distribution\" in\n    debian | ubuntu)\n        $sudo_cmd apt update\n        install_packages apt install \"curl git\"\n\n        # Install dotnet packages and dependencies if needed.\n        if [ -z \"$(verify_existing_dotnet_installation)\" ]; then\n            # First try to use native feeds (Ubuntu 22.04 and later).\n            if ! apt_install dotnet8; then\n                # If the native feeds fail, we fall back to\n                # packages.microsoft.com. We begin by adding the dotnet package\n                # repository/signing key.\n                $sudo_cmd apt update && $sudo_cmd apt install wget -y\n                curl -LO https://packages.microsoft.com/config/\"$distribution\"/\"$version\"/packages-microsoft-prod.deb\n                $sudo_cmd dpkg -i packages-microsoft-prod.deb\n                rm packages-microsoft-prod.deb\n\n                # Proactively install tzdata to prevent prompts.\n                export DEBIAN_FRONTEND=noninteractive\n                $sudo_cmd apt install -y --no-install-recommends tzdata\n\n                $sudo_cmd apt update\n                $sudo_cmd apt install apt-transport-https -y\n                $sudo_cmd apt update\n                $sudo_cmd apt install dotnet-sdk-8.0 dpkg-dev -y\n            fi\n        fi\n    ;;\n    fedora | centos | rhel | ol)\n        $sudo_cmd dnf upgrade -y\n\n        # Install dotnet/GCM dependencies.\n        install_packages dnf install \"curl git krb5-libs libicu openssl-libs zlib findutils which bash\"\n\n        ensure_dotnet_installed\n    ;;\n    alpine)\n        $sudo_cmd apk update\n\n        # Install dotnet/GCM dependencies.\n        # Alpine 3.14 and earlier need libssl1.1, while later versions need libssl3.\n        if ( version_at_least \"3.15\" $version ) then\n            libssl_pkg=\"libssl3\"\n        else\n            libssl_pkg=\"libssl1.1\"\n        fi\n\n        install_packages apk add \"curl git icu-libs krb5-libs libgcc libintl $libssl_pkg libstdc++ zlib which bash coreutils gcompat\"\n\n        ensure_dotnet_installed\n    ;;\n    sles | opensuse*)\n        $sudo_cmd zypper -n update\n\n        # Install dotnet/GCM dependencies.\n        install_packages zypper install \"curl git find krb5 libicu\"\n\n        ensure_dotnet_installed\n    ;;\n    arch)\n        print_unsupported_distro \"WARNING\" \"$distribution\"\n\n        # --noconfirm required when running from container\n        $sudo_cmd pacman -Syu --noconfirm\n\n        # Install dotnet/GCM dependencies.\n        install_packages pacman -Sy \"curl git glibc gcc krb5 icu openssl libc++ zlib\"\n\n        ensure_dotnet_installed\n    ;;\n    mariner | azurelinux*)\n        print_unsupported_distro \"WARNING\" \"$distribution\"\n        $sudo_cmd tdnf update -y\n\n        # Install dotnet/GCM dependencies.\n        install_packages tdnf install \"curl ca-certificates git krb5-libs libicu openssl-libs zlib findutils which bash awk\"\n\n        ensure_dotnet_installed\n    ;;\n    *)\n        print_unsupported_distro \"ERROR\" \"$distribution\"\n        exit\n    ;;\nesac\n\n# Detect if the script is part of a full source checkout or standalone instead.\nscript_path=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\ntoplevel_path=\"${script_path%/src/linux/Packaging.Linux}\"\nif [ \"z$script_path\" = \"z$toplevel_path\" ] || [ ! -f \"$toplevel_path/Git-Credential-Manager.sln\" ]; then\n    toplevel_path=\"$PWD/git-credential-manager\"\n    test -d \"$toplevel_path\" || git clone https://github.com/git-ecosystem/git-credential-manager\nfi\n\nif [ -z \"$DOTNET_ROOT\" ]; then\n    DOTNET_ROOT=\"$(dirname $(which dotnet))\"\nfi\n\ncd \"$toplevel_path\"\n$sudo_cmd env \"PATH=$PATH\" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:installPrefix=$installPrefix\nadd_to_PATH \"$installPrefix/bin\"\n"
  },
  {
    "path": "src/linux/Packaging.Linux/layout.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\n\nmake_absolute () {\n    case \"$1\" in\n    /*)\n        echo \"$1\"\n        ;;\n    *)\n        echo \"$PWD/$1\"\n        ;;\n    esac\n}\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --configuration=*)\n    CONFIGURATION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --output=*)\n    PAYLOAD=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --runtime=*)\n    RUNTIME=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --symbol-output=*)\n    SYMBOLOUT=\"${i#*=}\"\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nGCM_SRC=\"$SRC/shared/Git-Credential-Manager\"\nPROJ_OUT=\"$OUT/linux/Packaging.Linux\"\n\n# Build parameters\nFRAMEWORK=net8.0\n\n# Perform pre-execution checks\nCONFIGURATION=\"${CONFIGURATION:=Debug}\"\nif [ -z \"$PAYLOAD\" ]; then\n    die \"--output was not set\"\nfi\nif [ -z \"$SYMBOLOUT\" ]; then\n    SYMBOLOUT=\"$PAYLOAD.sym\"\nfi\n\n# Cleanup payload directory\nif [ -d \"$PAYLOAD\" ]; then\n    echo \"Cleaning existing payload directory '$PAYLOAD'...\"\n    rm -rf \"$PAYLOAD\"\nfi\n\n# Cleanup symbol directory\nif [ -d \"$SYMBOLOUT\" ]; then\n    echo \"Cleaning existing symbols directory '$SYMBOLOUT'...\"\n    rm -rf \"$SYMBOLOUT\"\nfi\n\n# Ensure directories exists\nmkdir -p \"$PAYLOAD\" \"$SYMBOLOUT\"\n\nif [ -z \"$DOTNET_ROOT\" ]; then\n    DOTNET_ROOT=\"$(dirname $(which dotnet))\"\nfi\n\n# Publish core application executables\necho \"Publishing core application...\"\nif [ -z \"$RUNTIME\" ]; then\n    $DOTNET_ROOT/dotnet publish \"$GCM_SRC\" \\\n        --configuration=\"$CONFIGURATION\" \\\n        --framework=\"$FRAMEWORK\" \\\n        --self-contained \\\n        -p:PublishSingleFile=true \\\n        --output=\"$(make_absolute \"$PAYLOAD\")\" || exit 1\nelse\n    $DOTNET_ROOT/dotnet publish \"$GCM_SRC\" \\\n        --configuration=\"$CONFIGURATION\" \\\n        --framework=\"$FRAMEWORK\" \\\n        --runtime=\"$RUNTIME\" \\\n        --self-contained \\\n        -p:PublishSingleFile=true \\\n        --output=\"$(make_absolute \"$PAYLOAD\")\" || exit 1\nfi\n\n# Collect symbols\necho \"Collecting managed symbols...\"\nmv \"$PAYLOAD\"/*.pdb \"$SYMBOLOUT\" || exit 1\n\necho \"Build complete.\"\n"
  },
  {
    "path": "src/linux/Packaging.Linux/pack.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nPROJ_OUT=\"$OUT/linux/Packaging.Linux\"\nINSTALLER_SRC=\"$SRC/osx/Installer.Mac\"\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --version=*)\n    VERSION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --payload=*)\n    PAYLOAD=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --symbols=*)\n    SYMBOLS=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --runtime=*)\n    RUNTIME=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --configuration=*)\n    CONFIGURATION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --output=*)\n    OUTPUT_ROOT=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Perform pre-execution checks\nCONFIGURATION=\"${CONFIGURATION:=Debug}\"\nif [ -z \"$VERSION\" ]; then\n    die \"--version was not set\"\nfi\nif [ -z \"$PAYLOAD\" ]; then\n    die \"--payload was not set\"\nelif [ ! -d \"$PAYLOAD\" ]; then\n    die \"Could not find '$PAYLOAD'. Did you run layout.sh first?\"\nfi\nif [ -z \"$SYMBOLS\" ]; then\n    die \"--symbols was not set\"\nfi\nif [ -z \"$OUTPUT_ROOT\" ]; then\n    OUTPUT_ROOT=\"$PROJ_OUT/$CONFIGURATION\"\nfi\n\n# Fall back to host architecture if no explicit runtime is given.\nif test -z \"$RUNTIME\"; then\n    HOST_ARCH=\"`uname -m`\"\n\n    case $HOST_ARCH in\n        x86_64|amd64)\n            RUNTIME=\"linux-x64\"\n            ;;\n        aarch64|arm64)\n            RUNTIME=\"linux-arm64\"\n            ;;\n        armhf)\n            RUNTIME=\"linux-arm\"\n            ;;\n        *)\n            die \"Could not determine host architecture! ($HOST_ARCH)\"\n            ;;\n    esac\nfi\n\nTAROUT=\"$OUTPUT_ROOT/tar\"\nTARBALL=\"$TAROUT/gcm-$RUNTIME-$VERSION.tar.gz\"\nSYMTARBALL=\"$TAROUT/gcm-$RUNTIME-$VERSION-symbols.tar.gz\"\n\nDEBOUT=\"$OUTPUT_ROOT/deb\"\nDEBROOT=\"$DEBOUT/root\"\nDEBPKG=\"$DEBOUT/gcm-$RUNTIME-$VERSION.deb\"\nmkdir -p \"$DEBROOT\"\n\n# Set full read, write, execute permissions for owner and just read and execute permissions for group and other\necho \"Setting file permissions...\"\n/bin/chmod -R 755 \"$PAYLOAD\" || exit 1\n\necho \"Packing Packaging.Linux...\"\n\n# Cleanup any old archive files\nif [ -e \"$TAROUT\" ]; then\n    echo \"Deleting old archive '$TAROUT'...\"\n    rm \"$TAROUT\"\nfi\n\n# Ensure the parent directory for the archive exists\nmkdir -p \"$TAROUT\" || exit 1\n\n# Build binaries tarball\necho \"Building binaries tarball...\"\npushd \"$PAYLOAD\"\ntar -czvf \"$TARBALL\" * || exit 1\npopd\n\n# Build symbols tarball\necho \"Building symbols tarball...\"\npushd \"$SYMBOLS\"\ntar -czvf \"$SYMTARBALL\" * || exit 1\npopd\n\n# Build .deb\nINSTALL_TO=\"$DEBROOT/usr/local/share/gcm-core/\"\nLINK_TO=\"$DEBROOT/usr/local/bin/\"\nmkdir -p \"$DEBROOT/DEBIAN\" \"$INSTALL_TO\" \"$LINK_TO\" || exit 1\n\n# Determine architecture for debian control file from the runtime architecture\ncase $RUNTIME in\n    linux-x64)\n        ARCH=\"amd64\"\n        ;;\n    linux-arm64)\n        ARCH=\"arm64\"\n        ;;\n    linux-arm)\n        ARCH=\"armhf\"\n        ;;\n    *)\n        die \"Incompatible runtime architecture given for pack.sh\"\n        ;;\nesac\n\n# make the debian control file\n# this is purposefully not indented, see\n# https://stackoverflow.com/questions/9349616/bash-eof-in-if-statement\n# for details\ncat >\"$DEBROOT/DEBIAN/control\" <<EOF\nPackage: gcm\nVersion: $VERSION\nSection: vcs\nPriority: optional\nArchitecture: $ARCH\nDepends:\nMaintainer: GCM <gitfundamentals@github.com>\nDescription: Cross Platform Git Credential Manager command line utility.\n GCM supports authentication with a number of Git hosting providers\n including GitHub, BitBucket, and Azure DevOps.\n For more information see https://aka.ms/gcm\nEOF\n\n# Copy all binaries and shared libraries to target installation location\ncp -R \"$PAYLOAD\"/* \"$INSTALL_TO\" || exit 1\n\n# Create symlink\nif [ ! -f \"$LINK_TO/git-credential-manager\" ]; then\n    ln -s -r \"$INSTALL_TO/git-credential-manager\" \\\n        \"$LINK_TO/git-credential-manager\" || exit 1\nfi\n\ndpkg-deb -Zxz --root-owner-group --build \"$DEBROOT\" \"$DEBPKG\" || exit 1\n\necho $MESSAGE\n"
  },
  {
    "path": "src/osx/.gitignore",
    "content": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n## Build generated\nbuild/\nDerivedData/\n\n## Various settings\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\nxcuserdata/\n\n## Other\n*.moved-aside\n*.xccheckout\n*.xcscmblueprint\n\n## Obj-C/Swift specific\n*.hmap\n*.ipa\n*.dSYM.zip\n*.dSYM\n\n# CocoaPods\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\nPods/\n#\n# Add this line if you want to avoid checking in source code from the Xcode workspace\n# *.xcworkspace\n\n# Carthage\n#\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build\n\n# fastlane\n#\n# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the\n# screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/#source-control\n\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots/**/*.png\nfastlane/test_output\n\n# Code Injection\n#\n# After new code Injection tools there's a generated folder /iOSInjectionProject\n# https://github.com/johnno1962/injectionforxcode\n\niOSInjectionProject/\n"
  },
  {
    "path": "src/osx/Directory.Build.props",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <!-- Import parent Directory.Build.props file -->\n  <Import Project=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))\" />\n\n  <!-- Set binary and intermediate output directories -->\n  <PropertyGroup>\n    <PlatformOutPath>$(RepoOutPath)osx\\</PlatformOutPath>\n    <ProjectOutPath>$(PlatformOutPath)$(MSBuildProjectName)\\</ProjectOutPath>\n    <BaseOutputPath>$(ProjectOutPath)bin\\</BaseOutputPath>\n    <BaseIntermediateOutputPath>$(ProjectOutPath)obj\\</BaseIntermediateOutputPath>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/osx/Installer.Mac/Installer.Mac.csproj",
    "content": "﻿<Project>\n  <!-- Implicit SDK props import -->\n  <Import Project=\"Sdk.props\" Sdk=\"Microsoft.NET.Sdk\" />\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"**/*\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../../shared/Git-Credential-Manager/Git-Credential-Manager.csproj\" ReferenceOutputAssembly=\"false\" />\n  </ItemGroup>\n\n  <!-- Implicit SDK targets import (so we can override the default targets below) -->\n  <Import Project=\"Sdk.targets\" Sdk=\"Microsoft.NET.Sdk\" />\n\n  <Target Name=\"CoreCompile\" Condition=\"'$(OSPlatform)'=='osx'\">\n    <Message Text=\"$(MSBuildProjectDirectory)\\build.sh --configuration='$(Configuration)' --version='$(Version)' --runtime='$(RuntimeIdentifier)'\" Importance=\"High\" />\n    <Exec Command=\"$(MSBuildProjectDirectory)\\build.sh --configuration='$(Configuration)' --version='$(Version)' --runtime='$(RuntimeIdentifier)'\" />\n  </Target>\n\n  <Target Name=\"CoreClean\">\n    <RemoveDir Directories=\"$(ProjectOutPath)\" />\n  </Target>\n\n  <Target Name=\"PrepareForRun\" />\n\n</Project>\n"
  },
  {
    "path": "src/osx/Installer.Mac/build.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\n\necho \"Building Installer.Mac...\"\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nINSTALLER_SRC=\"$SRC/osx/Installer.Mac\"\nINSTALLER_OUT=\"$OUT/osx/Installer.Mac\"\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --configuration=*)\n    CONFIGURATION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --runtime=*)\n    RUNTIME=\"${i#*=}\"\n    shift\n    ;;\n    --version=*)\n    VERSION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Perform pre-execution checks\nCONFIGURATION=\"${CONFIGURATION:=Debug}\"\nif [ -z \"$VERSION\" ]; then\n    die \"--version was not set\"\nfi\n\nif [ -z \"$RUNTIME\" ]; then\n    TEST_RUNTIME=`uname -m`\n    case $TEST_RUNTIME in\n        \"x86_64\")\n            RUNTIME=\"osx-x64\"\n            ;;\n        \"arm64\")\n            RUNTIME=\"osx-arm64\"\n            ;;\n        *)\n            die \"Unknown runtime '$TEST_RUNTIME'\"\n            ;;\n    esac\nfi\n\nOUTDIR=\"$INSTALLER_OUT/pkg/$CONFIGURATION\"\nPAYLOAD=\"$OUTDIR/payload\"\nCOMPONENTDIR=\"$OUTDIR/components\"\nCOMPONENTOUT=\"$COMPONENTDIR/com.microsoft.gitcredentialmanager.component.pkg\"\nDISTOUT=\"$OUTDIR/gcm-$RUNTIME-$VERSION.pkg\"\n\n# Layout and pack\n\"$INSTALLER_SRC/layout.sh\" --configuration=\"$CONFIGURATION\" --output=\"$PAYLOAD\" --runtime=\"$RUNTIME\" || exit 1\n\"$INSTALLER_SRC/pack.sh\" --payload=\"$PAYLOAD\" --version=\"$VERSION\" --output=\"$COMPONENTOUT\" || exit 1\n\"$INSTALLER_SRC/dist.sh\" --package-path=\"$COMPONENTDIR\" --version=\"$VERSION\" --output=\"$DISTOUT\" --runtime=\"$RUNTIME\" || exit 1\n\necho \"Build of Installer.Mac complete.\"\n"
  },
  {
    "path": "src/osx/Installer.Mac/codesign.sh",
    "content": "#!/bin/bash\n\nSIGN_DIR=$1\nDEVELOPER_ID=$2\nENTITLEMENTS_FILE=$3\n\nif [ -z \"$SIGN_DIR\" ]; then\n    echo \"error: missing directory argument\"\n    exit 1\nelif [ -z \"$DEVELOPER_ID\" ]; then\n    echo \"error: missing developer id argument\"\n    exit 1\nelif [ -z \"$ENTITLEMENTS_FILE\" ]; then\n    echo \"error: missing entitlements file argument\"\n    exit 1\nfi\n\n# The codesign command needs the entitlements file to be given as an absolute\n# file path; relative paths can cause issues.\nif [[ \"${ENTITLEMENTS_FILE}\" != /* ]]; then\n  echo \"error: entitlements file argument must be an absolute path\"\n  exit 1\nfi\n\necho \"======== INPUTS ========\"\necho \"Directory: $SIGN_DIR\"\necho \"Developer ID: $DEVELOPER_ID\"\necho \"Entitlements: $ENTITLEMENTS_FILE\"\necho \"======== END INPUTS ========\"\necho\necho \"======== ENTITLEMENTS ========\"\ncat \"$ENTITLEMENTS_FILE\"\necho \"======== END ENTITLEMENTS ========\"\necho\n\ncd \"$SIGN_DIR\" || exit 1\nfor f in *\ndo\n    macho=$(file --mime \"$f\" | grep mach)\n    # Runtime sign dylibs and Mach-O binaries\n    if [[ $f == *.dylib ]] || [ -n \"$macho\" ];\n    then\n        echo \"Signing with entitlements and hardening: $f\"\n        codesign -s \"$DEVELOPER_ID\" \"$f\" --timestamp --force --options=runtime --entitlements \"$ENTITLEMENTS_FILE\"\n    elif [ -d \"$f\" ];\n    then\n        echo \"Signing files in subdirectory: $f\"\n        (\n            cd \"$f\" || exit 1\n            for i in *\n            do\n                codesign -s \"$DEVELOPER_ID\" \"$i\" --timestamp --force\n            done\n        )\n    else\n        echo \"Signing: $f\"\n        codesign -s \"$DEVELOPER_ID\" \"$f\" --timestamp --force\n    fi\ndone\n"
  },
  {
    "path": "src/osx/Installer.Mac/dist.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nINSTALLER_SRC=\"$SRC/osx/Installer.Mac\"\nRESXPATH=\"$INSTALLER_SRC/resources\"\n\n# Product information\nIDENTIFIER=\"com.microsoft.gitcredentialmanager.dist\"\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --version=*)\n    VERSION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --package-path=*)\n    PACKAGEPATH=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --output=*)\n    DISTOUT=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --runtime=*)\n    RUNTIME=\"${i#*=}\"\n    shift\n    ;;\n    --identity=*)\n    IDENTITY=\"${i#*=}\"\n    shift\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Perform pre-execution checks\nif [ -z \"$VERSION\" ]; then\n    die \"--version was not set\"\nfi\nif [ -z \"$PACKAGEPATH\" ]; then\n    die \"--package-path was not set\"\nelif [ ! -d \"$PACKAGEPATH\" ]; then\n    die \"Could not find '$PACKAGEPATH'. Did you run pack.sh first?\"\nfi\nif [ -z \"$DISTOUT\" ]; then\n    die \"--output was not set\"\nfi\nif [ -z \"$RUNTIME\" ]; then\n    TEST_RUNTIME=`uname -m`\n    case $TEST_RUNTIME in\n        \"x86_64\")\n            RUNTIME=\"osx-x64\"\n            ;;\n        \"arm64\")\n            RUNTIME=\"osx-arm64\"\n            ;;\n        *)\n            die \"Unknown runtime '$TEST_RUNTIME'\"\n            ;;\n    esac\nfi\n\necho \"Building for runtime '$RUNTIME'\"\n\nif [ \"$RUNTIME\" == \"osx-x64\" ]; then\n    DISTPATH=\"$INSTALLER_SRC/distribution.x64.xml\"\nelse\n    DISTPATH=\"$INSTALLER_SRC/distribution.arm64.xml\"\nfi\n\n# Cleanup any old package\nif [ -e \"$DISTOUT\" ]; then\n    echo \"Deleting old product package '$DISTOUT'...\"\n    rm \"$DISTOUT\"\nfi\n\n# Ensure the parent directory for the package exists\nmkdir -p \"$(dirname \"$DISTOUT\")\"\n\n# Build product installer\necho \"Building product package...\"\n/usr/bin/productbuild \\\n    --package-path \"$PACKAGEPATH\" \\\n    --resources \"$RESXPATH\" \\\n    --distribution \"$DISTPATH\" \\\n    --identifier \"$IDENTIFIER\" \\\n    --version \"$VERSION\" \\\n    ${IDENTITY:+\"--sign\"} ${IDENTITY:+\"$IDENTITY\"} \\\n    \"$DISTOUT\" || exit 1\n\necho \"Product build complete.\"\n"
  },
  {
    "path": "src/osx/Installer.Mac/distribution.arm64.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<installer-gui-script minSpecVersion=\"1\">\n    <title>Git Credential Manager</title>\n    <background file=\"background.png\" mime-type=\"image/png\" alignment=\"bottomleft\" scaling=\"tofit\" />\n    <options customize=\"never\" hostArchitectures=\"arm64\"/>\n    <welcome file=\"welcome.html\" mime-type=\"text/html\" />\n    <conclusion file=\"conclusion.html\" mime-type=\"text/html\" />\n    <license file=\"LICENSE\" />\n    <volume-check>\n        <allowed-os-versions>\n            <os-version min=\"10.13\" />\n        </allowed-os-versions>\n    </volume-check>\n    <pkg-ref id=\"gcmcore\" />\n    <choices-outline>\n        <line choice=\"default\" />\n    </choices-outline>\n    <choice id=\"default\" visible=\"true\" enabled=\"false\" title=\"Git Credential Manager\" description=\"Git Credential Manager application.\">\n        <pkg-ref id=\"gcmcore\">com.microsoft.gitcredentialmanager.component.pkg</pkg-ref>\n    </choice>\n</installer-gui-script>\n"
  },
  {
    "path": "src/osx/Installer.Mac/distribution.x64.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<installer-gui-script minSpecVersion=\"1\">\n    <title>Git Credential Manager</title>\n    <background file=\"background.png\" mime-type=\"image/png\" alignment=\"bottomleft\" scaling=\"tofit\" />\n    <options customize=\"never\" hostArchitectures=\"x86_64\" />\n    <welcome file=\"welcome.html\" mime-type=\"text/html\" />\n    <conclusion file=\"conclusion.html\" mime-type=\"text/html\" />\n    <license file=\"LICENSE\" />\n    <volume-check>\n        <allowed-os-versions>\n            <os-version min=\"10.13\" />\n        </allowed-os-versions>\n    </volume-check>\n    <pkg-ref id=\"gcmcore\" />\n    <choices-outline>\n        <line choice=\"default\" />\n    </choices-outline>\n    <choice id=\"default\" visible=\"true\" enabled=\"false\" title=\"Git Credential Manager\" description=\"Git Credential Manager application.\">\n        <pkg-ref id=\"gcmcore\">com.microsoft.gitcredentialmanager.component.pkg</pkg-ref>\n    </choice>\n</installer-gui-script>\n"
  },
  {
    "path": "src/osx/Installer.Mac/entitlements.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n</dict>\n</plist>"
  },
  {
    "path": "src/osx/Installer.Mac/layout.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\nmake_absolute () {\n    case \"$1\" in\n    /*)\n        echo \"$1\"\n        ;;\n    *)\n        echo \"$PWD/$1\"\n        ;;\n    esac\n}\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nINSTALLER_SRC=\"$SRC/osx/Installer.Mac\"\nGCM_SRC=\"$SRC/shared/Git-Credential-Manager\"\nGCM_UI_SRC=\"$SRC/shared/Git-Credential-Manager.UI.Avalonia\"\n\n# Build parameters\nFRAMEWORK=net8.0\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --configuration=*)\n    CONFIGURATION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --output=*)\n    PAYLOAD=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --runtime=*)\n    RUNTIME=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --symbol-output=*)\n    SYMBOLOUT=\"${i#*=}\"\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Determine a runtime if one was not provided\nif [ -z \"$RUNTIME\" ]; then\n    TEST_RUNTIME=`uname -m`\n    case $TEST_RUNTIME in\n        \"x86_64\")\n            RUNTIME=\"osx-x64\"\n            ;;\n        \"arm64\")\n            RUNTIME=\"osx-arm64\"\n            ;;\n        *)\n            die \"Unknown runtime '$TEST_RUNTIME'\"\n            ;;\n    esac\nfi\n\necho \"Building for runtime '$RUNTIME'\"\n\n# Perform pre-execution checks\nCONFIGURATION=\"${CONFIGURATION:=Debug}\"\nif [ -z \"$PAYLOAD\" ]; then\n\tdie \"--output was not set\"\nfi\nif [ -z \"$SYMBOLOUT\" ]; then\n    SYMBOLOUT=\"$PAYLOAD.sym\"\nfi\n\n# Cleanup any old payload directory\nif [ -d \"$PAYLOAD\" ]; then\n    echo \"Cleaning old payload directory '$PAYLOAD'...\"\n    rm -rf \"$PAYLOAD\"\nfi\n\n# Ensure payload and symbol directories exists\nmkdir -p \"$PAYLOAD\" \"$SYMBOLOUT\"\n\n# Copy uninstaller script\necho \"Copying uninstall script...\"\ncp \"$INSTALLER_SRC/uninstall.sh\" \"$PAYLOAD\" || exit 1\n\n# Publish core application executables\necho \"Publishing core application...\"\ndotnet publish \"$GCM_SRC\" \\\n\t--configuration=\"$CONFIGURATION\" \\\n\t--framework=\"$FRAMEWORK\" \\\n\t--runtime=\"$RUNTIME\" \\\n\t--self-contained \\\n\t--output=\"$(make_absolute \"$PAYLOAD\")\" || exit 1\n\n# Collect symbols\necho \"Collecting managed symbols...\"\nmv \"$PAYLOAD\"/*.pdb \"$SYMBOLOUT\" || exit 1\n\n# Remove any unwanted .DS_Store files\necho \"Removing unnecessary files...\"\nfind \"$PAYLOAD\" -name '*.DS_Store' -type f -delete || exit 1\n\necho \"Layout complete.\"\n"
  },
  {
    "path": "src/osx/Installer.Mac/notarize.sh",
    "content": "#!/bin/bash\n\nfor i in \"$@\"\ndo\ncase \"$i\" in\n\t--package=*)\n\tPACKAGE=\"${i#*=}\"\n\tshift # past argument=value\n\t;;\n\t--keychain-profile=*)\n\tKEYCHAIN_PROFILE=\"${i#*=}\"\n\tshift # past argument=value\n\t;;\n\t*)\n\tdie \"unknown option '$i'\"\n\t;;\nesac\ndone\n\nif [ -z \"$PACKAGE\" ]; then\n    echo \"error: missing package argument\"\n    exit 1\nelif [ -z \"$KEYCHAIN_PROFILE\" ]; then\n    echo \"error: missing keychain profile argument\"\n    exit 1\nfi\n\n# Exit as soon as any line fails\nset -e\n\n# Send the notarization request\nxcrun notarytool submit -v \"$PACKAGE\" -p \"$KEYCHAIN_PROFILE\" --wait\n\n# Staple the notarization ticket (to allow offline installation)\nxcrun stapler staple -v \"$PACKAGE\"\n"
  },
  {
    "path": "src/osx/Installer.Mac/pack.sh",
    "content": "#!/bin/bash\ndie () {\n    echo \"$*\" >&2\n    exit 1\n}\n\n# Directories\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nROOT=\"$( cd \"$THISDIR\"/../../.. ; pwd -P )\"\nSRC=\"$ROOT/src\"\nOUT=\"$ROOT/out\"\nINSTALLER_SRC=\"$SRC/osx/Installer.Mac\"\n\n# Product information\nIDENTIFIER=\"com.microsoft.gitcredentialmanager\"\nINSTALL_LOCATION=\"/usr/local/share/gcm-core\"\n\n# Parse script arguments\nfor i in \"$@\"\ndo\ncase \"$i\" in\n    --version=*)\n    VERSION=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --payload=*)\n    PAYLOAD=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    --output=*)\n    PKGOUT=\"${i#*=}\"\n    shift # past argument=value\n    ;;\n    *)\n          # unknown option\n    ;;\nesac\ndone\n\n# Perform pre-execution checks\nif [ -z \"$VERSION\" ]; then\n    die \"--version was not set\"\nfi\nif [ -z \"$PAYLOAD\" ]; then\n    die \"--payload was not set\"\nelif [ ! -d \"$PAYLOAD\" ]; then\n    die \"Could not find '$PAYLOAD'. Did you run layout.sh first?\"\nfi\nif [ -z \"$PKGOUT\" ]; then\n    die \"--output was not set\"\nfi\n\n# Cleanup any old component\nif [ -e \"$PKGOUT\" ]; then\n    echo \"Deleting old component '$PKGOUT'...\"\n    rm \"$PKGOUT\"\nfi\n\n# Ensure the parent directory for the component exists\nmkdir -p \"$(dirname \"$PKGOUT\")\"\n\n# Set full read, write, execute permissions for owner and just read and execute permissions for group and other\necho \"Setting file permissions...\"\n/bin/chmod -R 755 \"$PAYLOAD\" || exit 1\n\n# Remove any extended attributes (ACEs)\necho \"Removing extended attributes...\"\n/usr/bin/xattr -rc \"$PAYLOAD\" || exit 1\n\n# Build component packages\necho \"Building core component package...\"\n/usr/bin/pkgbuild \\\n    --root \"$PAYLOAD/\" \\\n    --install-location \"$INSTALL_LOCATION\" \\\n    --scripts \"$INSTALLER_SRC/scripts\" \\\n    --identifier \"$IDENTIFIER\" \\\n    --version \"$VERSION\" \\\n    \"$PKGOUT\" || exit 1\n\necho \"Component pack complete.\"\n"
  },
  {
    "path": "src/osx/Installer.Mac/resources/en.lproj/conclusion.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\" />\n        <style>\n            body { font-family: Helvetica; margin: 0 20px; }\n            h2 { font-weight: normal; }\n            .section { overflow: auto; }\n            .mono { font-family: monospace; }\n        </style>\n    </head>\n    <body>\n        <div class=\"section\">\n            <p>Git Credential Manager was installed in <a href=\"file:///usr/local/share/gcm-core\"><code>/usr/local/share/gcm-core</code></a> and configured for the current user.</p>\n        </div>\n        <div class=\"section\">\n            <h2>Other users</h2>\n            <p>\n                GCM has already been automatically configured for use by the current user with Git.\n                If other users wish to use GCM, have them run the following command to update their global Git configuration (<code>~/.gitconfig</code>):\n            </p>\n            <p class=\"mono\">$ git-credential-manager configure</p>\n            <p>\n                To configure GCM for all users, run the following command to update the system Git configuration:\n            </p>\n            <p class=\"mono\">$ git-credential-manager configure --system</p>\n        </div>\n        <div class=\"section\">\n            <h2>Uninstall</h2>\n            <p>If you wish to uninstall GCM, run the following script:</p>\n            <p class=\"mono\">$ /usr/local/share/gcm-core/uninstall.sh</p>\n        </div>\n        <div class=\"section\">\n            <h2>Resources</h3>\n            <ul>\n                <li><a href=\"https://aka.ms/gcm\">Project homepage</a></li>\n                <li><a href=\"https://aka.ms/gcm/faq\">Frequently asked questions</a></li>\n                <li><a href=\"https://aka.ms/gcm/config\">Configuration options</a></li>\n            </ul>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "src/osx/Installer.Mac/resources/en.lproj/welcome.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\" />\n        <style>\n            body { font-family: Helvetica; margin: 0 20px; }\n            h2 { font-weight: normal; }\n            .section { overflow: auto; }\n        </style>\n    </head>\n    <body>\n        <div class=\"section\">\n            <h2>Git Credential Manager</h2>\n            <p>\n                Git Credential Manager is a secure, cross-platform Git credential helper with authentication support for GitHub, Azure Repos, and other popular Git hosting services.\n            </p>\n        </div>\n        <div class=\"section\">\n            <h2>Installation notes</h2>\n            <p>\n                If you have the old Java-based <a href=\"https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux\">Git Credential Manager for Mac & Linux</a> installed through Homebrew, it will be unlinked after installation.\n            </p>\n            <p>\n                Git Credential Manager will be configured as the Git credential helper for the current user by updating the global Git configuration file (<code>~/.gitconfig</code>).\n            </p>\n        </div>\n        <div class=\"section\">\n            <h2>Learn more</h2>\n            <ul>\n                <li><a href=\"https://aka.ms/gcm\">Project homepage</a></li>\n                <li><a href=\"https://aka.ms/gcm/faq\">Frequently asked questions</a></li>\n            </ul>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "src/osx/Installer.Mac/scripts/postinstall",
    "content": "#!/bin/bash\nset -e\n\nPACKAGE=$1\nINSTALL_DESTINATION=$2\n\nfunction IsBrewPkgInstalled\n{\n    # Check if Homebrew is installed\n    /usr/bin/which brew > /dev/null\n    if [ $? -eq 0 ]\n    then\n        # Check if the package has been installed\n        brew ls --versions \"$1\" > /dev/null\n        if [ $? -eq 0 ]\n        then\n            return 0\n        fi\n    fi\n    return 1\n}\n\n# Check if Java GCM is present on this system and unlink it should it exist\nif [ -L /usr/local/bin/git-credential-manager ] && IsBrewPkgInstalled \"git-credential-manager\";\nthen\n    brew unlink git-credential-manager\nfi\n\n# Create symlink to GCM in /usr/local/bin\nmkdir -p /usr/local/bin\n/bin/ln -Fs \"$INSTALL_DESTINATION/git-credential-manager\" /usr/local/bin/git-credential-manager\n\n# Configure GCM for the current user (running as the current user to avoid root\n# from taking ownership of ~/.gitconfig)\nsudo -u ${USER} \"$INSTALL_DESTINATION/git-credential-manager\" configure\n\nexit 0\n"
  },
  {
    "path": "src/osx/Installer.Mac/uninstall.sh",
    "content": "#!/bin/bash\n\nTHISDIR=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\nGCMBIN=\"$THISDIR/git-credential-manager\"\n\n# Ensure we're running as root\nif [ $(id -u) != \"0\" ]\nthen\n\tsudo \"$0\" \"$@\"\n\texit $?\nfi\n\n# Unconfigure (as the current user)\necho \"Unconfiguring credential helper...\"\nsudo -u `/usr/bin/logname` -E \"$GCMBIN\" unconfigure\n\n# Remove symlink\nif [ -L /usr/local/bin/git-credential-manager ]\nthen\n\techo \"Deleting symlink...\"\n\trm /usr/local/bin/git-credential-manager\nelse\n\techo \"No symlink found.\"\nfi\n\n# Remove legacy symlink\nif [ -L /usr/local/bin/git-credential-manager-core ]\nthen\n\techo \"Deleting legacy symlink...\"\n\trm /usr/local/bin/git-credential-manager-core\nelse\n\techo \"No legacy symlink found.\"\nfi\n\n# Forget package installation/delete receipt\necho \"Removing installation receipt...\"\npkgutil --forget com.microsoft.gitcredentialmanager\n\n# Remove application files\nif [ -d \"$THISDIR\" ]\nthen\n\techo \"Deleting application files...\"\n\trm -rf \"$THISDIR\"\nelse\n\techo \"No application files found.\"\nfi\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"'$(OSPlatform)'=='windows'\">net8.0;net472</TargetFrameworks>\n    <AssemblyName>Atlassian.Bitbucket</AssemblyName>\n    <RootNamespace>Atlassian.Bitbucket</RootNamespace>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' == 'net472'\">\n    <Reference Include=\"System.Net.Http\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <AvaloniaResource Include=\"UI\\Assets\\atlassian-logo.png\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/AuthenticationMethod.cs",
    "content": "﻿using System;\nnamespace Atlassian.Bitbucket\n{\n    public enum AuthenticationMethod\n    {\n        BasicAuth,\n        Sso\n    }\n}\n\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.UI.ViewModels;\nusing Atlassian.Bitbucket.UI.Views;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.UI;\n\nnamespace Atlassian.Bitbucket\n{\n\n    [Flags]\n    public enum AuthenticationModes\n    {\n        None = 0,\n        Basic = 1,\n        OAuth = 1 << 1,\n\n        All = Basic | OAuth\n    }\n    public interface IBitbucketAuthentication : IDisposable\n    {\n        Task<CredentialsPromptResult> GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes);\n        Task<OAuth2TokenResult> CreateOAuthCredentialsAsync(InputArguments input);\n        Task<OAuth2TokenResult> RefreshOAuthCredentialsAsync(InputArguments input, string refreshToken);\n        string GetRefreshTokenServiceName(InputArguments input);\n    }\n\n    public class CredentialsPromptResult\n    {\n        public CredentialsPromptResult(AuthenticationModes mode)\n        {\n            AuthenticationMode = mode;\n        }\n\n        public CredentialsPromptResult(AuthenticationModes mode, ICredential credential)\n            : this(mode)\n        {\n            Credential = credential;\n        }\n\n        public AuthenticationModes AuthenticationMode { get; }\n\n        public ICredential Credential { get; set; }\n    }\n\n    public class BitbucketAuthentication : AuthenticationBase, IBitbucketAuthentication\n    {\n        public static readonly string[] AuthorityIds =\n        {\n            BitbucketConstants.Id,\n        };\n\n        private readonly IRegistry<BitbucketOAuth2Client> _oauth2ClientRegistry;\n\n        public BitbucketAuthentication(ICommandContext context)\n            : this(context, new OAuth2ClientRegistry(context)) { }\n\n        public BitbucketAuthentication(ICommandContext context, IRegistry<BitbucketOAuth2Client> oauth2ClientRegistry)\n    : base(context)\n        {\n            EnsureArgument.NotNull(oauth2ClientRegistry, nameof(oauth2ClientRegistry));\n            this._oauth2ClientRegistry = oauth2ClientRegistry;\n        }\n\n        public async Task<CredentialsPromptResult> GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            // If we don't have a desktop session/GUI then we cannot offer OAuth since the only\n            // supported grant is authcode (i.e, using a web browser; device code is not supported).\n            if (!Context.SessionManager.IsDesktopSession)\n            {\n                modes = modes & ~AuthenticationModes.OAuth;\n            }\n\n            // If the only supported mode is OAuth then just return immediately\n            if (modes == AuthenticationModes.OAuth)\n            {\n                return new CredentialsPromptResult(AuthenticationModes.OAuth);\n            }\n\n            // We need at least one mode!\n            if (modes == AuthenticationModes.None)\n            {\n                throw new ArgumentException(@$\"Must specify at least one {nameof(AuthenticationModes)}\", nameof(modes));\n            }\n\n            // Shell out to the UI helper and show the Bitbucket u/p prompt\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string helperCommand, out string args))\n                {\n                    return await GetCredentialsViaHelperAsync(targetUri, userName, modes, helperCommand, args);\n                }\n\n                return await GetCredentialsViaUiAsync(targetUri, userName, modes);\n            }\n\n            return GetCredentialsViaTty(targetUri, userName, modes);\n        }\n\n        private async Task<CredentialsPromptResult> GetCredentialsViaUiAsync(\n            Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            var viewModel = new CredentialsViewModel(Context.SessionManager)\n            {\n                ShowOAuth = (modes & AuthenticationModes.OAuth) != 0,\n                ShowBasic = (modes & AuthenticationModes.Basic) != 0\n            };\n\n            if (!BitbucketHelper.IsBitbucketOrg(targetUri))\n            {\n                viewModel.Url = targetUri;\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                viewModel.UserName = userName;\n            }\n\n            await AvaloniaUi.ShowViewAsync<CredentialsView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n            ThrowIfWindowCancelled(viewModel);\n\n            switch (viewModel.SelectedMode)\n            {\n                case AuthenticationModes.OAuth:\n                    return new CredentialsPromptResult(AuthenticationModes.OAuth);\n\n                case AuthenticationModes.Basic:\n                    return new CredentialsPromptResult(\n                        AuthenticationModes.Basic,\n                        new GitCredential(viewModel.UserName, viewModel.Password)\n                        );\n\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(AuthenticationModes),\n                        \"Unknown authentication mode\", viewModel.SelectedMode.ToString());\n            }\n        }\n\n        private CredentialsPromptResult GetCredentialsViaTty(Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            switch (modes)\n            {\n                case AuthenticationModes.Basic:\n                    Context.Terminal.WriteLine(\"Enter Bitbucket credentials for '{0}'...\", targetUri);\n\n                    if (!string.IsNullOrWhiteSpace(userName))\n                    {\n                        // Don't need to prompt for the username if it has been specified already\n                        Context.Terminal.WriteLine(\"Username: {0}\", userName);\n                    }\n                    else\n                    {\n                        // Prompt for username\n                        userName = Context.Terminal.Prompt(\"Username\");\n                    }\n\n                    // Prompt for password\n                    string password = Context.Terminal.PromptSecret(\"Password\");\n\n                    return new CredentialsPromptResult(\n                        AuthenticationModes.Basic,\n                        new GitCredential(userName, password));\n\n                case AuthenticationModes.OAuth:\n                    return new CredentialsPromptResult(AuthenticationModes.OAuth);\n\n                case AuthenticationModes.None:\n                    throw new ArgumentOutOfRangeException(nameof(modes),\n                        @$\"At least one {nameof(AuthenticationModes)} must be supplied\");\n\n                default:\n                    var menuTitle = $\"Select an authentication method for '{targetUri}'\";\n                    var menu = new TerminalMenu(Context.Terminal, menuTitle);\n\n                    TerminalMenuItem oauthItem = null;\n                    TerminalMenuItem basicItem = null;\n\n                    if ((modes & AuthenticationModes.OAuth) != 0) oauthItem = menu.Add(\"OAuth\");\n                    if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add(\"Username/password\");\n\n                    // Default to the 'first' choice in the menu\n                    TerminalMenuItem choice = menu.Show(0);\n\n                    if (choice == oauthItem) goto case AuthenticationModes.OAuth;\n                    if (choice == basicItem) goto case AuthenticationModes.Basic;\n\n                    throw new Exception();\n            }\n        }\n\n        private async Task<CredentialsPromptResult> GetCredentialsViaHelperAsync(\n            Uri targetUri, string userName, AuthenticationModes modes, string helperCommand, string args)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"prompt\");\n            if (!BitbucketHelper.IsBitbucketOrg(targetUri))\n            {\n                promptArgs.AppendFormat(\" --url {0}\", QuoteCmdArg(targetUri.ToString()));\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                promptArgs.AppendFormat(\" --username {0}\", QuoteCmdArg(userName));\n            }\n\n            if ((modes & AuthenticationModes.Basic) != 0)\n            {\n                promptArgs.Append(\" --show-basic\");\n            }\n\n            if ((modes & AuthenticationModes.OAuth) != 0)\n            {\n                promptArgs.Append(\" --show-oauth\");\n            }\n\n            IDictionary<string, string> output = await InvokeHelperAsync(helperCommand, promptArgs.ToString());\n\n            if (output.TryGetValue(\"mode\", out string mode) &&\n                StringComparer.OrdinalIgnoreCase.Equals(mode, \"oauth\"))\n            {\n                return new CredentialsPromptResult(AuthenticationModes.OAuth);\n            }\n            else\n            {\n                if (!output.TryGetValue(\"username\", out userName))\n                {\n                    throw new Trace2Exception(Context.Trace2, \"Missing username in response\");\n                }\n\n                if (!output.TryGetValue(\"password\", out string password))\n                {\n                    throw new Trace2Exception(Context.Trace2, \"Missing password in response\");\n                }\n\n                return new CredentialsPromptResult(\n                    AuthenticationModes.Basic,\n                    new GitCredential(userName, password));\n            }\n        }\n\n        public async Task<OAuth2TokenResult> CreateOAuthCredentialsAsync(InputArguments input)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            var browserOptions = new OAuth2WebBrowserOptions\n            {\n                SuccessResponseHtml = BitbucketResources.AuthenticationResponseSuccessHtml,\n                FailureResponseHtmlFormat = BitbucketResources.AuthenticationResponseFailureHtmlFormat\n            };\n\n            var browser = new OAuth2SystemWebBrowser(Context.SessionManager, browserOptions);\n            var oauth2Client = _oauth2ClientRegistry.Get(input);\n\n            var authCodeResult = await oauth2Client.GetAuthorizationCodeAsync(browser, CancellationToken.None);\n            return await oauth2Client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);\n        }\n\n        public async Task<OAuth2TokenResult> RefreshOAuthCredentialsAsync(InputArguments input, string refreshToken)\n        {\n            var client = _oauth2ClientRegistry.Get(input);\n            return await client.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None);\n        }\n\n        public string GetRefreshTokenServiceName(InputArguments input)\n        {\n            var client = _oauth2ClientRegistry.Get(input);\n            return client.GetRefreshTokenServiceName(input);\n        }\n\n        protected internal virtual bool TryFindHelperCommand(out string command, out string args)\n        {\n            return TryFindHelperCommand(\n                BitbucketConstants.EnvironmentVariables.AuthenticationHelper,\n                BitbucketConstants.GitConfiguration.Credential.AuthenticationHelper,\n                BitbucketConstants.DefaultAuthenticationHelper,\n                out command,\n                out args);\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient => _httpClient ??= Context.HttpClientFactory.CreateClient();\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketConstants.cs",
    "content": "using System;\n\nnamespace Atlassian.Bitbucket\n{\n    public static class BitbucketConstants\n    {\n        public const string Id = \"bitbucket\";\n\n        public const string Name = \"Bitbucket\";\n\n        public const string DefaultAuthenticationHelper = \"Atlassian.Bitbucket.UI\";\n\n        public static class EnvironmentVariables\n        {\n            public const string AuthenticationHelper = \"GCM_BITBUCKET_HELPER\";\n            public const string AuthenticationModes = \"GCM_BITBUCKET_AUTHMODES\";\n            public const string AlwaysRefreshCredentials = \"GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS\";\n            public const String ValidateStoredCredentials = \"GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string AuthenticationHelper = \"bitbucketHelper\";\n                public const string AuthenticationModes = \"bitbucketAuthModes\";\n                public const string AlwaysRefreshCredentials = \"bitbucketAlwaysRefreshCredentials\";\n                public const string ValidateStoredCredentials = \"bitbucketValidateStoredCredentials\";\n            }\n        }\n        public static class HelpUrls\n        {\n            public const string DataCenterPasswordReset = \"/passwordreset\";\n            public const string DataCenterLogin = \"/login\";\n            public const string PasswordReset = \"https://bitbucket.org/account/password/reset/\";\n            public const string SignUp = \"https://bitbucket.org/account/signup/\";\n            public const string TwoFactor = \"https://support.atlassian.com/bitbucket-cloud/docs/enable-two-step-verification/\";\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketHelper.cs",
    "content": "﻿using System;\nusing Atlassian.Bitbucket.Cloud;\nusing GitCredentialManager;\n\nnamespace Atlassian.Bitbucket\n{\n    public static class BitbucketHelper\n    {\n        public static string GetBaseUri(Uri remoteUri)\n        {\n            var pathParts = remoteUri.PathAndQuery.Split('/');\n            var pathPart = remoteUri.PathAndQuery.StartsWith(\"/\") ? pathParts[1] : pathParts[0];\n            var path = !string.IsNullOrWhiteSpace(pathPart) ? \"/\" + pathPart : null;\n            return $\"{remoteUri.Scheme}://{remoteUri.Host}:{remoteUri.Port}{path}\";\n        }\n\n        public static bool IsBitbucketOrg(InputArguments input)     \n        {\n            return IsBitbucketOrg(input.GetRemoteUri());\n        }\n\n        public static bool IsBitbucketOrg(Uri targetUri)\n        {\n            return IsBitbucketOrg(targetUri.Host);\n        }\n\n        public static bool IsBitbucketOrg(string targetHost)\n        {\n            return StringComparer.OrdinalIgnoreCase.Equals(targetHost, CloudConstants.BitbucketBaseUrlHost);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.Cloud;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace Atlassian.Bitbucket\n{\n    public class BitbucketHostProvider : IHostProvider\n    {\n        private readonly ICommandContext _context;\n        private readonly IBitbucketAuthentication _bitbucketAuth;\n        private readonly IRegistry<IBitbucketRestApi> _restApiRegistry;\n\n        public BitbucketHostProvider(ICommandContext context)\n            : this(context, new BitbucketAuthentication(context), new BitbucketRestApiRegistry(context)) { }\n\n        public BitbucketHostProvider(ICommandContext context, IBitbucketAuthentication bitbucketAuth, IRegistry<IBitbucketRestApi> restApiRegistry)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n            EnsureArgument.NotNull(bitbucketAuth, nameof(bitbucketAuth));\n            EnsureArgument.NotNull(restApiRegistry, nameof(restApiRegistry));\n\n            _context = context;\n            _bitbucketAuth = bitbucketAuth;\n            _restApiRegistry = restApiRegistry;\n        }\n\n        #region IHostProvider\n\n        public string Id => BitbucketConstants.Id;\n\n        public string Name => BitbucketConstants.Name;\n\n        public IEnumerable<string> SupportedAuthorityIds => BitbucketAuthentication.AuthorityIds;\n\n        public bool IsSupported(InputArguments input)\n        {\n            if (input is null)\n            {\n                return false;\n            }\n\n            if (input.WwwAuth.Any(x => x.Contains(\"realm=\\\"Atlassian Bitbucket\\\"\", StringComparison.InvariantCultureIgnoreCase)))\n            {\n                return true;\n            }\n\n            // Split port number and hostname from host input argument\n            if (!input.TryGetHostAndPort(out string hostName, out _))\n            {\n                return false;\n            }\n\n            // We do not recommend unencrypted HTTP communications to Bitbucket, but it is possible.\n            // Therefore, we report `true` here for HTTP so that we can show a helpful\n            // error message for the user in `GetCredentialAsync`.\n            return (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\") ||\n                    StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"https\")) &&\n                    hostName.EndsWith(CloudConstants.BitbucketBaseUrlHost, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public bool IsSupported(HttpResponseMessage response)\n        {\n            if (response is null)\n            {\n                return false;\n            }\n\n            // Identify Bitbucket on-prem instances from the HTTP response using the Atlassian specific header X-AREQUESTID\n            var supported = response.Headers.Contains(\"X-AREQUESTID\");\n\n            _context.Trace.WriteLine($\"Host is{(supported ? null : \"n't\")} supported as Bitbucket\");\n\n            return supported;\n        }\n\n        public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)\n        {\n            // We should not allow unencrypted communication and should inform the user\n            if (!_context.Settings.AllowUnsafeRemotes &&\n                StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\") &&\n                BitbucketHelper.IsBitbucketOrg(input))\n            {\n                throw new Trace2Exception(_context.Trace2,\n                    \"Unencrypted HTTP is not recommended for Bitbucket.org. \" +\n                    \"Ensure the repository remote URL is using HTTPS \" +\n                    $\"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.\");\n            }\n\n            var authModes = await GetSupportedAuthenticationModesAsync(input);\n\n            ICredential credential = await GetStoredCredentials(input, authModes) ??\n                                     await GetRefreshedCredentials(input, authModes);\n            return new GetCredentialResult(credential);\n        }\n\n        private async Task<ICredential> GetStoredCredentials(InputArguments input, AuthenticationModes authModes)\n        {\n            if (_context.Settings.TryGetSetting(BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials,\n                Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.AlwaysRefreshCredentials,\n                out string alwaysRefreshCredentials) && alwaysRefreshCredentials.ToBooleanyOrDefault(false))\n            {\n                _context.Trace.WriteLine(\"Ignore stored credentials\");\n                return null;\n            }\n\n            Uri remoteUri = input.GetRemoteUri();\n            string credentialService = GetServiceName(remoteUri);\n            _context.Trace.WriteLine($\"Look for existing credentials under {credentialService} ...\");\n\n            ICredential credentials = _context.CredentialStore.Get(credentialService, input.UserName);\n\n            if (credentials == null)\n            {\n                _context.Trace.WriteLine(\"No stored credentials found\");\n                return null;\n            }\n\n            _context.Trace.WriteLineSecrets($\"Found stored credentials: {credentials.Account}/{{0}}\", new object[] { credentials.Password });\n\n            // Check credentials are still valid\n            if (!await ValidateCredentialsWork(input, credentials, authModes))\n            {\n                return null;\n            }\n\n            return credentials;\n        }\n\n        private async Task<ICredential> GetRefreshedCredentials(InputArguments input, AuthenticationModes authModes)\n        {\n            _context.Trace.WriteLine(\"Refresh credentials...\");\n\n            // Check for presence of refresh_token entry in credential store\n            Uri remoteUri = input.GetRemoteUri();\n            var refreshTokenService = GetRefreshTokenServiceName(remoteUri);\n\n            _context.Trace.WriteLine(\"Checking for refresh token...\");\n            ICredential refreshToken = SupportsOAuth(authModes)\n                ? _context.CredentialStore.Get(refreshTokenService, input.UserName)\n                : null;\n\n            if (refreshToken is null)\n            {\n                _context.Trace.WriteLine(\"No stored refresh token found\");\n                // There is no refresh token either because this is a non-2FA enabled account (where OAuth is not\n                // required), or because we previously erased the RT.\n\n                _context.Trace.WriteLine(\"Prompt for credentials...\");\n\n                var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, input.UserName, authModes);\n                if (result is null || result.AuthenticationMode == AuthenticationModes.None)\n                {\n                    var message = \"User cancelled credential prompt\";\n                    _context.Trace.WriteLine(message);\n                    throw new Trace2Exception(_context.Trace2, message);\n                }\n\n                switch (result.AuthenticationMode)\n                {\n                    case AuthenticationModes.Basic:\n                        // Return the valid credential\n                        return result.Credential;\n\n                    case AuthenticationModes.OAuth:\n                        // If the user wants to use OAuth fall through to interactive auth\n                        break;\n\n                    default:\n                        throw new ArgumentOutOfRangeException(\n                            $\"Unexpected {nameof(AuthenticationModes)} returned from prompt\");\n                }\n\n                // Fall through to the start of the interactive OAuth authentication flow\n            }\n            else\n            {\n                _context.Trace.WriteLineSecrets(\"Found stored refresh token: {0}\", new object[] { refreshToken });\n\n                try\n                {\n                    return await GetOAuthCredentialsViaRefreshFlow(input, refreshToken);\n                }\n                catch (OAuth2Exception ex)\n                {\n                    var message = \"Failed to refresh existing OAuth credential using refresh token\";\n                    _context.Trace.WriteLine(message);\n                    _context.Trace.WriteException(ex);\n                    _context.Trace2.WriteError(message);\n\n                    // We failed to refresh the AT using the RT; log the refresh failure and fall through to restart\n                    // the OAuth authentication flow\n                }\n            }\n\n            return await GetOAuthCredentialsViaInteractiveBrowserFlow(input);\n        }\n\n        private async Task<ICredential> GetOAuthCredentialsViaRefreshFlow(InputArguments input, ICredential refreshToken)\n        {\n            Uri remoteUri = input.GetRemoteUri();\n\n            var refreshTokenService = GetRefreshTokenServiceName(remoteUri);\n            _context.Trace.WriteLine(\"Refreshing OAuth credentials using refresh token...\");\n\n            OAuth2TokenResult refreshResult = await _bitbucketAuth.RefreshOAuthCredentialsAsync(input, refreshToken.Password);\n\n            // Resolve the username\n            _context.Trace.WriteLine(\"Resolving username for refreshed OAuth credential...\");\n            string refreshUserName = await ResolveOAuthUserNameAsync(input, refreshResult.AccessToken);\n            _context.Trace.WriteLine($\"Username for refreshed OAuth credential is '{refreshUserName}'\");\n\n            // Store the refreshed RT\n            _context.Trace.WriteLine(\"Storing new refresh token...\");\n            _context.CredentialStore.AddOrUpdate(refreshTokenService, remoteUri.GetUserName(), refreshResult.RefreshToken);\n\n            // Return new access token\n            return new GitCredential(refreshUserName, refreshResult.AccessToken);\n        }\n\n        private async Task<ICredential> GetOAuthCredentialsViaInteractiveBrowserFlow(InputArguments input)\n        {\n            Uri remoteUri = input.GetRemoteUri();\n\n            var refreshTokenService = GetRefreshTokenServiceName(remoteUri);\n\n            // We failed to use the refresh token either because it didn't exist, or because the refresh token is no\n            // longer valid. Either way we must now try authenticating using OAuth interactively.\n\n            // Start OAuth authentication flow\n            _context.Trace.WriteLine(\"Starting OAuth authentication flow...\");\n            OAuth2TokenResult oauthResult = await _bitbucketAuth.CreateOAuthCredentialsAsync(input);\n\n            // Resolve the username\n            _context.Trace.WriteLine(\"Resolving username for OAuth credential...\");\n            string newUserName = await ResolveOAuthUserNameAsync(input, oauthResult.AccessToken);\n            _context.Trace.WriteLine($\"Username for OAuth credential is '{newUserName}'\");\n\n            // Store the new RT\n            _context.Trace.WriteLine(\"Storing new refresh token...\");\n            _context.CredentialStore.AddOrUpdate(refreshTokenService, newUserName, oauthResult.RefreshToken);\n            _context.Trace.WriteLine(\"Refresh token was successfully stored.\");\n\n            // Return the new AT as the credential\n            return new GitCredential(newUserName, oauthResult.AccessToken);\n        }\n\n        private static bool SupportsOAuth(AuthenticationModes authModes)\n        {\n            return (authModes & AuthenticationModes.OAuth) != 0;\n        }\n\n        private static bool SupportsBasicAuth(AuthenticationModes authModes)\n        {\n            return (authModes & AuthenticationModes.Basic) != 0;\n        }\n\n        public async Task<AuthenticationModes> GetSupportedAuthenticationModesAsync(InputArguments input)\n        {\n            // Check for an explicit override for supported authentication modes\n            if (_context.Settings.TryGetSetting(\n                BitbucketConstants.EnvironmentVariables.AuthenticationModes,\n                Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.AuthenticationModes,\n                out string authModesStr))\n            {\n                if (Enum.TryParse(authModesStr, true, out AuthenticationModes authModes) && authModes != AuthenticationModes.None)\n                {\n                    _context.Trace.WriteLine($\"Supported authentication modes override present: {authModes}\");\n                    return authModes;\n                }\n                else\n                {\n                    _context.Trace.WriteLine($\"Invalid value for supported authentication modes override setting: '{authModesStr}'\");\n                }\n            }\n\n            // It isn't possible to detect what Bitbucket.org is expecting so return the predefined answers.\n            if (BitbucketHelper.IsBitbucketOrg(input))\n            {\n                // Bitbucket should use Basic, OAuth or manual PAT based authentication only\n                _context.Trace.WriteLine($\"{input.GetRemoteUri()} is bitbucket.org - authentication schemes: '{CloudConstants.DotOrgAuthenticationModes}'\");\n                return CloudConstants.DotOrgAuthenticationModes;\n            }\n\n            // For Bitbucket DC/Server the supported modes can be detected\n            _context.Trace.WriteLine($\"{input.GetRemoteUri()} is Bitbucket DC - checking for supported authentication schemes...\");\n\n            try\n            {\n                var authenticationMethods = await _restApiRegistry.Get(input).GetAuthenticationMethodsAsync();\n\n                var modes = AuthenticationModes.None;\n\n                if (authenticationMethods.Contains(AuthenticationMethod.BasicAuth))\n                {\n                    modes |= AuthenticationModes.Basic;\n                }\n\n                var isOauthInstalled = await _restApiRegistry.Get(input).IsOAuthInstalledAsync();\n                if (isOauthInstalled)\n                {\n                    modes |= AuthenticationModes.OAuth;\n                }\n\n                _context.Trace.WriteLine($\"Bitbucket DC/Server instance supports authentication schemes: {modes}\");\n                return modes;\n            }\n            catch (Exception ex)\n            {\n                var format = \"Failed to query '{0}' for supported authentication schemes.\";\n                var message = string.Format(format, input.GetRemoteUri());\n\n                _context.Trace.WriteLine(message);\n                _context.Trace.WriteException(ex);\n                _context.Trace2.WriteError(message, format);\n\n                _context.Terminal.WriteLine($\"warning: {message}\");\n\n                // Fall-back to offering all modes so the user is never blocked from authenticating by at least one mode\n                return AuthenticationModes.All;\n            }\n        }\n\n        public Task StoreCredentialAsync(InputArguments input)\n        {\n            // It doesn't matter if this is an OAuth access token, or the literal username & password\n            // because we store them the same way, against the same credential key in the store.\n            // The OAuth refresh token is already stored on the 'get' request.\n            Uri remoteUri = input.GetRemoteUri();\n            string service = GetServiceName(remoteUri);\n\n            _context.Trace.WriteLine(\"Storing credential...\");\n            _context.CredentialStore.AddOrUpdate(service, input.UserName, input.Password);\n            _context.Trace.WriteLine(\"Credential was successfully stored.\");\n\n            return Task.CompletedTask;\n        }\n\n        public Task EraseCredentialAsync(InputArguments input)\n        {\n            // Erase the stored credential (which may be either the literal username & password, or\n            // the OAuth access token). We don't need to erase the OAuth refresh token because on the\n            // next 'get' request, if the RT is bad we will erase and reacquire a new one at that point.\n            Uri remoteUri = input.GetRemoteUri();\n            string service = GetServiceName(remoteUri);\n\n            _context.Trace.WriteLine(\"Erasing credential...\");\n            if (_context.CredentialStore.Remove(service, input.UserName))\n            {\n                _context.Trace.WriteLine(\"Credential was successfully erased.\");\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"Credential was not erased.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        #region Private Methods\n\n        private async Task<string> ResolveOAuthUserNameAsync(InputArguments input, string accessToken)\n        {\n            RestApiResult<IUserInfo> result = await _restApiRegistry.Get(input).GetUserInformationAsync(null, accessToken, isBearerToken: true);\n            if (result.Succeeded)\n            {\n                return result.Response.UserName;\n            }\n\n            throw new Trace2Exception(_context.Trace2,\n                $\"Failed to resolve username. HTTP: {result.StatusCode}\");\n        }\n\n        private async Task<string> ResolveBasicAuthUserNameAsync(InputArguments input, string username, string password)\n        {\n            RestApiResult<IUserInfo> result = await _restApiRegistry.Get(input).GetUserInformationAsync(username, password, isBearerToken: false);\n            if (result.Succeeded)\n            {\n                return result.Response.UserName;\n            }\n\n            throw new Trace2Exception(_context.Trace2,\n                $\"Failed to resolve username. HTTP: {result.StatusCode}\");\n        }\n\n        private async Task<bool> ValidateCredentialsWork(InputArguments input, ICredential credentials, AuthenticationModes authModes)\n        {\n            if (_context.Settings.TryGetSetting(\n                BitbucketConstants.EnvironmentVariables.ValidateStoredCredentials,\n                Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials,\n                out string validateStoredCredentials) && !validateStoredCredentials.ToBooleanyOrDefault(true))\n            {\n                _context.Trace.WriteLine($\"Skipping validation of stored credentials due to {BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials} = {validateStoredCredentials}\");\n                return true;\n            }\n\n            if (credentials is null)\n            {\n                return false;\n            }\n\n            // TODO: ideally we'd also check if the credentials have expired based on some local metadata\n            // (once/if we get such metadata storage), and return false if they have.\n            // This would be more efficient than having to make REST API calls to check.\n            Uri remoteUri = input.GetRemoteUri();\n            _context.Trace.WriteLineSecrets($\"Validate credentials ({credentials.Account}/{{0}}) are fresh for {remoteUri} ...\", new object[] { credentials.Password });\n\n            // Bitbucket supports both OAuth + Basic Auth unless there is explicit GCM configuration.\n            // The credentials could be for either scheme therefore need to potentially test both possibilities.\n            if (SupportsOAuth(authModes))\n            {\n                try\n                {\n                    await ResolveOAuthUserNameAsync(input, credentials.Password);\n                    _context.Trace.WriteLine(\"Validated existing credentials using OAuth\");\n                    return true;\n                }\n                catch (Exception ex)\n                {\n                    var message = \"Failed to validate existing credentials using OAuth\";\n                    _context.Trace.WriteLine(message);\n                    _context.Trace.WriteException(ex);\n                    _context.Trace2.WriteError(message);\n                }\n            }\n\n            if (SupportsBasicAuth(authModes))\n            {\n                try\n                {\n                    await ResolveBasicAuthUserNameAsync(input, credentials.Account, credentials.Password);\n                    _context.Trace.WriteLine(\"Validated existing credentials using BasicAuth\");\n                    return true;\n                }\n                catch (Exception ex)\n                {\n                    var message = \"Failed to validate existing credentials using Basic Auth\";\n                    _context.Trace.WriteLine(message);\n                    _context.Trace.WriteException(ex);\n                    _context.Trace2.WriteError(message);\n                    return false;\n                }\n            }\n\n            return true;\n        }\n\n        private static string GetServiceName(Uri remoteUri)\n        {\n            return remoteUri.WithoutUserInfo().AbsoluteUri.TrimEnd('/');\n        }\n\n        internal /* for testing */ static string GetRefreshTokenServiceName(Uri remoteUri)\n        {\n            Uri baseUri = remoteUri.WithoutUserInfo();\n\n            // The refresh token key never includes the path component.\n            // Instead we use the path component to specify this is the \"refresh_token\".\n            Uri uri = new UriBuilder(baseUri) { Path = \"/refresh_token\" }.Uri;\n\n            return uri.AbsoluteUri.TrimEnd('/');\n        }\n\n        #endregion\n\n        public void Dispose()\n        {\n            _restApiRegistry.Dispose();\n            _bitbucketAuth.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace Atlassian.Bitbucket\n{\n    public abstract class BitbucketOAuth2Client : OAuth2Client\n    {\n        public BitbucketOAuth2Client(HttpClient httpClient,\n            OAuth2ServerEndpoints endpoints,\n            string clientId,\n            Uri redirectUri,\n            string clientSecret,\n            ITrace2 trace2) : base(httpClient, endpoints, clientId, trace2, redirectUri, clientSecret, false)\n        {\n        }\n\n        public abstract IEnumerable<string> Scopes { get; }\n\n        public string GetRefreshTokenServiceName(InputArguments input)\n        {\n            Uri baseUri = input.GetRemoteUri(includeUser: false);\n\n            // The refresh token key never includes the path component.\n            // Instead we use the path component to specify this is the \"refresh_token\".\n            Uri uri = new UriBuilder(baseUri) { Path = \"/refresh_token\" }.Uri;\n\n            return uri.AbsoluteUri.TrimEnd('/');\n        }\n\n        public Task<OAuth2AuthorizationCodeResult> GetAuthorizationCodeAsync(IOAuth2WebBrowser browser, CancellationToken ct)\n        {\n            return this.GetAuthorizationCodeAsync(Scopes, browser, ct);\n        }\n\n        protected override bool TryCreateTokenEndpointResult(string json, out OAuth2TokenResult result)\n        {\n            // We override the token endpoint response parsing because the Bitbucket authority returns\n            // the non-standard 'scopes' property for the list of scopes, rather than the (optional)\n            // 'scope' (note the singular vs plural) property as outlined in the standard.\n            if (TryDeserializeJson(json, out BitbucketTokenEndpointResponseJson jsonObj))\n            {\n                result = jsonObj.ToResult();\n                return true;\n            }\n\n            result = null;\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketResources.Designer.cs",
    "content": "﻿//------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//     Runtime Version:4.0.30319.42000\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n//------------------------------------------------------------------------------\n\nnamespace Atlassian.Bitbucket {\n    using System;\n    \n    \n    [System.CodeDom.Compiler.GeneratedCodeAttribute(\"System.Resources.Tools.StronglyTypedResourceBuilder\", \"4.0.0.0\")]\n    [System.Diagnostics.DebuggerNonUserCodeAttribute()]\n    [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]\n    internal class BitbucketResources {\n        \n        private static System.Resources.ResourceManager resourceMan;\n        \n        private static System.Globalization.CultureInfo resourceCulture;\n        \n        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(\"Microsoft.Performance\", \"CA1811:AvoidUncalledPrivateCode\")]\n        internal BitbucketResources() {\n        }\n        \n        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]\n        internal static System.Resources.ResourceManager ResourceManager {\n            get {\n                if (object.Equals(null, resourceMan)) {\n                    System.Resources.ResourceManager temp = new System.Resources.ResourceManager(\"Atlassian.Bitbucket.BitbucketResources\", typeof(BitbucketResources).Assembly);\n                    resourceMan = temp;\n                }\n                return resourceMan;\n            }\n        }\n        \n        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]\n        internal static System.Globalization.CultureInfo Culture {\n            get {\n                return resourceCulture;\n            }\n            set {\n                resourceCulture = value;\n            }\n        }\n        \n        internal static string AuthenticationResponseSuccessHtml {\n            get {\n                return ResourceManager.GetString(\"AuthenticationResponseSuccessHtml\", resourceCulture);\n            }\n        }\n        \n        internal static string AuthenticationResponseFailureHtmlFormat {\n            get {\n                return ResourceManager.GetString(\"AuthenticationResponseFailureHtmlFormat\", resourceCulture);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketResources.resx",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<root>\n    <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n        <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n\n        </xsd:element>\n    </xsd:schema>\n    <resheader name=\"resmimetype\">\n        <value>text/microsoft-resx</value>\n    </resheader>\n    <resheader name=\"version\">\n        <value>1.3</value>\n    </resheader>\n    <resheader name=\"reader\">\n        <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n    </resheader>\n    <resheader name=\"writer\">\n        <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n    </resheader>\n    <data name=\"AuthenticationResponseSuccessHtml\" xml:space=\"preserve\"><![CDATA[<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Bitbucket Authentication</title>\n    <link rel=\"stylesheet\" href=\"http://aui-cdn.atlassian.com/aui-adg/5.9.19/css/aui.min.css\" media=\"all\">\n    <script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js\"></script>\n    <script src=\"http://aui-cdn.atlassian.com/aui-adg/5.9.19/js/aui.min.js\"></script>\n</head>\n<body class=\"aui-page-notification aui-page-size-large\">\n\t<div id=\"page\" style=\"text-align: center;\">\n\t\t<img src=\"https://wac-cdn.atlassian.com/dam/jcr:01f0ea80-2b31-4009-8eaf-da8ebcabdfe1/Bitbucket-blue.svg\" height=\"23px\" alt=\"Bitbucket\" style=\"margin-top:50px\">\n        <div class=\"aui-page-panel\">\n            <div class=\"aui-page-panel-inner\">\n                <section class=\"aui-page-panel-content\">\n                    <h2>Authentication Successful</h2>\n                    <p>Git Credential Manager has been successfully authenticated. You may now close this page.</p>\n                </section>\n            </div>\n        </div>\n        <footer id=\"footer\" role=\"contentinfo\">\n            <section class=\"footer-body\">\n                <ul>\n                    <li>&hearts;</li>\n                </ul>\n                <div id=\"footer-logo\"><a href=\"http://www.atlassian.com/\" target=\"_blank\">Atlassian</a></div>\n            </section>\n        </footer>\n    </div>\n</body>\n</html>]]></data>\n    <data name=\"AuthenticationResponseFailureHtmlFormat\" xml:space=\"preserve\"><![CDATA[<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Bitbucket Authentication</title>\n    <link rel=\"stylesheet\" href=\"http://aui-cdn.atlassian.com/aui-adg/5.9.19/css/aui.min.css\" media=\"all\">\n    <script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js\"></script>\n\t<script src=\"http://aui-cdn.atlassian.com/aui-adg/5.9.19/js/aui.min.js\"></script>\n\t<style>dt {{ font-weight: bold; margin-top: 15px }} dd {{ margin-left: 0; }}</style>\n</head>\n<body class=\"aui-page-notification aui-page-size-large\">\n\t<div id=\"page\" style=\"text-align: center;\">\n\t\t<img src=\"https://wac-cdn.atlassian.com/dam/jcr:01f0ea80-2b31-4009-8eaf-da8ebcabdfe1/Bitbucket-blue.svg\" height=\"23px\" alt=\"Bitbucket\" style=\"margin-top:50px\">\n        <div class=\"aui-page-panel\">\n            <div class=\"aui-page-panel-inner\">\n                <section class=\"aui-page-panel-content\">\n                    <div>\n\t\t\t\t\t\t<h2>Authentication Failed</h2>\n\t\t\t\t\t\t<p>Git Credential Manager failed to be authenticated.</p>\n\t\t\t\t\t\t<dl class=\"error\">\n\t\t\t\t\t\t\t<dt>Error</dt>\n\t\t\t\t\t\t\t<dd>{0}</dt>\n\t\t\t\t\t\t\t<dt>Description</dt>\n\t\t\t\t\t\t\t<dd>{1}</dt>\n\t\t\t\t\t\t\t<dt>URL</dt>\n\t\t\t\t\t\t\t<dd>{2}</dt>\n\t\t\t\t\t\t</dl>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\t\t\t</div>\n        </div>\n        <footer id=\"footer\" role=\"contentinfo\">\n            <section class=\"footer-body\">\n                <ul>\n                    <li>&hearts;</li>\n                </ul>\n                <div id=\"footer-logo\"><a href=\"http://www.atlassian.com/\" target=\"_blank\">Atlassian</a></div>\n            </section>\n        </footer>\n    </div>\n</body>\n</html>]]></data>\n</root>\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs",
    "content": "using Atlassian.Bitbucket.Cloud;\nusing GitCredentialManager;\n\nnamespace Atlassian.Bitbucket\n{\n    public class BitbucketRestApiRegistry : IRegistry<IBitbucketRestApi>\n    {\n        private readonly ICommandContext context;\n        private BitbucketRestApi cloudApi;\n        private DataCenter.BitbucketRestApi dataCenterApi;\n\n        public BitbucketRestApiRegistry(ICommandContext context)\n        {\n            this.context = context;\n        }\n\n        public IBitbucketRestApi Get(InputArguments input)\n        {\n            if(!BitbucketHelper.IsBitbucketOrg(input))\n            {\n                return DataCenterApi;\n            }\n\n            return CloudApi;\n        }\n\n        public void Dispose()\n        {\n            context.Dispose();\n            cloudApi?.Dispose();\n            dataCenterApi?.Dispose();\n        }\n\n        private Cloud.BitbucketRestApi CloudApi => cloudApi ??= new Cloud.BitbucketRestApi(context);\n        private DataCenter.BitbucketRestApi DataCenterApi => dataCenterApi ??= new DataCenter.BitbucketRestApi(context);\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs",
    "content": "using System;\nusing System.Text.Json;\nusing GitCredentialManager.Authentication.OAuth.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Atlassian.Bitbucket\n{\n    [JsonConverter(typeof(BitbucketCustomTokenEndpointResponseJsonConverter))]\n    public class BitbucketTokenEndpointResponseJson : TokenEndpointResponseJson\n    {\n        // To ensure the \"scopes\" property used by Bitbucket is deserialized successfully with System.Text.Json, we must\n        // use a custom converter. Otherwise, ordering will matter (i.e. if \"scopes\" is the final property, its value\n        // will be used, but if \"scope\" is the final property, its value will be used).\n    }\n\n    public class BitbucketCustomTokenEndpointResponseJsonConverter : JsonConverter<BitbucketTokenEndpointResponseJson>\n    {\n        public override BitbucketTokenEndpointResponseJson Read(\n            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n        {\n            if (reader.TokenType != JsonTokenType.StartObject)\n            {\n                throw new JsonException();\n            }\n\n            var response = new BitbucketTokenEndpointResponseJson();\n\n            while (reader.Read())\n            {\n                if (reader.TokenType == JsonTokenType.EndObject)\n                {\n                    return response;\n                }\n\n                if (reader.TokenType == JsonTokenType.PropertyName)\n                {\n                    var propertyName = reader.GetString();\n                    reader.Read();\n                    if (propertyName != null)\n                    {\n                        switch (propertyName)\n                        {\n                            case \"access_token\":\n                                response.AccessToken = reader.GetString();\n                                break;\n                            case \"token_type\":\n                                response.TokenType = reader.GetString();\n                                break;\n                            case \"expires_in\":\n                                if (reader.TryGetUInt32(out var expiration))\n                                    response.ExpiresIn = expiration;\n                                else\n                                    response.ExpiresIn = null;\n                                break;\n                            case \"refresh_token\":\n                                response.RefreshToken = reader.GetString();\n                                break;\n                            case \"scopes\":\n                                response.Scope = reader.GetString();\n                                break;\n                        }\n                    }\n                }\n            }\n\n            throw new JsonException();\n        }\n\n        public override void Write(\n            Utf8JsonWriter writer, BitbucketTokenEndpointResponseJson tokenEndpointResponseJson, JsonSerializerOptions options)\n        { }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs",
    "content": "// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace Atlassian.Bitbucket.Cloud\n{\n    public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client\n    {\n        public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2)\n            : base(httpClient, GetEndpoints(),\n                GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2)\n        {\n        }\n\n        public override IEnumerable<string> Scopes => new string[] {\n            CloudConstants.OAuthScopes.RepositoryWrite,\n            CloudConstants.OAuthScopes.Account,\n        };\n\n        private static string GetClientId(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                CloudConstants.EnvironmentVariables.OAuthClientId,\n                Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientId,\n                out string clientId))\n            {\n                return clientId;\n            }\n\n            return CloudConstants.OAuth2ClientId;\n        }\n\n        private static Uri GetRedirectUri(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                CloudConstants.EnvironmentVariables.OAuthRedirectUri,\n                Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthRedirectUri,\n                out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))\n            {\n                return redirectUri;\n            }\n\n            return CloudConstants.OAuth2RedirectUri;\n        }\n\n        private static string GetClientSecret(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                CloudConstants.EnvironmentVariables.OAuthClientSecret,\n                Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientSecret,\n                out string clientSecret))\n            {\n                return clientSecret;\n            }\n\n            return CloudConstants.OAuth2ClientSecret;\n        }\n\n        private static OAuth2ServerEndpoints GetEndpoints()\n        {\n            return new OAuth2ServerEndpoints(\n                CloudConstants.OAuth2AuthorizationEndpoint,\n                CloudConstants.OAuth2TokenEndpoint\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Atlassian.Bitbucket.Cloud\n{\n    public class BitbucketRestApi : IBitbucketRestApi\n    {\n        private readonly ICommandContext _context;\n        private readonly Uri _apiUri = CloudConstants.BitbucketApiUri;\n\n        public BitbucketRestApi(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n        }\n\n        public async Task<RestApiResult<IUserInfo>> GetUserInformationAsync(string userName, string password, bool isBearerToken)\n        {\n            var requestUri = new Uri(_apiUri, \"2.0/user\");\n            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri))\n            {\n                if (isBearerToken)\n                {\n                    request.AddBearerAuthenticationHeader(password);\n                }\n                else\n                {\n                    request.AddBasicAuthenticationHeader(userName, password);\n                }\n\n                _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n                using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n                {\n                    _context.Trace.WriteLine($\"HTTP: Response {(int) response.StatusCode} [{response.StatusCode}]\");\n\n                    string json = await response.Content.ReadAsStringAsync();\n\n                    if (response.IsSuccessStatusCode)\n                    {\n                        var obj = JsonSerializer.Deserialize<UserInfo>(json,\n                            new JsonSerializerOptions\n                            {\n                                PropertyNameCaseInsensitive = true,\n                            });\n\n                        return new RestApiResult<IUserInfo>(response.StatusCode, obj);\n                    }\n\n                    return new RestApiResult<IUserInfo>(response.StatusCode);\n                }\n            }\n        }\n\n        public Task<bool> IsOAuthInstalledAsync()\n        {\n            return Task.FromResult(true);\n        }\n\n        public Task<List<AuthenticationMethod>> GetAuthenticationMethodsAsync()\n        {\n            // For Bitbucket Cloud there is no REST API to determine login methods\n            // instead this is determined later in the process by attempting\n            // authenticated REST API requests and checking the response.\n            return Task.FromResult(new List<AuthenticationMethod>());\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient();\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/Cloud/CloudConstants.cs",
    "content": "using System;\n\nnamespace Atlassian.Bitbucket.Cloud\n{\n    public static class CloudConstants\n    {\n        public const string BitbucketBaseUrlHost = \"bitbucket.org\";\n        public static readonly Uri BitbucketApiUri = new Uri(\"https://api.bitbucket.org\");\n\n        // TODO: use the GCM client ID and secret once we have this approved.\n        // Until then continue to use Sourcetree's values like GCM Windows.\n        //public const string OAuth2ClientId = \"b5AKdPfpgFdEGpKzPE\";\n        //public const string OAuth2ClientSecret = \"7NUP5qUtSR3SxdFK4xAGaU6PMNvNdE59\";\n        //public static readonly Uri OAuth2RedirectUri = new Uri(\"http://localhost:46337/\");\n        public const string OAuth2ClientId = \"HJdmKXV87DsmC9zSWB\";\n        public const string OAuth2ClientSecret = \"wwWw47VB9ZHwMsD4Q4rAveHkbxNrMp3n\";\n        public static readonly Uri OAuth2RedirectUri = new Uri(\"http://localhost:34106/\");\n\n        public static readonly Uri OAuth2AuthorizationEndpoint = new Uri(\"https://bitbucket.org/site/oauth2/authorize\");\n        public static readonly Uri OAuth2TokenEndpoint = new Uri(\"https://bitbucket.org/site/oauth2/access_token\");\n\n        public static class OAuthScopes\n        {\n            public const string RepositoryWrite = \"repository:write\";\n            public const string Account = \"account\";\n        }\n\n        /// <summary>\n        /// Supported authentication modes for Bitbucket.org\n        /// </summary>\n        public const AuthenticationModes DotOrgAuthenticationModes = AuthenticationModes.Basic | AuthenticationModes.OAuth;\n\n        public static class EnvironmentVariables\n        {\n            public const string OAuthClientId = \"GCM_BITBUCKET_CLOUD_CLIENTID\";\n            public const string OAuthClientSecret = \"GCM_BITBUCKET_CLOUD_CLIENTSECRET\";\n            public const string OAuthRedirectUri = \"GCM_BITBUCKET_CLOUD_OAUTH_REDIRECTURI\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string OAuthClientId = \"cloudOAuthClientId\";\n                public const string OAuthClientSecret = \"cloudOAuthClientSecret\";\n                public const string OAuthRedirectUri = \"cloudOauthRedirectUri\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/Cloud/UserInfo.cs",
    "content": "using System;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Atlassian.Bitbucket.Cloud\n{\n    public class UserInfo : IUserInfo\n    {\n        [JsonPropertyName(\"username\")]\n        public string UserName { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs",
    "content": "// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace Atlassian.Bitbucket.DataCenter\n{\n    public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client\n    {\n        public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2)\n            : base(httpClient, GetEndpoints(settings),\n                GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2)\n        {\n        }\n\n        public override IEnumerable<string> Scopes => new string[] {\n            DataCenterConstants.OAuthScopes.PublicRepos,\n            DataCenterConstants.OAuthScopes.RepoRead,\n            DataCenterConstants.OAuthScopes.RepoWrite\n        };\n\n        private static string GetClientId(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                DataCenterConstants.EnvironmentVariables.OAuthClientId,\n                Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientId,\n                out string clientId))\n            {\n                return clientId;\n            }\n\n            throw new ArgumentException(\"Bitbucket DC OAuth Client ID must be defined\");\n        }\n\n        private static Uri GetRedirectUri(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                DataCenterConstants.EnvironmentVariables.OAuthRedirectUri,\n                Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri,\n                out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))\n            {\n                return redirectUri;\n            }\n\n            return DataCenterConstants.OAuth2RedirectUri;\n        }\n\n        private static string GetClientSecret(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                DataCenterConstants.EnvironmentVariables.OAuthClientSecret,\n                Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret,\n                out string clientSecret))\n            {\n                return clientSecret;\n            }\n\n            throw new ArgumentException(\"Bitbucket DC OAuth Client Secret must be defined\");\n        }\n\n        private static OAuth2ServerEndpoints GetEndpoints(ISettings settings)\n        {\n            var remoteUri = settings.RemoteUri;\n            if (remoteUri == null)\n            {\n                throw new ArgumentException(\"RemoteUri must be defined to generate Bitbucket DC OAuth2 endpoint Urls\");\n            }\n\n            return new OAuth2ServerEndpoints(\n                new Uri(BitbucketHelper.GetBaseUri(remoteUri) + \"/rest/oauth2/latest/authorize\"),\n                new Uri(BitbucketHelper.GetBaseUri(remoteUri) + \"/rest/oauth2/latest/token\")\n                );\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Atlassian.Bitbucket.DataCenter\n{\n    public class BitbucketRestApi : IBitbucketRestApi\n    {\n        private readonly ICommandContext _context;\n\n        private HttpClient _httpClient;\n\n        public BitbucketRestApi(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n\n        }\n\n        public async Task<RestApiResult<IUserInfo>> GetUserInformationAsync(string userName, string password, bool isBearerToken)\n        {\n            if (_context.Settings.TryGetSetting(\n                BitbucketConstants.EnvironmentVariables.ValidateStoredCredentials,\n                Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials,\n                out string validateStoredCredentials) && !validateStoredCredentials.ToBooleanyOrDefault(true))\n            {\n                _context.Trace.WriteLine($\"Skipping retreival of user information due to {BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials} = {validateStoredCredentials}\");\n                return new RestApiResult<IUserInfo>(HttpStatusCode.OK, new UserInfo() { UserName = DataCenterConstants.OAuthUserName });;\n            }\n\n            // Bitbucket Server/DC doesn't actually provide a REST API we can use to trade an access_token for the owning username,\n            // therefore this is always going to return a placeholder username, however this call does provide a way to validate the\n            // credentials we do have\n            var requestUri = new Uri(ApiUri, \"api/1.0/users\");\n            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri))\n            {\n                if (isBearerToken)\n                {\n                    request.AddBearerAuthenticationHeader(password);\n                }\n                else\n                {\n                    request.AddBasicAuthenticationHeader(userName, password);\n                }\n\n                _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n                using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n                {\n                    _context.Trace.WriteLine($\"HTTP: Response {(int) response.StatusCode} [{response.StatusCode}]\");\n\n                    string json = await response.Content.ReadAsStringAsync();\n\n                    if (response.IsSuccessStatusCode)\n                    {\n                        // No REST API in BBS that can be used to return just my user account based on my login AFAIK.\n                        // but we can prove the credentials work.\n                        return new RestApiResult<IUserInfo>(HttpStatusCode.OK, new UserInfo() { UserName = DataCenterConstants.OAuthUserName });\n                    }\n\n                    return new RestApiResult<IUserInfo>(response.StatusCode);\n                }\n            }\n\n        }\n\n        public async Task<bool> IsOAuthInstalledAsync()\n        {\n            var requestUri = new Uri(ApiUri.AbsoluteUri + \"oauth2/1.0/client\");\n            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri))\n            {\n                _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n                using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n                {\n                    _context.Trace.WriteLine($\"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]\");\n\n                    if (HttpStatusCode.Unauthorized == response.StatusCode)\n                    {\n                        // accessed anonymously so no access but it does exist.\n                        return true;\n                    }\n\n                    return false;\n                }\n            }\n        }\n\n        public async Task<List<AuthenticationMethod>> GetAuthenticationMethodsAsync()\n        {\n            var authenticationMethods = new List<AuthenticationMethod>();\n\n            var requestUri = new Uri(ApiUri.AbsoluteUri + \"authconfig/1.0/login-options\");\n            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri))\n            {\n                _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n                using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n                {\n                    _context.Trace.WriteLine($\"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]\");\n\n                    string json = await response.Content.ReadAsStringAsync();\n\n                    if (response.IsSuccessStatusCode)\n                    {\n                        var loginOptions = JsonSerializer.Deserialize<LoginOptions>(json,\n                            new JsonSerializerOptions\n                            {\n                                PropertyNameCaseInsensitive = true,\n                                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull\n                            });\n\n                        if (loginOptions.Results.Any(r => \"LOGIN_FORM\".Equals(r.Type)))\n                        {\n                            authenticationMethods.Add(AuthenticationMethod.BasicAuth);\n                        }\n\n                        if (loginOptions.Results.Any(r => \"IDP\".Equals(r.Type)))\n                        {\n                            authenticationMethods.Add(AuthenticationMethod.Sso);\n                        }\n                    }\n                }\n            }\n\n            return authenticationMethods;\n        }\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n\n        private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient();\n\n        private Uri ApiUri\n        {\n            get\n            {\n                var remoteUri = _context.Settings?.RemoteUri;\n                if (remoteUri == null)\n                {\n                    throw new ArgumentException(\"RemoteUri must be defined to generate Bitbucket DC OAuth2 endpoint Urls\");\n                }\n\n                return new Uri(BitbucketHelper.GetBaseUri(remoteUri) + \"/rest/\");\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs",
    "content": "using System;\n\nnamespace Atlassian.Bitbucket.DataCenter\n{\n    public static class DataCenterConstants\n    {\n        public static class OAuthScopes\n        {\n            public const string PublicRepos = \"PUBLIC_REPOS\";\n            public const string RepoWrite = \"REPO_WRITE\";\n            public const string RepoRead = \"REPO_READ\";\n        }\n\n        public static readonly Uri OAuth2RedirectUri = new Uri(\"http://localhost:34106/\");\n\n        /// <summary>\n        /// Supported authentication modes for Bitbucket Server/DC\n        /// </summary>\n        public const AuthenticationModes ServerAuthenticationModes = AuthenticationModes.Basic  | AuthenticationModes.OAuth;\n\n        /// <summary>\n        /// Bitbucket Server/DC does not have a REST API we can use to trade an OAuth access_token for the owning username.\n        /// However one is needed to construct the Basic Auth request made by Git HTTP requests, therefore use a hardcoded\n        /// placeholder for the username.\n        /// </summary>\n        public const string OAuthUserName = \"OAUTH_USERNAME\";\n\n        public static class EnvironmentVariables\n        {\n            public const string OAuthClientId = \"GCM_BITBUCKET_DATACENTER_CLIENTID\";\n            public const string OAuthClientSecret = \"GCM_BITBUCKET_DATACENTER_CLIENTSECRET\";\n            public const string OAuthRedirectUri = \"GCM_BITBUCKET_DATACENTER_OAUTH_REDIRECTURI\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string OAuthClientId = \"bitbucketDataCenterOAuthClientId\";\n                public const string OAuthClientSecret = \"bitbucketDataCenterOAuthClientSecret\";\n                public const string OAuthRedirectUri = \"bitbucketDataCenterOauthRedirectUri\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/DataCenter/LoginOption.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Atlassian.Bitbucket.DataCenter\n{\n    public class LoginOption\n    {\n        [JsonPropertyName(\"type\")]\n        public string Type { get ; set; }\n\n        [JsonPropertyName(\"id\")]\n        public int Id { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/DataCenter/LoginOptions.cs",
    "content": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Atlassian.Bitbucket.DataCenter\n{\n    public class LoginOptions\n    {\n        [JsonPropertyName(\"results\")]\n        public List<LoginOption> Results { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/DataCenter/UserInfo.cs",
    "content": "using System;\n\nnamespace Atlassian.Bitbucket.DataCenter\n{\n    public class UserInfo : IUserInfo\n    {\n        public string UserName { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/IBitbucketRestApi.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Atlassian.Bitbucket\n{\n    public interface IBitbucketRestApi : IDisposable\n    {\n        Task<RestApiResult<IUserInfo>> GetUserInformationAsync(string userName, string password, bool isBearerToken);\n        Task<bool> IsOAuthInstalledAsync();\n        Task<List<AuthenticationMethod>> GetAuthenticationMethodsAsync();\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/IRegistry.cs",
    "content": "using System;\nusing GitCredentialManager;\n\nnamespace Atlassian.Bitbucket\n{\n    public interface IRegistry<T> : IDisposable\n    {\n        T Get(InputArguments input);\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/IUserInfo.cs",
    "content": "﻿using System;\nnamespace Atlassian.Bitbucket\n{\n    public interface IUserInfo\n    {\n        string UserName{ get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/InternalsVisibleTo.cs",
    "content": "using System.Runtime.CompilerServices;\n\n[assembly: InternalsVisibleTo(\"Atlassian.Bitbucket.Tests\")]\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs",
    "content": "using System.Net.Http;\nusing GitCredentialManager;\n\nnamespace Atlassian.Bitbucket\n{\n    public class OAuth2ClientRegistry : DisposableObject, IRegistry<BitbucketOAuth2Client>\n    {\n        private readonly ICommandContext _context;\n\n        private HttpClient _httpClient;\n        private Cloud.BitbucketOAuth2Client _cloudClient;\n        private DataCenter.BitbucketOAuth2Client _dataCenterClient;\n\n        public OAuth2ClientRegistry(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n            _context = context;\n        }\n\n        public BitbucketOAuth2Client Get(InputArguments input)\n        {\n            if (!BitbucketHelper.IsBitbucketOrg(input))\n            {\n                return DataCenterClient;\n            }\n\n            return CloudClient;\n        }\n\n        protected override void ReleaseManagedResources()\n        {\n            _httpClient?.Dispose();\n            _cloudClient = null;\n            _dataCenterClient = null;\n            base.ReleaseManagedResources();\n        }\n\n        private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient();\n\n        private Cloud.BitbucketOAuth2Client CloudClient =>\n            _cloudClient ??= new Cloud.BitbucketOAuth2Client(HttpClient, _context.Settings, _context.Trace2);\n\n        private DataCenter.BitbucketOAuth2Client DataCenterClient =>\n            _dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(HttpClient, _context.Settings, _context.Trace2);\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/RestApiResult.cs",
    "content": "﻿using System.Net;\n\nnamespace Atlassian.Bitbucket\n{\n    public class RestApiResult<T>\n    {\n        public RestApiResult(HttpStatusCode statusCode)\n            : this(statusCode, default(T)) { }\n\n        public RestApiResult(HttpStatusCode statusCode, T response)\n        {\n            StatusCode = statusCode;\n            Response = response;\n        }\n\n        public HttpStatusCode StatusCode { get; }\n\n        public T Response { get; }\n\n        public bool Succeeded => 199 < (int)StatusCode && (int)StatusCode < 300;\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.CommandLine.Invocation;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.UI.ViewModels;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace Atlassian.Bitbucket.UI.Commands\n{\n    public abstract class CredentialsCommand : HelperCommand\n    {\n        protected CredentialsCommand(ICommandContext context)\n            : base(context, \"prompt\", \"Show authentication prompt.\")\n        {\n            var url = new Option<Uri>(\"--url\", \"Bitbucket Server or Data Center URL\");\n            AddOption(url);\n\n            var userName = new Option<string>(\"--username\", \"Username or email.\");\n            AddOption(userName);\n\n            var oauth = new Option<bool>(\"--show-oauth\", \"Show OAuth option.\");\n            AddOption(oauth);\n\n            var basic = new Option<bool>(\"--show-basic\", \"Show username/password option.\");\n            AddOption(basic);\n\n            this.SetHandler(ExecuteAsync, url, userName, oauth, basic);\n        }\n\n        private async Task<int> ExecuteAsync(Uri url, string userName, bool showOAuth, bool showBasic)\n        {\n            var viewModel = new CredentialsViewModel(Context.SessionManager)\n            {\n                Url = url,\n                UserName = userName,\n                ShowOAuth = showOAuth,\n                ShowBasic = showBasic\n            };\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult || viewModel.SelectedMode == AuthenticationModes.None)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            switch (viewModel.SelectedMode)\n            {\n                case AuthenticationModes.OAuth:\n                    WriteResult(new Dictionary<string, string>\n                    {\n                        [\"mode\"] = \"oauth\"\n                    });\n                    break;\n\n                case AuthenticationModes.Basic:\n                    WriteResult(new Dictionary<string, string>\n                    {\n                        [\"mode\"] = \"basic\",\n                        [\"username\"] = viewModel.UserName,\n                        [\"password\"] = viewModel.Password,\n                    });\n                    break;\n\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(AuthenticationModes),\n                        \"Unknown authentication mode\", viewModel.SelectedMode.ToString());\n            }\n\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs",
    "content": "using System;\nusing System.ComponentModel;\nusing System.Windows.Input;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace Atlassian.Bitbucket.UI.ViewModels\n{\n    public class CredentialsViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n\n        private Uri _url;\n        private string _userName;\n        private string _password;\n        private bool _showOAuth;\n        private bool _showBasic;\n\n        public CredentialsViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public CredentialsViewModel(ISessionManager sessionManager)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n\n            _sessionManager = sessionManager;\n\n            Title = \"Connect to Bitbucket\";\n            LoginCommand = new RelayCommand(AcceptBasic, CanLogin);\n            CancelCommand = new RelayCommand(Cancel);\n            OAuthCommand = new RelayCommand(AcceptOAuth, CanAcceptOAuth);\n            ForgotPasswordCommand = new RelayCommand(ForgotPassword);\n            SignUpCommand = new RelayCommand(SignUp);\n\n            PropertyChanged += OnPropertyChanged;\n        }\n\n        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)\n        {\n            switch (e.PropertyName)\n            {\n                case nameof(UserName):\n                case nameof(Password):\n                    LoginCommand.RaiseCanExecuteChanged();\n                    break;\n            }\n        }\n\n        private bool CanLogin()\n        {\n            return !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password);\n        }\n\n        private void AcceptBasic()\n        {\n            SelectedMode = AuthenticationModes.Basic;\n            Accept();\n        }\n\n        private void AcceptOAuth()\n        {\n            SelectedMode = AuthenticationModes.OAuth;\n            Accept();\n        }\n\n        private bool CanAcceptOAuth()\n        {\n            return ShowOAuth;\n        }\n\n        private void ForgotPassword()\n        {\n            Uri passwordResetUri = _url is null\n                ? new Uri(BitbucketConstants.HelpUrls.PasswordReset)\n                : new Uri(_url, BitbucketConstants.HelpUrls.DataCenterPasswordReset);\n\n            _sessionManager.OpenBrowser(passwordResetUri);\n        }\n\n        private void SignUp()\n        {\n            Uri signUpUri = _url is null\n                ? new Uri(BitbucketConstants.HelpUrls.SignUp)\n                : new Uri(_url, BitbucketConstants.HelpUrls.DataCenterLogin);\n\n            _sessionManager.OpenBrowser(signUpUri);\n        }\n\n        public Uri Url\n        {\n            get => _url;\n            set => SetAndRaisePropertyChanged(ref _url, value);\n        }\n\n        public string UserName\n        {\n            get => _userName;\n            set => SetAndRaisePropertyChanged(ref _userName, value);\n        }\n\n        public string Password\n        {\n            get => _password;\n            set => SetAndRaisePropertyChanged(ref _password, value);\n        }\n\n        /// <summary>\n        /// Show the OAuth option.\n        /// </summary>\n        public bool ShowOAuth\n        {\n            get => _showOAuth;\n            set => SetAndRaisePropertyChanged(ref _showOAuth, value);\n        }\n\n        /// <summary>\n        /// Show the basic authentication options.\n        /// </summary>\n        public bool ShowBasic\n        {\n            get => _showBasic;\n            set => SetAndRaisePropertyChanged(ref _showBasic, value);\n        }\n\n        public AuthenticationModes SelectedMode { get; private set; }\n\n        /// <summary>\n        /// Start the process to validate the username/password\n        /// </summary>\n        public RelayCommand LoginCommand { get; }\n\n        /// <summary>\n        /// Cancel the authentication attempt.\n        /// </summary>\n        public ICommand CancelCommand { get; }\n\n        /// <summary>\n        /// Use OAuth authentication instead of username/password.\n        /// </summary>\n        public ICommand OAuthCommand { get; }\n\n        /// <summary>\n        /// Hyperlink to the Bitbucket forgotten password process.\n        /// </summary>\n        public ICommand ForgotPasswordCommand { get; }\n\n        /// <summary>\n        /// Hyperlink to the Bitbucket sign up process.\n        /// </summary>\n        public ICommand SignUpCommand { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:vm=\"clr-namespace:Atlassian.Bitbucket.UI.ViewModels;assembly=Atlassian.Bitbucket\"\n             xmlns:converters=\"clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"800\" d:DesignHeight=\"450\"\n             x:Class=\"Atlassian.Bitbucket.UI.Views.CredentialsView\">\n    <Design.DataContext>\n        <vm:CredentialsViewModel/>\n    </Design.DataContext>\n    <DockPanel LastChildFill=\"True\">\n        <StackPanel DockPanel.Dock=\"Bottom\">\n            <Button HorizontalAlignment=\"Center\" Margin=\"0,0,0,10\"\n                    Classes=\"hyperlink\"\n                    Command=\"{Binding ForgotPasswordCommand}\"\n                    Content=\"Can't log in?\"/>\n            <Button HorizontalAlignment=\"Center\" Margin=\"0,0,0,8\"\n                    Classes=\"hyperlink\"\n                    Command=\"{Binding SignUpCommand}\"\n                    Content=\"Sign up for an account\" />\n        </StackPanel>\n\n        <StackPanel DockPanel.Dock=\"Top\">\n            <Image HorizontalAlignment=\"Center\" Source=\"/UI/Assets/atlassian-logo.png\" Height=\"90\" />\n            <TextBlock HorizontalAlignment=\"Center\" Text=\"Log in to your account\" Margin=\"0,-10,0,0\" />\n            <TextBlock HorizontalAlignment=\"Center\" Text=\"{Binding Url}\"\n                       IsVisible=\"{Binding Url, Converter={x:Static StringConverters.IsNotNullOrEmpty}}\"\n                       Margin=\"0,10,0,0\"/>\n        </StackPanel>\n\n        <TabControl x:Name=\"_authModesTabControl\"\n                    VerticalContentAlignment=\"Center\"\n                    AutoScrollToSelectedItem=\"True\"\n                    Width=\"288\"\n                    Margin=\"0,20\">\n            <TabControl.Styles>\n                <Style Selector=\"TabItem\">\n                    <Setter Property=\"MinHeight\" Value=\"30\" />\n                </Style>\n                <Style Selector=\"DockPanel > ItemsPresenter > WrapPanel\">\n                    <Setter Property=\"HorizontalAlignment\" Value=\"Center\"/>\n                </Style>\n            </TabControl.Styles>\n\n            <TabItem IsVisible=\"{Binding ShowOAuth}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"Browser\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,15\">\n                    <Button x:Name=\"_oauthLoginButton\" IsDefault=\"True\"\n                            HorizontalAlignment=\"Center\" HorizontalContentAlignment=\"Center\" VerticalContentAlignment=\"Center\"\n                            Command=\"{Binding OAuthCommand}\"\n                            Classes=\"accent\" Height=\"40\" Padding=\"15,5\"\n                            Content=\"Sign in with your browser\" />\n                </StackPanel>\n            </TabItem>\n\n            <TabItem IsVisible=\"{Binding ShowBasic}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"Password/Token\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,15\">\n                    <TextBox x:Name=\"_userNameTextBox\" Margin=\"0,0,0,10\"\n                             HorizontalAlignment=\"Stretch\"\n                             Watermark=\"Email or username\"\n                             Text=\"{Binding UserName}\" />\n                    <TextBox x:Name=\"_passwordTextBox\" Margin=\"0,0,0,10\"\n                             HorizontalAlignment=\"Stretch\"\n                             Watermark=\"Password or token\" PasswordChar=\"●\"\n                             Text=\"{Binding Password}\" />\n                    <Button IsDefault=\"True\"\n                            HorizontalAlignment=\"Center\" HorizontalContentAlignment=\"Center\" VerticalContentAlignment=\"Center\"\n                            Command=\"{Binding LoginCommand}\"\n                            Content=\"Sign in\" Width=\"140\" Height=\"40\"\n                            Classes=\"accent\" />\n                </StackPanel>\n            </TabItem>\n        </TabControl>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml.cs",
    "content": "using Atlassian.Bitbucket.UI.ViewModels;\nusing Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager;\nusing GitCredentialManager.UI.Controls;\n\nnamespace Atlassian.Bitbucket.UI.Views\n{\n    public partial class CredentialsView : UserControl, IFocusable\n    {\n        public CredentialsView()\n        {\n            InitializeComponent();\n        }\n\n        public void SetFocus()\n        {\n            if (!(DataContext is CredentialsViewModel vm))\n            {\n                return;\n            }\n\n            if (vm.ShowOAuth)\n            {\n                _authModesTabControl.SelectedIndex = 0;\n                _oauthLoginButton.Focus();\n            }\n            else if (vm.ShowBasic)\n            {\n                _authModesTabControl.SelectedIndex = 1;\n                if (string.IsNullOrWhiteSpace(vm.UserName))\n                {\n                    // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                    if (!PlatformUtils.IsMacOS())\n                        _userNameTextBox.Focus();\n                }\n                else\n                {\n                    // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                    if (!PlatformUtils.IsMacOS())\n                        _passwordTextBox.Focus();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.2\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.11.1\" />\n    <PackageReference Include=\"ReportGenerator\" Version=\"5.3.10\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <DotNetCliToolReference Include=\"dotnet-xunit\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Atlassian.Bitbucket\\Atlassian.Bitbucket.csproj\" />\n    <ProjectReference Include=\"..\\TestInfrastructure\\TestInfrastructure.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests\n{\n    public class BitbucketAuthenticationTest\n    {\n        [Theory]\n        [InlineData(\"jsquire\", \"password\")]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_Basic_SucceedsAfterUserInput(string username, string password)\n        {\n            var context = new TestCommandContext();\n            context.Terminal.Prompts[\"Username\"] = username;\n            context.Terminal.SecretPrompts[\"Password\"] = password;\n            Uri targetUri = null;\n\n            var bitbucketAuthentication = new BitbucketAuthentication(context);\n\n            var result = await bitbucketAuthentication.GetCredentialsAsync(targetUri, username, AuthenticationModes.Basic);\n\n            Assert.NotNull(result);\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(username, result.Credential.Account);\n            Assert.Equal(password, result.Credential.Password);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_OAuth_ReturnsOAuth()\n        {\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = true; // Allow OAuth mode\n            Uri targetUri = null;\n\n            var bitbucketAuthentication = new BitbucketAuthentication(context);\n\n            var result = await bitbucketAuthentication.GetCredentialsAsync(targetUri, null, AuthenticationModes.OAuth);\n\n            Assert.NotNull(result);\n            Assert.Equal(AuthenticationModes.OAuth, result.AuthenticationMode);\n            Assert.Null(result.Credential);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_All_ShowsMenu_OAuthOption1()\n        {\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = true; // Allow OAuth mode\n            context.Settings.IsGuiPromptsEnabled = false; // Force text prompts\n            context.Terminal.Prompts[\"option (enter for default)\"] = \"1\";\n            Uri targetUri = null;\n\n            var bitbucketAuthentication = new BitbucketAuthentication(context);\n\n            var result = await bitbucketAuthentication.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);\n\n            Assert.NotNull(result);\n            Assert.Equal(AuthenticationModes.OAuth, result.AuthenticationMode);\n            Assert.Null(result.Credential);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_All_ShowsMenu_BasicOption2()\n        {\n            const string username = \"jsquire\";\n            const string password = \"password\";\n\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = true; // Allow OAuth mode\n            context.Settings.IsGuiPromptsEnabled = false; // Force text prompts\n            context.Terminal.Prompts[\"option (enter for default)\"] = \"2\";\n            context.Terminal.Prompts[\"Username\"] = username;\n            context.Terminal.SecretPrompts[\"Password\"] = password;\n            Uri targetUri = null;\n\n            var bitbucketAuthentication = new BitbucketAuthentication(context);\n\n            var result = await bitbucketAuthentication.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);\n\n            Assert.NotNull(result);\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(username, result.Credential.Account);\n            Assert.Equal(password, result.Credential.Password);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_All_NoDesktopSession_BasicOnly()\n        {\n            const string username = \"jsquire\";\n            const string password = \"password\";\n\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = false; // Disallow OAuth mode\n            context.Terminal.Prompts[\"Username\"] = username;\n            context.Terminal.SecretPrompts[\"Password\"] = password;\n            Uri targetUri = null;\n\n            var bitbucketAuthentication = new BitbucketAuthentication(context);\n\n            var result = await bitbucketAuthentication.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);\n\n            Assert.NotNull(result);\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(username, result.Credential.Account);\n            Assert.Equal(password, result.Credential.Password);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBCloud_HelperCmdLine()\n        {\n            var targetUri = new Uri(\"https://bitbucket.org\");\n\n            var command = \"/usr/bin/test-helper\";\n            var args = \"\";\n            var expectedUserName = \"jsquire\";\n            var expectedPassword = \"password\";\n            var resultDict = new Dictionary<string, string>\n            {\n                [\"username\"] = expectedUserName,\n                [\"password\"] = expectedPassword\n            };\n\n            string expectedArgs = $\"prompt --show-basic --show-oauth\";\n\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection\n\n            var authMock = new Mock<BitbucketAuthentication>(context) { CallBase = true };\n            authMock.Setup(x => x.TryFindHelperCommand(out command, out args))\n                .Returns(true);\n            authMock.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), It.IsAny<string>(), null, CancellationToken.None))\n                .ReturnsAsync(resultDict);\n\n            BitbucketAuthentication auth = authMock.Object;\n            CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);\n\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(result.Credential.Account, expectedUserName);\n            Assert.Equal(result.Credential.Password, expectedPassword);\n\n            authMock.Verify(x => x.InvokeHelperAsync(command, expectedArgs, null, CancellationToken.None),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBCloud_HelperCmdLine()\n        {\n            var targetUri = new Uri(\"https://bitbucket.org\");\n\n            var command = \"/usr/bin/test-helper\";\n            var args = \"\";\n            var expectedUserName = \"jsquire\";\n            var expectedPassword = \"password\";\n            var resultDict = new Dictionary<string, string>\n            {\n                [\"username\"] = expectedUserName,\n                [\"password\"] = expectedPassword\n            };\n\n            string expectedArgs = $\"prompt --username {expectedUserName} --show-basic\";\n\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = true; // Enable UI helper selection\n\n            var authMock = new Mock<BitbucketAuthentication>(context) { CallBase = true };\n            authMock.Setup(x => x.TryFindHelperCommand(out command, out args))\n                .Returns(true);\n            authMock.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), It.IsAny<string>(), null, CancellationToken.None))\n                .ReturnsAsync(resultDict);\n\n            BitbucketAuthentication auth = authMock.Object;\n            CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, expectedUserName, AuthenticationModes.Basic);\n\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(result.Credential.Account, expectedUserName);\n            Assert.Equal(result.Credential.Password, expectedPassword);\n\n            authMock.Verify(x => x.InvokeHelperAsync(command, expectedArgs, null, CancellationToken.None),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBServerDC_HelperCmdLine()\n        {\n            var targetUri = new Uri(\"https://example.com/bitbucket\");\n\n            var command = \"/usr/bin/test-helper\";\n            var args = \"\";\n            var expectedUserName = \"jsquire\";\n            var expectedPassword = \"password\";\n            var resultDict = new Dictionary<string, string>\n            {\n                [\"username\"] = expectedUserName,\n                [\"password\"] = expectedPassword\n            };\n\n            string expectedArgs = $\"prompt --url {targetUri} --show-basic --show-oauth\";\n\n            var context = new TestCommandContext();\n            context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection\n\n            var authMock = new Mock<BitbucketAuthentication>(context) { CallBase = true };\n            authMock.Setup(x => x.TryFindHelperCommand(out command, out args))\n                .Returns(true);\n            authMock.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), It.IsAny<string>(), null, CancellationToken.None))\n                .ReturnsAsync(resultDict);\n\n            BitbucketAuthentication auth = authMock.Object;\n            CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);\n\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(result.Credential.Account, expectedUserName);\n            Assert.Equal(result.Credential.Password, expectedPassword);\n\n            authMock.Verify(x => x.InvokeHelperAsync(command, expectedArgs, null, CancellationToken.None),\n                Times.Once);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs",
    "content": "using System;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests\n{\n    public class BitbucketHelperTest\n    {\n        [Theory]\n        [InlineData(null, false)]\n        [InlineData(\"\", false)]\n        [InlineData(\"    \", false)]\n        [InlineData(\"bitbucket.org\", true)]\n        [InlineData(\"BITBUCKET.ORG\", true)]\n        [InlineData(\"BiTbUcKeT.OrG\", true)]\n        [InlineData(\"bitbucket.example.com\", false)]\n        [InlineData(\"bitbucket.example.org\", false)]\n        [InlineData(\"bitbucket.org.com\", false)]\n        [InlineData(\"bitbucket.org.org\", false)]\n        public void BitbucketHelper_IsBitbucketOrg_StringHost(string str, bool expected)\n        {\n            bool actual = BitbucketHelper.IsBitbucketOrg(str);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"http://bitbucket.org\", true)]\n        [InlineData(\"https://bitbucket.org\", true)]\n        [InlineData(\"http://bitbucket.org/path\", true)]\n        [InlineData(\"https://bitbucket.org/path\", true)]\n        [InlineData(\"http://BITBUCKET.ORG\", true)]\n        [InlineData(\"https://BITBUCKET.ORG\", true)]\n        [InlineData(\"http://BITBUCKET.ORG/PATH\", true)]\n        [InlineData(\"https://BITBUCKET.ORG/PATH\", true)]\n        [InlineData(\"http://BiTbUcKeT.OrG\", true)]\n        [InlineData(\"https://BiTbUcKeT.OrG\", true)]\n        [InlineData(\"http://BiTbUcKeT.OrG/pAtH\", true)]\n        [InlineData(\"https://BiTbUcKeT.OrG/pAtH\", true)]\n        [InlineData(\"http://bitbucket.example.com\", false)]\n        [InlineData(\"https://bitbucket.example.com\", false)]\n        [InlineData(\"http://bitbucket.example.com/path\", false)]\n        [InlineData(\"https://bitbucket.example.com/path\", false)]\n        [InlineData(\"http://bitbucket.example.org\", false)]\n        [InlineData(\"https://bitbucket.example.org\", false)]\n        [InlineData(\"http://bitbucket.example.org/path\", false)]\n        [InlineData(\"https://bitbucket.example.org/path\", false)]\n        [InlineData(\"http://bitbucket.org.com\", false)]\n        [InlineData(\"https://bitbucket.org.com\", false)]\n        [InlineData(\"http://bitbucket.org.com/path\", false)]\n        [InlineData(\"https://bitbucket.org.com/path\", false)]\n        [InlineData(\"http://bitbucket.org.org\", false)]\n        [InlineData(\"https://bitbucket.org.org\", false)]\n        [InlineData(\"http://bitbucket.org.org/path\", false)]\n        [InlineData(\"https://bitbucket.org.org/path\", false)]\n        public void BitbucketHelper_IsBitbucketOrg_Uri(string str, bool expected)\n        {\n            bool actual = BitbucketHelper.IsBitbucketOrg(new Uri(str));\n            Assert.Equal(expected, actual);\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs",
    "content": "﻿using Atlassian.Bitbucket.Cloud;\nusing Atlassian.Bitbucket.DataCenter;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests\n{\n    public class BitbucketHostProviderTest\n    {\n        #region Tests\n\n        private const string MOCK_ACCESS_TOKEN = \"at-0987654321\";\n        private const string MOCK_ACCESS_TOKEN_ALT = \"at-onetwothreefour-1234\";\n        private const string MOCK_EXPIRED_ACCESS_TOKEN = \"at-1234567890-expired\";\n        private const string MOCK_REFRESH_TOKEN = \"rt-1234567809\";\n        private const string BITBUCKET_DOT_ORG_HOST = \"bitbucket.org\";\n        private const string DC_SERVER_HOST = \"example.com\";\n        private Mock<IBitbucketAuthentication> bitbucketAuthentication = new Mock<IBitbucketAuthentication>(MockBehavior.Strict);\n        private Mock<IBitbucketRestApi> bitbucketApi = new Mock<IBitbucketRestApi>(MockBehavior.Strict);\n\n        [Theory]\n        [InlineData(\"https\", null, false)]\n        // We report that we support unencrypted HTTP here so that we can fail and\n        // show a helpful error message in the call to `GenerateCredentialAsync` instead.\n        [InlineData(\"http\", BITBUCKET_DOT_ORG_HOST, true)]\n        [InlineData(\"ssh\", BITBUCKET_DOT_ORG_HOST, false)]\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, true)]\n        [InlineData(\"https\", \"api.bitbucket.org\", true)] // Currently does support sub domains.\n\n        [InlineData(\"https\", \"bitbucket.ogg\", false)] // No support of phony similar tld.\n        [InlineData(\"https\", \"bitbucket.com\", false)] // No support of wrong tld.\n        [InlineData(\"https\", DC_SERVER_HOST, false)] // No support of non bitbucket domains.\n\n        [InlineData(\"http\", \"bitbucket.my-company-server.com\", false)]  // Currently no support for named on-premise instances\n        [InlineData(\"https\", \"my-company-server.com\", false)]\n        [InlineData(\"https\", \"bitbucket.my.company.server.com\", false)]\n        [InlineData(\"https\", \"api.bitbucket.my-company-server.com\", false)]\n        [InlineData(\"https\", \"BITBUCKET.My-Company-Server.Com\", false)]\n        public void BitbucketHostProvider_IsSupported(string protocol, string host, bool expected)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"] = host,\n            });\n\n            var provider = new BitbucketHostProvider(new TestCommandContext());\n            Assert.Equal(expected, provider.IsSupported(input));\n        }\n\n        [Theory]\n        [InlineData(\"Basic realm=\\\"Atlassian Bitbucket\\\"\", true)]\n        [InlineData(\"Basic realm=\\\"GitSponge\\\"\", false)]\n        public void BitbucketHostProvider_IsSupported_WWWAuth(string wwwauth, bool expected)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"wwwauth\"] = wwwauth,\n            });\n\n            var provider = new BitbucketHostProvider(new TestCommandContext());\n            Assert.Equal(expected, provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void BitbucketHostProvider_IsSupported_FailsForNullInput()\n        {\n            InputArguments input = null;\n            var provider = new BitbucketHostProvider(new TestCommandContext());\n            Assert.False(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void BitbucketHostProvider_IsSupported_FailsForNullHttpResponseMessage()\n        {\n            HttpResponseMessage httpResponseMessage = null;\n            var provider = new BitbucketHostProvider(new TestCommandContext());\n            Assert.False(provider.IsSupported(httpResponseMessage));\n        }\n\n        [Theory]\n        [InlineData(\"X-AREQUESTID\", \"123456789\", true)] // only the specific header is acceptable\n        [InlineData(\"X-REQUESTID\", \"123456789\", false)]\n        [InlineData(null, null, false)]\n        public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, string value, bool expected)\n        {\n            var input = new HttpResponseMessage();\n            if (header != null)\n            {\n                input.Headers.Add(header, value);\n            }\n\n            var provider = new BitbucketHostProvider(new TestCommandContext());\n            Assert.Equal(expected, provider.IsSupported(input));\n        }\n\n        [Theory]\n        [InlineData(\"https\", DC_SERVER_HOST, \"jsquire\", \"password\")]\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", \"password\")]\n        public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic(\n            string protocol, string host, string username, string password)\n        {\n            InputArguments input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            if (DC_SERVER_HOST.Equals(host))\n            {\n                MockDCSSOEnabled();\n            }\n            MockStoredAccount(context, input, password);\n            MockRemoteBasicValid(input, password);\n            // HACK rebase MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password, username);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(password, credential.Password);\n\n            // Verify bitbucket.org credentials were validated\n                VerifyValidateBasicAuthCredentialsRan(input, password);\n                // Verify DC/Server credentials were not validated\n\n            // Stored credentials so don't ask for more\n            VerifyInteractiveAuthNeverRan();\n        }\n\n        public Mock<IBitbucketRestApi> GetBitbucketApi()\n        {\n            return bitbucketApi;\n        }\n\n        [Theory]\n        // Cloud\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", \"password\")]\n        public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth(\n            string protocol, string host, string username, string token)\n        {\n            InputArguments input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            if (DC_SERVER_HOST.Equals(host))\n            {\n                MockDCSSOEnabled();\n            }\n            MockStoredAccount(context, input, token);\n            MockRemoteAccessTokenValid(input, token);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(token, credential.Password);\n\n            // Verify bitbucket.org credentials were validated\n            VerifyValidateAccessTokenRan(input, token);\n\n            // Stored credentials so don't ask for more\n            VerifyInteractiveAuthNeverRan();\n        }\n\n        private void MockDCSSOEnabled()\n        {\n            bitbucketApi.Setup(ba => ba.GetAuthenticationMethodsAsync()).Returns(Task.FromResult(new List<AuthenticationMethod>(){AuthenticationMethod.BasicAuth, AuthenticationMethod.Sso}));\n            bitbucketApi.Setup(ba => ba.IsOAuthInstalledAsync()).Returns(Task.FromResult(true));\n        }\n\n        [Theory]\n        // DC\n        [InlineData(\"https\", DC_SERVER_HOST, \"jsquire\", \"password\")]\n        // Cloud\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", \"password\")]\n        public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_Basic(\n            string protocol, string host, string username, string password)\n        {\n            InputArguments input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            MockPromptBasic(input, password);\n            MockRemoteBasicValid(input, password);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(password, credential.Password);\n\n            VerifyInteractiveAuthRan(input);\n        }\n\n        [Theory]\n        // DC/Server does not currently support OAuth\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)]\n        public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_OAuth(\n            string protocol, string host, string username, string refreshToken, string accessToken)\n        {\n            InputArguments input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            MockPromptOAuth(input);\n            MockRemoteOAuthTokenCreate(input, accessToken, refreshToken);\n            MockRemoteAccessTokenValid(input, accessToken);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n\n            VerifyInteractiveAuthRan(input);\n            VerifyOAuthFlowRan(input, accessToken);\n            VerifyValidateAccessTokenRan(input, accessToken);\n            VerifyOAuthRefreshTokenStored(context, input, refreshToken);\n        }\n\n        [Theory]\n        // DC/Server does not currently support OAuth\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)]\n        public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refresh(\n            string protocol, string host, string username, string refreshToken, string accessToken)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            // AT has does not exist, but RT is still valid\n            MockStoredRefreshToken(context, input, refreshToken);\n            MockRemoteAccessTokenValid(input, accessToken);\n            MockRemoteRefreshTokenValid(input, refreshToken, accessToken);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n\n            VerifyValidateAccessTokenRan(input, accessToken);\n            VerifyOAuthRefreshRan(input, refreshToken);\n            VerifyInteractiveAuthNeverRan();\n        }\n\n        [Theory]\n        // DC/Server does not currently support OAuth\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", MOCK_REFRESH_TOKEN, MOCK_EXPIRED_ACCESS_TOKEN, MOCK_ACCESS_TOKEN)]\n        public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refresh(\n            string protocol, string host, string username, string refreshToken, string expiredAccessToken, string accessToken)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            // AT exists but has expired, but RT is still valid\n            MockStoredAccount(context, input, expiredAccessToken);\n            MockRemoteAccessTokenExpired(input, expiredAccessToken);\n\n            MockStoredRefreshToken(context, input, refreshToken);\n            MockRemoteAccessTokenValid(input, accessToken);\n            MockRemoteRefreshTokenValid(input, refreshToken, accessToken);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n\n            VerifyValidateAccessTokenRan(input, accessToken);\n            VerifyOAuthRefreshRan(input, refreshToken);\n            VerifyInteractiveAuthNeverRan();\n        }\n\n        [Theory]\n        // Cloud\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)]\n        public async Task BitbucketHostProvider_GetCredentialAsync_PreconfiguredMode_OAuth_ValidRT_IsRespected(\n            string protocol, string host, string username, string refreshToken, string accessToken)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n            context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, \"oauth\");\n\n            // We have a stored RT so we can just use that without any prompts\n            MockStoredRefreshToken(context, input, refreshToken);\n            MockRemoteAccessTokenValid(input, accessToken);\n            MockRemoteRefreshTokenValid(input, refreshToken, accessToken);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n\n            VerifyInteractiveAuthNeverRan();\n            VerifyOAuthRefreshRan(input, refreshToken);\n        }\n\n        [Theory]\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", MOCK_ACCESS_TOKEN, MOCK_ACCESS_TOKEN_ALT, MOCK_REFRESH_TOKEN)]\n        public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_OAuth_IsRespected(\n            string protocol, string host, string username, string storedToken, string newToken, string refreshToken)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n            context.Environment.Variables.Add(\n                BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString);\n\n            // User has stored access token that we shouldn't use - RT should be used to mint new AT\n            MockStoredAccount(context, input, storedToken);\n            MockStoredRefreshToken(context, input, refreshToken);\n            MockRemoteAccessTokenValid(input, newToken);\n            MockRemoteRefreshTokenValid(input, refreshToken, newToken);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(newToken, credential.Password);\n\n            VerifyInteractiveAuthNeverRan();\n            VerifyOAuthRefreshRan(input, refreshToken);\n        }\n\n        [Theory]\n        // Cloud\n        [InlineData(\"https\", BITBUCKET_DOT_ORG_HOST, \"jsquire\", \"old-password\", \"new-password\")]\n        // DC\n        [InlineData(\"https\", DC_SERVER_HOST, \"jsquire\", \"old-password\", \"new-password\")]\n        public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_Basic_IsRespected(\n            string protocol, string host, string username, string storedPassword, string freshPassword)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n            context.Environment.Variables.Add(\n                BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString);\n\n            // User has stored password that we shouldn't use\n            MockStoredAccount(context, input, storedPassword);\n            MockPromptBasic(input, freshPassword);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(username, credential.Account);\n            Assert.Equal(freshPassword, credential.Password);\n\n            VerifyInteractiveAuthRan(input);\n        }\n\n        [Theory]\n        // DC - supports Basic, OAuth\n        [InlineData(\"https\", \"example.com\", \"basic\", AuthenticationModes.Basic)]\n        [InlineData(\"https\", \"example.com\", \"oauth\", AuthenticationModes.OAuth)]\n        [InlineData(\"https\", \"example.com\", \"NOT-A-REAL-VALUE\", DataCenterConstants.ServerAuthenticationModes)]\n        [InlineData(\"https\", \"example.com\", \"none\", DataCenterConstants.ServerAuthenticationModes)]\n        [InlineData(\"https\", \"example.com\", null, DataCenterConstants.ServerAuthenticationModes)]\n        // Cloud - supports Basic, OAuth\n        [InlineData(\"https\", \"bitbucket.org\", \"oauth\", AuthenticationModes.OAuth)]\n        [InlineData(\"https\", \"bitbucket.org\", \"basic\", AuthenticationModes.Basic)]\n        [InlineData(\"https\", \"bitbucket.org\", \"NOT-A-REAL-VALUE\", CloudConstants.DotOrgAuthenticationModes)]\n        [InlineData(\"https\", \"bitbucket.org\", \"none\", CloudConstants.DotOrgAuthenticationModes)]\n        [InlineData(\"https\", \"bitbucket.org\", null, CloudConstants.DotOrgAuthenticationModes)]\n        public async Task BitbucketHostProvider_GetSupportedAuthenticationModes(string protocol, string host, string bitbucketAuthModes, AuthenticationModes expectedModes)\n        {\n            var input = MockInput(protocol, host, null);\n\n            var context = new TestCommandContext();\n            if (bitbucketAuthModes != null)\n            {\n                context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, bitbucketAuthModes);\n            }\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            AuthenticationModes actualModes = await provider.GetSupportedAuthenticationModesAsync(input);\n\n            Assert.Equal(expectedModes, actualModes);\n        }\n\n        [Theory]\n        [InlineData(\"https\", DC_SERVER_HOST, \"jsquire\")]\n        public async Task BitbucketHostProvider_StoreCredentialAsync(string protocol, string host, string username)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            Assert.Equal(0, context.CredentialStore.Count);\n\n            await provider.StoreCredentialAsync(input);\n\n            Assert.Equal(1, context.CredentialStore.Count);\n        }\n\n        [Theory]\n        [InlineData(\"https\", DC_SERVER_HOST, \"jsquire\", \"password\")]\n        public async Task BitbucketHostProvider_EraseCredentialAsync(string protocol, string host, string username, string password)\n        {\n            var input = MockInput(protocol, host, username);\n\n            var context = new TestCommandContext();\n\n            MockStoredAccount(context, input, password);\n\n            var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);\n\n            Assert.Equal(1, context.CredentialStore.Count);\n\n            await provider.EraseCredentialAsync(input);\n\n            Assert.Equal(0, context.CredentialStore.Count);\n        }\n\n        #endregion\n\n        #region Test helpers\n\n        private static InputArguments MockInput(string protocol, string host, string username)\n        {\n            return new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"] = host,\n                [\"username\"] = username\n            });\n        }\n\n        private void VerifyOAuthFlowRan(InputArguments input, string token)\n        {\n            // Get new access token and refresh token\n            bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(input), Times.Once);\n\n            // Check access token works/resolve username\n            bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once);\n        }\n\n        private void VerifyValidateBasicAuthCredentialsNeverRan()\n        {\n            // Never check username/password works\n            bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny<string>(), It.IsAny<string>(), false), Times.Never);\n        }\n\n        private void VerifyValidateBasicAuthCredentialsRan(InputArguments input, string password)\n        {\n            // Check username/password works\n            bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once);\n        }\n\n        private void VerifyValidateAccessTokenRan(InputArguments input, string token)\n        {\n            // Check tokens works\n            bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once);\n        }\n\n        private void VerifyInteractiveAuthRan(InputArguments input)\n        {\n            var remoteUri = input.GetRemoteUri();\n\n            bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny<AuthenticationModes>()), Times.Once);\n        }\n\n        private void VerifyInteractiveAuthNeverRan()\n        {\n            bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(It.IsAny<Uri>(), It.IsAny<string>(), It.IsAny<AuthenticationModes>()), Times.Never);\n        }\n\n        private void VerifyOAuthRefreshRan(InputArguments input, string refreshToken)\n        {\n            // Check refresh was called\n            bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(input, refreshToken), Times.Once);\n        }\n\n        private void MockRemoteRefreshTokenValid(InputArguments input, string refreshToken, string accessToken)\n        {\n            bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(input, refreshToken)).ReturnsAsync(new OAuth2TokenResult(accessToken, \"access_token\"));\n        }\n\n        private void MockPromptBasic(InputArguments input, string password)\n        {\n            var remoteUri = input.GetRemoteUri();\n            bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny<AuthenticationModes>()))\n                .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password)));\n        }\n\n        private void MockPromptOAuth(InputArguments input)\n        {\n            var remoteUri = input.GetRemoteUri();\n            bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny<AuthenticationModes>()))\n                .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.OAuth));\n        }\n\n        private void MockRemoteBasicValid(InputArguments input, string password)\n        {\n            var userInfo = new Mock<IUserInfo>(MockBehavior.Strict);\n            userInfo.Setup(ui => ui.UserName).Returns(input.UserName);\n\n            // Basic\n            bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false))\n                .ReturnsAsync(new RestApiResult<IUserInfo>(System.Net.HttpStatusCode.OK, userInfo.Object));\n        }\n\n        private void MockRemoteAccessTokenExpired(InputArguments input, string token)\n        {\n            // OAuth\n            bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true))\n                .ReturnsAsync(new RestApiResult<IUserInfo>(System.Net.HttpStatusCode.Unauthorized));\n        }\n\n        private void MockRemoteAccessTokenValid(InputArguments input, string token)\n        {\n            var userInfo = new Mock<IUserInfo>(MockBehavior.Strict);\n            userInfo.Setup(ui => ui.UserName).Returns(input.UserName);\n\n            // OAuth\n            bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true))\n                .ReturnsAsync(new RestApiResult<IUserInfo>(System.Net.HttpStatusCode.OK, userInfo.Object));\n        }\n\n        private static void MockRemoteOAuthAccountIsInvalid(Mock<IBitbucketRestApi> bitbucketApi)\n        {\n            // OAuth\n            bitbucketApi.Setup(x => x.GetUserInformationAsync(null, It.IsAny<string>(), true)).ReturnsAsync(new RestApiResult<IUserInfo>(System.Net.HttpStatusCode.BadRequest));\n        }\n\n        private static void MockStoredAccount(TestCommandContext context, InputArguments input, string password)\n        {\n            var remoteUri = input.GetRemoteUri();\n            var remoteUrl = remoteUri.AbsoluteUri.Substring(0, remoteUri.AbsoluteUri.Length - 1);\n            context.CredentialStore.Add(remoteUrl, new TestCredential(input.Host, input.UserName, password));\n        }\n\n        private static void MockStoredRefreshToken(TestCommandContext context, InputArguments input, string token)\n        {\n            var remoteUri = input.GetRemoteUri();\n            var refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri);\n            context.CredentialStore.Add(refreshService, new TestCredential(refreshService, input.UserName, token));\n        }\n\n        private void MockRemoteOAuthTokenCreate(InputArguments input, string accessToken, string refreshToken)\n        {\n            bitbucketAuthentication.Setup(x => x.CreateOAuthCredentialsAsync(input))\n                .ReturnsAsync(new OAuth2TokenResult(accessToken, \"access_token\") { RefreshToken = refreshToken });\n        }\n\n        private void VerifyOAuthRefreshTokenStored(TestCommandContext context, InputArguments input, string refreshToken)\n        {\n            var remoteUri = input.GetRemoteUri();\n            string refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri);\n            bool result = context.CredentialStore.TryGet(refreshService, input.UserName, out var credential);\n\n            Assert.True(result);\n            Assert.Equal(refreshToken, credential.Password);\n        }\n\n        private static Mock<IRegistry<IBitbucketRestApi>> MockRestApiRegistry(InputArguments input, Mock<IBitbucketRestApi> bitbucketApi)\n        {\n            var restApiRegistry = new Mock<IRegistry<IBitbucketRestApi>>(MockBehavior.Strict);\n\n            restApiRegistry.Setup(rar => rar.Get(input)).Returns(bitbucketApi.Object);\n\n            return restApiRegistry;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs",
    "content": "using System.Collections.Generic;\nusing GitCredentialManager;\nusing Moq;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests\n{\n    public class BitbucketRestApiRegistryTest\n    {\n        private Mock<ICommandContext> context = new Mock<ICommandContext>(MockBehavior.Strict);\n        private Mock<ISettings> settings = new Mock<ISettings>(MockBehavior.Strict);\n\n        [Fact]\n        public void BitbucketRestApiRegistry_Get_ReturnsCloudApi_ForBitbucketOrg()\n        {\n            // Given\n            settings.Setup(s => s.RemoteUri).Returns(new System.Uri(\"https://bitbucket.org\"));\n            context.Setup(c => c.Settings).Returns(settings.Object);\n\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"bitbucket.org\",\n            });\n\n            // When\n            var registry = new BitbucketRestApiRegistry(context.Object);\n            var api = registry.Get(input);\n        \n            // Then\n            Assert.NotNull(api);\n            Assert.IsType<Atlassian.Bitbucket.Cloud.BitbucketRestApi>(api);\n\n        }\n\n        [Fact]\n        public void BitbucketRestApiRegistry_Get_ReturnsDataCenterApi_ForBitbucketDC()\n        {\n            // Given\n            settings.Setup(s => s.RemoteUri).Returns(new System.Uri(\"https://example.com\"));\n            context.Setup(c => c.Settings).Returns(settings.Object);\n\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"] = \"example.com\",\n            });\n\n            // When\n            var registry = new BitbucketRestApiRegistry(context.Object);\n            var api = registry.Get(input);\n\n            // Then\n            Assert.NotNull(api);\n            Assert.IsType<Atlassian.Bitbucket.DataCenter.BitbucketRestApi>(api);\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs",
    "content": "using System;\nusing System.Text.Json;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests\n{\n    public class BitbucketTokenEndpointResponseJsonTest\n    {\n        [Fact]\n        public void BitbucketTokenEndpointResponseJson_Deserialize_Uses_Scopes()\n        {\n            var accessToken = \"123\";\n            var tokenType = \"Bearer\";\n            var expiresIn = 1000;\n            var scopesString = \"x,y,z\";\n            var scopeString = \"a,b,c\";\n\n            var json = $\"{{\\\"access_token\\\": \\\"{accessToken}\\\", \\\"token_type\\\": \\\"{tokenType}\\\", \\\"expires_in\\\": {expiresIn}, \\\"scopes\\\": \\\"{scopesString}\\\", \\\"scope\\\": \\\"{scopeString}\\\"}}\";\n\n            var result = JsonSerializer.Deserialize<BitbucketTokenEndpointResponseJson>(json,\n                new JsonSerializerOptions\n                {\n                    PropertyNameCaseInsensitive = true\n                });\n\n            Assert.Equal(accessToken, result.AccessToken);\n            Assert.Equal(tokenType, result.TokenType);\n            Assert.Equal(expiresIn, result.ExpiresIn);\n            Assert.Equal(scopesString, result.Scope);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.Cloud;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.Cloud\n{\n    public class BitbucketOAuth2ClientTest\n    {\n        private Mock<HttpClient> httpClient = new Mock<HttpClient>(MockBehavior.Strict);\n        private Mock<ISettings> settings = new Mock<ISettings>(MockBehavior.Loose);\n        private Mock<IOAuth2WebBrowser> browser = new Mock<IOAuth2WebBrowser>(MockBehavior.Strict);\n        private Mock<IOAuth2CodeGenerator> codeGenerator = new Mock<IOAuth2CodeGenerator>(MockBehavior.Strict);\n        private IEnumerable<string> scopes = new List<string>();\n        private CancellationToken ct = new CancellationToken();\n        private Uri rootCallbackUri = new Uri(\"http://localhost:34106/\");\n        private string nonce = \"12345\";\n        private string pkceCodeVerifier = \"abcde\";\n        private string pkceCodeChallenge = \"xyz987\";\n        private string authorization_code = \"authorization_token\";\n\n        [Fact]\n        public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_ReturnsCode()\n        {\n            MockClientIdOverride(false, \"never used\");\n\n            Uri finalCallbackUri = MockFinalCallbackUri();\n\n            Bitbucket.Cloud.BitbucketOAuth2Client client = GetBitbucketOAuth2Client();\n\n            MockGetAuthenticationCodeAsync(finalCallbackUri, null, client.Scopes);\n\n            MockCodeGenerator();\n\n            var result = await client.GetAuthorizationCodeAsync(browser.Object, ct);\n\n            VerifyAuthorizationCodeResult(result);\n        }\n\n        [Theory]\n        [InlineData(null)]\n        [InlineData(\"i234\")]\n        public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClientIdOverride_ReturnsCode(string clientId)\n        {\n            MockClientIdOverride(clientId != null, clientId);\n\n            Uri finalCallbackUri = MockFinalCallbackUri();\n\n            Bitbucket.Cloud.BitbucketOAuth2Client client = GetBitbucketOAuth2Client();\n\n            MockGetAuthenticationCodeAsync(finalCallbackUri, clientId, client.Scopes);\n\n            MockCodeGenerator();\n\n            var result = await client.GetAuthorizationCodeAsync(browser.Object, ct);\n\n            VerifyAuthorizationCodeResult(result);\n        }\n\n        [Fact]\n        public async Task BitbucketOAuth2Client_GetDeviceCodeAsync()\n        {\n            var trace2 = new NullTrace2();\n            var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);\n            await Assert.ThrowsAsync<Trace2InvalidOperationException>(async () => await client.GetDeviceCodeAsync(scopes, ct));\n        }\n\n        [Theory]\n        [InlineData(\"https\", \"example.com\", \"john\", \"https://example.com/refresh_token\")]\n        [InlineData(\"http\", \"example.com\", \"john\", \"http://example.com/refresh_token\")]\n        [InlineData(\"https\", \"example.com\", \"dave\", \"https://example.com/refresh_token\")]\n        [InlineData(\"https\", \"example.com/\", \"john\", \"https://example.com/refresh_token\")]\n        public void BitbucketOAuth2Client_GetRefreshTokenServiceName(string protocol, string host, string username, string expectedResult)\n        {\n            var trace2 = new NullTrace2();\n            var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"] = host,\n                [\"username\"] = username\n            });\n            Assert.Equal(expectedResult, client.GetRefreshTokenServiceName(input));\n        }\n\n\n        private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result)\n        {\n            Assert.NotNull(result);\n            Assert.Equal(authorization_code, result.Code);\n            Assert.Equal(rootCallbackUri, result.RedirectUri);\n            Assert.Equal(pkceCodeVerifier, result.CodeVerifier);\n        }\n\n        private Bitbucket.Cloud.BitbucketOAuth2Client GetBitbucketOAuth2Client()\n        {\n            var trace2 = new NullTrace2();\n            var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);\n            client.CodeGenerator = codeGenerator.Object;\n            return client;\n        }\n\n        private void MockCodeGenerator()\n        {\n            codeGenerator.Setup(c => c.CreateNonce()).Returns(nonce);\n            codeGenerator.Setup(c => c.CreatePkceCodeVerifier()).Returns(pkceCodeVerifier);\n            codeGenerator.Setup(c => c.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, pkceCodeVerifier)).Returns(pkceCodeChallenge);\n        }\n\n        private void MockGetAuthenticationCodeAsync(Uri finalCallbackUri, string overrideClientId, IEnumerable<string> scopes)\n        {\n            var authorizationUri = new UriBuilder(CloudConstants.OAuth2AuthorizationEndpoint)\n            {\n                Query = \"?response_type=code\"\n             + \"&client_id=\" + (overrideClientId ?? CloudConstants.OAuth2ClientId)\n             + \"&state=12345\"\n             + \"&code_challenge_method=\" + OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodS256\n             + \"&code_challenge=\" + WebUtility.UrlEncode(pkceCodeChallenge).ToLower()\n             + \"&redirect_uri=\" + WebUtility.UrlEncode(rootCallbackUri.AbsoluteUri).ToLower()\n             + \"&scope=\" + WebUtility.UrlEncode(string.Join(\" \", scopes)).ToLower()\n            }.Uri;\n\n            browser.Setup(b => b.GetAuthenticationCodeAsync(authorizationUri, rootCallbackUri, ct)).Returns(Task.FromResult(finalCallbackUri));\n        }\n\n        private Uri MockFinalCallbackUri()\n        {\n            var finalUri = new Uri(rootCallbackUri, \"?state=\" + nonce + \"&code=\" + authorization_code);\n            browser.Setup(b => b.UpdateRedirectUri(rootCallbackUri)).Returns(rootCallbackUri);\n            return finalUri;\n        }\n\n        private string MockeClientIdOverride(bool set)\n        {\n            return MockClientIdOverride(set, null);\n        }\n        private string MockClientIdOverride(bool set, string value)\n        {\n            settings.Setup(s => s.TryGetSetting(\n                CloudConstants.EnvironmentVariables.OAuthClientId,\n                Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientId,\n                out value)).Returns(set);\n            return value;\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.Cloud;\nusing GitCredentialManager.Tests;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.Cloud\n{\n    public class BitbucketRestApiTest\n    {\n        [Theory]\n        [InlineData(\"jsquire\", \"token\", true)]\n        [InlineData(\"jsquire\", \"password\", false)]\n        public async Task BitbucketRestApi_GetUserInformationAsync_ReturnsUserInfo_ForSuccessfulRequest(string username, string password, bool isBearerToken)\n        {\n            var uuid = Guid.NewGuid();\n            var accountId = \"1234\";\n\n            var context = new TestCommandContext();\n\n            var expectedRequestUri = new Uri(\"https://api.bitbucket.org/2.0/user\");\n\n            var userinfoResponseJson = $\"{{\\\"username\\\":\\\"{username}\\\",\\\"has_2fa_enabled\\\":false,\\\"account_id\\\":\\\"{accountId}\\\",\\\"uuid\\\":\\\"{uuid}\\\"}}\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new StringContent(userinfoResponseJson)\n            };\n\n            var httpHandler = new TestHttpMessageHandler();\n            httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request =>\n            {\n                if (isBearerToken)\n                {\n                    RestTestUtilities.AssertBearerAuth(request, password);\n                }\n                else\n                {\n                    RestTestUtilities.AssertBasicAuth(request, username, password);\n                }\n\n                return httpResponse;\n            });\n            context.HttpClientFactory.MessageHandler = httpHandler;\n\n            var api = new BitbucketRestApi(context);\n            var result = await api.GetUserInformationAsync(username, password, isBearerToken);\n\n            Assert.NotNull(result);\n            Assert.Equal(username, result.Response.UserName);\n\n            httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs",
    "content": "using System;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.Cloud;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.Cloud\n{\n    public class UserInfoTest\n    {\n        [Fact]\n        public void UserInfo_Set()\n        {\n            var userInfo = new UserInfo()\n            {\n                UserName = \"123\",\n            };\n\n            Assert.Equal(\"123\", userInfo.UserName);\n        }\n\n        [Fact]\n        public void Deserialize_UserInfo()\n        {\n            var uuid = \"{bef4bd75-03fe-4f19-9c6c-ed57b05ab6f6}\";\n            var userName = \"bob\";\n            var accountId = \"123abc\";\n\n            var json = $\"{{\\\"uuid\\\": \\\"{uuid}\\\", \\\"has_2fa_enabled\\\": null, \\\"username\\\": \\\"{userName}\\\", \\\"account_id\\\": \\\"{accountId}\\\"}}\";\n\n            var result = JsonSerializer.Deserialize<UserInfo>(json, new JsonSerializerOptions()\n            {\n                PropertyNameCaseInsensitive = true,\n            });\n\n            Assert.Equal(userName, result.UserName);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.DataCenter;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.DataCenter\n{\n    public class BitbucketOAuth2ClientTest\n    {\n        private Mock<HttpClient> httpClient = new Mock<HttpClient>(MockBehavior.Strict);\n        private Mock<ISettings> settings = new Mock<ISettings>(MockBehavior.Loose);\n        private Mock<IOAuth2WebBrowser> browser = new Mock<IOAuth2WebBrowser>(MockBehavior.Strict);\n        private Mock<IOAuth2CodeGenerator> codeGenerator = new Mock<IOAuth2CodeGenerator>(MockBehavior.Strict);\n        private CancellationToken ct = new CancellationToken();\n        private Uri rootCallbackUri = new Uri(\"http://localhost:34106/\");\n        private string nonce = \"12345\";\n        private string pkceCodeVerifier = \"abcde\";\n        private string pkceCodeChallenge = \"xyz987\";\n        private string authorization_code = \"authorization_token\";\n\n        [Fact]\n        public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_ReturnsCode()\n        {\n            var remoteUrl = MockRemoteUri(\"http://example.com\");\n            var clientId = MockClientIdOverride(\"dc-client-id\");\n            MockClientSecretOverride(\"dc-client-seccret\");\n\n            Uri finalCallbackUri = MockFinalCallbackUri(rootCallbackUri);\n\n            var client = GetBitbucketOAuth2Client();\n\n            MockGetAuthenticationCodeAsync(remoteUrl, rootCallbackUri, finalCallbackUri, clientId, client.Scopes);\n\n            MockCodeGenerator();\n\n            var result = await client.GetAuthorizationCodeAsync(browser.Object, ct);\n\n            VerifyAuthorizationCodeResult(result, rootCallbackUri);\n        }\n\n        [Fact]\n        public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_ReturnsCode_WhileRespectingRedirectUriOverride()\n        {\n            var rootCallbackUrl = MockRootCallbackUriOverride(\"http://localhost:12345/\");\n            var remoteUrl = MockRemoteUri(\"http://example.com\");\n            var clientId = MockClientIdOverride(\"dc-client-id\");\n            MockClientSecretOverride(\"dc-client-seccret\");\n\n            Uri finalCallbackUri = MockFinalCallbackUri(new Uri(rootCallbackUrl));\n\n            var client = GetBitbucketOAuth2Client();\n\n            MockGetAuthenticationCodeAsync(remoteUrl, new Uri(rootCallbackUrl), finalCallbackUri, clientId, client.Scopes);\n\n            MockCodeGenerator();\n\n            var result = await client.GetAuthorizationCodeAsync(browser.Object, ct);\n\n            VerifyAuthorizationCodeResult(result, new Uri(rootCallbackUrl));\n        }\n\n        private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result, Uri redirectUri)\n        {\n            Assert.NotNull(result);\n            Assert.Equal(authorization_code, result.Code);\n            Assert.Equal(redirectUri, result.RedirectUri);\n            Assert.Equal(pkceCodeVerifier, result.CodeVerifier);\n        }\n\n        private Bitbucket.DataCenter.BitbucketOAuth2Client GetBitbucketOAuth2Client()\n        {\n            var trace2 = new NullTrace2();\n            var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);\n            client.CodeGenerator = codeGenerator.Object;\n            return client;\n        }\n\n        private void MockCodeGenerator()\n        {\n            codeGenerator.Setup(c => c.CreateNonce()).Returns(nonce);\n            codeGenerator.Setup(c => c.CreatePkceCodeVerifier()).Returns(pkceCodeVerifier);\n            codeGenerator.Setup(c => c.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, pkceCodeVerifier)).Returns(pkceCodeChallenge);\n        }\n\n        private void MockGetAuthenticationCodeAsync(string url, Uri redirectUri, Uri finalCallbackUri, string overrideClientId, IEnumerable<string> scopes)\n        {\n            var authorizationUri = new UriBuilder(url + \"/rest/oauth2/latest/authorize\")\n            {\n                Query = \"?response_type=code\"\n             + \"&client_id=\" + (overrideClientId ?? \"clientId\")\n             + \"&state=12345\"\n             + \"&code_challenge_method=\" + OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodS256\n             + \"&code_challenge=\" + WebUtility.UrlEncode(pkceCodeChallenge).ToLower()\n             + \"&redirect_uri=\" + WebUtility.UrlEncode(redirectUri.AbsoluteUri).ToLower()\n             + \"&scope=\" + WebUtility.UrlEncode(string.Join(\" \", scopes)).ToUpper()\n            }.Uri;\n\n            browser.Setup(b => b.GetAuthenticationCodeAsync(authorizationUri, redirectUri, ct)).Returns(Task.FromResult(finalCallbackUri));\n        }\n\n        private Uri MockFinalCallbackUri(Uri redirectUri)\n        {\n            var finalUri = new Uri(rootCallbackUri, \"?state=\" + nonce + \"&code=\" + authorization_code);\n            // This is a simplification but consistent\n            browser.Setup(b => b.UpdateRedirectUri(redirectUri)).Returns(redirectUri);\n            return finalUri;\n        }\n\n        private string MockRemoteUri(string value)\n        {\n            settings.Setup(s => s.RemoteUri).Returns(new Uri(value));\n            return value;\n        }\n\n        private string MockClientIdOverride(string value)\n        {\n            settings.Setup(s => s.TryGetSetting(\n                DataCenterConstants.EnvironmentVariables.OAuthClientId,\n                Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientId,\n                out value)).Returns(true);\n            return value;\n        }\n\n        private string MockClientSecretOverride(string value)\n        {\n            settings.Setup(s => s.TryGetSetting(\n                DataCenterConstants.EnvironmentVariables.OAuthClientSecret,\n                Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret,\n                out value)).Returns(true);\n            return value;\n        }\n\n        private string MockRootCallbackUriOverride(string value)\n        {\n            settings.Setup(s => s.TryGetSetting(\n                DataCenterConstants.EnvironmentVariables.OAuthRedirectUri,\n                Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri,\n                out value)).Returns(true);\n            return value;\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.DataCenter;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.DataCenter\n{\n    public class BitbucketRestApiTest\n    {\n        [Fact]\n        public async Task BitbucketRestApi_GetUserInformationAsync_ReturnsUserInfo_ForSuccessfulRequest_DoesNothing()\n        {\n            var context = new TestCommandContext();\n\n            var expectedRequestUri = new Uri(\"http://example.com/rest/api/1.0/users\");\n            var httpHandler = new TestHttpMessageHandler();\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.OK);\n            httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request =>\n            {\n                return httpResponse;\n            });\n            context.HttpClientFactory.MessageHandler = httpHandler;\n\n            context.Settings.RemoteUri = new Uri(\"http://example.com\");\n\n            var api = new BitbucketRestApi(context);\n            var result = await api.GetUserInformationAsync(\"never used\", \"never used\", false);\n\n            Assert.NotNull(result);\n            Assert.Equal(DataCenterConstants.OAuthUserName, result.Response.UserName);\n\n            httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1);\n        }\n\n        [Theory]\n        [InlineData(HttpStatusCode.Unauthorized, true)]\n        [InlineData(HttpStatusCode.NotFound, false)]\n        public async Task BitbucketRestApi_IsOAuthInstalledAsync_ReflectsBitbucketAuthenticationResponse(HttpStatusCode responseCode, bool impliedSupport)\n        {\n            var context = new TestCommandContext();\n            var httpHandler = new TestHttpMessageHandler();\n\n            var expectedRequestUri = new Uri(\"http://example.com/rest/oauth2/1.0/client\");\n\n            var httpResponse = new HttpResponseMessage(responseCode);\n            httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request =>\n            {\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            context.Settings.RemoteUri = new Uri(\"http://example.com\");\n\n            var api = new BitbucketRestApi(context);\n\n            var isInstalled = await api.IsOAuthInstalledAsync();\n\n            httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1);\n\n            Assert.Equal(impliedSupport, isInstalled);\n        }\n\n        [Theory]\n        [MemberData(nameof(GetAuthenticationMethodsAsyncData))]\n        public async Task BitbucketRestApi_GetAuthenticationMethodsAsync_ReflectRestApiResponse(string loginOptionResponseJson, List<AuthenticationMethod> impliedSupportedMethods, List<AuthenticationMethod> impliedUnsupportedMethods)\n        {\n            var context = new TestCommandContext();\n            var httpHandler = new TestHttpMessageHandler();\n\n            var expectedRequestUri = new Uri(\"http://example.com/rest/authconfig/1.0/login-options\");\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new StringContent(loginOptionResponseJson)\n            };\n\n            httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request =>\n            {\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            context.Settings.RemoteUri = new Uri(\"http://example.com\");\n\n            var api = new BitbucketRestApi(context);\n\n            var authMethods = await api.GetAuthenticationMethodsAsync();\n\n            httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1);\n\n            Assert.NotNull(authMethods);\n            Assert.Equal(authMethods.Count, impliedSupportedMethods.Count);\n            Assert.Contains(authMethods, m => impliedSupportedMethods.Contains(m));\n            Assert.DoesNotContain(authMethods, m => impliedUnsupportedMethods.Contains(m));\n        }\n\n        public static IEnumerable<object[]> GetAuthenticationMethodsAsyncData =>\n        new List<object[]>\n        {\n            new object[] { $\"{{ \\\"results\\\":[ {{ \\\"type\\\":\\\"LOGIN_FORM\\\"}}]}}\",\n                            new List<AuthenticationMethod>{AuthenticationMethod.BasicAuth},\n                            new List<AuthenticationMethod>{AuthenticationMethod.Sso}},\n            new object[] { $\"{{ \\\"results\\\":[{{\\\"type\\\":\\\"IDP\\\"}}]}}\",\n                            new List<AuthenticationMethod>{AuthenticationMethod.Sso},\n                            new List<AuthenticationMethod>{AuthenticationMethod.BasicAuth}},\n            new object[] { $\"{{ \\\"results\\\":[{{\\\"type\\\":\\\"IDP\\\"}}, {{ \\\"type\\\":\\\"LOGIN_FORM\\\"}},  {{ \\\"type\\\":\\\"UNDEFINED\\\"}}]}}\",\n                            new List<AuthenticationMethod>{AuthenticationMethod.Sso, AuthenticationMethod.BasicAuth},\n                            new List<AuthenticationMethod>()},\n        };\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Atlassian.Bitbucket.DataCenter;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.DataCenter\n{\n    public class LoginOptionsTest\n    {\n\n        [Fact]\n        public void LoginOptions_Set()\n        {\n            var loginOption = new LoginOption() \n            { \n                Type = \"abc\", \n                Id = 1\n            };\n\n            var results = new List<LoginOption>() \n            { \n                loginOption\n            };\n\n            var loginOptions = new LoginOptions()\n            {\n                Results = results\n            };\n\n            Assert.NotNull(loginOptions.Results);\n            Assert.Contains(loginOption, loginOptions.Results);\n\n            Assert.Equal(\"abc\", loginOptions.Results.First().Type);\n            Assert.Equal(1, loginOptions.Results.First().Id);\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs",
    "content": "using System.Threading.Tasks;\nusing Atlassian.Bitbucket.DataCenter;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests.DataCenter\n{\n    public class UserInfoTest\n    {\n        [Fact]\n        public void UserInfo_Set()\n        {\n            var userInfo = new UserInfo()\n            {\n                UserName = \"123\"\n            };\n\n            Assert.Equal(\"123\", userInfo.UserName);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing Atlassian.Bitbucket.Cloud;\nusing Atlassian.Bitbucket.DataCenter;\nusing GitCredentialManager;\nusing Moq;\nusing Xunit;\n\nnamespace Atlassian.Bitbucket.Tests\n{\n    public class OAuth2ClientRegistryTest\n    {\n        private Mock<ICommandContext> context = new Mock<ICommandContext>(MockBehavior.Loose);\n        private Mock<ISettings> settings = new Mock<ISettings>(MockBehavior.Strict);\n        private Mock<IHttpClientFactory> httpClientFactory = new Mock<IHttpClientFactory>(MockBehavior.Strict);\n        private Mock<ITrace> trace = new Mock<ITrace>(MockBehavior.Strict);\n\n        [Fact]\n        public void BitbucketRestApiRegistry_Get_ReturnsCloudOAuth2Client()\n        {\n            var host = \"bitbucket.org\";\n\n            // Given\n            settings.Setup(s => s.RemoteUri).Returns(new System.Uri(\"https://\" + host));\n            context.Setup(c => c.Settings).Returns(settings.Object);\n            MockSettingOverride(CloudConstants.EnvironmentVariables.OAuthClientId, CloudConstants.GitConfiguration.Credential.OAuthClientId, \"never used\", false);\n            MockSettingOverride(CloudConstants.EnvironmentVariables.OAuthClientSecret, CloudConstants.GitConfiguration.Credential.OAuthClientSecret, \"never used\", false);\n            MockSettingOverride(CloudConstants.EnvironmentVariables.OAuthRedirectUri, CloudConstants.GitConfiguration.Credential.OAuthRedirectUri,  \"never used\", false);\n            MockHttpClientFactory();\n            var input = MockInputArguments(host);\n\n            // When\n            var registry = new OAuth2ClientRegistry(context.Object);\n            var api = registry.Get(input);\n\n            // Then\n            Assert.NotNull(api);\n            Assert.IsType<Atlassian.Bitbucket.Cloud.BitbucketOAuth2Client>(api);\n\n        }\n\n        [Fact]\n        public void BitbucketRestApiRegistry_Get_ReturnsDataCenterOAuth2Client_ForBitbucketDC()\n        {\n            var host = \"example.com\";\n\n            // Given\n            settings.Setup(s => s.RemoteUri).Returns(new System.Uri(\"https://example.com\"));\n            context.Setup(c => c.Settings).Returns(settings.Object);\n            MockSettingOverride(DataCenterConstants.EnvironmentVariables.OAuthClientId, DataCenterConstants.GitConfiguration.Credential.OAuthClientId, \"\", true);\n            MockSettingOverride(DataCenterConstants.EnvironmentVariables.OAuthClientSecret, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret, \"\", true); ;\n            MockSettingOverride(DataCenterConstants.EnvironmentVariables.OAuthRedirectUri, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri,  \"never used\", false);\n            MockHttpClientFactory();\n            var input = MockInputArguments(host);\n\n            // When\n            var registry = new OAuth2ClientRegistry(context.Object);\n            var api = registry.Get(input);\n\n            // Then\n            Assert.NotNull(api);\n            Assert.IsType<Atlassian.Bitbucket.DataCenter.BitbucketOAuth2Client>(api);\n\n        }\n\n        private static InputArguments MockInputArguments(string host)\n        {\n            return new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = host,\n            });\n        }\n\n        private void MockHttpClientFactory()\n        {\n            context.Setup(c => c.HttpClientFactory).Returns(httpClientFactory.Object);\n            httpClientFactory.Setup(f => f.CreateClient()).Returns(new HttpClient());\n        }\n\n        private string MockSettingOverride(string envKey, string configKey, string settingValue, bool isOverridden)\n        {\n            settings.Setup(s => s.TryGetSetting(\n                envKey,\n                Constants.GitConfiguration.Credential.SectionName, configKey,\n                out settingValue)).Returns(isOverridden);\n            return settingValue;\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Core/Application.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.CommandLine.Builder;\nusing System.CommandLine.Invocation;\nusing System.CommandLine.Parsing;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing GitCredentialManager.Diagnostics;\nusing GitCredentialManager.Interop;\n\nnamespace GitCredentialManager\n{\n    public class Application : ApplicationBase, IConfigurableComponent\n    {\n        private readonly IHostProviderRegistry _providerRegistry;\n        private readonly IConfigurationService _configurationService;\n        private readonly IList<ProviderCommand> _providerCommands = new List<ProviderCommand>();\n        private readonly List<IDiagnostic> _diagnostics = new List<IDiagnostic>();\n\n        public Application(ICommandContext context)\n            : this(context, new HostProviderRegistry(context), new ConfigurationService(context))\n        {\n        }\n\n        internal Application(ICommandContext context,\n                             IHostProviderRegistry providerRegistry,\n                             IConfigurationService configurationService)\n            : base(context)\n        {\n            EnsureArgument.NotNull(providerRegistry, nameof(providerRegistry));\n            EnsureArgument.NotNull(configurationService, nameof(configurationService));\n\n            _providerRegistry = providerRegistry;\n            _configurationService = configurationService;\n\n            _configurationService.AddComponent(this);\n        }\n\n        public void RegisterProvider(IHostProvider provider, HostProviderPriority priority)\n        {\n            _providerRegistry.Register(provider, priority);\n\n            // If the provider is also a configurable component, add that to the configuration service\n            if (provider is IConfigurableComponent configurableProvider)\n            {\n                _configurationService.AddComponent(configurableProvider);\n            }\n\n            // If the provider has custom commands to offer then create them here\n            if (provider is ICommandProvider cmdProvider)\n            {\n                ProviderCommand providerCommand = cmdProvider.CreateCommand();\n                _providerCommands.Add(providerCommand);\n            }\n\n            // If the provider exposes custom diagnostics use them\n            if (provider is IDiagnosticProvider diagnosticProvider)\n            {\n                IEnumerable<IDiagnostic> providerDiagnostics = diagnosticProvider.GetDiagnostics();\n                _diagnostics.AddRange(providerDiagnostics);\n            }\n        }\n\n        protected override async Task<int> RunInternalAsync(string[] args)\n        {\n            var rootCommand = new RootCommand();\n            var diagnoseCommand = new DiagnoseCommand(Context);\n\n            // Add common options\n            var noGuiOption = new Option<bool>(\"--no-ui\", \"Do not use graphical user interface prompts\");\n            rootCommand.AddGlobalOption(noGuiOption);\n\n            void NoGuiOptionHandler(InvocationContext context)\n            {\n                if (context.ParseResult.HasOption(noGuiOption))\n                {\n                    Context.Settings.IsGuiPromptsEnabled = false;\n                }\n            }\n\n            // Add standard commands\n            rootCommand.AddCommand(new GetCommand(Context, _providerRegistry));\n            rootCommand.AddCommand(new StoreCommand(Context, _providerRegistry));\n            rootCommand.AddCommand(new EraseCommand(Context, _providerRegistry));\n            rootCommand.AddCommand(new ConfigureCommand(Context, _configurationService));\n            rootCommand.AddCommand(new UnconfigureCommand(Context, _configurationService));\n            rootCommand.AddCommand(diagnoseCommand);\n\n            // Add any custom provider commands\n            foreach (ProviderCommand providerCommand in _providerCommands)\n            {\n                rootCommand.AddCommand(providerCommand);\n            }\n\n            // Add any custom provider diagnostic tests\n            foreach (IDiagnostic providerDiagnostic in _diagnostics)\n            {\n                diagnoseCommand.AddDiagnostic(providerDiagnostic);\n            }\n\n            // Trace the current version, OS, runtime, and program arguments\n            PlatformInformation info = PlatformUtils.GetPlatformInformation(Context.Trace2);\n            Context.Trace.WriteLine($\"Version: {Constants.GcmVersion}\");\n            Context.Trace.WriteLine($\"Runtime: {info.ClrVersion}\");\n            Context.Trace.WriteLine($\"Platform: {info.OperatingSystemType} ({info.CpuArchitecture})\");\n            Context.Trace.WriteLine($\"OSVersion: {info.OperatingSystemVersion}\");\n            Context.Trace.WriteLine($\"AppPath: {Context.ApplicationPath}\");\n            Context.Trace.WriteLine($\"InstallDir: {Context.InstallationDirectory}\");\n            Context.Trace.WriteLine($\"Arguments: {string.Join(\" \", args)}\");\n\n            var parser = new CommandLineBuilder(rootCommand)\n                .UseDefaults()\n                .UseExceptionHandler(OnException)\n                .AddMiddleware(NoGuiOptionHandler)\n                .Build();\n\n            return await parser.InvokeAsync(args);\n        }\n\n        protected override void Dispose(bool disposing)\n        {\n            if (disposing)\n            {\n                _providerRegistry?.Dispose();\n            }\n\n            base.Dispose(disposing);\n        }\n\n        private void OnException(Exception ex, InvocationContext invocationContext)\n        {\n            if (ex is AggregateException aex)\n            {\n                aex.Handle(WriteException);\n            }\n            else\n            {\n                WriteException(ex);\n            }\n\n            invocationContext.ExitCode = -1;\n        }\n\n        private bool WriteException(Exception ex)\n        {\n            // Try and use a nicer format for some well-known exception types\n            switch (ex)\n            {\n                case GitException gitEx:\n                    Context.Streams.Error.WriteLine(\"fatal: {0} [{1}]\", gitEx.Message, gitEx.ExitCode);\n                    Context.Streams.Error.WriteLine(gitEx.GitErrorMessage);\n                    break;\n                case InteropException interopEx:\n                    Context.Streams.Error.WriteLine(\"fatal: {0} [0x{1:x}]\", interopEx.Message, interopEx.ErrorCode);\n                    break;\n                default:\n                    Context.Streams.Error.WriteLine(\"fatal: {0}\", ex.Message);\n                    break;\n            }\n\n            // If tracing is enabled then also print the stack trace to stderr\n            bool printStack = Context.Settings.GetTracingEnabled(out _);\n            if (printStack)\n                Context.Streams.Error.WriteLine(ex.StackTrace);\n\n            // Recurse to print all inner exceptions\n            if (!(ex.InnerException is null))\n            {\n                WriteException(ex.InnerException);\n            }\n\n            return true;\n        }\n\n        #region IConfigurableComponent\n\n        string IConfigurableComponent.Name => \"Git Credential Manager\";\n\n        Task IConfigurableComponent.ConfigureAsync(ConfigurationTarget target)\n        {\n            string helperKey = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\n            string appPath = GetGitConfigAppPath();\n\n            GitConfigurationLevel configLevel = target == ConfigurationTarget.System\n                    ? GitConfigurationLevel.System\n                    : GitConfigurationLevel.Global;\n\n            Context.Trace.WriteLine($\"Configuring for config level '{configLevel}'.\");\n\n            IGitConfiguration config = Context.Git.GetConfiguration();\n\n            // We are looking for the following to be set in the config:\n            //\n            // [credential]\n            //     ...                # any number of helper entries (possibly none)\n            //     helper =           # an empty value to reset/clear any previous entries (if applicable)\n            //     helper = {appPath} # the expected executable value & directly following the empty value\n            //     ...                # any number of helper entries (possibly none, but not the empty value '')\n            //\n            string[] currentValues = config.GetAll(configLevel, GitConfigurationType.Raw, helperKey).ToArray();\n\n            // Try to locate an existing app entry with a blank reset/clear entry immediately preceding,\n            // and no other blank empty/clear entries following (which effectively disable us).\n            int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath));\n            int lastEmptyIndex = Array.FindLastIndex(currentValues, string.IsNullOrWhiteSpace);\n            if (appIndex > 0 && string.IsNullOrWhiteSpace(currentValues[appIndex - 1]) && lastEmptyIndex < appIndex)\n            {\n                Context.Trace.WriteLine(\"Credential helper configuration is already set correctly.\");\n            }\n            else\n            {\n                Context.Trace.WriteLine(\"Updating Git credential helper configuration...\");\n\n                // Clear any existing app entries in the configuration\n                config.UnsetAll(configLevel, helperKey, Regex.Escape(appPath));\n\n                // Add an empty value for `credential.helper`, which has the effect of clearing any helper value\n                // from any lower-level Git configuration, then add a second value which is the actual executable path.\n                config.Add(configLevel, helperKey, string.Empty);\n                config.Add(configLevel, helperKey, appPath);\n            }\n\n            return Task.CompletedTask;\n        }\n\n        Task IConfigurableComponent.UnconfigureAsync(ConfigurationTarget target)\n        {\n            string helperKey = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\n            string appPath = GetGitConfigAppPath();\n\n            GitConfigurationLevel configLevel = target == ConfigurationTarget.System\n                    ? GitConfigurationLevel.System\n                    : GitConfigurationLevel.Global;\n\n            Context.Trace.WriteLine($\"Unconfiguring for config level '{configLevel}'.\");\n\n            IGitConfiguration config = Context.Git.GetConfiguration();\n\n            // We are looking for the following to be set in the config:\n            //\n            // [credential]\n            //     ...                 # any number of helper entries (possibly none)\n            //     helper =            # an empty value to reset/clear any previous entries (if applicable)\n            //     helper = {appPath} # the expected executable value & directly following the empty value\n            //     ...                 # any number of helper entries (possibly none)\n            //\n            // We should remove the {appPath} entry, and any blank entries immediately preceding IFF there are no more entries following.\n            //\n            Context.Trace.WriteLine(\"Removing Git credential helper configuration...\");\n\n            string[] currentValues = config.GetAll(configLevel, GitConfigurationType.Raw, helperKey).ToArray();\n\n            int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath));\n            if (appIndex > -1)\n            {\n                // Check for the presence of a blank entry immediately preceding an app entry in the last position\n                if (appIndex > 0 && appIndex == currentValues.Length - 1 &&\n                    string.IsNullOrWhiteSpace(currentValues[appIndex - 1]))\n                {\n                    // Clear the blank entry\n                    config.UnsetAll(configLevel, helperKey, Constants.RegexPatterns.Empty);\n                }\n\n                // Clear app entry\n                string appEntryValue = currentValues[appIndex];\n                config.UnsetAll(configLevel, helperKey, Regex.Escape(appEntryValue));\n            }\n\n            return Task.CompletedTask;\n        }\n\n        private string GetGitConfigAppPath()\n        {\n            string path = Context.ApplicationPath;\n\n            // On Windows we must use UNIX style path separators\n            if (PlatformUtils.IsWindows())\n            {\n                path = path.Replace('\\\\', '/');\n            }\n\n            // We must escape escape characters like ' ', '(', and ')'\n            return path\n                .Replace(\" \", \"\\\\ \")\n                .Replace(\"(\", \"\\\\(\")\n                .Replace(\")\", \"\\\\)\");;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ApplicationBase.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI;\n\nnamespace GitCredentialManager\n{\n    public abstract class ApplicationBase : IDisposable\n    {\n        private static readonly Encoding Utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);\n\n        private TextWriter _traceFileWriter;\n\n        protected ICommandContext Context { get; }\n\n        protected ApplicationBase(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            Context = context;\n        }\n\n        public Task<int> RunAsync(string[] args)\n        {\n            // Launch debugger\n            if (Context.Settings.IsDebuggingEnabled)\n            {\n                Context.Streams.Error.WriteLine(\"Waiting for debugger to be attached...\");\n                WaitForDebuggerAttached();\n\n                // Now the debugger is attached, break!\n                Debugger.Break();\n            }\n\n            // Add the debug tracer if the debugger is attached\n            if (Debugger.IsAttached)\n            {\n                Context.Trace.AddListener(new DebugTraceWriter());\n            }\n\n            // Enable tracing\n            if (Context.Settings.GetTracingEnabled(out string traceValue))\n            {\n                if (traceValue.IsTruthy()) // Trace to stderr\n                {\n                    Context.Trace.AddListener(Context.Streams.Error);\n                }\n                else if (Path.IsPathRooted(traceValue)) // Trace to a file\n                {\n                    try\n                    {\n                        Stream stream = Context.FileSystem.OpenFileStream(traceValue, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);\n                        _traceFileWriter = new StreamWriter(stream, Utf8NoBomEncoding, 4096, leaveOpen: false);\n\n                        Context.Trace.AddListener(_traceFileWriter);\n                    }\n                    catch (Exception ex)\n                    {\n                        Context.Streams.Error.WriteLine($\"warning: unable to trace to file '{traceValue}': {ex.Message}\");\n                    }\n                }\n                else\n                {\n                    Context.Streams.Error.WriteLine($\"warning: unknown value for {Constants.EnvironmentVariables.GcmTrace} '{traceValue}'\");\n                }\n            }\n\n            // Enable sensitive tracing and show warning\n            if (Context.Settings.IsSecretTracingEnabled)\n            {\n                Context.Trace.IsSecretTracingEnabled = true;\n                Context.Trace.WriteLine(\"Tracing of secrets is enabled. Trace output may contain sensitive information.\");\n            }\n\n            // Set software rendering if defined in settings\n            if (Context.Settings.UseSoftwareRendering)\n            {\n                AvaloniaUi.Initialize(win32SoftwareRendering: true);\n            }\n\n            return RunInternalAsync(args);\n        }\n\n        protected abstract Task<int> RunInternalAsync(string[] args);\n\n        #region Helpers\n\n        /// <summary>\n        /// Wait until a debugger has attached to the currently executing process.\n        /// </summary>\n        private static void WaitForDebuggerAttached()\n        {\n            // Attempt to launch the debugger if the OS supports the explicit launching\n            if (!Debugger.Launch())\n            {\n                // The prompt to debug was declined\n                return;\n            }\n\n            // Some platforms do not support explicit debugger launching..\n            // Wait for the debugger to attach - poll & sleep until then\n            while (!Debugger.IsAttached)\n            {\n                Thread.Sleep(TimeSpan.FromSeconds(1));\n            }\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (disposing)\n            {\n                _traceFileWriter?.Dispose();\n            }\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/AssemblyUtils.cs",
    "content": "using System.Reflection;\n\nnamespace GitCredentialManager;\n\npublic static class AssemblyUtils\n{\n    public static bool TryGetAssemblyVersion(out string version)\n    {\n        try\n        {\n            var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();\n            var assemblyVersionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();\n            version = assemblyVersionAttribute is null\n                ? assembly.GetName().Version.ToString()\n                : assemblyVersionAttribute.InformationalVersion;\n            return true;\n        }\n        catch\n        {\n            version = null;\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/AuthenticationBase.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.Authentication\n{\n    public abstract class AuthenticationBase\n    {\n        protected readonly ICommandContext Context;\n\n        protected AuthenticationBase(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            Context = context;\n        }\n\n        protected Task<IDictionary<string, string>> InvokeHelperAsync(string path, string args,\n            StreamReader standardInput = null)\n        {\n            return InvokeHelperAsync(path, args, standardInput, CancellationToken.None);\n        }\n\n        protected internal virtual async Task<IDictionary<string, string>> InvokeHelperAsync(string path, string args,\n            StreamReader standardInput, CancellationToken ct)\n        {\n            var procStartInfo = new ProcessStartInfo(path)\n            {\n                Arguments = args,\n                RedirectStandardInput = true,\n                RedirectStandardOutput = true,\n                RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled\n                UseShellExecute = false,\n                StandardOutputEncoding = EncodingEx.UTF8NoBom,\n            };\n\n            Context.Trace.WriteLine($\"Starting helper process: {path} {args}\");\n\n            // We flush the trace writers here so that the we don't stomp over the\n            // authentication helper's messages.\n            Context.Trace.Flush();\n\n            var process = ChildProcess.Start(Context.Trace2, procStartInfo, Trace2ProcessClass.UIHelper);\n            if (process is null)\n            {\n                var format = \"Failed to start helper process: {0} {1}\";\n                var message = string.Format(format, path, args);\n\n                throw new Trace2Exception(Context.Trace2, message, format);\n            }\n\n            // Kill the process upon a cancellation request\n            ct.Register(() => process.Kill());\n\n            // Write the standard input to the process if we have any to write\n            if (standardInput is not null)\n            {\n#if NETFRAMEWORK\n                await standardInput.BaseStream.CopyToAsync(process.StandardInput.BaseStream);\n#else\n                await standardInput.BaseStream.CopyToAsync(process.StandardInput.BaseStream, ct);\n#endif\n                process.StandardInput.Close();\n            }\n\n            IDictionary<string, string> resultDict = await process.StandardOutput.ReadDictionaryAsync(StringComparer.OrdinalIgnoreCase);\n\n            await Task.Run(() => process.WaitForExit(), ct);\n            int exitCode = process.ExitCode;\n\n            if (exitCode != 0)\n            {\n                if (!resultDict.TryGetValue(\"error\", out string errorMessage))\n                {\n                    errorMessage = \"Unknown\";\n                }\n\n                throw new Trace2Exception(Context.Trace2, $\"helper error ({exitCode}): {errorMessage}\");\n            }\n\n            return resultDict;\n        }\n\n        protected void ThrowIfUserInteractionDisabled()\n        {\n            if (!Context.Settings.IsInteractionAllowed)\n            {\n                string envName = Constants.EnvironmentVariables.GcmInteractive;\n                string cfgName = string.Format(\"{0}.{1}\",\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.Interactive);\n\n                Context.Trace.WriteLine($\"{envName} / {cfgName} is false/never; user interactivity has been disabled.\");\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Cannot prompt because user interactivity has been disabled.\");\n            }\n        }\n\n        protected void ThrowIfGuiPromptsDisabled()\n        {\n            if (!Context.Settings.IsGuiPromptsEnabled)\n            {\n                Context.Trace.WriteLine($\"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; GUI prompts have been disabled.\");\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Cannot show prompt because GUI prompts have been disabled.\");\n            }\n        }\n\n        protected void ThrowIfTerminalPromptsDisabled()\n        {\n            if (!Context.Settings.IsTerminalPromptsEnabled)\n            {\n                Context.Trace.WriteLine($\"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled.\");\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Cannot prompt because terminal prompts have been disabled.\");\n            }\n        }\n        \n        protected void ThrowIfWindowCancelled(WindowViewModel viewModel)\n        {\n            if (!viewModel.WindowResult)\n            {\n                throw new Exception(\"User cancelled dialog.\");\n            }\n        }\n        \n        protected IntPtr GetParentWindowHandle()\n        {\n            if (int.TryParse(Context.Settings.ParentWindowId, out int id))\n            {\n                return new IntPtr(id);\n            }\n\n            return IntPtr.Zero;\n        }\n\n        protected bool TryFindHelperCommand(string envar, string configName, string defaultValue, out string command, out string args)\n        {\n            command = null;\n            args = null;\n\n            //\n            // Search for UI helpers with the following precedence and logic..\n            //\n            // 1. (unset): use the default helper name that's in the source code and go to #3\n            // 2. <absolute>: use the absolute path only and exactly as entered\n            // 3. <relative>: search for..\n            //     a. <appdir>/<relative>(.exe) - run this directly\n            //     b. <appdir>/<relative>(.dll) - use `dotnet exec` to run\n            //     c. Search PATH for <relative>(.exe) - run this directly\n            //        NOTE: do NOT search PATH for <relative>(.dll) as we don't know if this is a dotnet executable..\n            //\n            // We print warning messages for missing helpers specified by the user, not the in-box ones.\n            //\n            if (Context.Settings.TryGetPathSetting(\n                   envar, Constants.GitConfiguration.Credential.SectionName, configName, out string helperName))\n            {\n                // If the user set the helper override to the empty string then they are signalling not to use a helper\n                if (string.IsNullOrEmpty(helperName))\n                {\n                    Context.Trace.WriteLine(\"UI helper override specified as the empty string.\");\n                    return false;\n                }\n\n                Context.Trace.WriteLine($\"UI helper override specified: '{helperName}'.\");\n            }\n            else if (string.IsNullOrWhiteSpace(defaultValue))\n            {\n                Context.Trace.WriteLine(\"No default UI supplied.\");\n                return false;\n            }\n            else\n            {\n                // Whilst we evaluate using the Avalonia/in-proc GUIs on Windows we include\n                // a 'fallback' flag that lets us continue to use the WPF out-of-proc helpers.\n                if (PlatformUtils.IsWindows() &&\n                    Context.Settings.TryGetSetting(\n                        Constants.EnvironmentVariables.GcmDevUseLegacyUiHelpers,\n                        Constants.GitConfiguration.Credential.SectionName,\n                        Constants.GitConfiguration.Credential.DevUseLegacyUiHelpers,\n                        out string str) && str.IsTruthy())\n                {\n                    Context.Trace.WriteLine($\"Using default legacy UI helper: '{defaultValue}'.\");\n                    helperName = defaultValue;\n                }\n                else\n                {\n                    return false;\n                }\n            }\n\n            //\n            // Check for an absolute path.. run this directly without intermediaries or modification\n            //\n            if (Path.IsPathRooted(helperName))\n            {\n                if (Context.FileSystem.FileExists(helperName))\n                {\n                    Context.Trace.WriteLine($\"UI helper found at '{helperName}'.\");\n                    command = helperName;\n                    return true;\n                }\n\n                Context.Trace.WriteLine($\"UI helper was not found at '{helperName}'.\");\n                Context.Streams.Error.WriteLine($\"warning: could not find configured UI helper '{helperName}'\");\n                return false;\n            }\n\n            //\n            // Search the installation directory for an in-box helper\n            //\n            string appDir = Context.InstallationDirectory;\n            string inBoxExePath = Path.Combine(appDir, PlatformUtils.IsWindows() ? $\"{helperName}.exe\" : helperName);\n            string inBoxDllPath = Path.Combine(appDir, $\"{helperName}.dll\");\n\n            // Look for in-box native executables\n            if (Context.FileSystem.FileExists(inBoxExePath))\n            {\n                Context.Trace.WriteLine($\"Found in-box native UI helper: '{inBoxExePath}'\");\n                command = inBoxExePath;\n                return true;\n            }\n\n            // Look for in-box .NET framework-dependent executables\n            if (Context.FileSystem.FileExists(inBoxDllPath))\n            {\n                string dotnetName = PlatformUtils.IsWindows() ? \"dotnet.exe\" : \"dotnet\";\n                if (!Context.Environment.TryLocateExecutable(dotnetName, out string dotnetPath))\n                {\n                    Context.Trace.WriteLine($\"Unable to run UI helper '{inBoxDllPath}' without the .NET CLI.\");\n                    Context.Streams.Error.WriteLine($\"warning: could not find .NET CLI to run UI helper '{inBoxDllPath}'\");\n                    return false;\n                }\n\n                Context.Trace.WriteLine($\"Found in-box framework-dependent UI helper: '{inBoxDllPath}'\");\n                command = dotnetPath;\n                args = $\"exec {QuoteCmdArg(inBoxDllPath)} \";\n                return true;\n            }\n\n            //\n            // Search the PATH for a native executable (do NOT search for out-of-box .NET framework-dependent DLLs)\n            //\n            if (Context.Environment.TryLocateExecutable(helperName, out command))\n            {\n                Context.Trace.WriteLine($\"Found UI helper on PATH: '{helperName}'\");\n                return true;\n            }\n\n            //\n            // No helper found!\n            //\n            Context.Trace.WriteLine($\"UI helper '{helperName}' was not found.\");\n            Context.Streams.Error.WriteLine($\"warning: could not find UI helper '{helperName}'\");\n            return false;\n        }\n\n        public static string QuoteCmdArg(string str)\n        {\n            char[] needsQuoteChars = { '\"', ' ', '\\\\', '\\n', '\\r', '\\t' };\n            bool needsQuotes = str.Any(x => needsQuoteChars.Contains(x));\n\n            if (!needsQuotes)\n            {\n                return str;\n            }\n\n            // Replace all '\\' characters with an escaped '\\\\', and all '\"' with '\\\"'\n            string escapedStr = str.Replace(\"\\\\\", \"\\\\\\\\\").Replace(\"\\\"\", \"\\\\\\\"\");\n\n            // Bookend the escaped string with double-quotes '\"'\n            return $\"\\\"{escapedStr}\\\"\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/BasicAuthentication.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\nusing GitCredentialManager.UI.Views;\n\nnamespace GitCredentialManager.Authentication\n{\n    public interface IBasicAuthentication\n    {\n        Task<ICredential> GetCredentialsAsync(string resource, string userName);\n    }\n\n    public static class BasicAuthenticationExtensions\n    {\n        public static Task<ICredential> GetCredentialsAsync(this IBasicAuthentication basicAuth, string resource)\n        {\n            return basicAuth.GetCredentialsAsync(resource, null);\n        }\n    }\n\n    public class BasicAuthentication : AuthenticationBase, IBasicAuthentication\n    {\n        public static readonly string[] AuthorityIds =\n        {\n            \"basic\",\n        };\n\n        public BasicAuthentication(ICommandContext context)\n            : base (context) { }\n\n        public async Task<ICredential> GetCredentialsAsync(string resource, string userName)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));\n\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string command, out string args))\n                {\n                    return await GetCredentialsViaHelperAsync(command, args, resource, userName);\n                }\n\n                return await GetCredentialsViaUiAsync(resource, userName);\n            }\n\n            ThrowIfTerminalPromptsDisabled();\n\n            return GetCredentialsViaTty(resource, userName);\n        }\n\n        private async Task<ICredential> GetCredentialsViaUiAsync(string resource, string userName)\n        {\n            var viewModel = new CredentialsViewModel\n            {\n                Description = !string.IsNullOrWhiteSpace(resource)\n                    ? $\"Enter your credentials for '{resource}'\"\n                    : \"Enter your credentials\",\n                UserName = string.IsNullOrWhiteSpace(userName) ? null : userName,\n            };\n\n            await AvaloniaUi.ShowViewAsync<CredentialsView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n            ThrowIfWindowCancelled(viewModel);\n\n            return new GitCredential(viewModel.UserName, viewModel.Password);\n        }\n\n        private ICredential GetCredentialsViaTty(string resource, string userName)\n        {\n            Context.Terminal.WriteLine(\"Enter basic credentials for '{0}':\", resource);\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                // Don't need to prompt for the username if it has been specified already\n                Context.Terminal.WriteLine(\"Username: {0}\", userName);\n            }\n            else\n            {\n                // Prompt for username\n                userName = Context.Terminal.Prompt(\"Username\");\n            }\n\n            // Prompt for password\n            string password = Context.Terminal.PromptSecret(\"Password\");\n\n            return new GitCredential(userName, password);\n        }\n\n        private async Task<ICredential> GetCredentialsViaHelperAsync(string command, string args, string resource, string userName)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"basic\");\n\n            if (!string.IsNullOrWhiteSpace(resource))\n            {\n                promptArgs.AppendFormat(\" --resource {0}\", QuoteCmdArg(resource));\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                promptArgs.AppendFormat(\" --username {0}\", QuoteCmdArg(userName));\n            }\n\n            IDictionary<string, string> resultDict = await InvokeHelperAsync(command, promptArgs.ToString(), null);\n\n            if (!resultDict.TryGetValue(\"username\", out userName))\n            {\n                throw new Trace2Exception(Context.Trace2, \"Missing 'username' in response\");\n            }\n\n            if (!resultDict.TryGetValue(\"password\", out string password))\n            {\n                throw new Trace2Exception(Context.Trace2, \"Missing 'password' in response\");\n            }\n\n            return new GitCredential(userName, password);\n        }\n\n        private bool TryFindHelperCommand(out string command, out string args)\n        {\n            return TryFindHelperCommand(\n                Constants.EnvironmentVariables.GcmUiHelper,\n                Constants.GitConfiguration.Credential.UiHelper,\n                Constants.DefaultUiHelper,\n                out command,\n                out args);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/MicrosoftAuthentication.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Interop.Windows.Native;\nusing Microsoft.Identity.Client;\nusing Microsoft.Identity.Client.Extensions.Msal;\nusing System.Text;\nusing System.Threading;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.Controls;\nusing GitCredentialManager.UI.ViewModels;\nusing GitCredentialManager.UI.Views;\nusing Microsoft.Identity.Client.AppConfig;\n\n#if NETFRAMEWORK\nusing Microsoft.Identity.Client.Broker;\n#endif\n\nnamespace GitCredentialManager.Authentication\n{\n    public interface IMicrosoftAuthentication\n    {\n        /// <summary>\n        /// Acquire an access token for a user principal.\n        /// </summary>\n        /// <param name=\"authority\">Azure authority.</param>\n        /// <param name=\"clientId\">Client ID.</param>\n        /// <param name=\"redirectUri\">Redirect URI for the client.</param>\n        /// <param name=\"scopes\">Set of scopes to request.</param>\n        /// <param name=\"userName\">Optional user name for an existing account.</param>\n        /// <param name=\"msaPt\">Use MSA-Passthrough behavior when authenticating.</param>\n        /// <returns>Authentication result.</returns>\n        Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, string clientId, Uri redirectUri,\n            string[] scopes, string userName, bool msaPt = false);\n\n        /// <summary>\n        /// Acquire an access token for the given service principal with the specified scopes.\n        /// </summary>\n        /// <param name=\"sp\">Service principal identity.</param>\n        /// <param name=\"scopes\">Scopes to request.</param>\n        /// <returns>Authentication result.</returns>\n        Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes);\n\n        /// <summary>\n        /// Acquire a token using the managed identity in the current environment.\n        /// </summary>\n        /// <param name=\"managedIdentity\">Managed identity to use.</param>\n        /// <param name=\"resource\">Resource to obtain an access token for.</param>\n        /// <returns>Authentication result including access token.</returns>\n        /// <remarks>\n        /// There are several formats for the <paramref name=\"managedIdentity\"/> parameter:\n        /// <para/>\n        ///  - <c>\"system\"</c> - Use the system-assigned managed identity.\n        /// <para/>\n        ///  - <c>\"{guid}\"</c> - Use the user-assigned managed identity with client ID <c>{guid}</c>.\n        /// <para/>\n        ///  - <c>\"id://{guid}\"</c> - Use the user-assigned managed identity with client ID <c>{guid}</c>.\n        /// <para/>\n        ///  - <c>\"resource://{guid}\"</c> - Use the user-assigned managed identity with resource ID <c>{guid}</c>.\n        /// </remarks>\n        Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsync(string managedIdentity, string resource);\n    }\n\n    public class ServicePrincipalIdentity\n    {\n        /// <summary>\n        /// Client ID of the service principal.\n        /// </summary>\n        public string Id { get; set; }\n\n        /// <summary>\n        /// Tenant ID of the service principal.\n        /// </summary>\n        public string TenantId { get; set; }\n\n        /// <summary>\n        /// Certificate used to authenticate the service principal.\n        /// </summary>\n        /// <remarks>\n        /// If both <see cref=\"Certificate\"/> and <see cref=\"ClientSecret\"/> are set, the certificate will be used.\n        /// </remarks>\n        public X509Certificate2 Certificate { get; set; }\n\n        /// <summary>\n        /// Secret used to authenticate the service principal.\n        /// </summary>\n        /// <remarks>\n        /// If both <see cref=\"Certificate\"/> and <see cref=\"ClientSecret\"/> are set, the certificate will be used.\n        /// </remarks>\n        public string ClientSecret { get; set; }\n\n        /// <summary>\n        /// Whether the authentication should send X5C\n        /// </summary>\n        public bool SendX5C { get; set; }\n    }\n\n    public interface IMicrosoftAuthenticationResult\n    {\n        string AccessToken { get; }\n        string AccountUpn { get; }\n    }\n\n    public enum MicrosoftAuthenticationFlowType\n    {\n        Auto = 0,\n        EmbeddedWebView = 1,\n        SystemWebView = 2,\n        DeviceCode = 3\n    }\n\n    public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthentication\n    {\n        public static readonly string[] AuthorityIds =\n        {\n            \"msa\",  \"microsoft\",   \"microsoftaccount\",\n            \"aad\",  \"azure\",       \"azuredirectory\",\n            \"live\", \"liveconnect\", \"liveid\",\n        };\n\n        public MicrosoftAuthentication(ICommandContext context)\n            : base(context) { }\n\n        #region IMicrosoftAuthentication\n\n        public async Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(\n            string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt)\n        {\n            var uiCts = new CancellationTokenSource();\n\n            // Check if we can and should use OS broker authentication\n            bool useBroker = CanUseBroker();\n            Context.Trace.WriteLine(useBroker\n                ? \"OS broker is available and enabled.\"\n                : \"OS broker is not available or enabled.\");\n\n            if (msaPt)\n            {\n                Context.Trace.WriteLine(\"MSA passthrough is enabled.\");\n            }\n\n            try\n            {\n                // Create the public client application for authentication\n                IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker, msaPt, uiCts);\n\n                AuthenticationResult result = null;\n\n                // Try silent authentication first if we know about an existing user\n                bool hasExistingUser = !string.IsNullOrWhiteSpace(userName);\n                if (hasExistingUser)\n                {\n                    result = await GetAccessTokenSilentlyAsync(app, scopes, userName, msaPt);\n                }\n\n                //\n                // If we failed to acquire an AT silently (either because we don't have an existing user, or the user's\n                // RT has expired) we need to prompt the user for credentials.\n                //\n                // If the user has expressed a preference in how they want to perform the interactive authentication\n                // flows then we respect that. Otherwise, depending on the current platform and session type we try to\n                // show the most appropriate authentication interface:\n                //\n                // On Windows 10+ & .NET Framework, MSAL supports the Web Account Manager (WAM) broker - we try to use\n                // that if possible in the first instance.\n                //\n                // On .NET Framework MSAL supports the WinForms based 'embedded' webview UI. This experience is less\n                // jarring that the system webview flow so try that option next.\n                //\n                // On other runtimes (e.g., .NET 6+) MSAL only supports the system webview flow (launch the user's\n                // browser), and the device-code flows. The system webview flow requires that the redirect URI is a\n                // loopback address, and that we are in an interactive session.\n                //\n                // The device code flow has no limitations other than a way to communicate to the user the code required\n                // to authenticate.\n                //\n                if (result is null)\n                {\n                    // If the user has disabled interaction all we can do is fail at this point\n                    ThrowIfUserInteractionDisabled();\n\n                    // If we're using the OS broker then delegate everything to that\n                    if (useBroker)\n                    {\n                        // If the user has enabled the default account feature then we can try to acquire an access\n                        // token 'silently' without knowing the user's UPN. Whilst this could be done truly silently,\n                        // we still prompt the user to confirm this action because if the OS account is the incorrect\n                        // account then the user may become stuck in a loop of authentication failures.\n                        if (!hasExistingUser && Context.Settings.UseMsAuthDefaultAccount)\n                        {\n                            result = await GetAccessTokenSilentlyAsync(app, scopes, null, msaPt);\n\n                            if (result is null || !await UseDefaultAccountAsync(result.Account.Username))\n                            {\n                                result = null;\n                            }\n                        }\n\n                        if (result is null)\n                        {\n                            Context.Trace.WriteLine(\"Performing interactive auth with broker...\");\n                            result = await app.AcquireTokenInteractive(scopes)\n                                .WithPrompt(Prompt.SelectAccount)\n                                // We must configure the system webview as a fallback\n                                .WithSystemWebViewOptions(GetSystemWebViewOptions())\n                                .ExecuteAsync();\n                        }\n                    }\n                    else\n                    {\n                        // Check for a user flow preference if they've specified one\n                        MicrosoftAuthenticationFlowType flowType = GetFlowType();\n                        switch (flowType)\n                        {\n                            case MicrosoftAuthenticationFlowType.Auto:\n                                if (CanUseEmbeddedWebView())\n                                    goto case MicrosoftAuthenticationFlowType.EmbeddedWebView;\n\n                                if (CanUseSystemWebView(app, redirectUri))\n                                    goto case MicrosoftAuthenticationFlowType.SystemWebView;\n\n                                // Fall back to device code flow\n                                goto case MicrosoftAuthenticationFlowType.DeviceCode;\n\n                            case MicrosoftAuthenticationFlowType.EmbeddedWebView:\n                                Context.Trace.WriteLine(\"Performing interactive auth with embedded web view...\");\n                                EnsureCanUseEmbeddedWebView();\n                                result = await app.AcquireTokenInteractive(scopes)\n                                    .WithPrompt(Prompt.SelectAccount)\n                                    .WithUseEmbeddedWebView(true)\n                                    .WithEmbeddedWebViewOptions(GetEmbeddedWebViewOptions())\n                                    .ExecuteAsync();\n                                break;\n\n                            case MicrosoftAuthenticationFlowType.SystemWebView:\n                                Context.Trace.WriteLine(\"Performing interactive auth with system web view...\");\n                                EnsureCanUseSystemWebView(app, redirectUri);\n                                result = await app.AcquireTokenInteractive(scopes)\n                                    .WithPrompt(Prompt.SelectAccount)\n                                    .WithSystemWebViewOptions(GetSystemWebViewOptions())\n                                    .ExecuteAsync();\n                                break;\n\n                            case MicrosoftAuthenticationFlowType.DeviceCode:\n                                Context.Trace.WriteLine(\"Performing interactive auth with device code...\");\n                                // We don't have a way to display a device code without a terminal at the moment\n                                // TODO: introduce a small GUI window to show a code if no TTY exists\n                                ThrowIfTerminalPromptsDisabled();\n                                result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();\n                                break;\n\n                            default:\n                                goto case MicrosoftAuthenticationFlowType.Auto;\n                        }\n                    }\n                }\n\n                return new MsalResult(result);\n            }\n            finally\n            {\n                // If we created some global UI (e.g. progress) during authentication we should dismiss them now that we're done\n                uiCts.Cancel();\n            }\n        }\n\n        public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes)\n        {\n            IConfidentialClientApplication app = await CreateConfidentialClientApplicationAsync(sp);\n\n            try\n            {\n                Context.Trace.WriteLine($\"Sending with X5C: '{sp.SendX5C}'.\");\n                AuthenticationResult result = await app.AcquireTokenForClient(scopes).WithSendX5C(sp.SendX5C).ExecuteAsync();;\n\n                return new MsalResult(result);\n            }\n            catch (Exception ex)\n            {\n                Context.Trace.WriteLine($\"Failed to acquire token for service principal '{sp.TenantId}/{sp.Id}'.\");\n                Context.Trace.WriteException(ex);\n                throw;\n            }\n        }\n\n        public async Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsync(string managedIdentity, string resource)\n        {\n            var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);\n\n            ManagedIdentityId mid = GetManagedIdentity(managedIdentity);\n\n            IManagedIdentityApplication app = ManagedIdentityApplicationBuilder.Create(mid)\n                .WithHttpClientFactory(httpFactoryAdaptor)\n                .Build();\n\n            try\n            {\n                AuthenticationResult result = await app.AcquireTokenForManagedIdentity(resource).ExecuteAsync();\n                return new MsalResult(result);\n            }\n            catch (Exception ex)\n            {\n                Context.Trace.WriteLine(mid == ManagedIdentityId.SystemAssigned\n                    ? \"Failed to acquire token for system managed identity.\"\n                    : $\"Failed to acquire token for user managed identity '{managedIdentity:D}'.\");\n                Context.Trace.WriteException(ex);\n                throw;\n            }\n        }\n\n        private async Task<bool> UseDefaultAccountAsync(string userName)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.SessionManager.IsDesktopSession && Context.Settings.IsGuiPromptsEnabled)\n            {\n                if (TryFindHelperCommand(out string command, out string args))\n                {\n                    var sb = new StringBuilder(args);\n                    sb.Append(\"default-account\");\n                    sb.AppendFormat(\" --username {0}\", QuoteCmdArg(userName));\n\n                    IDictionary<string, string> result = await InvokeHelperAsync(command, sb.ToString());\n\n                    if (result.TryGetValue(\"use_default_account\", out string str) && !string.IsNullOrWhiteSpace(str))\n                    {\n                        return str.ToBooleanyOrDefault(false);\n                    }\n                    else\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing use_default_account in response\");\n                    }\n                }\n\n                var viewModel = new DefaultAccountViewModel(Context.SessionManager)\n                {\n                    UserName = userName\n                };\n\n                await AvaloniaUi.ShowViewAsync<DefaultAccountView>(\n                    viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n                ThrowIfWindowCancelled(viewModel);\n\n                return viewModel.UseDefaultAccount;\n            }\n            else\n            {\n                string question = $\"Continue with current account ({userName})?\";\n\n                var menu = new TerminalMenu(Context.Terminal, question);\n                TerminalMenuItem yesItem = menu.Add(\"Yes\");\n                TerminalMenuItem noItem = menu.Add(\"No, use another account\");\n                TerminalMenuItem choice = menu.Show();\n\n                if (choice == yesItem)\n                    return true;\n\n                if (choice == noItem)\n                    return false;\n\n                throw new Exception();\n            }\n        }\n\n        internal MicrosoftAuthenticationFlowType GetFlowType()\n        {\n            if (Context.Settings.TryGetSetting(\n                Constants.EnvironmentVariables.MsAuthFlow,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.MsAuthFlow,\n                out string valueStr))\n            {\n                Context.Trace.WriteLine($\"Microsoft auth flow overriden to '{valueStr}'.\");\n                switch (valueStr.ToLowerInvariant())\n                {\n                    case \"auto\":\n                        return MicrosoftAuthenticationFlowType.Auto;\n                    case \"embedded\":\n                        return MicrosoftAuthenticationFlowType.EmbeddedWebView;\n                    case \"system\":\n                        return MicrosoftAuthenticationFlowType.SystemWebView;\n                    default:\n                        if (Enum.TryParse(valueStr, ignoreCase: true, out MicrosoftAuthenticationFlowType value))\n                            return value;\n                        break;\n                }\n\n                Context.Streams.Error.WriteLine($\"warning: unknown Microsoft Authentication flow type '{valueStr}'; using 'auto'\");\n            }\n\n            return MicrosoftAuthenticationFlowType.Auto;\n        }\n\n        /// <summary>\n        /// Obtain an access token without showing UI or prompts.\n        /// </summary>\n        private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(\n            IPublicClientApplication app, string[] scopes, string userName, bool msaPt)\n        {\n            try\n            {\n                if (userName is null)\n                {\n                    Context.Trace.WriteLine(\n                        \"Attempting to acquire token silently for current operating system account...\");\n\n                    return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount)\n                        .ExecuteAsync();\n                }\n                else\n                {\n                    Context.Trace.WriteLine($\"Attempting to acquire token silently for user '{userName}'...\");\n\n                    // Enumerate all accounts and find the one matching the user name\n                    IEnumerable<IAccount> accounts = await app.GetAccountsAsync();\n                    IAccount account = accounts.FirstOrDefault(x =>\n                        StringComparer.OrdinalIgnoreCase.Equals(x.Username, userName));\n                    if (account is null)\n                    {\n                        Context.Trace.WriteLine($\"No cached account found for user '{userName}'...\");\n                        return null;\n                    }\n\n                    var atsBuilder = app.AcquireTokenSilent(scopes, account);\n\n                    // Is we are operating with an MSA passthrough app we need to ensure that we target the\n                    // special MSA 'transfer' tenant explicitly. This is a workaround for MSAL issue:\n                    // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3077\n                    if (msaPt && Guid.TryParse(account.HomeAccountId.TenantId, out Guid homeTenantId) &&\n                        homeTenantId == Constants.MsaHomeTenantId)\n                    {\n                        atsBuilder = atsBuilder.WithTenantId(Constants.MsaTransferTenantId.ToString(\"D\"));\n                    }\n\n                    return await atsBuilder.ExecuteAsync();\n                }\n            }\n            catch (MsalUiRequiredException)\n            {\n                Context.Trace.WriteLine(\"Failed to acquire token silently; user interaction is required.\");\n                return null;\n            }\n            catch (Exception ex)\n            {\n                Context.Trace.WriteLine(\"Failed to acquire token silently.\");\n                Context.Trace.WriteException(ex);\n                return null;\n            }\n        }\n\n        private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(string authority,\n            string clientId, Uri redirectUri, bool enableBroker, bool msaPt, CancellationTokenSource uiCts)\n        {\n            var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);\n\n            var appBuilder = PublicClientApplicationBuilder.Create(clientId)\n                .WithAuthority(authority)\n                .WithRedirectUri(redirectUri.ToString())\n                .WithHttpClientFactory(httpFactoryAdaptor);\n\n            // Listen to MSAL logs if GCM_TRACE_MSAUTH is set\n            if (Context.Settings.IsMsalTracingEnabled)\n            {\n                // If GCM secret tracing is enabled also enable \"PII\" logging in MSAL\n                bool enablePiiLogging = Context.Trace.IsSecretTracingEnabled;\n\n                appBuilder.WithLogging(OnMsalLogMessage, LogLevel.Verbose, enablePiiLogging, false);\n            }\n\n            // On Windows we should set the parent window handle for the authentication dialogs\n            // so that they are displayed as a child of the correct window.\n            if (PlatformUtils.IsWindows())\n            {\n                // If we have a parent window ID then use that, otherwise use the hosting terminal window.\n                if (!string.IsNullOrWhiteSpace(Context.Settings.ParentWindowId) &&\n                    int.TryParse(Context.Settings.ParentWindowId, out int hWndInt) && hWndInt > 0)\n                {\n                    Context.Trace.WriteLine($\"Using provided parent window ID '{hWndInt}' for MSAL authentication dialogs.\");\n                    appBuilder.WithParentActivityOrWindow(() => new IntPtr(hWndInt));\n                }\n                else\n                {\n                    IntPtr consoleHandle = Kernel32.GetConsoleWindow();\n                    IntPtr parentHandle = User32.GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);\n\n                    // If we don't have a console window then create a dummy top-level window (for .NET Framework)\n                    // that we can use as a parent. When not on .NET Framework just use the Desktop window.\n                    if (parentHandle != IntPtr.Zero)\n                    {\n                        Context.Trace.WriteLine($\"Using console parent window ID '{parentHandle}' for MSAL authentication dialogs.\");\n                        appBuilder.WithParentActivityOrWindow(() => parentHandle);\n                    }\n                    else if (enableBroker) // Only actually need to set a parent window when using the Windows broker\n                    {\n                        Context.Trace.WriteLine(\"Using progress parent window for MSAL authentication dialogs.\");\n                        appBuilder.WithParentActivityOrWindow(() => ProgressWindow.ShowAndGetHandle(uiCts.Token));\n                    }\n                }\n            }\n\n            // Configure the broker if enabled\n            // Currently only supported on Windows so only included in the .NET Framework builds\n            // to save on the distribution size of the .NET builds (no need for MSALRuntime bits).\n            if (enableBroker)\n            {\n#if NETFRAMEWORK\n                appBuilder.WithBroker(\n                    new BrokerOptions(BrokerOptions.OperatingSystems.Windows)\n                    {\n                        Title = \"Git Credential Manager\",\n                        MsaPassthrough = msaPt,\n                    }\n                );\n#endif\n            }\n\n            IPublicClientApplication app = appBuilder.Build();\n\n            // Register the user token cache\n            await RegisterTokenCacheAsync(app.UserTokenCache, CreateUserTokenCacheProps, Context.Trace2);\n\n            return app;\n        }\n\n        private async Task<IConfidentialClientApplication> CreateConfidentialClientApplicationAsync(ServicePrincipalIdentity sp)\n        {\n            var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);\n\n            Context.Trace.WriteLine($\"Creating confidential client application for {sp.TenantId}/{sp.Id}...\");\n            var appBuilder = ConfidentialClientApplicationBuilder.Create(sp.Id)\n                .WithTenantId(sp.TenantId)\n                .WithHttpClientFactory(httpFactoryAdaptor);\n\n            if (sp.Certificate is not null)\n            {\n                Context.Trace.WriteLineSecrets(\"Using certificate with thumbprint: '{0}'\", new object[] { sp.Certificate.Thumbprint });\n                appBuilder = appBuilder.WithCertificate(sp.Certificate);\n            }\n            else if (!string.IsNullOrWhiteSpace(sp.ClientSecret))\n            {\n                Context.Trace.WriteLineSecrets(\"Using client secret: '{0}'\", new object[] { sp.ClientSecret });\n                appBuilder = appBuilder.WithClientSecret(sp.ClientSecret);\n            }\n            else\n            {\n                throw new InvalidOperationException(\"Service principal identity does not contain a certificate or client secret.\");\n            }\n\n            IConfidentialClientApplication app = appBuilder.Build();\n\n            await RegisterTokenCacheAsync(app.AppTokenCache, CreateAppTokenCacheProps, Context.Trace2);\n\n            return app;\n        }\n\n        #endregion\n\n        #region Helpers\n\n        private delegate StorageCreationProperties StoragePropertiesBuilder(bool useLinuxFallback);\n\n        private async Task RegisterTokenCacheAsync(ITokenCache cache, StoragePropertiesBuilder propsBuilder, ITrace2 trace2)\n        {\n            Context.Trace.WriteLine(\"Configuring MSAL token cache...\");\n\n            if (!PlatformUtils.IsWindows() && !PlatformUtils.IsPosix())\n            {\n                string osType = PlatformUtils.GetPlatformInformation(trace2).OperatingSystemType;\n                Context.Trace.WriteLine($\"Token cache integration is not supported on {osType}.\");\n                return;\n            }\n\n            // We use the MSAL extension library to provide us consistent cache file access semantics (synchronisation, etc)\n            // as other GCM processes, and other Microsoft developer tools such as the Azure PowerShell CLI.\n            MsalCacheHelper helper = null;\n            try\n            {\n                StorageCreationProperties storageProps = propsBuilder(useLinuxFallback: false);\n                helper = await MsalCacheHelper.CreateAsync(storageProps);\n\n                // Test that cache access is working correctly\n                helper.VerifyPersistence();\n            }\n            catch (MsalCachePersistenceException ex)\n            {\n                var message = \"Cannot persist Microsoft Authentication data securely!\";\n                Context.Streams.Error.WriteLine(\"warning: cannot persist Microsoft authentication token cache securely!\");\n                Context.Trace.WriteLine(message);\n                Context.Trace.WriteException(ex);\n                Context.Trace2.WriteError(message);\n\n                if (PlatformUtils.IsMacOS())\n                {\n                    // On macOS sometimes the Keychain returns the \"errSecAuthFailed\" error - we don't know why\n                    // but it appears to be something to do with not being able to access the keychain.\n                    // Locking and unlocking (or restarting) often fixes this.\n                    Context.Streams.Error.WriteLine(\n                        \"warning: there is a problem accessing the login Keychain - either manually lock and unlock the \" +\n                        \"login Keychain, or restart the computer to remedy this\");\n                }\n                else if (PlatformUtils.IsLinux())\n                {\n                    // On Linux the SecretService/keyring might not be available so we must fall-back to a plaintext file.\n                    Context.Streams.Error.WriteLine(\"warning: using plain-text fallback token cache\");\n                    Context.Trace.WriteLine(\"Using fall-back plaintext token cache on Linux.\");\n                    StorageCreationProperties storageProps = propsBuilder(useLinuxFallback: true);\n                    helper = await MsalCacheHelper.CreateAsync(storageProps);\n                }\n            }\n\n            if (helper is null)\n            {\n                Context.Streams.Error.WriteLine(\"error: failed to set up token cache!\");\n                Context.Trace.WriteLine(\"Failed to integrate with token cache!\");\n            }\n            else\n            {\n                helper.RegisterCache(cache);\n                Context.Trace.WriteLine(\"Token cache configured.\");\n            }\n        }\n\n        /// <summary>\n        /// Create the properties for the user token cache. This is used by public client applications only.\n        /// This cache is shared between GCM processes, and also other Microsoft developer tools such as the Azure\n        /// PowerShell CLI.\n        /// </summary>\n        /// <param name=\"useLinuxFallback\"></param>\n        /// <returns></returns>\n        internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallback)\n        {\n            const string cacheFileName = \"msal.cache\";\n            string cacheDirectory;\n            if (PlatformUtils.IsWindows())\n            {\n                // The shared MSAL cache is located at \"%LocalAppData%\\.IdentityService\\msal.cache\" on Windows.\n                cacheDirectory = Path.Combine(\n                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n                    \".IdentityService\"\n                );\n            }\n            else\n            {\n                // The shared MSAL cache metadata is located at \"~/.local/.IdentityService/msal.cache\" on UNIX.\n                cacheDirectory = Path.Combine(Context.FileSystem.UserHomePath, \".local\", \".IdentityService\");\n            }\n\n            // The keychain is used on macOS with the following service & account names\n            var builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory)\n                .WithMacKeyChain(\"Microsoft.Developer.IdentityService\", \"MSALCache\");\n\n            if (useLinuxFallback)\n            {\n                builder.WithLinuxUnprotectedFile();\n            }\n            else\n            {\n                // The SecretService/keyring is used on Linux with the following collection name and attributes\n                builder.WithLinuxKeyring(cacheFileName,\n                    \"default\", \"MSALCache\",\n                    new KeyValuePair<string, string>(\"MsalClientID\", \"Microsoft.Developer.IdentityService\"),\n                    new KeyValuePair<string, string>(\"Microsoft.Developer.IdentityService\", \"1.0.0.0\"));\n            }\n\n            return builder.Build();\n        }\n\n        internal static ManagedIdentityId GetManagedIdentity(string str)\n        {\n            // An empty string or \"system\" means system-assigned managed identity\n            if (string.IsNullOrWhiteSpace(str) || str.Equals(\"system\", StringComparison.OrdinalIgnoreCase))\n            {\n                return ManagedIdentityId.SystemAssigned;\n            }\n\n            //\n            // A GUID-looking value means a user-assigned managed identity specified by the client ID.\n            // If the \"{value}\" is the empty GUID then we use the system-assigned MI.\n            //\n            if (Guid.TryParse(str, out Guid guid))\n            {\n                return guid == Guid.Empty\n                    ? ManagedIdentityId.SystemAssigned\n                    : ManagedIdentityId.WithUserAssignedClientId(str);\n            }\n\n            //\n            // A value of the form \"id://{value}\" means a user-assigned managed identity specified by the client ID.\n            // If the \"{value}\" is the empty GUID then we use the system-assigned MI.\n            //\n            // If the value is \"resource://{value}\" then it is a user-assigned managed identity specified\n            // by the resource ID.\n            //\n            if (Uri.TryCreate(str, UriKind.Absolute, out Uri uri))\n            {\n                if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, \"id\"))\n                {\n                    return Guid.TryParse(uri.Host, out Guid g) && g == Guid.Empty\n                        ? ManagedIdentityId.SystemAssigned\n                        : ManagedIdentityId.WithUserAssignedClientId(uri.Host);\n                }\n\n                if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, \"resource\"))\n                {\n                    return ManagedIdentityId.WithUserAssignedResourceId(uri.Host);\n                }\n            }\n\n            throw new ArgumentException(\"Invalid managed identity value.\", nameof(str));\n        }\n\n        /// <summary>\n        /// Create the properties for the application token cache. This is used by confidential client applications only\n        /// and is not shared between applications other than GCM.\n        /// </summary>\n        internal StorageCreationProperties CreateAppTokenCacheProps(bool useLinuxFallback)\n        {\n            const string cacheFileName = \"app.cache\";\n\n            // The confidential client MSAL cache is located at \"%UserProfile%\\.gcm\\msal\\app.cache\" on Windows\n            // and at \"~/.gcm/msal/app.cache\" on UNIX.\n            string cacheDirectory = Path.Combine(Context.FileSystem.UserDataDirectoryPath, \"msal\");\n\n            // The keychain is used on macOS with the following service & account names\n            var builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory)\n                .WithMacKeyChain(\"GitCredentialManager.MSAL\", \"AppCache\");\n\n            if (useLinuxFallback)\n            {\n                builder.WithLinuxUnprotectedFile();\n            }\n            else\n            {\n                // The SecretService/keyring is used on Linux with the following collection name and attributes\n                builder.WithLinuxKeyring(cacheFileName,\n                    \"default\", \"AppCache\",\n                    new KeyValuePair<string, string>(\"MsalClientID\", \"GitCredentialManager.MSAL\"),\n                    new KeyValuePair<string, string>(\"GitCredentialManager.MSAL\", \"1.0.0.0\"));\n            }\n\n            return builder.Build();\n        }\n\n        private static EmbeddedWebViewOptions GetEmbeddedWebViewOptions()\n        {\n            return new EmbeddedWebViewOptions\n            {\n                Title = \"Git Credential Manager\"\n            };\n        }\n\n        private SystemWebViewOptions GetSystemWebViewOptions()\n        {\n            // TODO: add nicer HTML success and error pages\n            return new SystemWebViewOptions\n            {\n                OpenBrowserAsync = OpenBrowserFunc\n            };\n\n            // We have special handling for Linux and WSL to open the system browser\n            // so we need to use our own function here. Sorry MSAL!\n            Task OpenBrowserFunc(Uri uri)\n            {\n                try\n                {\n                    Context.SessionManager.OpenBrowser(uri);\n                }\n                catch (Exception ex)\n                {\n                    Context.Trace.WriteLine(\"Failed to open system web browser - using MSAL fallback\");\n                    Context.Trace.WriteException(ex);\n\n                    // Fallback to MSAL's default browser opening logic, preferring Edge.\n                    return SystemWebViewOptions.OpenWithChromeEdgeBrowserAsync(uri);\n                }\n\n                return Task.CompletedTask;\n            }\n        }\n\n        private Task ShowDeviceCodeInTty(DeviceCodeResult dcr)\n        {\n            Context.Terminal.WriteLine(dcr.Message);\n\n            return Task.CompletedTask;\n        }\n\n        private void OnMsalLogMessage(LogLevel level, string message, bool containspii)\n        {\n            Context.Trace.WriteLine($\"[{level.ToString()}] {message}\", memberName: \"MSAL\");\n        }\n\n        private bool TryFindHelperCommand(out string command, out string args)\n        {\n            return TryFindHelperCommand(\n                Constants.EnvironmentVariables.GcmUiHelper,\n                Constants.GitConfiguration.Credential.UiHelper,\n                Constants.DefaultUiHelper,\n                out command,\n                out args);\n        }\n\n        private class MsalHttpClientFactoryAdaptor : IMsalHttpClientFactory\n        {\n            private readonly IHttpClientFactory _factory;\n            private HttpClient _instance;\n\n            public MsalHttpClientFactoryAdaptor(IHttpClientFactory factory)\n            {\n                EnsureArgument.NotNull(factory, nameof(factory));\n\n                _factory = factory;\n            }\n\n            public HttpClient GetHttpClient()\n            {\n                // MSAL calls this method each time it wants to use an HTTP client.\n                // We ensure we only create a single instance to avoid socket exhaustion.\n                return _instance ?? (_instance = _factory.CreateClient());\n            }\n        }\n\n        #endregion\n\n        #region Auth flow capability detection\n\n        public bool CanUseBroker()\n        {\n#if NETFRAMEWORK\n            // We only support the broker on Windows 10+ and in an interactive session\n            if (!Context.SessionManager.IsDesktopSession || !PlatformUtils.IsWindowsBrokerSupported())\n            {\n                return false;\n            }\n\n            // Default to using the OS broker only on DevBox for the time being\n            bool defaultValue = PlatformUtils.IsDevBox();\n\n            if (Context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.MsAuthUseBroker,\n                    out string valueStr))\n            {\n                return valueStr.ToBooleanyOrDefault(defaultValue);\n            }\n\n            return defaultValue;\n#else\n            // OS broker requires .NET Framework right now until we migrate to .NET 5.0 (net5.0-windows10.x.y.z)\n            return false;\n#endif\n        }\n\n        private bool CanUseEmbeddedWebView()\n        {\n            // If we're in an interactive session and on .NET Framework then MSAL can show the WinForms-based embedded UI\n#if NETFRAMEWORK\n            return Context.SessionManager.IsDesktopSession;\n#else\n            return false;\n#endif\n        }\n\n        private void EnsureCanUseEmbeddedWebView()\n        {\n#if NETFRAMEWORK\n            if (!Context.SessionManager.IsDesktopSession)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"Embedded web view is not available without a desktop session.\");\n            }\n#else\n            throw new Trace2InvalidOperationException(Context.Trace2,\n                \"Embedded web view is not available on .NET Core.\");\n#endif\n        }\n\n        private bool CanUseSystemWebView(IPublicClientApplication app, Uri redirectUri)\n        {\n            //\n            // MSAL requires the application redirect URI is a loopback address to use the System WebView\n            //\n            // Note: we do NOT check the MSAL 'IsSystemWebViewAvailable' property as it only\n            // looks for the presence of the DISPLAY environment variable on UNIX systems.\n            // This is insufficient as we instead handle launching the default browser ourselves.\n            //\n            return Context.SessionManager.IsWebBrowserAvailable && redirectUri.IsLoopback;\n        }\n\n        private void EnsureCanUseSystemWebView(IPublicClientApplication app, Uri redirectUri)\n        {\n            if (!Context.SessionManager.IsWebBrowserAvailable)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"System web view is not available without a way to start a browser.\");\n            }\n\n            if (!redirectUri.IsLoopback)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"System web view is not available for this service configuration.\");\n            }\n        }\n\n        #endregion\n\n        private class MsalResult : IMicrosoftAuthenticationResult\n        {\n            private readonly AuthenticationResult _msalResult;\n\n            public MsalResult(AuthenticationResult msalResult)\n            {\n                _msalResult = msalResult;\n            }\n\n            public string AccessToken => _msalResult.AccessToken;\n            public string AccountUpn => _msalResult.Account?.Username;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/HttpListenerExtensions.cs",
    "content": "using System.Net;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public static class HttpListenerExtensions\n    {\n        public static async Task WriteResponseAsync(this HttpListenerResponse response, string responseText)\n        {\n            byte[] responseData = Encoding.UTF8.GetBytes(responseText);\n            response.ContentLength64 = responseData.Length;\n            await response.OutputStream.WriteAsync(responseData, 0, responseData.Length);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/IOAuth2WebBrowser.cs",
    "content": "using System;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public interface IOAuth2WebBrowser\n    {\n        Uri UpdateRedirectUri(Uri uri);\n\n        Task<Uri> GetAuthenticationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/Json/DeviceAuthorizationEndpointResponseJson.cs",
    "content": "using System;\nusing System.Text.Json.Serialization;\n\nnamespace GitCredentialManager.Authentication.OAuth.Json\n{\n    public class DeviceAuthorizationEndpointResponseJson\n    {\n        [JsonRequired]\n        [JsonPropertyName(\"device_code\")]\n        public string DeviceCode { get; set; }\n\n        [JsonRequired]\n        [JsonPropertyName(\"user_code\")]\n        public string UserCode { get; set; }\n\n        [JsonRequired]\n        [JsonPropertyName(\"verification_uri\")]\n        public Uri VerificationUri { get; set; }\n\n        [JsonPropertyName(\"expires_in\")]\n        public long ExpiresIn { get; set; }\n\n        [JsonPropertyName(\"interval\")]\n        public long PollingInterval { get; set; }\n\n        public OAuth2DeviceCodeResult ToResult()\n        {\n            return new OAuth2DeviceCodeResult(DeviceCode, UserCode, VerificationUri, TimeSpan.FromSeconds(PollingInterval))\n            {\n                ExpiresIn = TimeSpan.FromSeconds(ExpiresIn)\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/Json/ErrorResponseJson.cs",
    "content": "using System;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace GitCredentialManager.Authentication.OAuth.Json\n{\n    public class ErrorResponseJson\n    {\n        [JsonRequired]\n        [JsonPropertyName(\"error\")]\n        public string Error { get; set; }\n\n        [JsonPropertyName(\"error_description\")]\n        public string Description { get; set; }\n\n        [JsonPropertyName(\"error_uri\")]\n        public Uri Uri { get; set; }\n\n        public OAuth2Exception ToException(Exception innerException = null)\n        {\n            var message = new StringBuilder(Error);\n\n            if (!string.IsNullOrEmpty(Description))\n            {\n                message.AppendFormat(\": {0}\", Description);\n            }\n\n            if (Uri != null)\n            {\n                message.AppendFormat(\" [{0}]\", Uri);\n            }\n\n            return new OAuth2Exception(message.ToString(), innerException) {HelpLink = Uri?.ToString()};\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/Json/TokenEndpointResponseJson.cs",
    "content": "using System;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace GitCredentialManager.Authentication.OAuth.Json\n{\n    public class TokenEndpointResponseJson\n    {\n        [JsonRequired]\n        [JsonPropertyName(\"access_token\")]\n        public string AccessToken { get; set; }\n\n        [JsonRequired]\n        [JsonPropertyName(\"token_type\")]\n        public string TokenType { get; set; }\n\n        [JsonPropertyName(\"expires_in\")]\n        public long? ExpiresIn { get; set; }\n\n        [JsonPropertyName(\"refresh_token\")]\n        public string RefreshToken { get; set; }\n\n        [JsonPropertyName(\"scope\")]\n        public virtual string Scope { get; set; }\n\n        public OAuth2TokenResult ToResult()\n        {\n            return new OAuth2TokenResult(AccessToken, TokenType)\n            {\n                ExpiresIn = ExpiresIn.HasValue ? TimeSpan.FromSeconds(ExpiresIn.Value) : null,\n                RefreshToken = RefreshToken,\n                Scopes = Scope?.Split(' ')\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2AuthorizationCodeResult.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public class OAuth2AuthorizationCodeResult\n    {\n        public OAuth2AuthorizationCodeResult(string code, Uri redirectUri = null, string codeVerifier = null)\n        {\n            Code = code;\n            RedirectUri = redirectUri;\n            CodeVerifier = codeVerifier;\n        }\n\n        public string Code { get; }\n        public Uri RedirectUri { get; }\n        public string CodeVerifier { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2Client.cs",
    "content": "﻿using System;\r\nusing System.Collections.Generic;\r\nusing System.Net.Http;\r\nusing System.Net.Http.Headers;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing GitCredentialManager.Authentication.OAuth.Json;\r\nusing System.Text.Json;\r\n\r\nnamespace GitCredentialManager.Authentication.OAuth\r\n{\r\n    /// <summary>\r\n    /// Represents an OAuth2 client application that can perform the basic flows outlined in RFC 6749,\r\n    /// as well as extensions such as OAuth2 Device Authorization Grant (RFC 8628).\r\n    /// </summary>\r\n    public interface IOAuth2Client\r\n    {\r\n        /// <summary>\r\n        /// Retrieve an authorization code grant using a user agent.\r\n        /// </summary>\r\n        /// <param name=\"scopes\">Scopes to request.</param>\r\n        /// <param name=\"browser\">User agent to use to start the authorization code grant flow.</param>\r\n        /// <param name=\"extraQueryParams\">Extra parameters to add to the URL query component.</param>\r\n        /// <param name=\"ct\">Token to cancel the operation.</param>\r\n        /// <returns>Authorization code.</returns>\r\n        Task<OAuth2AuthorizationCodeResult> GetAuthorizationCodeAsync(\r\n            IEnumerable<string> scopes,\r\n            IOAuth2WebBrowser browser,\r\n            IDictionary<string, string> extraQueryParams,\r\n            CancellationToken ct\r\n        );\r\n\r\n        /// <summary>\r\n        /// Retrieve a device code grant.\r\n        /// </summary>\r\n        /// <param name=\"scopes\">Scopes to request.</param>\r\n        /// <param name=\"ct\">Token to cancel the operation.</param>\r\n        /// <exception cref=\"InvalidOperationException\">Thrown if the client has not been configured with a device authorization endpoint.</exception>\r\n        /// <returns>Device code grant result.</returns>\r\n        Task<OAuth2DeviceCodeResult> GetDeviceCodeAsync(IEnumerable<string> scopes, CancellationToken ct);\r\n\r\n        /// <summary>\r\n        /// Exchange an authorization code acquired from <see cref=\"GetAuthorizationCodeAsync\"/> for an access token.\r\n        /// </summary>\r\n        /// <param name=\"authorizationCodeResult\">Authorization code grant result.</param>\r\n        /// <param name=\"ct\">Token to cancel the operation.</param>\r\n        /// <returns>Token result.</returns>\r\n        Task<OAuth2TokenResult> GetTokenByAuthorizationCodeAsync(OAuth2AuthorizationCodeResult authorizationCodeResult, CancellationToken ct);\r\n\r\n        /// <summary>\r\n        /// Use a refresh token to get a new access token.\r\n        /// </summary>\r\n        /// <param name=\"refreshToken\">Refresh token.</param>\r\n        /// <param name=\"ct\">Token to cancel the operation.</param>\r\n        /// <returns>Token result.</returns>\r\n        Task<OAuth2TokenResult> GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct);\r\n\r\n        /// <summary>\r\n        /// Exchange a device code grant acquired from <see cref=\"GetDeviceCodeAsync\"/> for an access token.\r\n        /// </summary>\r\n        /// <param name=\"deviceCodeResult\">Device code grant result.</param>\r\n        /// <param name=\"ct\">Token to cancel the operation.</param>\r\n        /// <returns>Token result.</returns>\r\n        Task<OAuth2TokenResult> GetTokenByDeviceCodeAsync(OAuth2DeviceCodeResult deviceCodeResult, CancellationToken ct);\r\n    }\r\n\r\n    public class OAuth2Client : IOAuth2Client\r\n    {\r\n        private readonly HttpClient _httpClient;\r\n        private readonly OAuth2ServerEndpoints _endpoints;\r\n        private readonly Uri _redirectUri;\r\n        private readonly string _clientId;\r\n        private readonly ITrace2 _trace2;\r\n        private readonly string _clientSecret;\r\n        private readonly bool _addAuthHeader;\r\n\r\n        private IOAuth2CodeGenerator _codeGenerator;\r\n\r\n        public OAuth2Client(HttpClient httpClient,\r\n            OAuth2ServerEndpoints endpoints,\r\n            string clientId,\r\n            ITrace2 trace2,\r\n            Uri redirectUri = null,\r\n            string clientSecret = null,\r\n            bool addAuthHeader = true)\r\n        {\r\n            _httpClient = httpClient;\r\n            _endpoints = endpoints;\r\n            _clientId = clientId;\r\n            _trace2 = trace2;\r\n            _redirectUri = redirectUri;\r\n            _clientSecret = clientSecret;\r\n            _addAuthHeader = addAuthHeader;\r\n        }\r\n\r\n        public IOAuth2CodeGenerator CodeGenerator\r\n        {\r\n            get => _codeGenerator ?? (_codeGenerator = new OAuth2CryptographicCodeGenerator());\r\n            set => _codeGenerator = value;\r\n        }\r\n\r\n        #region IOAuth2Client\r\n\r\n        public async Task<OAuth2AuthorizationCodeResult> GetAuthorizationCodeAsync(IEnumerable<string> scopes,\r\n            IOAuth2WebBrowser browser, IDictionary<string, string> extraQueryParams, CancellationToken ct)\r\n        {\r\n            string state = CodeGenerator.CreateNonce();\r\n            string codeVerifier = CodeGenerator.CreatePkceCodeVerifier();\r\n            string codeChallenge = CodeGenerator.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, codeVerifier);\r\n\r\n            var queryParams = new Dictionary<string, string>\r\n            {\r\n                [OAuth2Constants.AuthorizationEndpoint.ResponseTypeParameter] =\r\n                    OAuth2Constants.AuthorizationEndpoint.AuthorizationCodeResponseType,\r\n                [OAuth2Constants.ClientIdParameter] = _clientId,\r\n                [OAuth2Constants.AuthorizationEndpoint.StateParameter] = state,\r\n                [OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodParameter] =\r\n                    OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodS256,\r\n                [OAuth2Constants.AuthorizationEndpoint.PkceChallengeParameter] = codeChallenge\r\n            };\r\n\r\n            if (extraQueryParams?.Count > 0)\r\n            {\r\n                foreach (var kvp in extraQueryParams)\r\n                {\r\n                    if (queryParams.ContainsKey(kvp.Key))\r\n                    {\r\n                        throw new ArgumentException(\r\n                            $\"Extra query parameter '{kvp.Key}' would override required standard OAuth parameters.\",\r\n                            nameof(extraQueryParams));\r\n                    }\r\n\r\n                    queryParams[kvp.Key] = kvp.Value;\r\n                }\r\n            }\r\n\r\n            Uri redirectUri = null;\r\n            if (_redirectUri != null)\r\n            {\r\n                redirectUri = browser.UpdateRedirectUri(_redirectUri);\r\n\r\n                // We must use the .OriginalString property here over .ToString() because OAuth requires the redirect\r\n                // URLs to be compared exactly, respecting missing/present trailing slashes, byte-for-byte.\r\n                queryParams[OAuth2Constants.RedirectUriParameter] = redirectUri.OriginalString;\r\n            }\r\n\r\n            string scopesStr = string.Join(\" \", scopes);\r\n            if (!string.IsNullOrWhiteSpace(scopesStr))\r\n            {\r\n                queryParams[OAuth2Constants.ScopeParameter] = scopesStr;\r\n            }\r\n\r\n            var authorizationUriBuilder = new UriBuilder(_endpoints.AuthorizationEndpoint)\r\n            {\r\n                Query = queryParams.ToQueryString()\r\n            };\r\n\r\n            Uri authorizationUri = authorizationUriBuilder.Uri;\r\n\r\n            // Open the browser at the request URI to start the authorization code grant flow.\r\n            Uri finalUri = await browser.GetAuthenticationCodeAsync(authorizationUri, redirectUri, ct);\r\n\r\n            // Check for errors serious enough we should terminate the flow, such as if the state value returned does\r\n            // not match the one we passed. This indicates a badly implemented Authorization Server, or worse, some\r\n            // form of failed MITM or replay attack.\r\n            IDictionary<string, string> redirectQueryParams = finalUri.GetQueryParameters();\r\n            if (!redirectQueryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState))\r\n            {\r\n                throw new Trace2OAuth2Exception(_trace2, $\"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.\");\r\n            }\r\n            if (!StringComparer.Ordinal.Equals(state, replyState))\r\n            {\r\n                throw new Trace2OAuth2Exception(_trace2,\r\n                    $\"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.\");\r\n            }\r\n\r\n            // We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason)\r\n            if (!redirectQueryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode))\r\n            {\r\n                throw new Trace2OAuth2Exception(_trace2,\r\n                    $\"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response.\");\r\n            }\r\n\r\n            return new OAuth2AuthorizationCodeResult(authCode, redirectUri, codeVerifier);\r\n        }\r\n\r\n        public async Task<OAuth2DeviceCodeResult> GetDeviceCodeAsync(IEnumerable<string> scopes, CancellationToken ct)\r\n        {\r\n            var label = \"get device code\";\r\n            using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);\r\n\r\n            if (_endpoints.DeviceAuthorizationEndpoint is null)\r\n            {\r\n                throw new Trace2InvalidOperationException(_trace2,\r\n                    \"No device authorization endpoint has been configured for this client.\");\r\n            }\r\n\r\n            string scopesStr = string.Join(\" \", scopes);\r\n\r\n            var formData = new Dictionary<string, string>\r\n            {\r\n                [OAuth2Constants.ClientIdParameter] = _clientId\r\n            };\r\n\r\n            if (!string.IsNullOrWhiteSpace(scopesStr))\r\n            {\r\n                formData[OAuth2Constants.ScopeParameter] = scopesStr;\r\n            }\r\n\r\n            using (HttpContent requestContent = new FormUrlEncodedContent(formData))\r\n            using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.DeviceAuthorizationEndpoint, requestContent))\r\n            using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct))\r\n            {\r\n                string json = await response.Content.ReadAsStringAsync();\r\n\r\n                if (response.IsSuccessStatusCode && TryDeserializeJson(json, out DeviceAuthorizationEndpointResponseJson jsonObj))\r\n                {\r\n                    return jsonObj.ToResult();\r\n                }\r\n\r\n                throw CreateExceptionFromResponse(json);\r\n            }\r\n        }\r\n\r\n        public async Task<OAuth2TokenResult> GetTokenByAuthorizationCodeAsync(OAuth2AuthorizationCodeResult authorizationCodeResult, CancellationToken ct)\r\n        {\r\n            var label = \"get token by auth code\";\r\n            using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);\r\n\r\n            var formData = new Dictionary<string, string>\r\n            {\r\n                [OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.AuthorizationCodeGrantType,\r\n                [OAuth2Constants.TokenEndpoint.AuthorizationCodeParameter] = authorizationCodeResult.Code,\r\n                [OAuth2Constants.TokenEndpoint.PkceVerifierParameter] = authorizationCodeResult.CodeVerifier,\r\n                [OAuth2Constants.ClientIdParameter] = _clientId,\r\n                [OAuth2Constants.ClientSecretParameter] = _clientSecret\r\n            };\r\n\r\n            if (authorizationCodeResult.RedirectUri != null)\r\n            {\r\n                formData[OAuth2Constants.RedirectUriParameter] = authorizationCodeResult.RedirectUri.OriginalString;\r\n            }\r\n\r\n            if (authorizationCodeResult.CodeVerifier != null)\r\n            {\r\n                formData[OAuth2Constants.TokenEndpoint.PkceVerifierParameter] = authorizationCodeResult.CodeVerifier;\r\n            }\r\n\r\n            using (HttpContent requestContent = new FormUrlEncodedContent(formData))\r\n            using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, _addAuthHeader))\r\n            using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct))\r\n            {\r\n                string json = await response.Content.ReadAsStringAsync();\r\n\r\n                if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))\r\n                {\r\n                    return result;\r\n                }\r\n\r\n                throw CreateExceptionFromResponse(json);\r\n            }\r\n        }\r\n\r\n        public async Task<OAuth2TokenResult> GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct)\r\n        {\r\n            var label = \"get token by refresh token\";\r\n            using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);\r\n\r\n            var formData = new Dictionary<string, string>\r\n            {\r\n                [OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.RefreshTokenGrantType,\r\n                [OAuth2Constants.TokenEndpoint.RefreshTokenParameter] = refreshToken,\r\n                [OAuth2Constants.ClientIdParameter] = _clientId,\r\n                [OAuth2Constants.ClientSecretParameter] = _clientSecret\r\n            };\r\n\r\n            if (_redirectUri != null)\r\n            {\r\n                formData[OAuth2Constants.RedirectUriParameter] = _redirectUri.ToString();\r\n            }\r\n\r\n            using (HttpContent requestContent = new FormUrlEncodedContent(formData))\r\n            using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, _addAuthHeader))\r\n            using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct))\r\n            {\r\n                string json = await response.Content.ReadAsStringAsync();\r\n\r\n                if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))\r\n                {\r\n                    return result;\r\n                }\r\n\r\n                throw CreateExceptionFromResponse(json);\r\n            }\r\n        }\r\n\r\n        public async Task<OAuth2TokenResult> GetTokenByDeviceCodeAsync(OAuth2DeviceCodeResult deviceCodeResult, CancellationToken ct)\r\n        {\r\n            var formData = new Dictionary<string, string>\r\n            {\r\n                [OAuth2Constants.DeviceAuthorization.GrantTypeParameter] = OAuth2Constants.DeviceAuthorization.DeviceCodeGrantType,\r\n                [OAuth2Constants.DeviceAuthorization.DeviceCodeParameter] = deviceCodeResult.DeviceCode,\r\n                [OAuth2Constants.ClientIdParameter] = _clientId,\r\n            };\r\n\r\n            TimeSpan retryInterval = deviceCodeResult.PollingInterval;\r\n            while (true)\r\n            {\r\n                ct.ThrowIfCancellationRequested();\r\n\r\n                try\r\n                {\r\n                    using (HttpContent requestContent = new FormUrlEncodedContent(formData))\r\n                    using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent))\r\n                    using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct))\r\n                    {\r\n                        string json = await response.Content.ReadAsStringAsync();\r\n\r\n                        if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))\r\n                        {\r\n                            return result;\r\n                        }\r\n\r\n                        var error = JsonSerializer.Deserialize<ErrorResponseJson>(json, new JsonSerializerOptions\r\n                        {\r\n                            PropertyNameCaseInsensitive = true\r\n                        });\r\n\r\n                        switch (error.Error)\r\n                        {\r\n                            case OAuth2Constants.DeviceAuthorization.Errors.AuthorizationPending:\r\n                                // Retry with the current polling interval value\r\n                                break;\r\n                            case OAuth2Constants.DeviceAuthorization.Errors.SlowDown:\r\n                                // We must increase the polling interval by 5 seconds\r\n                                retryInterval = retryInterval.Add(TimeSpan.FromSeconds(5));\r\n                                break;\r\n                            default:\r\n                                // For all other errors do not retry\r\n                                throw CreateExceptionFromResponse(json);\r\n                        }\r\n                    }\r\n                }\r\n                catch (TimeoutException)\r\n                {\r\n                    // Back-off exponentially (2 * x = x + x)\r\n                    retryInterval += retryInterval;\r\n                }\r\n\r\n                // Wait the polling interval before retrying\r\n                await Task.Delay(retryInterval, ct);\r\n            }\r\n        }\r\n\r\n        #endregion\r\n\r\n        #region Extension Points\r\n\r\n        protected virtual bool TryCreateTokenEndpointResult(string json, out OAuth2TokenResult result)\r\n        {\r\n            if (TryDeserializeJson(json, out TokenEndpointResponseJson jsonObj))\r\n            {\r\n                result = jsonObj.ToResult();\r\n                return true;\r\n            }\r\n\r\n            result = null;\r\n            return false;\r\n        }\r\n\r\n        protected virtual bool TryCreateExceptionFromResponse(string json, out OAuth2Exception exception)\r\n        {\r\n            if (TryDeserializeJson(json, out ErrorResponseJson obj))\r\n            {\r\n                exception = obj.ToException();\r\n                return true;\r\n            }\r\n\r\n            exception = null;\r\n            return false;\r\n        }\r\n\r\n        #endregion\r\n\r\n        #region Helpers\r\n\r\n        private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri requestUri, HttpContent content = null, bool addAuthHeader = false)\r\n        {\r\n            var request = new HttpRequestMessage(method, requestUri) {Content = content};\r\n            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Http.MimeTypeJson));\r\n\r\n            if (addAuthHeader && !string.IsNullOrEmpty(_clientSecret))\r\n            {\r\n                request.AddBasicAuthenticationHeader(_clientId, _clientSecret);\r\n            }\r\n\r\n            return request;\r\n        }\r\n\r\n        protected Exception CreateExceptionFromResponse(string json)\r\n        {\r\n            if (TryCreateExceptionFromResponse(json, out OAuth2Exception exception))\r\n            {\r\n                _trace2.WriteError(exception.Message);\r\n                return exception;\r\n            }\r\n\r\n            var format = \"Unknown OAuth error: {0}\";\r\n            var message = string.Format(format, json);\r\n            return new Trace2OAuth2Exception(_trace2, message, format);\r\n        }\r\n\r\n        protected static bool TryDeserializeJson<T>(string json, out T obj)\r\n        {\r\n            try\r\n            {\r\n                obj = JsonSerializer.Deserialize<T>(json);\r\n                return true;\r\n            }\r\n            catch\r\n            {\r\n                obj = default;\r\n                return false;\r\n            }\r\n        }\r\n\r\n        #endregion\r\n    }\r\n\r\n    public static class OAuth2ClientExtensions\r\n    {\r\n        public static Task<OAuth2AuthorizationCodeResult> GetAuthorizationCodeAsync(\r\n            this IOAuth2Client client, IEnumerable<string> scopes, IOAuth2WebBrowser browser, CancellationToken ct)\r\n        {\r\n            return client.GetAuthorizationCodeAsync(scopes, browser, null, ct);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2Constants.cs",
    "content": "\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public static class OAuth2Constants\n    {\n        public const string ClientIdParameter = \"client_id\";\n        public const string ClientSecretParameter = \"client_secret\";\n        public const string RedirectUriParameter = \"redirect_uri\";\n        public const string ScopeParameter = \"scope\";\n        public const string Trace2Category = \"oauth2\";\n\n        public static class AuthorizationEndpoint\n        {\n            public const string StateParameter = \"state\";\n            public const string AuthorizationCodeResponseType = \"code\";\n            public const string ResponseTypeParameter = \"response_type\";\n            public const string PkceChallengeParameter = \"code_challenge\";\n            public const string PkceChallengeMethodParameter = \"code_challenge_method\";\n            public const string PkceChallengeMethodPlain = \"plain\";\n            public const string PkceChallengeMethodS256 = \"S256\";\n        }\n\n        public static class AuthorizationGrantResponse\n        {\n            public const string AuthorizationCodeParameter = \"code\";\n            public const string ErrorCodeParameter = \"error\";\n            public const string ErrorDescriptionParameter = \"error_description\";\n            public const string ErrorUriParameter = \"error_uri\";\n            public const string StateParameter = \"state\";\n        }\n\n        public static class TokenEndpoint\n        {\n            public const string GrantTypeParameter = \"grant_type\";\n            public const string AuthorizationCodeGrantType = \"authorization_code\";\n            public const string RefreshTokenGrantType = \"refresh_token\";\n            public const string PkceVerifierParameter = \"code_verifier\";\n            public const string AuthorizationCodeParameter = \"code\";\n            public const string RefreshTokenParameter = \"refresh_token\";\n        }\n\n        public static class DeviceAuthorization\n        {\n            public const string GrantTypeParameter = \"grant_type\";\n            public const string DeviceCodeParameter = \"device_code\";\n            public const string DeviceCodeGrantType = \"urn:ietf:params:oauth:grant-type:device_code\";\n\n            public static class Errors\n            {\n                public const string AuthorizationPending = \"authorization_pending\";\n                public const string SlowDown = \"slow_down\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2CryptographicGenerator.cs",
    "content": "using System;\nusing System.Security.Cryptography;\nusing System.Text;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public enum OAuth2PkceChallengeMethod\n    {\n        Plain = 0,\n        Sha256 = 1\n    }\n\n    public interface IOAuth2CodeGenerator\n    {\n        /// <summary>\n        /// Create a random string value suitable for use as a nonce.\n        /// </summary>\n        /// <returns>A random string.</returns>\n        string CreateNonce();\n\n        /// <summary>\n        /// Create a cryptographically random code verifier for use with Proof Key for Code Exchange (PKCE).\n        /// </summary>\n        /// <returns>PKCE code verifier.</returns>\n        string CreatePkceCodeVerifier();\n\n        /// <summary>\n        /// Create a code challenge for the given Proof Key for Code Exchange (PKCE) code verifier.\n        /// </summary>\n        /// <param name=\"challengeMethod\">Challenge method.</param>\n        /// /// <param name=\"codeVerifier\">PKCE code verifier.</param>\n        /// <returns>PKCE code challenge.</returns>\n        string CreatePkceCodeChallenge(OAuth2PkceChallengeMethod challengeMethod, string codeVerifier);\n    }\n\n    public class OAuth2CryptographicCodeGenerator : IOAuth2CodeGenerator\n    {\n        // Do not include padding of the base64url string to avoid percent-encoding when passed in a URI\n        private const bool PkceIncludeBase64UrlPadding = false;\n\n        public string CreateNonce()\n        {\n            return Guid.NewGuid().ToString(\"N\");\n        }\n\n        public string CreatePkceCodeVerifier()\n        {\n            //\n            // RFC 7636 requires the code verifier to match the following ABNF:\n            //\n            //   code-verifier = 43*128unreserved\n            //   unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n            //   ALPHA = %x41-5A / %x61-7A\n            //   DIGIT = %x30-39\n            //\n            // The base64url encoding (RFC 6749) is a subset of these characters\n            // so we opt to convert our random bytes into Base64URL format for\n            // the code verifier.\n            //\n            // RFC 7636 mandates the code verifier must be between 43 and 128 characters\n            // long (inclusive). We want to generate a string at the top end of this range\n            // for maximum entropy. At the same time we want to avoid using the padding\n            // character '=' because this character is percent-encoded when used in URLs.\n            // To avoid padding we need the number of input bytes to be divisible by 3.\n            //\n            // In order to achieve 128 base64url characters AND avoid padding we should\n            // generate exactly 96 random bytes. Why 96 bytes? 96 is divisible by 3 and:\n            //\n            //   96 bytes -> 768 bits -> 128 base64url characters (6 bits per character)\n            //\n            var buf = new byte[96];\n            var rng = RandomNumberGenerator.Create();\n            rng.GetBytes(buf);\n\n            return Base64UrlConvert.Encode(buf, PkceIncludeBase64UrlPadding);\n        }\n\n        public string CreatePkceCodeChallenge(OAuth2PkceChallengeMethod challengeMethod, string codeVerifier)\n        {\n            switch (challengeMethod)\n            {\n                case OAuth2PkceChallengeMethod.Plain:\n                    return codeVerifier;\n\n                case OAuth2PkceChallengeMethod.Sha256:\n                    // The \"S256\" code challenge is computed as follows, per RFC 7636:\n                    //\n                    //   code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))\n                    //\n                    using (var sha256 = SHA256.Create())\n                    {\n                        return Base64UrlConvert.Encode(\n                            sha256.ComputeHash(\n                                Encoding.ASCII.GetBytes(codeVerifier)\n                            ),\n                            PkceIncludeBase64UrlPadding\n                        );\n                    }\n\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(challengeMethod), challengeMethod, \"Unknown PKCE code challenge method.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2DeviceCodeResult.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public class OAuth2DeviceCodeResult\n    {\n        public OAuth2DeviceCodeResult(string deviceCode, string userCode, Uri verificationUri, TimeSpan? interval)\n        {\n            DeviceCode = deviceCode;\n            UserCode = userCode;\n            VerificationUri = verificationUri;\n            PollingInterval = interval ?? TimeSpan.FromSeconds(5);\n        }\n\n        public string DeviceCode { get; }\n\n        public string UserCode { get; }\n\n        public Uri VerificationUri { get; }\n\n        public TimeSpan PollingInterval { get; }\n\n        public TimeSpan? ExpiresIn { get; internal set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2Exception.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public class OAuth2Exception : Exception\n    {\n        public OAuth2Exception(string message) : base(message) { }\n\n        public OAuth2Exception(string message, Exception innerException) : base(message, innerException) { }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2ServerEndpoints.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    /// <summary>\n    /// Represents the various OAuth2 endpoints for an <see cref=\"OAuth2Client\"/>.\n    /// </summary>\n    public class OAuth2ServerEndpoints\n    {\n        private Uri _deviceAuthorizationEndpoint;\n\n        public OAuth2ServerEndpoints(Uri authorizationEndpoint, Uri tokenEndpoint)\n        {\n            EnsureArgument.AbsoluteUri(authorizationEndpoint, nameof(authorizationEndpoint));\n            EnsureArgument.AbsoluteUri(tokenEndpoint, nameof(tokenEndpoint));\n\n            AuthorizationEndpoint = authorizationEndpoint;\n            TokenEndpoint = tokenEndpoint;\n        }\n\n        public Uri AuthorizationEndpoint { get; }\n\n        public Uri TokenEndpoint { get; }\n\n        public Uri DeviceAuthorizationEndpoint\n        {\n            get => _deviceAuthorizationEndpoint;\n            set\n            {\n                if (value != null)\n                {\n                    EnsureArgument.AbsoluteUri(value, nameof(value));\n                }\n\n                _deviceAuthorizationEndpoint = value;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2SystemWebBrowser.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public class OAuth2WebBrowserOptions\n    {\n        internal const string DefaultSuccessHtml = @\"<!DOCTYPE html><html><head>\n<style>body{font-family:sans-serif;}dt{font-weight:bold;}dd{margin-bottom:10px;}</style>\n<title>Authentication successful</title></head>\n<body><h1>Authentication successful</h1><p>You can now close this page.</p></body>\n</html>\";\n        internal const string DefaultFailureHtmlFormat = @\"<!DOCTYPE html><html><head>\n<style>body{{font-family:sans-serif;}}dt{{font-weight:bold;}}dd{{margin-bottom:10px;}}</style>\n<title>Authentication failed</title></head>\n<body><h1>Authentication failed</h1><dl>\n<dt>Error:</dt><dd>{0}</dd>\n<dt>Description:</dt><dd>{1}</dd>\n<dt>URL:</dt><dd>{2}</dd>\n</dl></body></html>\";\n\n        public string SuccessResponseHtml { get; set; }\n        public string FailureResponseHtmlFormat { get; set; }\n\n        public Uri SuccessRedirect { get; set; }\n        public Uri FailureRedirectFormat { get; set; }\n    }\n\n    public class OAuth2SystemWebBrowser : IOAuth2WebBrowser\n    {\n        private readonly ISessionManager _sessionManager;\n        private readonly OAuth2WebBrowserOptions _options;\n\n        public OAuth2SystemWebBrowser(ISessionManager sessionManager, OAuth2WebBrowserOptions options)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n            EnsureArgument.NotNull(options, nameof(options));\n\n            _sessionManager = sessionManager;\n            _options = options;\n        }\n\n        public Uri UpdateRedirectUri(Uri uri)\n        {\n            if (!uri.IsLoopback)\n            {\n                throw new ArgumentException(\"Only localhost is supported as a redirect URI.\", nameof(uri));\n            }\n\n            // If a port has been specified use it, otherwise find a free one\n            if (uri.IsDefaultPort)\n            {\n                int port = GetFreeTcpPort();\n                return new UriBuilder(uri) {Port = port}.Uri;\n            }\n\n            return uri;\n        }\n\n        public async Task<Uri> GetAuthenticationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken ct)\n        {\n            if (!redirectUri.IsLoopback)\n            {\n                throw new ArgumentException(\"Only localhost is supported as a redirect URI.\", nameof(redirectUri));\n            }\n\n            Task<Uri> interceptTask = InterceptRequestsAsync(redirectUri, ct);\n\n            _sessionManager.OpenBrowser(authorizationUri);\n\n            return await interceptTask;\n        }\n\n        private async Task<Uri> InterceptRequestsAsync(Uri listenUri, CancellationToken ct)\n        {\n            // Create a TaskCompletionSource which completes when we're asked to cancel.\n            // We can then await the this task together with other tasks that don't take a\n            // CancellationToken and exit the method quickly when cancelled.\n            var tcs = new TaskCompletionSource<Uri>();\n            ct.Register(() => tcs.SetCanceled());\n\n            // Prefixes must end with a '/'\n            string prefix = listenUri.GetLeftPart(UriPartial.Path);\n            if (!prefix.EndsWith(\"/\"))\n            {\n                prefix += \"/\";\n            }\n\n            var listener = new HttpListener {Prefixes = {prefix}};\n            listener.Start();\n\n            try\n            {\n                Task<HttpListenerContext> contextTask = listener.GetContextAsync();\n                Task<Uri> cancelTask = tcs.Task;\n\n                Task completedTask = await Task.WhenAny(contextTask, tcs.Task);\n\n                // Check if we 'completed' the context task or the cancellation task\n                if (completedTask == cancelTask)\n                {\n                    // We were cancelled!\n                    return await cancelTask;\n                }\n\n                // We intercepted a request!\n                HttpListenerContext context = await contextTask;\n\n                await HandleInterceptedRequestAsync(context.Request, context.Response);\n\n                // Return the final intercepted URI\n                return context.Request.Url;\n            }\n            finally\n            {\n                listener.Stop();\n                listener.Close();\n            }\n        }\n\n        private async Task HandleInterceptedRequestAsync(HttpListenerRequest request, HttpListenerResponse response)\n        {\n            IDictionary<string, string> queryParams = request.QueryString.ToDictionary(StringComparer.OrdinalIgnoreCase);\n\n            // If we have an error value then the request failed and we should reply with a page containing the error information\n            bool hasError = queryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.ErrorCodeParameter, out string errorCode);\n            queryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.ErrorDescriptionParameter, out string errorDescription);\n            queryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.ErrorUriParameter, out string errorUri);\n            if (hasError)\n            {\n                string FormatError(string format)\n                {\n                    if (string.IsNullOrWhiteSpace(errorCode)) errorCode = \"unknown\";\n                    if (string.IsNullOrWhiteSpace(errorDescription)) errorDescription = \"Unknown error.\";\n                    if (string.IsNullOrWhiteSpace(errorUri)) errorUri = \"none\";\n                    return string.Format(format, errorCode, errorDescription, errorUri);\n                }\n\n                // Prefer redirection options to raw HTML\n                if (_options.FailureRedirectFormat != null)\n                {\n                    string failureUrl = FormatError(_options.FailureRedirectFormat.ToString());\n                    response.Redirect(failureUrl);\n                    response.Close();\n                }\n                else\n                {\n                    string failureHtml = FormatError(_options.FailureResponseHtmlFormat ?? OAuth2WebBrowserOptions.DefaultFailureHtmlFormat);\n                    await response.WriteResponseAsync(failureHtml);\n                    response.Close();\n                }\n            }\n            else\n            {\n                // Prefer redirection options to raw HTML\n                if (_options.SuccessRedirect != null)\n                {\n                    string successUrl = _options.SuccessRedirect.ToString();\n                    response.Redirect(successUrl);\n                    response.Close();\n                }\n                else\n                {\n                    string successHtml = _options.SuccessResponseHtml ?? OAuth2WebBrowserOptions.DefaultSuccessHtml;\n                    await response.WriteResponseAsync(successHtml);\n                    response.Close();\n                }\n            }\n        }\n\n        private static int GetFreeTcpPort()\n        {\n            var listener = new TcpListener(IPAddress.Loopback, 0);\n\n            try\n            {\n                listener.Start();\n                return ((IPEndPoint) listener.LocalEndpoint).Port;\n            }\n            finally\n            {\n                listener.Stop();\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuth/OAuth2TokenResult.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Authentication.OAuth\n{\n    public class OAuth2TokenResult\n    {\n        public OAuth2TokenResult(string accessToken, string tokenType)\n        {\n            AccessToken = accessToken;\n            TokenType = tokenType;\n        }\n\n        public string AccessToken { get; }\n\n        public string TokenType { get; }\n\n        public string RefreshToken { get; set; }\n\n        public TimeSpan? ExpiresIn { get; set; }\n\n        public string[] Scopes { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/OAuthAuthentication.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\nusing GitCredentialManager.UI.Views;\n\nnamespace GitCredentialManager.Authentication\n{\n    [Flags]\n    public enum OAuthAuthenticationModes\n    {\n        None        = 0,\n        Browser     = 1 << 0,\n        DeviceCode  = 1 << 1,\n\n        All = Browser | DeviceCode\n    }\n\n    public interface IOAuthAuthentication\n    {\n        Task<OAuthAuthenticationModes> GetAuthenticationModeAsync(string resource, OAuthAuthenticationModes modes);\n\n        Task<OAuth2TokenResult> GetTokenByBrowserAsync(OAuth2Client client, string[] scopes);\n\n        Task<OAuth2TokenResult> GetTokenByDeviceCodeAsync(OAuth2Client client, string[] scopes);\n    }\n\n    public class OAuthAuthentication : AuthenticationBase, IOAuthAuthentication\n    {\n        public OAuthAuthentication(ICommandContext context)\n            : base (context) { }\n\n        public async Task<OAuthAuthenticationModes> GetAuthenticationModeAsync(\n            string resource, OAuthAuthenticationModes modes)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));\n\n            ThrowIfUserInteractionDisabled();\n\n            // Browser requires a desktop session!\n            if (!Context.SessionManager.IsDesktopSession)\n            {\n                modes &= ~OAuthAuthenticationModes.Browser;\n            }\n\n            // We need at least one mode!\n            if (modes == OAuthAuthenticationModes.None)\n            {\n                throw new ArgumentException(@$\"Must specify at least one {nameof(OAuthAuthenticationModes)}\", nameof(modes));\n            }\n\n            // If there is no mode choice to be made then just return that result\n            if (modes == OAuthAuthenticationModes.Browser ||\n                modes == OAuthAuthenticationModes.DeviceCode)\n            {\n                return modes;\n            }\n\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string command, out string args))\n                {\n                    return await GetAuthenticationModeViaHelperAsync(resource, modes, args, command);\n                }\n\n                return await GetAuthenticationModeViaUiAsync(resource, modes);\n            }\n\n            return GetAuthenticationModeViaTty(resource, modes);\n        }\n\n        private async Task<OAuthAuthenticationModes> GetAuthenticationModeViaUiAsync(string resource, OAuthAuthenticationModes modes)\n        {\n            var viewModel = new OAuthViewModel\n            {\n                Description = !string.IsNullOrWhiteSpace(resource)\n                    ? $\"Sign in to '{resource}'\"\n                    : \"Select a sign-in option\",\n                ShowBrowserLogin = (modes & OAuthAuthenticationModes.Browser) != 0,\n                ShowDeviceCodeLogin = (modes & OAuthAuthenticationModes.DeviceCode) != 0,\n            };\n\n            await AvaloniaUi.ShowViewAsync<OAuthView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n            ThrowIfWindowCancelled(viewModel);\n\n            switch (viewModel.SelectedMode)\n            {\n                case OAuthAuthenticationModes.Browser:\n                    return OAuthAuthenticationModes.Browser;\n\n                case OAuthAuthenticationModes.DeviceCode:\n                    return OAuthAuthenticationModes.DeviceCode;\n\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n        }\n\n        private OAuthAuthenticationModes GetAuthenticationModeViaTty(string resource, OAuthAuthenticationModes modes)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            switch (modes)\n            {\n                case OAuthAuthenticationModes.Browser:\n                    return OAuthAuthenticationModes.Browser;\n\n                case OAuthAuthenticationModes.DeviceCode:\n                    return OAuthAuthenticationModes.DeviceCode;\n\n                default:\n                    var menuTitle = $\"Select an authentication method for '{resource}'\";\n                    var menu = new TerminalMenu(Context.Terminal, menuTitle);\n\n                    TerminalMenuItem browserItem = null;\n                    TerminalMenuItem deviceItem = null;\n\n                    if ((modes & OAuthAuthenticationModes.Browser) != 0) browserItem = menu.Add(\"Web browser\");\n                    if ((modes & OAuthAuthenticationModes.DeviceCode) != 0) deviceItem = menu.Add(\"Device code\");\n\n                    // Default to the 'first' choice in the menu\n                    TerminalMenuItem choice = menu.Show(0);\n\n                    if (choice == browserItem) goto case OAuthAuthenticationModes.Browser;\n                    if (choice == deviceItem) goto case OAuthAuthenticationModes.DeviceCode;\n\n                    throw new Exception();\n            }\n        }\n\n        private async Task<OAuthAuthenticationModes> GetAuthenticationModeViaHelperAsync(\n            string resource, OAuthAuthenticationModes modes, string args, string command)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"oauth\");\n\n            if (!string.IsNullOrWhiteSpace(resource))\n            {\n                promptArgs.AppendFormat(\" --resource {0}\", QuoteCmdArg(resource));\n            }\n\n            if ((modes & OAuthAuthenticationModes.Browser) != 0)\n            {\n                promptArgs.Append(\" --browser\");\n            }\n\n            if ((modes & OAuthAuthenticationModes.DeviceCode) != 0)\n            {\n                promptArgs.Append(\" --device-code\");\n            }\n\n            IDictionary<string, string> resultDict = await InvokeHelperAsync(command, promptArgs.ToString());\n\n            if (!resultDict.TryGetValue(\"mode\", out string responseMode))\n            {\n                throw new Trace2Exception(Context.Trace2, \"Missing 'mode' in response\");\n            }\n\n            switch (responseMode.ToLowerInvariant())\n            {\n                case \"browser\":\n                    return OAuthAuthenticationModes.Browser;\n\n                case \"devicecode\":\n                    return OAuthAuthenticationModes.DeviceCode;\n\n                default:\n                    throw new Trace2Exception(Context.Trace2,\n                        $\"Unknown mode value in response '{responseMode}'\");\n            }\n        }\n\n        public async Task<OAuth2TokenResult> GetTokenByBrowserAsync(OAuth2Client client, string[] scopes)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            // We require a desktop session to launch the user's default web browser\n            if (!Context.SessionManager.IsDesktopSession)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"Browser authentication requires a desktop session\");\n            }\n\n            var browserOptions = new OAuth2WebBrowserOptions();\n            var browser = new OAuth2SystemWebBrowser(Context.SessionManager, browserOptions);\n            var authCode = await client.GetAuthorizationCodeAsync(scopes, browser, CancellationToken.None);\n            return await client.GetTokenByAuthorizationCodeAsync(authCode, CancellationToken.None);\n        }\n\n        public async Task<OAuth2TokenResult> GetTokenByDeviceCodeAsync(OAuth2Client client, string[] scopes)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            OAuth2DeviceCodeResult dcr = await client.GetDeviceCodeAsync(scopes, CancellationToken.None);\n\n            // If we have a desktop session show the device code in a dialog\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                var promptCts = new CancellationTokenSource();\n                var tokenCts = new CancellationTokenSource();\n\n                // Show the dialog with the device code but don't await its closure\n                Task promptTask = TryFindHelperCommand(out string command, out string args)\n                    ? ShowDeviceCodeViaHelperAsync(dcr, command, args, promptCts.Token)\n                    : ShowDeviceCodeViaUiAsync(dcr, promptCts.Token);\n\n                // Start the request for an OAuth token but don't wait\n                Task<OAuth2TokenResult> tokenTask = client.GetTokenByDeviceCodeAsync(dcr, tokenCts.Token);\n\n                Task t = await Task.WhenAny(promptTask, tokenTask);\n\n                // If the dialog was closed the user wishes to cancel the request\n                if (t == promptTask)\n                {\n                    tokenCts.Cancel();\n                }\n\n                OAuth2TokenResult tokenResult;\n                try\n                {\n                    tokenResult = await tokenTask;\n                }\n                catch (OperationCanceledException)\n                {\n                    throw new Trace2Exception(Context.Trace2, \"User canceled device code authentication\");\n                }\n\n                // Close the dialog\n                promptCts.Cancel();\n\n                return tokenResult;\n            }\n\n            return await GetTokenByDeviceCodeViaTtyAsync(client, dcr);\n        }\n\n        private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationToken ct)\n        {\n            var viewModel = new DeviceCodeViewModel(Context.SessionManager)\n            {\n                UserCode = dcr.UserCode,\n                VerificationUrl = dcr.VerificationUri.ToString(),\n            };\n\n            return AvaloniaUi.ShowViewAsync<DeviceCodeView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n        }\n\n        private Task ShowDeviceCodeViaHelperAsync(\n            OAuth2DeviceCodeResult dcr, string command, string args, CancellationToken ct)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"device\");\n            promptArgs.AppendFormat(\" --code {0} \", QuoteCmdArg(dcr.UserCode));\n            promptArgs.AppendFormat(\" --url {0}\", QuoteCmdArg(dcr.VerificationUri.ToString()));\n\n            return InvokeHelperAsync(command, promptArgs.ToString(), null, ct);\n        }\n\n        private async Task<OAuth2TokenResult> GetTokenByDeviceCodeViaTtyAsync(OAuth2Client client, OAuth2DeviceCodeResult dcr)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            string deviceMessage =\n                $\"To complete authentication please visit {dcr.VerificationUri} and enter the following code:\" +\n                Environment.NewLine +\n                dcr.UserCode;\n            Context.Terminal.WriteLine(deviceMessage);\n\n            return await client.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None);\n        }\n\n        private bool TryFindHelperCommand(out string command, out string args)\n        {\n            return TryFindHelperCommand(\n                Constants.EnvironmentVariables.GcmUiHelper,\n                Constants.GitConfiguration.Credential.UiHelper,\n                Constants.DefaultUiHelper,\n                out command,\n                out args);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Authentication/WindowsIntegratedAuthentication.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\nusing GitCredentialManager.UI.Views;\n\nnamespace GitCredentialManager.Authentication\n{\n    public interface IWindowsIntegratedAuthentication : IDisposable\n    {\n        Task<NtlmSupport> AskEnableNtlmAsync(Uri uri);\n        Task<WindowsAuthenticationTypes> GetAuthenticationTypesAsync(Uri uri);\n    }\n\n    public enum NtlmSupport\n    {\n        Once,\n        Always,\n        Disabled,\n    }\n\n    [Flags]\n    public enum WindowsAuthenticationTypes\n    {\n        None,\n        Ntlm,\n        Negotiate,\n\n        All = Ntlm | Negotiate\n    }\n\n    public class WindowsIntegratedAuthentication : AuthenticationBase, IWindowsIntegratedAuthentication\n    {\n        public static readonly string[] AuthorityIds =\n        {\n            \"integrated\", \"windows\", \"kerberos\", \"ntlm\",\n            \"tfs\", \"sso\",\n        };\n\n        public WindowsIntegratedAuthentication(ICommandContext context)\n            : base(context) { }\n\n        public async Task<NtlmSupport> AskEnableNtlmAsync(Uri uri)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.SessionManager.IsDesktopSession && Context.Settings.IsGuiPromptsEnabled)\n            {\n                // Note: we do not support the UI helper for WIA so always show the in-proc GUI\n                var vm = new EnableNtlmViewModel(Context.SessionManager)\n                {\n                    Url = uri.ToString(),\n                };\n                await AvaloniaUi.ShowViewAsync<EnableNtlmView>(vm, GetParentWindowHandle(), CancellationToken.None);\n                ThrowIfWindowCancelled(vm);\n\n                return vm.SelectedOption;\n            }\n\n            ThrowIfTerminalPromptsDisabled();\n\n            var menu = new TerminalMenu(Context.Terminal, \"Re-enable NTLM support in Git?\");\n            TerminalMenuItem onceItem = menu.Add(\"Yes - just this time\");\n            TerminalMenuItem alwaysItem = menu.Add($\"Yes - always for {uri}\");\n            TerminalMenuItem noItem = menu.Add(\"No - do not enable NTLM\");\n            TerminalMenuItem choice = menu.Show(0);\n\n            if (choice == onceItem)\n            {\n                return NtlmSupport.Once;\n            }\n\n            if (choice == alwaysItem)\n            {\n                return NtlmSupport.Always;\n            }\n\n            return NtlmSupport.Disabled;\n        }\n\n        public async Task<WindowsAuthenticationTypes> GetAuthenticationTypesAsync(Uri uri)\n        {\n            EnsureArgument.AbsoluteUri(uri, nameof(uri));\n\n            var types = WindowsAuthenticationTypes.None;\n\n            Context.Trace.WriteLine($\"HTTP: HEAD {uri}\");\n            using (HttpResponseMessage response = await HttpClient.HeadAsync(uri))\n            {\n                Context.Trace.WriteLine(\"HTTP: Response code ignored.\");\n\n                Context.Trace.WriteLine(\"Inspecting WWW-Authenticate headers...\");\n                foreach (AuthenticationHeaderValue wwwHeader in response.Headers.WwwAuthenticate)\n                {\n                    if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.Http.WwwAuthenticateNegotiateScheme))\n                    {\n                        Context.Trace.WriteLine(\"Found WWW-Authenticate header for Negotiate\");\n                        types |= WindowsAuthenticationTypes.Negotiate;\n                    }\n                    else if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.Http.WwwAuthenticateNtlmScheme))\n                    {\n                        Context.Trace.WriteLine(\"Found WWW-Authenticate header for NTLM\");\n                        types |= WindowsAuthenticationTypes.Ntlm;\n                    }\n                }\n            }\n\n            return types;\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient => _httpClient ?? (_httpClient = Context.HttpClientFactory.CreateClient());\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Base64UrlConvert.cs",
    "content": "using System;\n\nnamespace GitCredentialManager\n{\n    public static class Base64UrlConvert\n    {\n        public static string Encode(byte[] data, bool includePadding = true)\n        {\n            const char base64PadCharacter = '=';\n            const char base64Character62 = '+';\n            const char base64Character63 = '/';\n            const char base64UrlCharacter62 = '-';\n            const char base64UrlCharacter63 = '_';\n\n            // The base64url format is the same as regular base64 format except:\n            //   1. character 62 is \"-\" (minus) not \"+\" (plus)\n            //   2. character 63 is \"_\" (underscore) not \"/\" (slash)\n            string base64Url = Convert.ToBase64String(data)\n                .Replace(base64Character62, base64UrlCharacter62)\n                .Replace(base64Character63, base64UrlCharacter63);\n\n            return includePadding ? base64Url : base64Url.TrimEnd(base64PadCharacter);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ChildProcess.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager;\n\npublic class ChildProcess : DisposableObject\n{\n    private readonly ITrace2 _trace2;\n\n    private DateTimeOffset _startTime;\n    private DateTimeOffset _exitTime => Process.ExitTime;\n    private ProcessStartInfo _startInfo => Process.StartInfo;\n\n    private int _id => Process.Id;\n\n    public ProcessStartInfo StartInfo => Process.StartInfo;\n    public Process Process { get; }\n    public StreamWriter StandardInput => Process.StandardInput;\n    public StreamReader StandardOutput => Process.StandardOutput;\n    public StreamReader StandardError => Process.StandardError;\n    public int ExitCode => Process.ExitCode;\n\n    public static ChildProcess Start(ITrace2 trace2, ProcessStartInfo startInfo, Trace2ProcessClass processClass)\n    {\n        var childProc = new ChildProcess(trace2, startInfo);\n        childProc.Start(processClass);\n        return childProc;\n    }\n\n    public ChildProcess(ITrace2 trace2, ProcessStartInfo startInfo)\n    {\n        _trace2 = trace2;\n        Process = new Process() { StartInfo = startInfo };\n        Process.Exited += ProcessOnExited;\n    }\n\n    public bool Start(Trace2ProcessClass processClass)\n    {\n        ThrowIfDisposed();\n        // Record the time just before the process starts, since:\n        // (1) There is no event related to Start as there is with Exit.\n        // (2) Using Process.StartTime causes a race condition that leads\n        // to an exception if the process finishes executing before the\n        // variable is passed to Trace2.\n        _startTime = DateTimeOffset.UtcNow;\n        _trace2.WriteChildStart(\n            _startTime,\n            processClass,\n            _startInfo.UseShellExecute,\n            _startInfo.FileName,\n            _startInfo.Arguments);\n        return Process.Start();\n    }\n\n    public void WaitForExit() => Process.WaitForExit();\n\n    public void Kill() => Process.Kill();\n\n    protected override void ReleaseManagedResources()\n    {\n        Process.Exited -= ProcessOnExited;\n        Process.Dispose();\n        base.ReleaseUnmanagedResources();\n    }\n\n    private void ProcessOnExited(object sender, EventArgs e)\n    {\n        if (sender is Process)\n        {\n            double elapsedTime = (_exitTime - _startTime).TotalSeconds;\n            _trace2.WriteChildExit(\n                elapsedTime,\n                _id,\n                Process.ExitCode);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/CommandContext.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing GitCredentialManager.Interop.Linux;\nusing GitCredentialManager.Interop.MacOS;\nusing GitCredentialManager.Interop.Posix;\nusing GitCredentialManager.Interop.Windows;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents the execution environment for a Git credential helper command.\n    /// </summary>\n    public interface ICommandContext : IDisposable\n    {\n        /// <summary>\n        /// Absolute path the application entry executable.\n        /// </summary>\n        string ApplicationPath { get; set; }\n\n        /// <summary>\n        /// Absolute path to the Git Credential Manager installation directory.\n        /// </summary>\n        string InstallationDirectory { get; }\n\n        /// <summary>\n        /// Settings and configuration for Git Credential Manager.\n        /// </summary>\n        ISettings Settings { get; }\n\n        /// <summary>\n        /// Standard I/O text streams, typically connected to the parent Git process.\n        /// </summary>\n        IStandardStreams Streams { get; }\n\n        /// <summary>\n        /// The attached terminal (TTY) to this process tree.\n        /// </summary>\n        ITerminal Terminal { get; }\n\n        /// <summary>\n        /// Provides services regarding user sessions.\n        /// </summary>\n        ISessionManager SessionManager { get; }\n\n        /// <summary>\n        /// Application tracing system.\n        /// </summary>\n        ITrace Trace { get; }\n\n        /// <summary>\n        /// Application TRACE2 tracing system.\n        /// </summary>\n        ITrace2 Trace2 { get; }\n\n        /// <summary>\n        /// File system abstraction (exists mainly for testing).\n        /// </summary>\n        IFileSystem FileSystem { get; }\n\n        /// <summary>\n        /// Secure credential storage.\n        /// </summary>\n        ICredentialStore CredentialStore { get; }\n\n        /// <summary>\n        /// Factory for creating new <see cref=\"System.Net.Http.HttpClient\"/> instances.\n        /// </summary>\n        IHttpClientFactory HttpClientFactory { get; }\n\n        /// <summary>\n        /// Component for interacting with Git.\n        /// </summary>\n        IGit Git { get; }\n\n        /// <summary>\n        /// The current process environment.\n        /// </summary>\n        IEnvironment Environment { get; }\n\n        /// <summary>\n        /// Process manager.\n        /// </summary>\n        IProcessManager ProcessManager { get; }\n    }\n\n    /// <summary>\n    /// Real command execution environment using the actual <see cref=\"Console\"/>, file system calls and environment.\n    /// </summary>\n    public class CommandContext : DisposableObject, ICommandContext\n    {\n        public CommandContext()\n        {\n            ApplicationPath = GetEntryApplicationPath();\n            InstallationDirectory = GetInstallationDirectory();\n\n            Streams = new StandardStreams();\n            Trace   = new Trace();\n            Trace2  = new Trace2(this);\n\n            if (PlatformUtils.IsWindows())\n            {\n                FileSystem        = new WindowsFileSystem();\n                Environment       = new WindowsEnvironment(FileSystem);\n                SessionManager    = new WindowsSessionManager(Trace, Environment, FileSystem);\n                ProcessManager    = new WindowsProcessManager(Trace2);\n                Terminal          = new WindowsTerminal(Trace, Trace2);\n                string gitPath    = GetGitPath(Environment, FileSystem, Trace);\n                Git               = new GitProcess(\n                                            Trace,\n                                            Trace2,\n                                            ProcessManager,\n                                            gitPath,\n                                            FileSystem.GetCurrentDirectory()\n                                        );\n                Settings          = new WindowsSettings(Environment, Git, Trace);\n            }\n            else if (PlatformUtils.IsMacOS())\n            {\n                FileSystem        = new MacOSFileSystem();\n                Environment       = new MacOSEnvironment(FileSystem);\n                SessionManager    = new MacOSSessionManager(Trace, Environment, FileSystem);\n                ProcessManager    = new ProcessManager(Trace2);\n                Terminal          = new MacOSTerminal(Trace, Trace2);\n                string gitPath    = GetGitPath(Environment, FileSystem, Trace);\n                Git               = new GitProcess(\n                                            Trace,\n                                            Trace2,\n                                            ProcessManager,\n                                            gitPath,\n                                            FileSystem.GetCurrentDirectory()\n                                        );\n                Settings          = new MacOSSettings(Environment, Git, Trace);\n            }\n            else if (PlatformUtils.IsLinux())\n            {\n                FileSystem        = new LinuxFileSystem();\n                Environment       = new PosixEnvironment(FileSystem);\n                SessionManager    = new LinuxSessionManager(Trace, Environment, FileSystem);\n                ProcessManager    = new ProcessManager(Trace2);\n                Terminal          = new LinuxTerminal(Trace, Trace2);\n                string gitPath    = GetGitPath(Environment, FileSystem, Trace);\n                Git               = new GitProcess(\n                                            Trace,\n                                            Trace2,\n                                            ProcessManager,\n                                            gitPath,\n                                            FileSystem.GetCurrentDirectory()\n                                        );\n                Settings          = new LinuxSettings(Environment, Git, Trace, FileSystem);\n            }\n            else\n            {\n                throw new PlatformNotSupportedException();\n            }\n\n            HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Trace2, Settings, Streams);\n            CredentialStore   = new CredentialStore(this);\n        }\n\n        private static string GetGitPath(IEnvironment environment, IFileSystem fileSystem, ITrace trace)\n        {\n            const string unixGitName = \"git\";\n            const string winGitName = \"git.exe\";\n\n            string gitExecPath;\n            string programName = PlatformUtils.IsWindows() ? winGitName : unixGitName;\n\n            // Use the GIT_EXEC_PATH environment variable if set\n            if (environment.Variables.TryGetValue(Constants.EnvironmentVariables.GitExecutablePath,\n                out gitExecPath))\n            {\n                // If we're invoked from WSL we must locate the UNIX Git executable\n                if (PlatformUtils.IsWindows() && WslUtils.IsWslPath(gitExecPath))\n                {\n                    programName = unixGitName;\n                }\n\n                string candidatePath = Path.Combine(gitExecPath, programName);\n                if (fileSystem.FileExists(candidatePath))\n                {\n                    trace.WriteLine($\"Using Git executable from GIT_EXEC_PATH: {candidatePath}\");\n                    return candidatePath;\n                }\n            }\n\n            // Otherwise try to locate the git(.exe) on the current PATH\n            gitExecPath = environment.LocateExecutable(programName);\n            trace.WriteLine($\"Using PATH-located Git executable: {gitExecPath}\");\n            return gitExecPath;\n        }\n\n        #region ICommandContext\n\n        public string ApplicationPath { get; set; }\n\n        public string InstallationDirectory { get; }\n\n        public ISettings Settings { get; }\n\n        public IStandardStreams Streams { get; }\n\n        public ITerminal Terminal { get; }\n\n        public ISessionManager SessionManager { get; }\n\n        public ITrace Trace { get; }\n\n        public ITrace2 Trace2 { get; }\n\n        public IFileSystem FileSystem { get; }\n\n        public ICredentialStore CredentialStore { get; }\n\n        public IHttpClientFactory HttpClientFactory { get; }\n\n        public IGit Git { get; }\n\n        public IEnvironment Environment { get; }\n\n        public IProcessManager ProcessManager { get; }\n\n        #endregion\n\n        #region IDisposable\n\n        protected override void ReleaseManagedResources()\n        {\n            Settings?.Dispose();\n            Trace?.Dispose();\n\n            base.ReleaseManagedResources();\n        }\n\n        #endregion\n\n        public static string GetEntryApplicationPath()\n        {\n            return PlatformUtils.GetNativeEntryPath() ??\n                   Process.GetCurrentProcess().MainModule?.FileName ??\n                   System.Environment.GetCommandLineArgs()[0];\n        }\n\n        public static string GetInstallationDirectory()\n        {\n            return AppContext.BaseDirectory;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/CommandExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Diagnostics;\nusing System.Linq;\n\nnamespace GitCredentialManager;\n\npublic static class CommandExtensions\n{\n    /// <summary>\n    /// Add options to a command.\n    /// </summary>\n    /// <param name=\"command\">Command to add options to.</param>\n    /// <param name=\"arity\">Specify the required arity of the options.</param>\n    /// <param name=\"options\">Set of options to add.</param>\n    public static void AddOptionSet(this Command command, OptionArity arity, params Option[] options)\n    {\n        foreach (Option option in options)\n        {\n            command.AddOption(option);\n        }\n\n        // No need to add a validator if 0..* options are OK\n        if (arity != OptionArity.ZeroOrMore)\n        {\n            command.AddValidator(r =>\n            {\n                int count = options.Count(o => r.FindResultFor(o) is not null);\n                string optionList = string.Join(\", \", options.Select(s => $\"--{s.Name}\"));\n                switch (arity)\n                {\n                    case OptionArity.ZeroOrOne:\n                        if (count > 1)\n                            r.ErrorMessage = $\"Can only specify one of: {optionList}\";\n                        break;\n\n                    case OptionArity.ExactlyOne:\n                        if (count != 1)\n                            r.ErrorMessage = $\"Require exactly one of: {optionList}\";\n                        break;\n\n                    case OptionArity.Zero:\n                        if (count != 0)\n                        {\n                            IEnumerable<string> usedOptions = options.Where(o => r.FindResultFor(o) is not null)\n                                .Select(x => $\"--{x.Name}\");\n                            r.ErrorMessage = $\"{command.Name} does not support options: {string.Join(\", \", usedOptions)}\";\n                        }\n                        break;\n\n                    case OptionArity.OneOrMore:\n                        if (count == 0)\n                            r.ErrorMessage = $\"Require at least one of: {optionList}\";\n                        break;\n\n                    case OptionArity.ZeroOrMore:\n                        Debug.Fail(\"Should not have a validator for an arity of ZeroOrMore.\");\n                        break;\n\n                    default:\n                        throw new ArgumentOutOfRangeException(nameof(arity), arity, null);\n                }\n            });\n        }\n    }\n}\n\npublic enum OptionArity\n{\n    /// <summary>\n    /// An arity that may have multiple values.\n    /// </summary>\n    ZeroOrMore = 0,\n\n    /// <summary>\n    /// An arity that must have exactly one value.\n    /// </summary>\n    ExactlyOne,\n\n    /// <summary>\n    /// An arity that does not allow any values.\n    /// </summary>\n    Zero,\n\n    /// <summary>\n    /// An arity that may have one value, but no more than one.\n    /// </summary>\n    ZeroOrOne,\n\n    /// <summary>\n    /// An arity that must have at least one value.\n    /// </summary>\n    OneOrMore,\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/ConfigurationCommands.cs",
    "content": "using System.CommandLine;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Commands\n{\n    public abstract class ConfigurationCommandBase : Command\n    {\n        protected ConfigurationCommandBase(ICommandContext context, string name, string description, IConfigurationService configurationService)\n            : base(name, description)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n            EnsureArgument.NotNull(configurationService, nameof(configurationService));\n\n            Context = context;\n            ConfigurationService = configurationService;\n\n            var system = new Option<bool>(\"--system\", \"Modify the system-wide Git configuration instead of the current user\");\n            AddOption(system);\n\n            this.SetHandler(ExecuteAsync, system);\n        }\n\n        protected ICommandContext Context { get; }\n\n        protected IConfigurationService ConfigurationService { get; }\n\n        internal Task ExecuteAsync(bool system)\n        {\n            var target = system\n                ? ConfigurationTarget.System\n                : ConfigurationTarget.User;\n\n            return ExecuteInternalAsync(target);\n        }\n\n        protected abstract Task ExecuteInternalAsync(ConfigurationTarget target);\n    }\n\n    public class ConfigureCommand : ConfigurationCommandBase\n    {\n        public ConfigureCommand(ICommandContext context, IConfigurationService configurationService)\n            : base(context, \"configure\", \"Configure Git Credential Manager as the Git credential helper\", configurationService) { }\n\n        protected override Task ExecuteInternalAsync(ConfigurationTarget target)\n        {\n            return ConfigurationService.ConfigureAsync(target);\n        }\n    }\n\n    public class UnconfigureCommand : ConfigurationCommandBase\n    {\n        public UnconfigureCommand(ICommandContext context, IConfigurationService configurationService)\n            : base(context, \"unconfigure\", \"Unconfigure Git Credential Manager as the Git credential helper\", configurationService) { }\n\n        protected override Task ExecuteInternalAsync(ConfigurationTarget target)\n        {\n            return ConfigurationService.UnconfigureAsync(target);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/DiagnoseCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.IO;\nusing System.Text;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Diagnostics;\n\nnamespace GitCredentialManager.Commands\n{\n    public class DiagnoseCommand : Command\n    {\n        private const string TestOutputIndent = \"    \";\n\n        private readonly ICommandContext _context;\n        private readonly ICollection<IDiagnostic> _diagnostics;\n\n        public DiagnoseCommand(ICommandContext context)\n            : base(\"diagnose\", \"Run diagnostics and gather logs to diagnose problems with Git Credential Manager\")\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n            _diagnostics = new List<IDiagnostic>\n            {\n                // Add standard diagnostics\n                new EnvironmentDiagnostic(context),\n                new FileSystemDiagnostic(context),\n                new NetworkingDiagnostic(context),\n                new GitDiagnostic(context),\n                new CredentialStoreDiagnostic(context),\n                new MicrosoftAuthenticationDiagnostic(context)\n            };\n\n            var output = new Option<string>(new[] { \"--output\", \"-o\" }, \"Output directory for diagnostic logs.\");\n            AddOption(output);\n\n            this.SetHandler(ExecuteAsync, output);\n        }\n\n        public void AddDiagnostic(IDiagnostic diagnostic)\n        {\n            _diagnostics.Add(diagnostic);\n        }\n\n        private async Task<int> ExecuteAsync(string output)\n        {\n            // Don't use IStandardStreams for writing output in this command as we\n            // cannot trust any component on the ICommandContext is working correctly.\n            Console.WriteLine($\"Running diagnostics...{Environment.NewLine}\");\n\n            if (_diagnostics.Count == 0)\n            {\n                Console.WriteLine(\"No diagnostics to run.\");\n                return 0;\n            }\n\n            int numFailed = 0;\n            int numSkipped = 0;\n\n            string currentDir = Directory.GetCurrentDirectory();\n            string outputDir;\n            if (string.IsNullOrWhiteSpace(output))\n            {\n                outputDir = currentDir;\n            }\n            else\n            {\n                if (!Directory.Exists(output))\n                {\n                    Directory.CreateDirectory(output);\n                }\n\n                outputDir = Path.GetFullPath(Path.Combine(currentDir, output));\n            }\n\n            string logFilePath = Path.Combine(outputDir, \"gcm-diagnose.log\");\n            var extraLogs = new List<string>();\n\n            using var fullLog = new StreamWriter(logFilePath, append: false, Encoding.UTF8);\n            fullLog.WriteLine(\"Diagnose log at {0:s}Z\", DateTime.UtcNow);\n            fullLog.WriteLine();\n            fullLog.WriteLine($\"AppPath: {_context.ApplicationPath}\");\n            fullLog.WriteLine($\"InstallDir: {_context.InstallationDirectory}\");\n            fullLog.WriteLine(\n                AssemblyUtils.TryGetAssemblyVersion(out string version)\n                    ? $\"Version: {version}\"\n                    : \"Version: [!] Failed to get version information [!]\"\n            );\n            fullLog.WriteLine();\n\n            foreach (IDiagnostic diagnostic in _diagnostics)\n            {\n                fullLog.WriteLine(\"------------\");\n                fullLog.WriteLine($\"Diagnostic: {diagnostic.Name}\");\n\n                if (!diagnostic.CanRun())\n                {\n                    fullLog.WriteLine(\"Skipped: True\");\n                    fullLog.WriteLine();\n\n                    Console.Write(\" \");\n                    ConsoleEx.WriteColor(\"[SKIP]\", ConsoleColor.Gray);\n                    Console.WriteLine(\" {0}\", diagnostic.Name);\n\n                    numSkipped++;\n                    continue;\n                }\n\n                string inProgressMsg = $\"  >>>>  {diagnostic.Name}\";\n                Console.Write(inProgressMsg);\n\n                fullLog.WriteLine(\"Skipped: False\");\n                DiagnosticResult result = await diagnostic.RunAsync();\n                fullLog.WriteLine(\"Success: {0}\", result.IsSuccess);\n\n                if (result.Exception is null)\n                {\n                    fullLog.WriteLine(\"Exception: None\");\n                }\n                else\n                {\n                    fullLog.WriteLine(\"Exception:\");\n                    fullLog.WriteLine(result.Exception.ToString());\n                }\n\n                fullLog.WriteLine(\"Log:\");\n                fullLog.WriteLine(result.DiagnosticLog);\n\n                Console.Write(new string('\\b', inProgressMsg.Length - 1));\n                ConsoleEx.WriteColor(\n                    result.IsSuccess ? \"[ OK ]\" : \"[FAIL]\",\n                    result.IsSuccess ? ConsoleColor.DarkGreen : ConsoleColor.Red\n                );\n                Console.WriteLine(\" {0}\", diagnostic.Name);\n\n                if (!result.IsSuccess)\n                {\n                    numFailed++;\n\n                    if (result.Exception is not null)\n                    {\n                        Console.WriteLine();\n                        ConsoleEx.WriteLineIndent(\"[!] Encountered an exception [!]\");\n                        ConsoleEx.WriteLineIndent(result.Exception.ToString());\n                    }\n\n                    Console.WriteLine();\n                    ConsoleEx.WriteLineIndent(\"[*] Diagnostic test log [*]\");\n                    ConsoleEx.WriteLineIndent(result.DiagnosticLog);\n\n                    Console.WriteLine();\n                }\n\n                foreach (string filePath in result.AdditionalFiles)\n                {\n                    string fileName = Path.GetFileName(filePath);\n                    string destPath = Path.Combine(outputDir, fileName);\n                    try\n                    {\n                        File.Copy(filePath, destPath, overwrite: true);\n                    }\n                    catch\n                    {\n                        ConsoleEx.WriteLineIndent($\"Failed to copy additional file '{filePath}'\");\n                    }\n\n                    extraLogs.Add(destPath);\n                }\n\n                fullLog.Flush();\n            }\n\n            Console.WriteLine();\n            string summary = $\"Diagnostic summary: {_diagnostics.Count - numFailed} passed, {numSkipped} skipped, {numFailed} failed.\";\n            Console.WriteLine(summary);\n            Console.WriteLine(\"Log files:\");\n            Console.WriteLine($\"  {logFilePath}\");\n            foreach (string log in extraLogs)\n            {\n                Console.WriteLine($\"  {log}\");\n            }\n            Console.WriteLine();\n            Console.WriteLine(\"Caution: Log files may include sensitive information - redact before sharing.\");\n            Console.WriteLine();\n\n            if (numFailed > 0)\n            {\n                Console.WriteLine(\"Diagnostics indicate a possible problem with your installation.\");\n                Console.WriteLine($\"Please open an issue at {Constants.HelpUrls.GcmNewIssue} and include log files.\");\n                Console.WriteLine();\n            }\n\n            fullLog.Close();\n            return numFailed;\n        }\n\n        private static class ConsoleEx\n        {\n            public static void WriteLineIndent(string str)\n            {\n                string[] lines = str?.Split('\\n', '\\r');\n\n                if (lines is null) return;\n\n                foreach (string line in lines)\n                {\n                    Console.Write(TestOutputIndent);\n                    Console.WriteLine(line);\n                }\n            }\n\n            public static  void WriteColor(string str, ConsoleColor fgColor)\n            {\n                var initFgColor = Console.ForegroundColor;\n                Console.ForegroundColor = fgColor;\n                Console.Write(str);\n                Console.ForegroundColor = initFgColor;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/EraseCommand.cs",
    "content": "using System.Threading.Tasks;\n\nnamespace GitCredentialManager.Commands\n{\n    /// <summary>\n    /// Erase a previously stored <see cref=\"GitCredential\"/> from the OS secure credential store.\n    /// </summary>\n    public class EraseCommand : GitCommandBase\n    {\n        public EraseCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry)\n            : base(context, \"erase\", \"[Git] Erase a stored credential\", hostProviderRegistry) { }\n\n        protected override Task ExecuteInternalAsync(InputArguments input, IHostProvider provider)\n        {\n            return provider.EraseCredentialAsync(input);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/GetCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Commands\n{\n    /// <summary>\n    /// Acquire a new <see cref=\"GitCredential\"/> from a <see cref=\"IHostProvider\"/>.\n    /// </summary>\n    public class GetCommand : GitCommandBase\n    {\n        public GetCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry)\n            : base(context, \"get\", \"[Git] Return a stored credential\", hostProviderRegistry) { }\n\n        protected override async Task ExecuteInternalAsync(InputArguments input, IHostProvider provider)\n        {\n            GetCredentialResult result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            var output = new Dictionary<string, string>();\n\n            // Echo protocol, host, and path back at Git\n            if (input.Protocol != null)\n            {\n                output[\"protocol\"] = input.Protocol;\n            }\n            if (input.Host != null)\n            {\n                output[\"host\"] = input.Host;\n            }\n            if (input.Path != null)\n            {\n                output[\"path\"] = input.Path;\n            }\n\n            // Return the credential to Git\n            output[\"username\"] = credential.Account;\n            output[\"password\"] = credential.Password;\n\n            // Write any additional output from the provider\n            foreach (var kvp in result.AdditionalProperties)\n            {\n                output[kvp.Key] = kvp.Value;\n            }\n\n            Context.Trace.WriteLine(\"Writing credentials to output:\");\n            Context.Trace.WriteDictionarySecrets(output, new []{ \"password\" }, StringComparer.OrdinalIgnoreCase);\n\n            // Write the values to standard out\n            Context.Streams.Out.WriteDictionary(output);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/GitCommandBase.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Commands\n{\n    /// <summary>\n    /// Represents a command which selects a <see cref=\"IHostProvider\"/> from a <see cref=\"IHostProviderRegistry\"/>\n    /// based on the <see cref=\"InputArguments\"/> from standard input, and interacts with a <see cref=\"GitCredential\"/>.\n    /// </summary>\n    public abstract class GitCommandBase : Command\n    {\n        private readonly IHostProviderRegistry _hostProviderRegistry;\n\n        protected GitCommandBase(ICommandContext context, string name, string description, IHostProviderRegistry hostProviderRegistry)\n            : base(name, description)\n        {\n            EnsureArgument.NotNull(hostProviderRegistry, nameof(hostProviderRegistry));\n            EnsureArgument.NotNull(context, nameof(context));\n\n            Context = context;\n            _hostProviderRegistry = hostProviderRegistry;\n\n            this.SetHandler(ExecuteAsync);\n        }\n\n        protected ICommandContext Context { get; }\n\n        internal async Task ExecuteAsync()\n        {\n            Context.Trace.WriteLine($\"Start '{Name}' command...\");\n\n            // Parse standard input arguments\n            // git-credential treats the keys as case-sensitive; so should we.\n            IDictionary<string, IList<string>> inputDict = await Context.Streams.In.ReadMultiDictionaryAsync(StringComparer.Ordinal);\n            var input = new InputArguments(inputDict);\n\n            // Validate minimum arguments are present\n            EnsureMinimumInputArguments(input);\n\n            // Set the remote URI to scope settings to throughout the process from now on\n            Context.Settings.RemoteUri = input.GetRemoteUri();\n\n            // Determine the host provider\n            Context.Trace.WriteLine(\"Detecting host provider for input:\");\n            Context.Trace.WriteDictionarySecrets(inputDict, new []{ \"password\" }, StringComparer.OrdinalIgnoreCase);\n            IHostProvider provider = await _hostProviderRegistry.GetProviderAsync(input);\n            Context.Trace.WriteLine($\"Host provider '{provider.Name}' was selected.\");\n\n            await ExecuteInternalAsync(input, provider);\n\n            Context.Trace.WriteLine($\"End '{Name}' command...\");\n        }\n\n        protected virtual void EnsureMinimumInputArguments(InputArguments input)\n        {\n            if (input.Protocol is null)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Missing 'protocol' input argument\");\n            }\n\n            if (string.IsNullOrWhiteSpace(input.Protocol))\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"Invalid 'protocol' input argument (cannot be empty)\");\n            }\n\n            if (input.Host is null)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Missing 'host' input argument\");\n            }\n\n            if (string.IsNullOrWhiteSpace(input.Host))\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"Invalid 'host' input argument (cannot be empty)\");\n            }\n        }\n\n        /// <summary>\n        /// Execute the command using the given <see cref=\"InputArguments\"/> and <see cref=\"IHostProvider\"/>.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of the current Git credential query.</param>\n        /// <param name=\"provider\">Host provider for the current <see cref=\"InputArguments\"/>.</param>\n        /// <returns>Awaitable task for the command execution.</returns>\n        protected abstract Task ExecuteInternalAsync(InputArguments input, IHostProvider provider);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/ProviderCommand.cs",
    "content": "using System.CommandLine;\n\nnamespace GitCredentialManager.Commands\n{\n    public interface ICommandProvider\n    {\n        /// <summary>\n        /// Create a custom provider command.\n        /// </summary>\n        ProviderCommand CreateCommand();\n    }\n\n    public class ProviderCommand : Command\n    {\n        public ProviderCommand(IHostProvider provider)\n            : base(provider.Id, $\"Commands for interacting with the {provider.Name} host provider\")\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Commands/StoreCommand.cs",
    "content": "using System;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Commands\n{\n    /// <summary>\n    /// Store a previously created <see cref=\"GitCredential\"/> in the OS secure credential store.\n    /// </summary>\n    public class StoreCommand : GitCommandBase\n    {\n        public StoreCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry)\n            : base(context, \"store\", \"[Git] Store a credential\", hostProviderRegistry) { }\n\n        protected override Task ExecuteInternalAsync(InputArguments input, IHostProvider provider)\n        {\n            return provider.StoreCredentialAsync(input);\n        }\n\n        protected override void EnsureMinimumInputArguments(InputArguments input)\n        {\n            base.EnsureMinimumInputArguments(input);\n\n            // An empty string username/password are valid inputs, so only check for `null` (not provided)\n            if (input.UserName is null)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Missing 'username' input argument\");\n            }\n\n            if (input.Password is null)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2, \"Missing 'password' input argument\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ConfigurationService.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Targets for performing configuration changes using the <see cref=\"IConfigurationService\"/>.\n    /// </summary>\n    public enum ConfigurationTarget\n    {\n        /// <summary>\n        /// Target configuration changes for the current user only.\n        /// </summary>\n        User,\n\n        /// <summary>\n        /// Target configuration changes for all users on the system.\n        /// </summary>\n        System,\n    }\n\n    /// <summary>\n    /// Component that requires Git or environment configuration to work.\n    /// </summary>\n    public interface IConfigurableComponent\n    {\n        /// <summary>\n        /// Name of the component that requires configuration.\n        /// </summary>\n        string Name { get; }\n\n        /// <summary>\n        /// Configure the environment and Git to work with this hosting provider.\n        /// </summary>\n        /// <param name=\"target\">Configuration target.</param>\n        Task ConfigureAsync(ConfigurationTarget target);\n\n        /// <summary>\n        /// Remove changes to the environment and Git configuration previously made with <see cref=\"ConfigureAsync\"/>.\n        /// </summary>\n        /// <param name=\"target\">Configuration target.</param>\n        Task UnconfigureAsync(ConfigurationTarget target);\n    }\n\n    public interface IConfigurationService\n    {\n        /// <summary>\n        /// Add a <see cref=\"IConfigurableComponent\"/> to the collection of components that will be configured.\n        /// </summary>\n        /// <param name=\"component\"></param>\n        void AddComponent(IConfigurableComponent component);\n\n        /// <summary>\n        /// Configure all components.\n        /// </summary>\n        /// <param name=\"target\">Target level to configure.</param>\n        Task ConfigureAsync(ConfigurationTarget target);\n\n        /// <summary>\n        /// Unconfigure all components.\n        /// </summary>\n        /// <param name=\"target\">Target level to unconfigure.</param>\n        Task UnconfigureAsync(ConfigurationTarget target);\n    }\n\n    public class ConfigurationService : IConfigurationService\n    {\n        private readonly ICommandContext _context;\n        private readonly IList<IConfigurableComponent> _components = new List<IConfigurableComponent>();\n\n        public ConfigurationService(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n        }\n\n        #region IConfigurationService\n\n        public void AddComponent(IConfigurableComponent component)\n        {\n            _components.Add(component);\n        }\n\n        public async Task ConfigureAsync(ConfigurationTarget target)\n        {\n            foreach (IConfigurableComponent component in _components)\n            {\n                _context.Trace.WriteLine($\"Configuring component '{component.Name}'...\");\n                _context.Streams.Error.WriteLine($\"Configuring component '{component.Name}'...\");\n                await component.ConfigureAsync(target);\n            }\n        }\n\n        public async Task UnconfigureAsync(ConfigurationTarget target)\n        {\n            foreach (IConfigurableComponent component in _components)\n            {\n                _context.Trace.WriteLine($\"Unconfiguring component '{component.Name}'...\");\n                _context.Streams.Error.WriteLine($\"Unconfiguring component '{component.Name}'...\");\n                await component.UnconfigureAsync(target);\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Constants.cs",
    "content": "using System;\nusing System.Reflection;\n\nnamespace GitCredentialManager\n{\n    public static class Constants\n    {\n        public const string DefaultWindowTitle = \"Git Credential Manager\";\n        public const string PersonalAccessTokenUserName = \"PersonalAccessToken\";\n        public const string DefaultCredentialNamespace = \"git\";\n        public const int DefaultAutoDetectProviderTimeoutMs = 2000; // 2 seconds\n        public const string DefaultUiHelper = \"git-credential-manager-ui\";\n\n        public const string ProviderIdAuto  = \"auto\";\n        public const string AuthorityIdAuto = \"auto\";\n\n        public const string GcmDataDirectoryName = \".gcm\";\n        public const string LinuxAppDefaultsDirectoryPath = \"/etc/git-credential-manager/config.d\";\n\n        public const string MacOSBundleId = \"git-credential-manager\";\n        public static readonly Guid DevBoxPartnerId = new(\"e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf\");\n\n        /// <summary>\n        /// Home tenant ID for Microsoft Accounts (MSA).\n        /// </summary>\n        public static readonly Guid MsaHomeTenantId = new(\"9188040d-6c67-4c5b-b112-36a304b66dad\");\n\n        /// <summary>\n        /// Special tenant ID for transferring between Microsoft Account (MSA) native tokens\n        /// and AAD tokens. Only required for MSA-Passthrough applications.\n        /// </summary>\n        public static readonly Guid MsaTransferTenantId = new(\"f8cdef31-a31e-4b4a-93e4-5f571e91255a\");\n\n        public static class CredentialProtocol\n        {\n            public const string NtlmKey = \"ntlm\";\n            public const string NtlmSuppressed = \"suppressed\";\n            public const string NtlmAllow = \"allow\";\n        }\n\n        public static class CredentialStoreNames\n        {\n            public const string WindowsCredentialManager = \"wincredman\";\n            public const string Dpapi = \"dpapi\";\n            public const string MacOSKeychain = \"keychain\";\n            public const string Gpg = \"gpg\";\n            public const string SecretService = \"secretservice\";\n            public const string Plaintext = \"plaintext\";\n            public const string Cache = \"cache\";\n            public const string None = \"none\";\n        }\n\n        public static class RegexPatterns\n        {\n            /// <summary>\n            /// A regular expression that matches any value.\n            /// </summary>\n            public const string Any = @\".*\";\n\n            /// <summary>\n            /// A regular expression that matches no value.\n            /// </summary>\n            public const string None = @\"$.+\";\n\n            /// <summary>\n            /// A regular expression that matches empty strings.\n            /// </summary>\n            public const string Empty = @\"^$\";\n        }\n\n        public static class EnvironmentVariables\n        {\n            public const string GcmTrace              = \"GCM_TRACE\";\n            public const string GcmTraceSecrets       = \"GCM_TRACE_SECRETS\";\n            public const string GcmTraceMsAuth        = \"GCM_TRACE_MSAUTH\";\n            public const string GcmDebug              = \"GCM_DEBUG\";\n            public const string GcmProvider           = \"GCM_PROVIDER\";\n            public const string GcmAuthority          = \"GCM_AUTHORITY\";\n            public const string GitTerminalPrompts    = \"GIT_TERMINAL_PROMPT\";\n            public const string GcmAllowWia           = \"GCM_ALLOW_WINDOWSAUTH\";\n            public const string GitTrace2Event        = \"GIT_TRACE2_EVENT\";\n            public const string GitTrace2Normal       = \"GIT_TRACE2\";\n            public const string GitTrace2Performance  = \"GIT_TRACE2_PERF\";\n\n            /*\n             * Unlike other environment variables, these proxy variables are normally lowercase only.\n             * However, libcurl also implemented checks for the uppercase variants! The lowercase\n             * variants should take precedence over the uppercase ones since the former are quasi-standard.\n             *\n             * One exception to this is that libcurl does not even look for the uppercase variant of\n             * http_proxy (for some security reasons).\n             */\n            public const string CurlNoProxy           = \"no_proxy\";\n            public const string CurlNoProxyUpper      = \"NO_PROXY\";\n            public const string CurlHttpsProxy        = \"https_proxy\";\n            public const string CurlHttpsProxyUpper   = \"HTTPS_PROXY\";\n            public const string CurlHttpProxy         = \"http_proxy\";\n            // Note there is no uppercase variant of the http_proxy since libcurl doesn't use it\n            public const string CurlAllProxy          = \"all_proxy\";\n            public const string CurlAllProxyUpper     = \"ALL_PROXY\";\n\n            public const string GcmHttpProxy          = \"GCM_HTTP_PROXY\";\n            public const string GitSslNoVerify        = \"GIT_SSL_NO_VERIFY\";\n            public const string GitSslCaInfo          = \"GIT_SSL_CAINFO\";\n            public const string GcmInteractive        = \"GCM_INTERACTIVE\";\n            public const string GcmParentWindow       = \"GCM_MODAL_PARENTHWND\";\n            public const string MsAuthFlow            = \"GCM_MSAUTH_FLOW\";\n            public const string MsAuthUseBroker       = \"GCM_MSAUTH_USEBROKER\";\n            public const string MsAuthUseDefaultAccount = \"GCM_MSAUTH_USEDEFAULTACCOUNT\";\n            public const string GcmCredNamespace      = \"GCM_NAMESPACE\";\n            public const string GcmCredentialStore    = \"GCM_CREDENTIAL_STORE\";\n            public const string GcmCredCacheOptions   = \"GCM_CREDENTIAL_CACHE_OPTIONS\";\n            public const string GcmPlaintextStorePath = \"GCM_PLAINTEXT_STORE_PATH\";\n            public const string GcmDpapiStorePath     = \"GCM_DPAPI_STORE_PATH\";\n            public const string GitExecutablePath     = \"GIT_EXEC_PATH\";\n            public const string GpgExecutablePath     = \"GCM_GPG_PATH\";\n            public const string GcmAutoDetectTimeout  = \"GCM_AUTODETECT_TIMEOUT\";\n            public const string GcmGuiPromptsEnabled  = \"GCM_GUI_PROMPT\";\n            public const string GcmUiHelper           = \"GCM_UI_HELPER\";\n            public const string OAuthAuthenticationModes = \"GCM_OAUTH_AUTHMODES\";\n            public const string OAuthClientId            = \"GCM_OAUTH_CLIENTID\";\n            public const string OAuthClientSecret        = \"GCM_OAUTH_CLIENTSECRET\";\n            public const string OAuthRedirectUri         = \"GCM_OAUTH_REDIRECTURI\";\n            public const string OAuthScopes              = \"GCM_OAUTH_SCOPES\";\n            public const string OAuthAuthzEndpoint       = \"GCM_OAUTH_AUTHORIZE_ENDPOINT\";\n            public const string OAuthTokenEndpoint       = \"GCM_OAUTH_TOKEN_ENDPOINT\";\n            public const string OAuthDeviceEndpoint      = \"GCM_OAUTH_DEVICE_ENDPOINT\";\n            public const string OAuthClientAuthHeader    = \"GCM_OAUTH_USE_CLIENT_AUTH_HEADER\";\n            public const string OAuthDefaultUserName     = \"GCM_OAUTH_DEFAULT_USERNAME\";\n            public const string GcmDevUseLegacyUiHelpers = \"GCM_DEV_USELEGACYUIHELPERS\";\n            public const string GcmGuiSoftwareRendering  = \"GCM_GUI_SOFTWARE_RENDERING\";\n            public const string GcmAllowUnsafeRemotes    = \"GCM_ALLOW_UNSAFE_REMOTES\";\n        }\n\n        public static class Http\n        {\n            public const string WwwAuthenticateBasicScheme     = \"Basic\";\n            public const string WwwAuthenticateBearerScheme    = \"Bearer\";\n            public const string WwwAuthenticateNegotiateScheme = \"Negotiate\";\n            public const string WwwAuthenticateNtlmScheme      = \"NTLM\";\n\n            public const string MimeTypeJson = \"application/json\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string SectionName = \"credential\";\n                public const string Helper      = \"helper\";\n                public const string Trace       = \"trace\";\n                public const string TraceSecrets = \"traceSecrets\";\n                public const string TraceMsAuth = \"traceMsAuth\";\n                public const string Debug       = \"debug\";\n                public const string Provider    = \"provider\";\n                public const string Authority   = \"authority\";\n                public const string AllowWia    = \"allowWindowsAuth\";\n                public const string HttpProxy   = \"httpProxy\";\n                public const string HttpsProxy  = \"httpsProxy\";\n                public const string UseHttpPath = \"useHttpPath\";\n                public const string Interactive = \"interactive\";\n                public const string MsAuthFlow  = \"msauthFlow\";\n                public const string MsAuthUseBroker = \"msauthUseBroker\";\n                public const string CredNamespace = \"namespace\";\n                public const string CredentialStore = \"credentialStore\";\n                public const string CredCacheOptions = \"cacheOptions\";\n                public const string PlaintextStorePath = \"plaintextStorePath\";\n                public const string DpapiStorePath = \"dpapiStorePath\";\n                public const string UserName = \"username\";\n                public const string AutoDetectTimeout = \"autoDetectTimeout\";\n                public const string GuiPromptsEnabled = \"guiPrompt\";\n                public const string UiHelper = \"uiHelper\";\n                public const string DevUseLegacyUiHelpers = \"devUseLegacyUiHelpers\";\n                public const string MsAuthUseDefaultAccount = \"msauthUseDefaultAccount\";\n                public const string GuiSoftwareRendering = \"guiSoftwareRendering\";\n                public const string GpgPassStorePath = \"gpgPassStorePath\";\n                public const string AllowUnsafeRemotes = \"allowUnsafeRemotes\";\n\n                public const string OAuthAuthenticationModes = \"oauthAuthModes\";\n                public const string OAuthClientId            = \"oauthClientId\";\n                public const string OAuthClientSecret        = \"oauthClientSecret\";\n                public const string OAuthRedirectUri         = \"oauthRedirectUri\";\n                public const string OAuthScopes              = \"oauthScopes\";\n                public const string OAuthAuthzEndpoint       = \"oauthAuthorizeEndpoint\";\n                public const string OAuthTokenEndpoint       = \"oauthTokenEndpoint\";\n                public const string OAuthDeviceEndpoint      = \"oauthDeviceEndpoint\";\n                public const string OAuthClientAuthHeader    = \"oauthUseClientAuthHeader\";\n                public const string OAuthDefaultUserName     = \"oauthDefaultUserName\";\n            }\n\n            public static class Http\n            {\n                public const string SectionName = \"http\";\n                public const string Proxy = \"proxy\";\n                public const string SchannelUseSslCaInfo = \"schannelUseSSLCAInfo\";\n                public const string SslBackend = \"sslBackend\";\n                public const string SslVerify = \"sslVerify\";\n                public const string SslCaInfo = \"sslCAInfo\";\n                public const string SslAutoClientCert = \"sslAutoClientCert\";\n                public const string CookieFile = \"cookieFile\";\n                public const string AllowNtlmAuth = \"allowNTLMAuth\";\n            }\n\n            public static class Remote\n            {\n                public const string SectionName = \"remote\";\n                public const string FetchUrl = \"url\";\n                public const string PushUrl = \"pushUrl\";\n            }\n\n            public static class Trace2\n            {\n                public const string SectionName       = \"trace2\";\n                public const string EventTarget       = \"eventtarget\";\n                public const string NormalTarget      = \"normaltarget\";\n                public const string PerformanceTarget = \"perftarget\";\n            }\n        }\n\n        public static class WindowsRegistry\n        {\n            public const string HKAppBasePath = @\"SOFTWARE\\GitCredentialManager\";\n            public const string HKConfigurationPath = HKAppBasePath + @\"\\Configuration\";\n\n            public const string HKWindows365Path = @\"SOFTWARE\\Microsoft\\Windows365\";\n            public const string IsW365EnvironmentKeyName = \"IsW365Environment\";\n            public const string W365PartnerIdKeyName = \"PartnerId\";\n        }\n\n        public static class HelpUrls\n        {\n            public const string GcmProjectUrl          = \"https://aka.ms/gcm\";\n            public const string GcmNewIssue            = \"https://aka.ms/gcm/bug\";\n            public const string GcmAuthorityDeprecated = \"https://aka.ms/gcm/authority\";\n            public const string GcmHttpProxyGuide      = \"https://aka.ms/gcm/httpproxy\";\n            public const string GcmTlsVerification     = \"https://aka.ms/gcm/tlsverify\";\n            public const string GcmCredentialStores    = \"https://aka.ms/gcm/credstores\";\n            public const string GcmWamComSecurity      = \"https://aka.ms/gcm/wamadmin\";\n            public const string GcmAutoDetect          = \"https://aka.ms/gcm/autodetect\";\n            public const string GcmDefaultAccount      = \"https://aka.ms/gcm/defaultaccount\";\n            public const string GcmMultipleUsers       = \"https://aka.ms/gcm/multipleusers\";\n            public const string GcmUnsafeRemotes       = \"https://aka.ms/gcm/unsaferemotes\";\n            public const string GcmNtlm                = \"https://aka.ms/gcm/ntlm\";\n        }\n\n        private static Version _gcmVersion;\n\n        public static Version GcmVersion\n        {\n            get\n            {\n                if (_gcmVersion is null)\n                {\n                    var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();\n                    var attr = assembly.GetCustomAttribute<AssemblyFileVersionAttribute>();\n                    if (attr is null)\n                    {\n                        _gcmVersion = assembly.GetName().Version;\n                    }\n                    else if (Version.TryParse(attr.Version, out Version asmVersion))\n                    {\n                        _gcmVersion = asmVersion;\n                    }\n                    else\n                    {\n                        // Unknown version!\n                        _gcmVersion = new Version(0, 0);\n                    }\n                }\n\n                return _gcmVersion;\n            }\n        }\n\n        /// <summary>\n        /// Get the HTTP user-agent for Git Credential Manager.\n        /// </summary>\n        /// <returns>User-agent string for HTTP requests.</returns>\n        public static string GetHttpUserAgent(ITrace2 trace2)\n        {\n            PlatformInformation info = PlatformUtils.GetPlatformInformation(trace2);\n            string osType     = info.OperatingSystemType;\n            string cpuArch    = info.CpuArchitecture;\n            string clrVersion = info.ClrVersion;\n\n            return string.Format($\"Git-Credential-Manager/{GcmVersion} ({osType}; {cpuArch}) CLR/{clrVersion}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ConvertUtils.cs",
    "content": "using System;\n\nnamespace GitCredentialManager\n{\n    public static class ConvertUtils\n    {\n        public static bool TryToInt32(object value, out int i)\n        {\n            return TryConvert(Convert.ToInt32, value, out i);\n        }\n\n        public static bool TryConvert<T>(Func<object, T> convert, object value, out T @out)\n        {\n            try\n            {\n                @out = convert(value);\n                return true;\n            }\n            catch\n            {\n                @out = default(T);\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Core.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"'$(OSPlatform)'=='windows'\">net8.0;net472</TargetFrameworks>\n    <AssemblyName>gcmcore</AssemblyName>\n    <RootNamespace>GitCredentialManager</RootNamespace>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n  </PropertyGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' == 'net472'\">\n    <Reference Include=\"System.Net.Http\" />\n    <Reference Include=\"System.Web\" />\n    <PackageReference Include=\"Microsoft.Identity.Client.Broker\" Version=\"4.65.0\" />\n    <PackageReference Include=\"Avalonia.Win32\" Version=\"11.1.3\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' != 'net472'\">\n    <PackageReference Include=\"Avalonia.Desktop\" Version=\"11.1.3\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Identity.Client\" Version=\"4.65.0\" />\n    <PackageReference Include=\"Microsoft.Identity.Client.Extensions.Msal\" Version=\"4.65.0\" />\n    <PackageReference Include=\"System.CommandLine\" Version=\"2.0.0-beta4.22272.1\" />\n    <PackageReference Include=\"Avalonia\" Version=\"11.1.3\" />\n    <PackageReference Include=\"Avalonia.Skia\" Version=\"11.1.3\" />\n    <PackageReference Include=\"Avalonia.Themes.Fluent\" Version=\"11.1.3\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(Configuration)' == 'Debug'\">\n    <PackageReference Include=\"Avalonia.Diagnostics\" Version=\"11.1.3\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Core/Credential.cs",
    "content": "\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents a credential.\n    /// </summary>\n    public interface ICredential\n    {\n        /// <summary>\n        /// Account associated with this credential.\n        /// </summary>\n        string Account { get; }\n\n        /// <summary>\n        /// Password.\n        /// </summary>\n        string Password { get; }\n    }\n\n    /// <summary>\n    /// Represents a credential (username/password pair) that Git can use to authenticate to a remote repository.\n    /// </summary>\n    public class GitCredential : ICredential\n    {\n        public GitCredential(string userName, string password)\n        {\n            Account = userName;\n            Password = password;\n        }\n\n        public string Account { get; }\n\n        public string Password { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/CredentialCacheStore.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace GitCredentialManager\n{\n    public class CredentialCacheStore : ICredentialStore\n    {\n        readonly IGit _git;\n        readonly string _options;\n\n        public CredentialCacheStore(IGit git, string options)\n        {\n            _git = git;\n            if (string.IsNullOrEmpty(options))\n            {\n                _options = string.Empty;\n            }\n            else\n            {\n                _options = options;\n            }\n        }\n\n        #region ICredentialStore\n\n        public IList<string> GetAccounts(string service)\n        {\n            // Listing accounts is not supported by the credential-cache store so we just attempt to retrieve\n            // the username from first credential for the given service and return an empty list if it fails.\n            var input = MakeGitCredentialsEntry(service, null);\n\n            var result = _git.InvokeHelperAsync(\n                $\"credential-cache get {_options}\",\n                input\n            ).GetAwaiter().GetResult();\n\n            if (result.TryGetValue(\"username\", out string value))\n            {\n                return new List<string> { value };\n            }\n\n            return Array.Empty<string>();\n        }\n\n        public ICredential Get(string service, string account)\n        {\n            var input = MakeGitCredentialsEntry(service, account);\n\n            var result = _git.InvokeHelperAsync(\n                $\"credential-cache get {_options}\",\n                input\n            ).GetAwaiter().GetResult();\n\n            if (result.ContainsKey(\"username\") && result.ContainsKey(\"password\"))\n            {\n                return new GitCredential(result[\"username\"], result[\"password\"]);\n            }\n\n            return null;\n        }\n\n        public void AddOrUpdate(string service, string account, string secret)\n        {\n            var input = MakeGitCredentialsEntry(service, account);\n            input[\"password\"] = secret;\n\n            // per https://git-scm.com/docs/gitcredentials :\n            // For a store or erase operation, the helper’s output is ignored.\n            _git.InvokeHelperAsync(\n                $\"credential-cache store {_options}\",\n                input\n            ).GetAwaiter().GetResult();\n        }\n\n        public bool Remove(string service, string account)\n        {\n            var input = MakeGitCredentialsEntry(service, account);\n\n            // per https://git-scm.com/docs/gitcredentials :\n            // For a store or erase operation, the helper’s output is ignored.\n            _git.InvokeHelperAsync(\n                $\"credential-cache erase {_options}\",\n                input\n            ).GetAwaiter().GetResult();\n\n            // the credential cache doesn't tell us whether anything was erased\n            // but we're optimistic sorts\n            return true;\n        }\n\n        #endregion\n\n        private Dictionary<string, string> MakeGitCredentialsEntry(string service, string account)\n        {\n            var result = new Dictionary<string, string>();\n\n            result[\"url\"] = service;\n            if (!string.IsNullOrEmpty(account))\n            {\n                result[\"username\"] = account;\n            }\n\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/CredentialStore.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing GitCredentialManager.Interop.Linux;\nusing GitCredentialManager.Interop.MacOS;\nusing GitCredentialManager.Interop.Posix;\nusing GitCredentialManager.Interop.Windows;\nusing StoreNames = GitCredentialManager.Constants.CredentialStoreNames;\n\nnamespace GitCredentialManager\n{\n    public class CredentialStore : ICredentialStore\n    {\n        private readonly ICommandContext _context;\n\n        private ICredentialStore _backingStore;\n\n        public CredentialStore(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n        }\n\n        #region ICredentialStore\n\n        public IList<string> GetAccounts(string service)\n        {\n            EnsureBackingStore();\n            return _backingStore.GetAccounts(service);\n        }\n\n        public ICredential Get(string service, string account)\n        {\n            EnsureBackingStore();\n            return _backingStore.Get(service, account);\n        }\n\n        public void AddOrUpdate(string service, string account, string secret)\n        {\n            EnsureBackingStore();\n            _backingStore.AddOrUpdate(service, account, secret);\n        }\n\n        public bool Remove(string service, string account)\n        {\n            EnsureBackingStore();\n            return _backingStore.Remove(service, account);\n        }\n\n        #endregion\n\n        private void EnsureBackingStore()\n        {\n            if (_backingStore != null)\n            {\n                return;\n            }\n\n            string ns = _context.Settings.CredentialNamespace;\n            string credStoreName = _context.Settings.CredentialBackingStore?.ToLowerInvariant()\n                                ?? GetDefaultStore();\n\n            switch (credStoreName)\n            {\n                case StoreNames.WindowsCredentialManager:\n                    ValidateWindowsCredentialManager();\n                    _backingStore = new WindowsCredentialManager(ns);\n                    break;\n\n                case StoreNames.Dpapi:\n                    ValidateDpapi(out string dpapiStoreRoot);\n                    _backingStore = new DpapiCredentialStore(_context.FileSystem, dpapiStoreRoot, ns);\n                    break;\n\n                case StoreNames.MacOSKeychain:\n                    ValidateMacOSKeychain();\n                    _backingStore = new MacOSKeychain(ns);\n                    break;\n\n                case StoreNames.SecretService:\n                    ValidateSecretService();\n                    _backingStore = new SecretServiceCollection(ns);\n                    break;\n\n                case StoreNames.Gpg:\n                    ValidateGpgPass(out string gpgStoreRoot, out string gpgExec);\n                    IGpg gpg = new Gpg(gpgExec, _context.SessionManager, _context.ProcessManager, _context.Trace2);\n                    _backingStore = new GpgPassCredentialStore(_context.FileSystem, gpg, gpgStoreRoot, ns);\n                    break;\n\n                case StoreNames.Cache:\n                    ValidateCredentialCache(out string options);\n                    _backingStore = new CredentialCacheStore(_context.Git, options);\n                    break;\n\n                case StoreNames.Plaintext:\n                    ValidatePlaintext(out string plainStoreRoot);\n                    _backingStore = new PlaintextCredentialStore(_context.FileSystem, plainStoreRoot, ns);\n                    break;\n\n                case StoreNames.None:\n                    _backingStore = new NullCredentialStore();\n                    break;\n\n                default:\n                    var sb = new StringBuilder();\n                    sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName)\n                        ? \"No credential store has been selected.\"\n                        : $\"Unknown credential store '{credStoreName}'.\");\n                    _context.Trace2.WriteError(sb.ToString());\n                    sb.AppendFormat(\n                        \"{3}Set the {0} environment variable or the {1}.{2} Git configuration setting to one of the following options:{3}{3}\",\n                        Constants.EnvironmentVariables.GcmCredentialStore,\n                        Constants.GitConfiguration.Credential.SectionName,\n                        Constants.GitConfiguration.Credential.CredentialStore,\n                        Environment.NewLine);\n                    AppendAvailableStoreList(sb);\n                    sb.AppendLine();\n                    sb.AppendLine($\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\");\n                    throw new Exception(sb.ToString());\n            }\n        }\n\n        private static string GetDefaultStore()\n        {\n            if (PlatformUtils.IsWindows())\n                return StoreNames.WindowsCredentialManager;\n\n            if (PlatformUtils.IsMacOS())\n                return StoreNames.MacOSKeychain;\n\n            // Other platforms have no default store\n            return null;\n        }\n\n        private static void AppendAvailableStoreList(StringBuilder sb)\n        {\n            if (PlatformUtils.IsWindows())\n            {\n                sb.AppendFormat(\"  {1,-13} : Windows Credential Manager (not available over network/SSH sessions){0}\",\n                    Environment.NewLine, StoreNames.WindowsCredentialManager);\n\n                sb.AppendFormat(\"  {1,-13} : DPAPI protected files{0}\",\n                    Environment.NewLine, StoreNames.Dpapi);\n            }\n\n            if (PlatformUtils.IsMacOS())\n            {\n                sb.AppendFormat(\"  {1,-13} : macOS Keychain{0}\",\n                    Environment.NewLine, StoreNames.MacOSKeychain);\n            }\n\n            if (PlatformUtils.IsLinux())\n            {\n                sb.AppendFormat(\"  {1,-13} : freedesktop.org Secret Service (requires graphical interface){0}\",\n                    Environment.NewLine, StoreNames.SecretService);\n            }\n\n            if (PlatformUtils.IsPosix())\n            {\n                sb.AppendFormat(\"  {1,-13} : GNU `pass` compatible credential storage (requires GPG and `pass`){0}\",\n                    Environment.NewLine, StoreNames.Gpg);\n            }\n\n            if (!PlatformUtils.IsWindows())\n            {\n                sb.AppendFormat(\"  {1,-13} : Git's in-memory credential cache{0}\",\n                    Environment.NewLine, StoreNames.Cache);\n            }\n\n            sb.AppendFormat(\"  {1,-13} : store credentials in plain-text files (UNSECURE){0}\",\n                Environment.NewLine, StoreNames.Plaintext);\n\n            sb.AppendFormat(\"  {1, -13} : disable internal credential storage{0}\",\n                Environment.NewLine, StoreNames.None);\n        }\n\n        private void ValidateWindowsCredentialManager()\n        {\n            if (!PlatformUtils.IsWindows())\n            {\n                var message = $\"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                            $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n\n            if (!WindowsCredentialManager.CanPersist())\n            {\n                var message = $\"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n        }\n\n        private void ValidateDpapi(out string storeRoot)\n        {\n            if (!PlatformUtils.IsWindows())\n            {\n                var message = $\"Can only use the '{StoreNames.Dpapi}' credential store on Windows.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message  + Environment.NewLine +\n                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n\n            // Check for a redirected credential store location\n            if (!_context.Settings.TryGetSetting(\n                Constants.EnvironmentVariables.GcmDpapiStorePath,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.DpapiStorePath,\n                out storeRoot))\n            {\n                // Use default store root at ~/.gcm/dpapi_store\n                storeRoot = Path.Combine(_context.FileSystem.UserDataDirectoryPath, \"dpapi_store\");\n            }\n        }\n\n        private void ValidateMacOSKeychain()\n        {\n            if (!PlatformUtils.IsMacOS())\n            {\n                var message = $\"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message  + Environment.NewLine +\n                                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n        }\n\n        private void ValidateSecretService()\n        {\n            if (!PlatformUtils.IsLinux())\n            {\n                var message = $\"Can only use the '{StoreNames.SecretService}' credential store on Linux.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n\n            if (!_context.SessionManager.IsDesktopSession)\n            {\n                var message = $\"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n        }\n\n        private void ValidateGpgPass(out string storeRoot, out string execPath)\n        {\n            if (!PlatformUtils.IsPosix())\n            {\n                var message = $\"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n\n            execPath = GetGpgPath();\n\n            // If we are in a headless environment, and don't have the GPG_TTY or SSH_TTY\n            // variables set, then error - we need a TTY device path for pin-entry to work headless.\n            if (!_context.SessionManager.IsDesktopSession &&\n                !_context.Environment.Variables.ContainsKey(\"GPG_TTY\") &&\n                !_context.Environment.Variables.ContainsKey(\"SSH_TTY\"))\n            {\n                var message = \"GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n\n            // Check for a redirected pass store location\n            if (!_context.Settings.TryGetSetting(\n                GpgPassCredentialStore.PasswordStoreDirEnvar,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.GpgPassStorePath,\n                out storeRoot))\n            {\n                // Use default store root at ~/.password-store\n                storeRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), \".password-store\");\n            }\n\n        }\n\n        private void ValidateCredentialCache(out string options)\n        {\n            if (PlatformUtils.IsWindows())\n            {\n                var message = $\"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows.\";\n                _context.Trace2.WriteError(message);\n                throw new Exception(message + Environment.NewLine +\n                                    $\"See {Constants.HelpUrls.GcmCredentialStores} for more information.\"\n                );\n            }\n\n            // allow for --timeout and other options\n            if (!_context.Settings.TryGetSetting(\n                Constants.EnvironmentVariables.GcmCredCacheOptions,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.CredCacheOptions,\n                out options))\n            {\n                options = string.Empty;\n            }\n        }\n\n        private void ValidatePlaintext(out string storeRoot)\n        {\n            // Check for a redirected credential store location\n            if (!_context.Settings.TryGetSetting(\n                Constants.EnvironmentVariables.GcmPlaintextStorePath,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.PlaintextStorePath,\n                out storeRoot))\n            {\n                // Use default store root at ~/.gcm/store\n                storeRoot = Path.Combine(_context.FileSystem.UserDataDirectoryPath, \"store\");\n            }\n        }\n\n        private string GetGpgPath()\n        {\n            string gpgPath;\n\n            // Use the GCM_GPG_PATH environment variable if set\n            if (_context.Environment.Variables.TryGetValue(Constants.EnvironmentVariables.GpgExecutablePath,\n                out gpgPath))\n            {\n                if (_context.FileSystem.FileExists(gpgPath))\n                {\n                    _context.Trace.WriteLine($\"Using Git executable from GCM_GPG_PATH: {gpgPath}\");\n                    return gpgPath;\n                }\n\n                var format = \"GPG executable does not exist with path '{0}'\";\n                var message = string.Format(format, gpgPath);\n                throw new Trace2Exception(_context.Trace2, message, format);\n            }\n\n            // If no explicit GPG path is specified, mimic the way `pass`\n            // determines GPG dependency (use gpg2 if available, otherwise gpg)\n            if (_context.Environment.TryLocateExecutable(\"gpg2\", out string gpg2Path))\n            {\n                _context.Trace.WriteLine($\"Using PATH-located GPG (gpg2) executable: {gpg2Path}\");\n                return gpg2Path;\n            }\n\n            gpgPath = _context.Environment.LocateExecutable(\"gpg\");\n            _context.Trace.WriteLine($\"Using PATH-located GPG (gpg) executable: {gpgPath}\");\n            return gpgPath;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/CurlCookie.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing GitCredentialManager;\n\nnamespace GitCredentialManager\n{\n    public class CurlCookieParser\n    {\n        private readonly ITrace _trace;\n\n        public CurlCookieParser(ITrace trace)\n        {\n            _trace = trace;\n        }\n\n        public IList<Cookie> Parse(string content)\n        {\n            if (string.IsNullOrWhiteSpace(content))\n            {\n                return Array.Empty<Cookie>();\n            }\n\n            const string HttpOnlyPrefix = \"#HttpOnly_\";\n\n            var cookies = new List<Cookie>();\n\n            // Parse the cookie file content\n            var lines = content.Split(new[] { '\\r', '\\n' }, StringSplitOptions.RemoveEmptyEntries);\n            foreach (var line in lines)\n            {\n                var parts = line.Split(new[] { '\\t' }, StringSplitOptions.None);\n                if (parts.Length >= 7 && (!parts[0].StartsWith(\"#\") || parts[0].StartsWith(HttpOnlyPrefix)))\n                {\n                    var domain = parts[0].StartsWith(HttpOnlyPrefix) ? parts[0].Substring(HttpOnlyPrefix.Length) : parts[0];\n                    var includeSubdomains = StringComparer.OrdinalIgnoreCase.Equals(parts[1], \"TRUE\");\n                    if (!includeSubdomains)\n                    {\n                        domain = domain.TrimStart('.');\n                    }\n                    var path = string.IsNullOrWhiteSpace(parts[2]) ? \"/\" : parts[2];\n                    var secureOnly = parts[3].Equals(\"TRUE\", StringComparison.OrdinalIgnoreCase);\n                    var expires = ParseExpires(parts[4]);\n                    var name = parts[5];\n                    var value = parts[6];\n\n                    cookies.Add(new Cookie()\n                    {\n                        Domain = domain,\n                        Path = path,\n                        Expires = expires,\n                        HttpOnly = true,\n                        Secure = secureOnly,\n                        Name = name,\n                        Value = value,\n                    });\n                }\n                else\n                {\n                    _trace.WriteLine($\"Invalid cookie line: {line}\");\n                }\n            }\n\n            return cookies;\n        }\n\n        private static DateTime ParseExpires(string expires)\n        {\n#if NETFRAMEWORK\n            DateTime epoch = new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc);\n#else\n            DateTime epoch = DateTime.UnixEpoch;\n#endif\n\n            if (long.TryParse(expires, out long i))\n            {\n                return epoch.AddSeconds(i);\n            }\n\n            return epoch;\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Core/Diagnostics/CredentialStoreDiagnostic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public class CredentialStoreDiagnostic : Diagnostic\n    {\n        public CredentialStoreDiagnostic(ICommandContext commandContext)\n            : base(\"Credential storage\", commandContext)\n        { }\n\n        protected override Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            log.AppendLine($\"ICredentialStore instance is of type: {CommandContext.CredentialStore.GetType().Name}\");\n\n            // Create a service that is guaranteed to be unique\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n            const string account = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            try\n            {\n                log.Append(\"Writing test credential...\");\n                CommandContext.CredentialStore.AddOrUpdate(service, account, password);\n                log.AppendLine(\" OK\");\n\n                log.Append(\"Reading test credential...\");\n                ICredential outCredential = CommandContext.CredentialStore.Get(service, account);\n                if (outCredential is null)\n                {\n                    log.AppendLine(\" Failed\");\n                    log.AppendLine(\"Test credential object is null!\");\n                    return Task.FromResult(false);\n                }\n\n                log.AppendLine(\" OK\");\n\n                if (!StringComparer.Ordinal.Equals(account, outCredential.Account))\n                {\n                    log.Append(\"Test credential account did not match!\");\n                    log.AppendLine($\"Expected: {account}\");\n                    log.AppendLine($\"Actual: {outCredential.Account}\");\n                    return Task.FromResult(false);\n                }\n\n                if (!StringComparer.Ordinal.Equals(password, outCredential.Password))\n                {\n                    log.Append(\"Test credential password did not match!\");\n                    log.AppendLine($\"Expected: {password}\");\n                    log.AppendLine($\"Actual: {outCredential.Password}\");\n                    return Task.FromResult(false);\n                }\n            }\n            finally\n            {\n                log.Append(\"Deleting test credential...\");\n                CommandContext.CredentialStore.Remove(service, account);\n                log.AppendLine(\" OK\");\n            }\n\n            return Task.FromResult(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/Diagnostic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public interface IDiagnostic\n    {\n        string Name { get; }\n\n        bool CanRun();\n\n        Task<DiagnosticResult> RunAsync();\n    }\n\n    public abstract class Diagnostic : IDiagnostic\n    {\n        protected ICommandContext CommandContext;\n\n        protected Diagnostic(string name, ICommandContext commandContext)\n        {\n            Name = name;\n            CommandContext = commandContext;\n        }\n\n        public string Name { get; }\n\n        public virtual bool CanRun()\n        {\n            return true;\n        }\n\n        public async Task<DiagnosticResult> RunAsync()\n        {\n            var log = new StringBuilder();\n\n            bool success = false;\n            Exception exception = null;\n            var additionalFiles = new List<string>();\n            try\n            {\n                success = await RunInternalAsync(log, additionalFiles);\n            }\n            catch (Exception ex)\n            {\n                exception = ex;\n            }\n\n            return new DiagnosticResult\n            {\n                IsSuccess = success,\n                DiagnosticLog = log.ToString(),\n                Exception = exception,\n                AdditionalFiles = additionalFiles\n            };\n        }\n\n        protected abstract Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles);\n    }\n\n    public class DiagnosticResult\n    {\n        public bool IsSuccess { get; set; }\n        public Exception Exception { get; set; }\n        public string DiagnosticLog { get; set; }\n        public ICollection<string> AdditionalFiles { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/EnvironmentDiagnostic.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Net.Mime;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public class EnvironmentDiagnostic : Diagnostic\n    {\n        public EnvironmentDiagnostic(ICommandContext commandContext)\n            : base(\"Environment\", commandContext)\n        { }\n\n        protected override Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            PlatformInformation platformInfo = PlatformUtils.GetPlatformInformation(CommandContext.Trace2);\n            log.AppendLine($\"OSType: {platformInfo.OperatingSystemType}\");\n            log.AppendLine($\"OSVersion: {platformInfo.OperatingSystemVersion}\");\n\n            log.Append(\"Reading environment variables...\");\n            IDictionary envars = Environment.GetEnvironmentVariables();\n            log.AppendLine(\" OK\");\n\n            log.AppendLine(\" Variables:\");\n            foreach (DictionaryEntry envar in envars)\n            {\n                log.AppendFormat(\"{0}={1}\", envar.Key, envar.Value);\n                log.AppendLine();\n            }\n            log.AppendLine();\n\n            return Task.FromResult(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/FileSystemDiagnostic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public class FileSystemDiagnostic : Diagnostic\n    {\n        public FileSystemDiagnostic(ICommandContext commandContext)\n            : base(\"File system\", commandContext)\n        {  }\n\n        protected override Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            string tempDir = Path.GetTempPath();\n            log.AppendLine($\"Temporary directory is '{tempDir}'...\");\n\n            log.AppendLine(\"Checking basic file I/O...\");\n            const string testContent = \"Hello, GCM!\";\n\n            string fileName = Guid.NewGuid().ToString(\"N\").Substring(8);\n            string path = Path.Combine(tempDir, fileName);\n            log.Append($\"Writing to temporary file '{path}'...\");\n            File.WriteAllText(path, testContent);\n            log.AppendLine(\" OK\");\n\n            log.Append($\"Reading from temporary file '{path}'...\");\n            string actualContent = File.ReadAllText(path);\n            log.AppendLine(\" OK\");\n\n            if (!StringComparer.Ordinal.Equals(testContent, actualContent))\n            {\n                log.AppendLine(\"File data did not match!\");\n                log.AppendLine($\"Expected: {testContent}\");\n                log.AppendLine($\"Actual: {actualContent}\");\n                return Task.FromResult(false);\n            }\n\n            log.Append($\"Deleting temporary file '{path}'...\");\n            File.Delete(path);\n            log.AppendLine(\" OK\");\n\n            log.AppendLine(\"Testing IFileSystem instance...\");\n            log.AppendLine($\"UserHomePath: {CommandContext.FileSystem.UserHomePath}\");\n            log.AppendLine($\"UserDataDirectoryPath: {CommandContext.FileSystem.UserDataDirectoryPath}\");\n            log.AppendLine($\"GetCurrentDirectory(): {CommandContext.FileSystem.GetCurrentDirectory()}\");\n\n            return Task.FromResult(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/GitDiagnostic.cs",
    "content": "using System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public class GitDiagnostic : Diagnostic\n    {\n        public GitDiagnostic(ICommandContext commandContext)\n            : base(\"Git\", commandContext)\n        { }\n\n        protected override Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            log.Append(\"Getting Git version...\");\n            GitVersion gitVersion = CommandContext.Git.Version;\n            log.AppendLine(\" OK\");\n            log.AppendLine($\"Git version is '{gitVersion.OriginalString}'\");\n\n            log.Append(\"Locating current repository...\");\n            if (!CommandContext.Git.IsInsideRepository())\n            {\n                log.AppendLine(\"Not inside a Git repository.\");\n            }\n            else\n            {\n                string thisRepo = CommandContext.Git.GetCurrentRepository();\n                log.AppendLine($\"Git repository at '{thisRepo}'\");\n            }\n            log.AppendLine(\" OK\");\n\n            log.Append(\"Listing all Git configuration...\");\n            ChildProcess configProc = CommandContext.Git.CreateProcess(\"config --list --show-origin\");\n            configProc.Start(Trace2ProcessClass.Git);\n            // To avoid deadlocks, always read the output stream first and then wait\n            // TODO: don't read in all the data at once; stream it\n            string gitConfig = configProc.StandardOutput.ReadToEnd().TrimEnd();\n            configProc.WaitForExit();\n            log.AppendLine(\" OK\");\n            log.AppendLine(\"Git configuration:\");\n            log.AppendLine(gitConfig);\n            log.AppendLine();\n\n            return Task.FromResult(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/IDiagnosticProvider.cs",
    "content": "using System.Collections.Generic;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public interface IDiagnosticProvider\n    {\n        IEnumerable<IDiagnostic> GetDiagnostics();\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing Microsoft.Identity.Client.Extensions.Msal;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public class MicrosoftAuthenticationDiagnostic : Diagnostic\n    {\n        public MicrosoftAuthenticationDiagnostic(ICommandContext context)\n            : base(\"Microsoft authentication (AAD/MSA)\", context)\n        { }\n\n        protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            var msAuth = new MicrosoftAuthentication(CommandContext);\n            log.AppendLine(msAuth.CanUseBroker() ? \"Broker is enabled.\" : \"Broker is not enabled.\");\n            log.AppendLine($\"Flow type is: {msAuth.GetFlowType()}\");\n\n            log.Append(\"Gathering MSAL token cache data...\");\n            StorageCreationProperties cacheProps = msAuth.CreateUserTokenCacheProps(true);\n            log.AppendLine(\" OK\");\n            log.AppendLine($\"CacheDirectory: {cacheProps.CacheDirectory}\");\n            log.AppendLine($\"CacheFileName: {cacheProps.CacheFileName}\");\n            log.AppendLine($\"CacheFilePath: {cacheProps.CacheFilePath}\");\n\n            if (PlatformUtils.IsMacOS())\n            {\n                log.AppendLine($\"MacKeyChainAccountName: {cacheProps.MacKeyChainAccountName}\");\n                log.AppendLine($\"MacKeyChainServiceName: {cacheProps.MacKeyChainServiceName}\");\n            }\n            else if (PlatformUtils.IsLinux())\n            {\n                log.AppendLine($\"KeyringCollection: {cacheProps.KeyringCollection}\");\n                log.AppendLine($\"KeyringSchemaName: {cacheProps.KeyringSchemaName}\");\n                log.AppendLine($\"KeyringSecretLabel: {cacheProps.KeyringSecretLabel}\");\n                log.AppendLine($\"KeyringAttribute1: ({cacheProps.KeyringAttribute1.Key},{cacheProps.KeyringAttribute1.Value})\");\n                log.AppendLine($\"KeyringAttribute2: ({cacheProps.KeyringAttribute2.Key},{cacheProps.KeyringAttribute2.Value})\");\n            }\n\n            log.Append(\"Creating cache helper...\");\n            var cacheHelper = await MsalCacheHelper.CreateAsync(cacheProps);\n            log.AppendLine(\" OK\");\n            try\n            {\n                log.Append(\"Verifying MSAL token cache persistence...\");\n                cacheHelper.VerifyPersistence();\n                log.AppendLine(\" OK\");\n            }\n            catch (Exception)\n            {\n                log.AppendLine(\" Failed\");\n                throw;\n            }\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Diagnostics/NetworkingDiagnostic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Diagnostics\n{\n    public class NetworkingDiagnostic : Diagnostic\n    {\n        private const string TestHttpUri = \"http://example.com\";\n        private const string TestHttpUriFallback = \"http://httpforever.com\";\n        private const string TestHttpsUri = \"https://example.com\";\n\n        public NetworkingDiagnostic(ICommandContext commandContext)\n            : base(\"Networking\", commandContext)\n        { }\n\n        protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            log.AppendLine(\"Checking networking and HTTP stack...\");\n            log.Append(\"Creating HTTP client...\");\n            using var httpClient = CommandContext.HttpClientFactory.CreateClient();\n            log.AppendLine(\" OK\");\n\n            bool hasNetwork = NetworkInterface.GetIsNetworkAvailable();\n            log.AppendLine($\"IsNetworkAvailable: {hasNetwork}\");\n\n            SendHttpRequest(log, httpClient);\n\n            log.Append($\"Sending HEAD request to {TestHttpsUri}...\");\n            using var httpsResponse = await httpClient.HeadAsync(TestHttpsUri);\n            log.AppendLine(\" OK\");\n\n            log.Append(\"Acquiring free TCP port...\");\n            var tcpListener = new TcpListener(IPAddress.Loopback, 0);\n            int tcpPort;\n            try\n            {\n                tcpListener.Start();\n                tcpPort = ((IPEndPoint) tcpListener.LocalEndpoint).Port;\n                log.AppendLine(\" OK\");\n            }\n            finally\n            {\n                tcpListener.Stop();\n            }\n\n            if (tcpPort <= 0)\n            {\n                log.AppendLine(\"Failed to acquire local TCP port - cannot test local HTTP loopback connections!\");\n                return false;\n            }\n\n            log.AppendLine(\"Testing local HTTP loopback connections...\");\n\n            const string responseContent = \"Hello, GCM!\";\n            byte[] responseData = Encoding.UTF8.GetBytes(responseContent);\n\n            var localAddress = $\"http://localhost:{tcpPort}/\";\n            log.Append($\"Creating new HTTP listener for {localAddress}...\");\n            var httpListener = new HttpListener {Prefixes = {localAddress}};\n            httpListener.Start();\n            log.AppendLine(\" OK\");\n\n            Task<HttpListenerContext> listenContextTask = httpListener.GetContextAsync();\n            Task<HttpResponseMessage> localResponseTask = httpClient.GetAsync(localAddress);\n\n            log.Append(\"Waiting for loopback connection...\");\n            HttpListenerContext listenContext = await listenContextTask;\n            log.AppendLine(\" OK\");\n\n            log.Append(\"Writing response...\");\n            listenContext.Response.ContentLength64 = responseData.Length;\n            listenContext.Response.OutputStream.Write(responseData, 0, responseData.Length);\n            listenContext.Response.Close();\n            log.AppendLine(\" OK\");\n\n            log.Append(\"Waiting for response data...\");\n            using HttpResponseMessage localResponse = await localResponseTask;\n            byte[] actualResponseData = await localResponse.Content.ReadAsByteArrayAsync();\n            string actualResponseContent = Encoding.UTF8.GetString(actualResponseData);\n            log.AppendLine(\" OK\");\n\n            if (!StringComparer.Ordinal.Equals(responseContent, actualResponseContent))\n            {\n                log.AppendLine(\"Loopback connection data did not match!\");\n                log.AppendLine($\"Expected: {responseContent}\");\n                log.AppendLine($\"Actual: {actualResponseContent}\");\n                return false;\n            }\n\n            log.AppendLine(\"Loopback connection data OK\");\n\n            return true;\n        }\n\n        internal /* For testing purposes */ async void SendHttpRequest(StringBuilder log, HttpClient httpClient)\n        {\n            foreach (var uri in new List<string> { TestHttpUri, TestHttpUriFallback })\n            {\n                try\n                {\n                    log.Append($\"Sending HEAD request to {uri}...\");\n                    using var httpResponse = await httpClient.HeadAsync(uri);\n                    log.AppendLine(\" OK\");\n                    break;\n                }\n                catch (HttpRequestException)\n                {\n                    log.AppendLine(\" warning: HEAD request failed\");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/DictionaryExtensions.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Web;\n\nnamespace GitCredentialManager\n{\n    public static class DictionaryExtensions\n    {\n        /// <summary>\n        /// Get the value of a dictionary entry as 'booleany' (either 'truthy' or 'falsey').\n        /// </summary>\n        /// <param name=\"dict\">Dictionary.</param>\n        /// <param name=\"key\">Dictionary entry key.</param>\n        /// <param name=\"defaultValue\">Default value if the key is not present, or was neither 'truthy' or 'falsey'.</param>\n        /// <returns>Dictionary entry value.</returns>\n        /// <remarks>\n        /// 'Truthy' and 'fasley' is defined by the implementation of <see cref=\"StringExtensions.IsTruthy\"/> and <see cref=\"StringExtensions.IsFalsey\"/>.\n        /// </remarks>\n        public static bool GetBooleanyOrDefault(this IReadOnlyDictionary<string, string> dict, string key, bool defaultValue)\n        {\n            if (dict.TryGetValue(key, out string value))\n            {\n                return value.ToBooleanyOrDefault(defaultValue);\n            }\n\n            return defaultValue;\n        }\n\n        public static string ToQueryString(this IDictionary<string, string> dict)\n        {\n            var sb = new StringBuilder();\n            int i = 0;\n\n            foreach (var kvp in dict)\n            {\n                string key  = HttpUtility.UrlEncode(kvp.Key);\n                string value = HttpUtility.UrlEncode(kvp.Value);\n\n                if (i > 0)\n                {\n                    sb.Append('&');\n                }\n\n                sb.AppendFormat(\"{0}={1}\", key, value);\n\n                i++;\n            }\n\n            return sb.ToString();\n        }\n\n        public static void Append<TKey, TValue>(this IDictionary<TKey, ICollection<TValue>> dict, TKey key, TValue value)\n        {\n            if (!dict.TryGetValue(key, out var values))\n            {\n                values = new List<TValue>();\n                dict[key] = values;\n            }\n\n            values.Add(value);\n        }\n\n        public static IEnumerable<TValue> GetValues<TKey, TValue>(this IDictionary<TKey, IEnumerable<TValue>> dict, TKey key)\n        {\n            return dict.TryGetValue(key, out var values) ? values : Enumerable.Empty<TValue>();\n        }\n        \n        public static IEnumerable<TValue> GetValues<TKey, TValue>(this IDictionary<TKey, ICollection<TValue>> dict, TKey key)\n        {\n            return dict.TryGetValue(key, out var values) ? values : Enumerable.Empty<TValue>();\n        }\n\n        public static IDictionary<TKey, IEnumerable<TValue>> ToDictionary<TKey, TValue>(this IEnumerable<IGrouping<TKey, TValue>> grouping)\n        {\n            return grouping.ToDictionary(x => x.Key, x => (IEnumerable<TValue>) x);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/DisposableObject.cs",
    "content": "using System;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// An object that implements the <see cref=\"IDisposable\"/> interface and the disposable pattern.\n    /// </summary>\n    public abstract class DisposableObject : IDisposable\n    {\n        private bool _isDisposed;\n\n        /// <summary>\n        /// Throw an exception if the object has been disposed.\n        /// </summary>\n        /// <exception cref=\"ObjectDisposedException\">Thrown if the object has been disposed.</exception>\n        protected void ThrowIfDisposed()\n        {\n            if (_isDisposed)\n            {\n                throw new ObjectDisposedException(GetType().Name);\n            }\n        }\n\n        /// <summary>\n        /// Called when unmanaged resources should be released and memory freed.\n        /// </summary>\n        protected virtual void ReleaseUnmanagedResources() { }\n\n        /// <summary>\n        /// Called when managed resources should be released.\n        /// </summary>\n        protected virtual void ReleaseManagedResources() { }\n\n        /// <summary>\n        /// Called when the application is being terminated. Clean up and release any resources.\n        /// </summary>\n        /// <param name=\"disposing\">True if the instance is being disposed, false if being finalized.</param>\n        private void Dispose(bool disposing)\n        {\n            if (_isDisposed)\n            {\n                return;\n            }\n\n            ReleaseUnmanagedResources();\n\n            if (disposing)\n            {\n                ReleaseManagedResources();\n            }\n\n            _isDisposed = true;\n        }\n\n        public void Dispose()\n        {\n            Dispose(true);\n            GC.SuppressFinalize(this);\n        }\n\n        ~DisposableObject()\n        {\n            Dispose(false);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/EncodingEx.cs",
    "content": "using System.Text;\n\nnamespace GitCredentialManager;\n\npublic static class EncodingEx\n{\n    public static readonly Encoding UTF8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);\n}\n"
  },
  {
    "path": "src/shared/Core/EnsureArgument.cs",
    "content": "using System;\n\nnamespace GitCredentialManager\n{\n    public static class EnsureArgument\n    {\n        public static void NotNull<T>(T arg, string name)\n        {\n            if (arg is null)\n            {\n                throw new ArgumentNullException(name);\n            }\n        }\n\n        public static void NotNullOrEmpty(string arg, string name)\n        {\n            NotNull(arg, name);\n\n            if (string.IsNullOrEmpty(arg))\n            {\n                throw new ArgumentException(\"Argument cannot be empty.\", name);\n            }\n        }\n\n        public static void NotNullOrWhiteSpace(string arg, string name)\n        {\n            NotNull(arg, name);\n\n            if (string.IsNullOrWhiteSpace(arg))\n            {\n                throw new ArgumentException(\"Argument cannot be empty or white space.\", name);\n            }\n        }\n\n        public static void AbsoluteUri(Uri arg, string name)\n        {\n            NotNull(arg, name);\n\n            if (!arg.IsAbsoluteUri)\n            {\n                throw new ArgumentException(\"Argument must be an absolute URI.\", name);\n            }\n        }\n\n        public static void PositiveOrZero(int arg, string name)\n        {\n            if (arg < 0)\n            {\n                throw new ArgumentOutOfRangeException(name, \"Argument must be positive or zero (non-negative).\");\n            }\n        }\n\n        public static void Positive(int arg, string name)\n        {\n            if (arg <= 0)\n            {\n                throw new ArgumentOutOfRangeException(name, \"Argument must be positive.\");\n            }\n        }\n\n        public static void NegativeOrZero(int arg, string name)\n        {\n            if (arg > 0)\n            {\n                throw new ArgumentOutOfRangeException(name, \"Argument must be negative or zero (non-positive).\");\n            }\n        }\n\n        public static void Negative(int arg, string name)\n        {\n            if (arg >= 0)\n            {\n                throw new ArgumentOutOfRangeException(name, \"Argument must be negative.\");\n            }\n        }\n\n        public static void InRange(int arg, string name, int lower, int upper, bool lowerInclusive = true, bool upperInclusive = true)\n        {\n            if (lowerInclusive && arg < lower)\n            {\n                throw new ArgumentOutOfRangeException(name, $\"Argument must be greater than or equal to {lower}.\");\n            }\n\n            if (!lowerInclusive && arg <= lower)\n            {\n                throw new ArgumentOutOfRangeException(name, $\"Argument must be strictly greater than {lower}.\");\n            }\n\n            if (upperInclusive && arg > upper)\n            {\n                throw new ArgumentOutOfRangeException(name, $\"Argument must be less than or equal to {upper}.\");\n            }\n\n            if (!upperInclusive && arg >= upper)\n            {\n                throw new ArgumentOutOfRangeException(name, $\"Argument must be strictly less than {upper}.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/EnumerableExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace GitCredentialManager\n{\n    public static class EnumerableExtensions\n    {\n        /// <summary>\n        /// Concatenates multiple sequences.\n        /// </summary>\n        /// <param name=\"first\">Initial sequence to concatenate other sequences with.</param>\n        /// <param name=\"others\">Other sequences to concatenate together with <paramref name=\"first\"/>.</param>\n        /// <typeparam name=\"TSource\">Type of the sequence elements.</typeparam>\n        /// <returns>Concatenated sequence.</returns>\n        public static IEnumerable<TSource> ConcatMany<TSource>(this IEnumerable<TSource> first, params IEnumerable<TSource>[] others)\n        {\n            IEnumerable<TSource> result = first;\n\n            foreach (IEnumerable<TSource> other in others)\n            {\n                result = result.Concat(other);\n            }\n\n            return result;\n        }\n\n        public static bool TryGetFirst<TSource>(this IEnumerable<TSource> collection, Func<TSource, bool> predicate, out TSource result)\n        {\n            result = collection.FirstOrDefault(predicate);\n            return !(result is null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/EnvironmentBase.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Component that encapsulates the process environment, including environment variables.\n    /// </summary>\n    public interface IEnvironment\n    {\n        /// <summary>\n        /// Current process environment variables.\n        /// </summary>\n        IReadOnlyDictionary<string, string> Variables { get; }\n\n        /// <summary>\n        /// Check if the given directory exists on the path.\n        /// </summary>\n        /// <param name=\"directoryPath\">Path to directory to check for existence on the path.</param>\n        /// <returns>True if the directory is on the path, false otherwise.</returns>\n        bool IsDirectoryOnPath(string directoryPath);\n\n        /// <summary>\n        /// Add the directory to the path.\n        /// </summary>\n        /// <param name=\"directoryPath\">Path to directory to add to the path.</param>\n        /// <param name=\"target\">The level of the path environment variable that should be modified.</param>\n        void AddDirectoryToPath(string directoryPath, EnvironmentVariableTarget target);\n\n        /// <summary>\n        /// Remove the directory from the path.\n        /// </summary>\n        /// <param name=\"directoryPath\">Path to directory to remove from the path.</param>\n        /// <param name=\"target\">The level of the path environment variable that should be modified.</param>\n        void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target);\n\n        /// <summary>\n        /// Locate an executable on the current PATH.\n        /// </summary>\n        /// <param name=\"program\">Executable program name.</param>\n        /// <param name=\"path\">First instance of the found executable program.</param>\n        /// <returns>True if the executable was found, false otherwise.</returns>\n        bool TryLocateExecutable(string program, out string path);\n\n        /// <summary>\n        /// Set an environment variable at the specified target level.\n        /// </summary>\n        /// <param name=\"variable\">Name of the environment variable to set.</param>\n        /// <param name=\"value\">Value of the environment variable to set.</param>\n        /// <param name=\"target\">Target level of environment variable to set (Machine, Process, or User).</param>\n        void SetEnvironmentVariable(string variable, string value,\n            EnvironmentVariableTarget target = EnvironmentVariableTarget.Process);\n\n        /// <summary>\n        /// Refresh the current process environment variables. See <see cref=\"Variables\"/>.\n        /// </summary>\n        /// <remarks>This is automatically called after <see cref=\"SetEnvironmentVariable\"/>.</remarks>\n        void Refresh();\n    }\n\n    public abstract class EnvironmentBase : IEnvironment\n    {\n        private IReadOnlyDictionary<string, string> _variables;\n\n        protected EnvironmentBase(IFileSystem fileSystem)\n        {\n            EnsureArgument.NotNull(fileSystem, nameof(fileSystem));\n            FileSystem = fileSystem;\n        }\n\n        internal EnvironmentBase(IFileSystem fileSystem, IReadOnlyDictionary<string, string> variables)\n            : this(fileSystem)\n        {\n            EnsureArgument.NotNull(variables, nameof(variables));\n            _variables = variables;\n        }\n\n        public IReadOnlyDictionary<string, string> Variables\n        {\n            get\n            {\n                // Variables are lazily loaded\n                if (_variables is null)\n                {\n                    Refresh();\n                }\n\n                Debug.Assert(_variables != null);\n                return _variables;\n            }\n        }\n\n        protected IFileSystem FileSystem { get; }\n\n        public bool IsDirectoryOnPath(string directoryPath)\n        {\n            if (Variables.TryGetValue(\"PATH\", out string pathValue))\n            {\n                string[] paths = SplitPathVariable(pathValue);\n                return paths.Any(x => FileSystem.IsSamePath(x, directoryPath));\n            }\n\n            return false;\n        }\n\n        public abstract void AddDirectoryToPath(string directoryPath, EnvironmentVariableTarget target);\n\n        public abstract void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target);\n\n        protected abstract string[] SplitPathVariable(string value);\n\n        public virtual bool TryLocateExecutable(string program, out string path)\n        {\n            return TryLocateExecutable(program, null, out path);\n        }\n\n        internal virtual bool TryLocateExecutable(string program, ICollection<string> pathsToIgnore, out string path)\n        {\n            // On UNIX-like systems we would normally use the \"which\" utility to locate a program,\n            // but since distributions don't always place \"which\" in a consistent location we cannot\n            // find it! Oh the irony..\n            // We could also try using \"env\" to then locate \"which\", but the same problem exists in\n            // that \"env\" isn't always in a standard location.\n            //\n            // On Windows we should avoid using the equivalent utility \"where.exe\" because this will\n            // include the current working directory in the search, and we don't want this.\n            //\n            // The upshot of the above means we cannot use either of \"which\" or \"where.exe\" and must\n            // instead manually scan the PATH variable looking for the program.\n            // At least both Windows and UNIX use the same name for the $PATH or %PATH% variable!\n            if (Variables.TryGetValue(\"PATH\", out string pathValue))\n            {\n                string[] paths = SplitPathVariable(pathValue);\n                foreach (var basePath in paths)\n                {\n                    string candidatePath = Path.Combine(basePath, program);\n                    if (FileSystem.FileExists(candidatePath) && (pathsToIgnore is null ||\n                        !pathsToIgnore.Contains(candidatePath, StringComparer.OrdinalIgnoreCase)))\n                    {\n                        path = candidatePath;\n                        return true;\n                    }\n                }\n            }\n\n            path = null;\n            return false;\n        }\n\n        public void SetEnvironmentVariable(string variable, string value,\n            EnvironmentVariableTarget target = EnvironmentVariableTarget.Process)\n        {\n            // Don't bother setting the variable if it already has the same value\n            if (Variables.TryGetValue(variable, out var currentValue) &&\n                StringComparer.Ordinal.Equals(currentValue, value))\n            {\n                return;\n            }\n\n            Environment.SetEnvironmentVariable(variable, value, target);\n\n            // Immediately refresh the variables so that the new value is available to callers using IEnvironment\n            Refresh();\n        }\n\n        public void Refresh()\n        {\n            _variables = GetCurrentVariables();\n        }\n\n        protected abstract IReadOnlyDictionary<string, string> GetCurrentVariables();\n    }\n\n    public static class EnvironmentExtensions\n    {\n        /// <summary>\n        /// Locate an executable on the current PATH.\n        /// </summary>\n        /// <param name=\"environment\">The <see cref=\"IEnvironment\"/>.</param>\n        /// <param name=\"program\">Executable program name.</param>\n        /// <returns>List of all instances of the found executable program, in order of most specific to least.</returns>\n        public static string LocateExecutable(this IEnvironment environment, string program)\n        {\n            if (environment.TryLocateExecutable(program, out string path))\n            {\n                return path;\n            }\n\n            throw new Exception($\"Failed to locate '{program}' executable on the path.\");\n        }\n\n        /// <summary>\n        /// Retrieves the value of an environment variable from the current process.\n        /// </summary>\n        /// <param name=\"environment\">The <see cref=\"IEnvironment\"/>.</param>\n        /// <param name=\"variable\">The name of the environment variable.</param>\n        /// <returns>\n        /// The value of the environment variable specified by variable, or null if the environment variable is not found.\n        /// </returns>\n        public static string GetEnvironmentVariable(this IEnvironment environment, string variable)\n        {\n            return environment.Variables.TryGetValue(variable, out string value) ? value : null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/FileCredential.cs",
    "content": "namespace GitCredentialManager\n{\n    public class FileCredential : ICredential\n    {\n        public FileCredential(string fullPath, string service, string account, string password)\n        {\n            FullPath = fullPath;\n            Service = service;\n            Account = account;\n            Password = password;\n        }\n\n        public string FullPath { get; }\n\n        public string Service { get; }\n\n        public string Account { get; }\n\n        public string Password { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/FileSystem.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents a file system and operations that can be performed.\n    /// </summary>\n    public interface IFileSystem\n    {\n        /// <summary>\n        /// Get the path to the user's home profile directory (Unix: $HOME, Windows: %USERPROFILE%).\n        /// </summary>\n        string UserHomePath { get; }\n\n        /// <summary>\n        /// Get the path the the user's Git Credential Manager data directory.\n        /// </summary>\n        string UserDataDirectoryPath { get; }\n\n        /// <summary>\n        /// Check if two paths are the same for the current platform and file system. Symbolic links are not followed.\n        /// </summary>\n        /// <param name=\"a\">File path.</param>\n        /// <param name=\"b\">File path.</param>\n        /// <returns>True if both file paths are the same, false otherwise.</returns>\n        bool IsSamePath(string a, string b);\n\n        /// <summary>\n        /// Check if a file exists at the specified path.\n        /// </summary>\n        /// <param name=\"path\">Full path to file to test.</param>\n        /// <returns>True if a file exists, false otherwise.</returns>\n        bool FileExists(string path);\n\n        /// <summary>\n        /// Check if a directory exists at the specified path.\n        /// </summary>\n        /// <param name=\"path\">Full path to directory to test.</param>\n        /// <returns>True if a directory exists, false otherwise.</returns>\n        bool DirectoryExists(string path);\n\n        /// <summary>\n        /// Get the path to the current directory of the currently executing process.\n        /// </summary>\n        /// <returns>Current process directory.</returns>\n        string GetCurrentDirectory();\n\n        /// <summary>\n        /// Open a file stream at the specified path with the given access and mode settings.\n        /// </summary>\n        /// <param name=\"path\">Full file path.</param>\n        /// <param name=\"fileMode\">File mode settings.</param>\n        /// <param name=\"fileAccess\">File access settings.</param>\n        /// <param name=\"fileShare\">File share settings.</param>\n        /// <returns></returns>\n        Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare);\n\n        /// <summary>\n        /// Creates all directories and subdirectories in the specified path unless they already exist.\n        /// </summary>\n        /// <param name=\"path\">The directory to create.</param>\n        void CreateDirectory(string path);\n\n        /// <summary>\n        /// Deletes the specified file.\n        /// </summary>\n        /// <param name=\"path\">Name of the file to be deleted.</param>\n        void DeleteFile(string path);\n\n        /// <summary>\n        /// Returns an enumerable collection of full file names that match a search pattern in a specified path.\n        /// </summary>\n        /// <param name=\"path\">The relative or absolute path to the directory to search.</param>\n        /// <param name=\"searchPattern\">\n        /// The search string to match against the names of files in path.\n        /// This parameter can contain a combination of valid literal path and wildcard (* and ?) characters,\n        /// but it doesn't support regular expressions.\n        /// </param>\n        /// <returns>\n        /// An enumerable collection of the full names (including paths) for the files in the directory\n        /// specified by path and that match the specified search pattern.\n        /// </returns>\n        IEnumerable<string> EnumerateFiles(string path, string searchPattern);\n\n        /// <summary>\n        /// Returns an enumerable collection of directory full names in a specified path.\n        /// </summary>\n        /// <param name=\"path\">The relative or absolute path to the directory to search. This string is not case-sensitive.</param>\n        /// <returns>\n        /// An enumerable collection of the full names (including paths) for the directories\n        /// in the directory specified by path.\n        /// </returns>\n        IEnumerable<string> EnumerateDirectories(string path);\n\n        /// <summary>\n        /// Opens a text file, reads all the text in the file, and then closes the file\n        /// </summary>\n        /// <param name=\"path\">The file to open for reading.</param>\n        /// <returns>A string containing all the text in the file.</returns>\n        string ReadAllText(string path);\n\n        /// <summary>\n        /// Opens a text file, reads all lines of the file, and then closes the file.\n        /// </summary>\n        /// <param name=\"path\">The file to open for reading.</param>\n        /// <returns>A string array containing all lines of the file.</returns>\n        string[] ReadAllLines(string path);\n    }\n\n    /// <summary>\n    /// The real file system.\n    /// </summary>\n    public abstract class FileSystem : IFileSystem\n    {\n        public string UserHomePath => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);\n\n        public string UserDataDirectoryPath => Path.Combine(UserHomePath, Constants.GcmDataDirectoryName);\n\n        public abstract bool IsSamePath(string a, string b);\n\n        public bool FileExists(string path) => File.Exists(path);\n\n        public bool DirectoryExists(string path) => Directory.Exists(path);\n\n        public string GetCurrentDirectory() => Directory.GetCurrentDirectory();\n\n        public Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare)\n            => File.Open(path, fileMode, fileAccess, fileShare);\n\n        public void CreateDirectory(string path) => Directory.CreateDirectory(path);\n\n        public void DeleteFile(string path) => File.Delete(path);\n\n        public IEnumerable<string> EnumerateFiles(string path, string searchPattern) => Directory.EnumerateFiles(path, searchPattern);\n\n        public IEnumerable<string> EnumerateDirectories(string path) => Directory.EnumerateDirectories(path);\n\n        public string ReadAllText(string path) => File.ReadAllText(path);\n\n        public string[] ReadAllLines(string path) => File.ReadAllLines(path);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GenericHostProvider.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace GitCredentialManager\n{\n    public class GenericHostProvider : DisposableObject, IHostProvider\n    {\n        private readonly ICommandContext _context;\n        private readonly IBasicAuthentication _basicAuth;\n        private readonly IWindowsIntegratedAuthentication _winAuth;\n        private readonly IOAuthAuthentication _oauth;\n\n        public GenericHostProvider(ICommandContext context)\n            : this(context, new BasicAuthentication(context), new WindowsIntegratedAuthentication(context),\n                new OAuthAuthentication(context)) { }\n\n        public GenericHostProvider(ICommandContext context,\n                                   IBasicAuthentication basicAuth,\n                                   IWindowsIntegratedAuthentication winAuth,\n                                   IOAuthAuthentication oauth)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n            EnsureArgument.NotNull(basicAuth, nameof(basicAuth));\n            EnsureArgument.NotNull(winAuth, nameof(winAuth));\n            EnsureArgument.NotNull(oauth, nameof(oauth));\n\n            _context = context;\n            _basicAuth = basicAuth;\n            _winAuth = winAuth;\n            _oauth = oauth;\n        }\n\n        public string Id => \"generic\";\n\n        public string Name => \"Generic\";\n\n        public IEnumerable<string> SupportedAuthorityIds =>\n            EnumerableExtensions.ConcatMany(\n                BasicAuthentication.AuthorityIds,\n                WindowsIntegratedAuthentication.AuthorityIds\n            );\n\n        public bool IsSupported(InputArguments input)\n        {\n            // The generic provider should support all possible protocols (HTTP, HTTPS, SMTP, IMAP, etc)\n            return input != null && !string.IsNullOrWhiteSpace(input.Protocol);\n        }\n\n        public bool IsSupported(HttpResponseMessage response)\n        {\n            return false;\n        }\n\n        public string GetServiceName(InputArguments input)\n        {\n            // By default we assume the service name will be the absolute URI based on the\n            // input arguments from Git, without any userinfo part.\n            return input.GetRemoteUri(includeUser: false).AbsoluteUri.TrimEnd('/');\n        }\n\n        public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)\n        {\n            // Try and locate an existing credential in the OS credential store\n            string service = GetServiceName(input);\n            _context.Trace.WriteLine($\"Looking for existing credential in store with service={service} account={input.UserName}...\");\n\n            ICredential credential = _context.CredentialStore.Get(service, input.UserName);\n            if (credential == null)\n            {\n                _context.Trace.WriteLine(\"No existing credentials found.\");\n\n                // No existing credential was found, create a new one\n                _context.Trace.WriteLine(\"Creating new credential...\");\n                return await GenerateCredentialAsync(input);\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"Existing credential found.\");\n            }\n\n            return new GetCredentialResult(credential);\n        }\n\n        public Task StoreCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n\n            // WIA-authentication is signaled to Git as an empty username/password pair\n            // and we will get called to 'store' these WIA credentials.\n            // We avoid storing empty credentials.\n            if (string.IsNullOrWhiteSpace(input.UserName) && string.IsNullOrWhiteSpace(input.Password))\n            {\n                _context.Trace.WriteLine(\"Not storing empty credential.\");\n            }\n            else\n            {\n                // Add or update the credential in the store.\n                _context.Trace.WriteLine($\"Storing credential with service={service} account={input.UserName}...\");\n                _context.CredentialStore.AddOrUpdate(service, input.UserName, input.Password);\n                _context.Trace.WriteLine(\"Credential was successfully stored.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public Task EraseCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n\n            // Try to locate an existing credential\n            _context.Trace.WriteLine($\"Erasing stored credential in store with service={service} account={input.UserName}...\");\n            if (_context.CredentialStore.Remove(service, input.UserName))\n            {\n                _context.Trace.WriteLine(\"Credential was successfully erased.\");\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"No credential was erased.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public async Task<GetCredentialResult> GenerateCredentialAsync(InputArguments input)\n        {\n            ThrowIfDisposed();\n\n            // We only want to *warn* about HTTP remotes for the generic provider because it supports all protocols\n            // and, historically, we never blocked HTTP remotes in this provider.\n            // The user can always set the 'GCM_ALLOW_UNSAFE' setting to silence the warning.\n            if (!_context.Settings.AllowUnsafeRemotes &&\n                StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\"))\n            {\n                _context.Streams.Error.WriteLine(\n                    \"warning: use of unencrypted HTTP remote URLs is not recommended; \" +\n                    $\"see {Constants.HelpUrls.GcmUnsafeRemotes} for more information.\");\n            }\n\n            Uri uri = input.GetRemoteUri();\n\n            // Determine the if the host supports Windows Integration Authentication (WIA) or OAuth\n            if (!StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, \"http\") &&\n                !StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, \"https\"))\n            {\n                // Cannot check WIA or OAuth support for non-HTTP based protocols\n            }\n            // Check for an OAuth configuration for this remote\n            else if (GenericOAuthConfig.TryGet(_context.Trace, _context.Settings, input, out GenericOAuthConfig oauthConfig))\n            {\n                _context.Trace.WriteLine($\"Found generic OAuth configuration for '{uri}':\");\n                _context.Trace.WriteLine($\"\\tAuthzEndpoint   = {oauthConfig.Endpoints.AuthorizationEndpoint}\");\n                _context.Trace.WriteLine($\"\\tTokenEndpoint   = {oauthConfig.Endpoints.TokenEndpoint}\");\n                _context.Trace.WriteLine($\"\\tDeviceEndpoint  = {oauthConfig.Endpoints.DeviceAuthorizationEndpoint}\");\n                _context.Trace.WriteLine($\"\\tClientId        = {oauthConfig.ClientId}\");\n                _context.Trace.WriteLine($\"\\tClientSecret    = {oauthConfig.ClientSecret}\");\n                _context.Trace.WriteLine($\"\\tRedirectUri     = {oauthConfig.RedirectUri}\");\n                _context.Trace.WriteLine($\"\\tScopes          = [{string.Join(\", \", oauthConfig.Scopes)}]\");\n                _context.Trace.WriteLine($\"\\tUseAuthHeader   = {oauthConfig.UseAuthHeader}\");\n                _context.Trace.WriteLine($\"\\tDefaultUserName = {oauthConfig.DefaultUserName}\");\n\n                return new  GetCredentialResult(\n                    await GetOAuthAccessToken(uri, input.UserName, oauthConfig, _context.Trace2)\n                );\n            }\n            // Try detecting WIA for this remote, if permitted\n            else if (IsWindowsAuthAllowed)\n            {\n                if (PlatformUtils.IsWindows())\n                {\n                    _context.Trace.WriteLine($\"Checking host '{uri.AbsoluteUri}' for Windows Integrated Authentication...\");\n                    var supportedWiaTypes = await _winAuth.GetAuthenticationTypesAsync(uri);\n                    bool isWiaSupported = supportedWiaTypes != WindowsAuthenticationTypes.None;\n\n                    if (!isWiaSupported)\n                    {\n                        _context.Trace.WriteLine(\"Host does not support WIA.\");\n                    }\n                    else\n                    {\n                        _context.Trace.WriteLine(\"Host supports WIA.\");\n\n                        var additionalProps = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n\n                        // Has Git suppressed its own built-in NTLM authentication support?\n                        if (input.TryGetArgument(Constants.CredentialProtocol.NtlmKey, out string ntlmArg) &&\n                            StringComparer.OrdinalIgnoreCase.Equals(Constants.CredentialProtocol.NtlmSuppressed, ntlmArg))\n                        {\n                            _context.Trace.WriteLine(\"NTLM support has been suppressed by Git - showing warning.\");\n\n                            // Show a warning that NTLM authentication will not work without Git's built-in support\n                            // and ask the user what they want to do about it.\n                            NtlmSupport ntlmSupport = await _winAuth.AskEnableNtlmAsync(uri);\n                            switch (ntlmSupport)\n                            {\n                                case NtlmSupport.Once:\n                                    _context.Trace.WriteLine(\"Enabling NTLM support just once.\");\n                                    additionalProps[Constants.CredentialProtocol.NtlmKey] =\n                                        Constants.CredentialProtocol.NtlmAllow;\n                                    break;\n\n                                case NtlmSupport.Always:\n                                    _context.Trace.WriteLine($\"Enabling NTLM support for {uri}.\");\n                                    additionalProps[Constants.CredentialProtocol.NtlmKey] =\n                                        Constants.CredentialProtocol.NtlmAllow;\n                                    EnableNtlmSupport(uri);\n                                    break;\n\n                                default:\n                                    _context.Trace.WriteLine(\"User declined to enable NTLM support. Showing basic auth prompt.\");\n                                    return new GetCredentialResult(\n                                        await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, null)\n                                    );\n                            }\n                        }\n\n                        // WIA is signaled to Git using an empty username/password\n                        _context.Trace.WriteLine(\"Returning empty username/password to trigger current user auth with WIA.\");\n                        ICredential creds = new GitCredential(string.Empty, string.Empty);\n                        return new GetCredentialResult(creds)\n                        {\n                            AdditionalProperties = additionalProps\n                        };\n                    }\n                }\n                else\n                {\n                    string osType = PlatformUtils.GetPlatformInformation(_context.Trace2).OperatingSystemType;\n                    _context.Trace.WriteLine($\"Skipping check for Windows Integrated Authentication on {osType}.\");\n                }\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"Windows Integrated Authentication detection has been disabled.\");\n            }\n\n            // Use basic authentication\n            _context.Trace.WriteLine(\"Prompting for basic credentials...\");\n            return new GetCredentialResult(\n                await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName)\n            );\n        }\n\n        private void EnableNtlmSupport(Uri uri)\n        {\n            string url = uri.AbsoluteUri.TrimEnd('/');\n            IGitConfiguration config = _context.Git.GetConfiguration();\n            string key = $\"{Constants.GitConfiguration.Http.SectionName}.{url}.{Constants.GitConfiguration.Http.AllowNtlmAuth}\";\n\n            try\n            {\n                config.Set(GitConfigurationLevel.Global, key, \"true\");\n            }\n            catch (Exception ex)\n            {\n                _context.Trace.WriteLine($\"Failed to set Git configuration to enable NTLM support for {uri}\");\n                _context.Trace.WriteException(ex);\n            }\n        }\n\n        private async Task<ICredential> GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config, ITrace2 trace2)\n        {\n            // TODO: Determined user info from a webcall? ID token? Need OIDC support\n            string oauthUser = userName ?? config.DefaultUserName;\n\n            var client = new OAuth2Client(\n                HttpClient,\n                config.Endpoints,\n                config.ClientId,\n                trace2,\n                config.RedirectUri,\n                config.ClientSecret,\n                config.UseAuthHeader);\n\n            //\n            // Prepend \"refresh_token\" to the hostname to get a (hopefully) unique service name that\n            // doesn't clash with an existing credential service.\n            //\n            // Appending \"/refresh_token\" to the end of the remote URI may not always result in a unique\n            // service because users may set credential.useHttpPath and include \"/refresh_token\" as a\n            // path name.\n            //\n            string refreshService = new UriBuilder(remoteUri) { Host = $\"refresh_token.{remoteUri.Host}\" }\n                .Uri.AbsoluteUri.TrimEnd('/');\n\n            // Try to use a refresh token if we have one\n            ICredential refreshToken = _context.CredentialStore.Get(refreshService, userName);\n            if (refreshToken != null)\n            {\n                try\n                {\n                    var refreshResult = await client.GetTokenByRefreshTokenAsync(refreshToken.Password, CancellationToken.None);\n\n                    // Store new refresh token if we have been given one\n                    if (!string.IsNullOrWhiteSpace(refreshResult.RefreshToken))\n                    {\n                        _context.CredentialStore.AddOrUpdate(refreshService, refreshToken.Account, refreshResult.RefreshToken);\n                    }\n\n                    // Return the new access token\n                    return new GitCredential(oauthUser,refreshResult.AccessToken);\n                }\n                catch (OAuth2Exception ex)\n                {\n                    // Failed to use refresh token. It may have expired or been revoked.\n                    // Fall through to an interactive OAuth flow.\n                    _context.Trace.WriteLine(\"Failed to use refresh token.\");\n                    _context.Trace.WriteException(ex);\n                }\n            }\n\n            // Determine which interactive OAuth mode to use. Start by checking for mode preference in config\n            var supportedModes = OAuthAuthenticationModes.All;\n            if (_context.Settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthAuthenticationModes,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthAuthenticationModes,\n                    out string authModesStr))\n            {\n                if (Enum.TryParse(authModesStr, true, out supportedModes) && supportedModes != OAuthAuthenticationModes.None)\n                {\n                    _context.Trace.WriteLine($\"Supported authentication modes override present: {supportedModes}\");\n                }\n                else\n                {\n                    _context.Trace.WriteLine($\"Invalid value for supported authentication modes override setting: '{authModesStr}'\");\n                }\n            }\n\n            // If the server doesn't support device code we need to remove it as an option here\n            if (!config.SupportsDeviceCode)\n            {\n                supportedModes &= ~OAuthAuthenticationModes.DeviceCode;\n            }\n\n            // Prompt the user to select a mode\n            OAuthAuthenticationModes mode = await _oauth.GetAuthenticationModeAsync(remoteUri.ToString(), supportedModes);\n\n            OAuth2TokenResult tokenResult;\n            switch (mode)\n            {\n                case OAuthAuthenticationModes.Browser:\n                    tokenResult = await _oauth.GetTokenByBrowserAsync(client, config.Scopes);\n                    break;\n\n                case OAuthAuthenticationModes.DeviceCode:\n                    tokenResult = await _oauth.GetTokenByDeviceCodeAsync(client, config.Scopes);\n                    break;\n\n                default:\n                    throw new Trace2Exception(_context.Trace2, \"No authentication mode selected!\");\n            }\n\n            // Store the refresh token if we have one\n            if (!string.IsNullOrWhiteSpace(tokenResult.RefreshToken))\n            {\n                _context.CredentialStore.AddOrUpdate(refreshService, oauthUser, tokenResult.RefreshToken);\n            }\n\n            return new GitCredential(oauthUser, tokenResult.AccessToken);\n        }\n\n        /// <summary>\n        /// Check if the user permits checking for Windows Integrated Authentication.\n        /// </summary>\n        /// <remarks>\n        /// Checks the explicit 'GCM_ALLOW_WINDOWSAUTH' setting and also the legacy 'GCM_AUTHORITY' setting iif equal to \"basic\".\n        /// </remarks>\n        private bool IsWindowsAuthAllowed\n        {\n            get\n            {\n                if (_context.Settings.IsWindowsIntegratedAuthenticationEnabled)\n                {\n                    /* COMPAT: In the old GCM one workaround for common authentication problems was to specify \"basic\" as the authority\n                     *         which prevents any smart detection of provider or NTLM etc, allowing the user a chance to manually enter\n                     *         a username/password or PAT.\n                     *\n                     *         We take this old setting into account to ensure a good migration experience.\n                     */\n                    return !BasicAuthentication.AuthorityIds.Contains(_context.Settings.LegacyAuthorityOverride, StringComparer.OrdinalIgnoreCase);\n                }\n\n                return false;\n            }\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient => _httpClient ?? (_httpClient = _context.HttpClientFactory.CreateClient());\n\n        protected override void ReleaseManagedResources()\n        {\n            _winAuth.Dispose();\n            _httpClient?.Dispose();\n            base.ReleaseManagedResources();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GenericOAuthConfig.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace GitCredentialManager\n{\n    public class GenericOAuthConfig\n    {\n        public static bool TryGet(ITrace trace, ISettings settings, InputArguments input, out GenericOAuthConfig config)\n        {\n            config = new GenericOAuthConfig();\n            Uri authzEndpointUri = null;\n            Uri tokenEndpointUri = null;\n            var remoteUri = input.GetRemoteUri();\n\n            if (input.WwwAuth.Any(x => x.Contains(\"Basic realm=\\\"Gitea\\\"\", StringComparison.OrdinalIgnoreCase)))\n            {\n                trace.WriteLine($\"Using universal Gitea OAuth configuration\");\n                // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications\n                config.ClientId = WellKnown.GiteaClientId;\n                authzEndpointUri = new Uri(remoteUri, WellKnown.GiteaAuthzEndpoint);\n                tokenEndpointUri = new Uri(remoteUri, WellKnown.GiteaTokenEndpoint);\n                config.RedirectUri = WellKnown.LocalIPv4RedirectUri;\n            }\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthAuthzEndpoint,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthAuthzEndpoint,\n                    out string authzEndpoint))\n            {\n                Uri.TryCreate(remoteUri, authzEndpoint, out authzEndpointUri);\n            }\n\n            if (authzEndpointUri == null)\n            {\n                trace.WriteLine($\"Invalid OAuth configuration - missing/invalid authorize endpoint: {authzEndpoint}\");\n                config = null;\n                return false;\n            }\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthTokenEndpoint,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthTokenEndpoint,\n                    out string tokenEndpoint))\n            {\n                Uri.TryCreate(remoteUri, tokenEndpoint, out tokenEndpointUri);\n            }\n            \n            if (tokenEndpointUri == null)\n            {\n                trace.WriteLine($\"Invalid OAuth configuration - missing/invalid token endpoint: {tokenEndpoint}\");\n                config = null;\n                return false;\n            }\n\n            // Device code endpoint is optional\n            Uri deviceEndpointUri = null;\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthDeviceEndpoint,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthDeviceEndpoint,\n                    out string deviceEndpoint))\n            {\n                if (!Uri.TryCreate(remoteUri, deviceEndpoint, out deviceEndpointUri))\n                {\n                    trace.WriteLine($\"Invalid OAuth configuration - invalid device endpoint: {deviceEndpoint}\");\n                }\n            }\n\n            config.Endpoints = new OAuth2ServerEndpoints(authzEndpointUri, tokenEndpointUri)\n            {\n                DeviceAuthorizationEndpoint = deviceEndpointUri\n            };\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthClientId,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthClientId,\n                    out string clientId))\n            {\n                config.ClientId = clientId;\n            }\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthClientSecret,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthClientSecret,\n                    out string clientSecret))\n            {\n                config.ClientSecret = clientSecret;\n            }\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthRedirectUri,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthRedirectUri,\n                    out string redirectUrl) && Uri.TryCreate(redirectUrl, UriKind.Absolute, out Uri redirectUri))\n            {\n                config.RedirectUri = redirectUri;\n            }\n\n            if (config.RedirectUri == null)\n            {\n                config.RedirectUri = new Uri(\"http://127.0.0.1\");\n            }\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthScopes,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthScopes,\n                    out string scopesStr) && !string.IsNullOrWhiteSpace(scopesStr))\n            {\n                config.Scopes = scopesStr.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);\n            }\n            else\n            {\n                config.Scopes = Array.Empty<string>();\n            }\n\n            if (settings.TryGetSetting(\n                    Constants.EnvironmentVariables.OAuthClientAuthHeader,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.OAuthClientAuthHeader,\n                    out string useHeader))\n            {\n                config.UseAuthHeader = useHeader.IsTruthy();\n            }\n            else\n            {\n                // Default to true\n                config.UseAuthHeader = true;\n            }\n\n            config.DefaultUserName = settings.TryGetSetting(\n                Constants.EnvironmentVariables.OAuthDefaultUserName,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.OAuthDefaultUserName,\n                out string userName)\n                ? userName\n                : \"OAUTH_USER\";\n\n            return true;\n        }\n\n\n        public OAuth2ServerEndpoints Endpoints { get; set; }\n        public string ClientId { get; set; }\n        public string ClientSecret { get; set; }\n        public Uri RedirectUri { get; set; }\n        public string[] Scopes { get; set; }\n        public bool UseAuthHeader { get; set; }\n        public string DefaultUserName { get; set; }\n\n        public bool SupportsDeviceCode => Endpoints.DeviceAuthorizationEndpoint != null;\n\n    public static class WellKnown\n    {\n        // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications\n        public const string GiteaClientId = \"e90ee53c-94e2-48ac-9358-a874fb9e0662\";\n        // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#endpoints\n        public const string GiteaAuthzEndpoint = \"/login/oauth/authorize\";\n        public const string GiteaTokenEndpoint = \"/login/oauth/access_token\";\n        public static Uri LocalIPv4RedirectUri = new Uri(\"http://127.0.0.1\");\n    }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Git.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    public interface IGit\n    {\n        /// <summary>\n        /// The version of the Git executable tied to this instance.\n        /// </summary>\n        GitVersion Version { get; }\n\n        /// <summary>\n        /// Create a Git process object with the specified arguments.\n        /// </summary>\n        /// <param name=\"args\">Arguments to pass to the Git process.</param>\n        /// <returns>Process object ready to be started.</returns>\n        ChildProcess CreateProcess(string args);\n\n        /// <summary>\n        /// Returns true if the current Git instance is scoped to a local repository.\n        /// </summary>\n        /// <returns>True if inside a local Git repository, false otherwise.</returns>\n        bool IsInsideRepository();\n\n        /// <summary>\n        /// Return the path to the current repository, or null if this instance is not\n        /// scoped to a Git repository.\n        /// </summary>\n        /// <returns>Absolute path to the current Git repository, or null.</returns>\n        string GetCurrentRepository();\n\n        /// <summary>\n        /// Get all remotes for the current repository.\n        /// </summary>\n        /// <returns>Names of all remotes in the current repository.</returns>\n        IEnumerable<GitRemote> GetRemotes();\n\n        /// <summary>\n        /// Get the configuration object.\n        /// </summary>\n        /// <returns>Git configuration.</returns>\n        IGitConfiguration GetConfiguration();\n\n        /// <summary>\n        /// Run a Git helper process which expects and returns key-value maps\n        /// </summary>\n        /// <param name=\"args\">Arguments to the executable</param>\n        /// <param name=\"standardInput\">key-value map to pipe into stdin</param>\n        /// <returns>stdout from helper executable as key-value map</returns>\n        Task<IDictionary<string, string>> InvokeHelperAsync(string args, IDictionary<string, string> standardInput);\n    }\n\n    public class GitRemote\n    {\n        public GitRemote(string name, string fetchUrl, string pushUrl)\n        {\n            Name = name;\n            FetchUrl = fetchUrl;\n            PushUrl = pushUrl;\n        }\n\n        public string Name { get; }\n        public string FetchUrl { get; }\n        public string PushUrl { get; }\n    }\n\n    public class GitProcess : IGit\n    {\n        private readonly ITrace _trace;\n        private readonly ITrace2 _trace2;\n        private readonly IProcessManager _processManager;\n        private readonly string _gitPath;\n        private readonly string _workingDirectory;\n\n        public GitProcess(ITrace trace, ITrace2 trace2, IProcessManager processManager, string gitPath, string workingDirectory = null)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            EnsureArgument.NotNull(trace2, nameof(trace2));\n            EnsureArgument.NotNull(processManager, nameof(processManager));\n            EnsureArgument.NotNullOrWhiteSpace(gitPath, nameof(gitPath));\n\n            _trace = trace;\n            _trace2 = trace2;\n            _processManager = processManager;\n            _gitPath = gitPath;\n            _workingDirectory = workingDirectory;\n        }\n\n        private GitVersion _version;\n        public GitVersion Version\n        {\n            get\n            {\n                if (_version is null)\n                {\n                    using (var git = CreateProcess(\"version\"))\n                    {\n                        git.Start(Trace2ProcessClass.Git);\n\n                        string data = git.StandardOutput.ReadToEnd();\n                        git.WaitForExit();\n\n                        Match match = Regex.Match(data, @\"^git version (?'value'.*)\");\n                        if (match.Success)\n                        {\n                            _version = new GitVersion(match.Groups[\"value\"].Value);\n                        }\n                        else\n                        {\n                            _version = new GitVersion();\n                        }\n                    }\n                }\n\n                return _version;\n            }\n        }\n\n        public IGitConfiguration GetConfiguration()\n        {\n            return new GitProcessConfiguration(_trace, this);\n        }\n\n        public bool IsInsideRepository()\n        {\n            return !string.IsNullOrWhiteSpace(GetCurrentRepositoryInternal(suppressStreams: true));\n        }\n\n        public string GetCurrentRepository()\n        {\n            return GetCurrentRepositoryInternal(suppressStreams: false);\n        }\n\n        private string GetCurrentRepositoryInternal(bool suppressStreams)\n        {\n            using (var git = CreateProcess(\"rev-parse --absolute-git-dir\"))\n            {\n                // Redirect standard error to ensure any error messages are captured and not exposed to the user's console\n                if (suppressStreams)\n                {\n                    git.StartInfo.RedirectStandardError = true;\n                }\n\n                git.Start(Trace2ProcessClass.Git);\n                string data = git.StandardOutput.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        return data.TrimEnd();\n                    case 128: // Not inside a Git repository\n                        return null;\n                    default:\n                        var message = \"Failed to get current Git repository\";\n                        _trace.WriteLine($\"{message} (exit={git.ExitCode})\");\n                        throw CreateGitException(git, message, _trace2);\n                }\n            }\n        }\n\n        public IEnumerable<GitRemote> GetRemotes()\n        {\n            using (var git = CreateProcess(\"remote -v show\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                // To avoid deadlocks, always read the output stream first and then wait\n                // TODO: don't read in all the data at once; stream it\n                string data = git.StandardOutput.ReadToEnd();\n                string stderr = git.StandardError.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        break;\n                    case 128 when stderr.Contains(\"not a git repository\"): // Not inside a Git repository\n                        yield break;\n                    default:\n                        var message = \"Failed to enumerate Git remotes\";\n                        _trace.WriteLine($\"{message} (exit={git.ExitCode})\");\n                        throw CreateGitException(git, message, _trace2);\n                }\n\n                string[] lines = data.Split('\\n');\n\n                // Remotes are always output in groups of two (fetch and push)\n                for (int i = 0; i + 1 < lines.Length; i += 2)\n                {\n                    // The fetch URL is written first, followed by the push URL\n                    string[] fetchLine = lines[i].Split();\n                    string[] pushLine = lines[i + 1].Split();\n\n                    // Remote name is always first (and should match between fetch/push)\n                    string remoteName = fetchLine[0];\n\n                    // The next part, if present, is the URL\n                    string fetchUrl = null;\n                    string pushUrl = null;\n                    if (fetchLine.Length > 1 && !string.IsNullOrWhiteSpace(fetchLine[1])) fetchUrl = fetchLine[1].TrimEnd();\n                    if (pushLine.Length > 1 && !string.IsNullOrWhiteSpace(pushLine[1]))   pushUrl  = pushLine[1].TrimEnd();\n\n                    yield return new GitRemote(remoteName, fetchUrl, pushUrl);\n                }\n            }\n        }\n\n        public ChildProcess CreateProcess(string args)\n        {\n            return _processManager.CreateProcess(_gitPath, args, false, _workingDirectory);\n        }\n\n        // This code was originally copied from\n        // src/shared/Core/Authentication/AuthenticationBase.cs\n        // That code is for GUI helpers in this codebase, while the below is for\n        // communicating over Git's stdin/stdout helper protocol. The GUI helper\n        // protocol will one day use a different IPC mechanism, whereas this code\n        // has to follow what upstream Git does.\n        public async Task<IDictionary<string, string>> InvokeHelperAsync(string args, IDictionary<string, string> standardInput = null)\n        {\n            var procStartInfo = new ProcessStartInfo(_gitPath)\n            {\n                Arguments = args,\n                RedirectStandardInput = true,\n                RedirectStandardOutput = true,\n                RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled\n                UseShellExecute = false\n            };\n\n            var process = _processManager.CreateProcess(procStartInfo);\n            if (!process.Start(Trace2ProcessClass.Git))\n            {\n                var format = \"Failed to start Git helper '{0}'\";\n                var message = string.Format(format, args);\n                throw new Trace2Exception(_trace2, message, format);\n            }\n\n            if (!(standardInput is null))\n            {\n                await process.StandardInput.WriteDictionaryAsync(standardInput);\n                // some helpers won't continue until they see EOF\n                // cf git-credential-cache\n                process.StandardInput.Close();\n            }\n\n            IDictionary<string, string> resultDict = await process.StandardOutput.ReadDictionaryAsync(StringComparer.OrdinalIgnoreCase);\n\n            await Task.Run(() => process.WaitForExit());\n            int exitCode = process.ExitCode;\n\n            if (exitCode != 0)\n            {\n                if (!resultDict.TryGetValue(\"error\", out string errorMessage))\n                {\n                    errorMessage = \"Unknown\";\n                }\n\n                throw new Exception($\"helper error ({exitCode}): {errorMessage}\");\n            }\n\n            return resultDict;\n        }\n\n        public static GitException CreateGitException(ChildProcess git, string message, ITrace2 trace2 = null)\n        {\n            var gitMessage = git.StandardError.ReadToEnd();\n\n            if (trace2 != null)\n                throw new Trace2GitException(trace2, message, git.ExitCode, gitMessage);\n\n            throw new GitException(message, gitMessage, git.ExitCode);\n        }\n    }\n\n    public class GitException : Exception\n    {\n        public string GitErrorMessage { get; }\n\n        public int ExitCode { get; }\n\n        public GitException(string message, string gitErrorMessage, int exitCode)\n            : base(message)\n        {\n            GitErrorMessage = gitErrorMessage;\n            ExitCode = exitCode;\n        }\n    }\n\n    public static class GitExtensions\n    {\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GitConfiguration.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Text;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Invoked for each Git configuration entry during an enumeration (<see cref=\"IGitConfiguration.Enumerate\"/>).\n    /// </summary>\n    /// <param name=\"entry\">Current configuration entry.</param>\n    /// <returns>True to continue enumeration, false to stop enumeration.</returns>\n    public delegate bool GitConfigurationEnumerationCallback(GitConfigurationEntry entry);\n\n    public enum GitConfigurationLevel\n    {\n        All,\n        System,\n        Global,\n        Local,\n        Unknown,\n    }\n\n    public enum GitConfigurationType\n    {\n        Raw,\n        Bool,\n        Path\n    }\n\n    public interface IGitConfiguration\n    {\n        /// <summary>\n        /// Enumerate all configuration entries invoking the specified callback for each entry.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"cb\">Callback to invoke for each configuration entry.</param>\n        void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb);\n\n        /// <summary>\n        /// Try and get the value of a configuration entry as a string.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"type\">Type constraint to which the config value should be canonicalized.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <param name=\"value\">Configuration entry value.</param>\n        /// <returns>True if the value was found, false otherwise.</returns>\n        bool TryGet(GitConfigurationLevel level, GitConfigurationType type, string name, out string value);\n\n        /// <summary>\n        /// Set the value of a configuration entry.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <param name=\"value\">Configuration entry value.</param>\n        void Set(GitConfigurationLevel level, string name, string value);\n\n        /// <summary>\n        /// Add a new value for a configuration entry.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <param name=\"value\">Configuration entry value.</param>\n        void Add(GitConfigurationLevel level, string name, string value);\n\n        /// <summary>\n        /// Deletes a configuration entry from the highest level.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        void Unset(GitConfigurationLevel level, string name);\n\n        /// <summary>\n        /// Get all value of a multivar configuration entry.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"type\">Type constraint to which the config values should be canonicalized.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <returns>All values of the multivar configuration entry.</returns>\n        IEnumerable<string> GetAll(GitConfigurationLevel level, GitConfigurationType type, string name);\n\n        /// <summary>\n        /// Get all values of a multivar configuration entry.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"type\">Type constraint to which the config values should be canonicalized.</param>\n        /// <param name=\"nameRegex\">Configuration entry name regular expression.</param>\n        /// <param name=\"valueRegex\">Regular expression to filter which variables we're interested in. Use null to indicate all.</param>\n        /// <returns>All values of the multivar configuration entry.</returns>\n        IEnumerable<string> GetRegex(GitConfigurationLevel level, GitConfigurationType type, string nameRegex, string valueRegex);\n\n        /// <summary>\n        /// Set a multivar configuration entry value.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"nameRegex\">Configuration entry name regular expression.</param>\n        /// <param name=\"valueRegex\">Regular expression to indicate which values to replace.</param>\n        /// <param name=\"value\">Configuration entry value.</param>\n        /// <remarks>If the regular expression does not match any existing entry, a new entry is created.</remarks>\n        void ReplaceAll(GitConfigurationLevel level, string nameRegex, string valueRegex, string value);\n\n        /// <summary>\n        /// Deletes one or several entries from a multivar.\n        /// </summary>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <param name=\"valueRegex\">Regular expression to indicate which values to delete.</param>\n        void UnsetAll(GitConfigurationLevel level, string name, string valueRegex);\n    }\n\n    /// <summary>\n    /// Represents a single configuration entry with its origin and level.\n    /// </summary>\n    internal class ConfigCacheEntry\n    {\n        public string Value { get; set; }\n        public GitConfigurationLevel Level { get; set; }\n\n        public ConfigCacheEntry(string scope, string value)\n        {\n            Value = value;\n            Level = ParseScope(scope);\n        }\n\n        private static GitConfigurationLevel ParseScope(string scope)\n        {\n            switch (scope)\n            {\n                case \"system\":\n                    return GitConfigurationLevel.System;\n                case \"global\":\n                    return GitConfigurationLevel.Global;\n                case \"local\":\n                case \"worktree\":\n                case \"command\":\n                    return GitConfigurationLevel.Local;\n                default:\n                    return GitConfigurationLevel.Unknown;\n            }\n        }\n    }\n\n    /// <summary>\n    /// Cache for Git configuration entries loaded from 'git config list --show-scope -z'.\n    /// </summary>\n    internal class ConfigCache\n    {\n        private Dictionary<string, List<ConfigCacheEntry>> _entries;\n        private readonly object _lock = new object();\n\n        public bool IsLoaded => _entries != null;\n\n        public void Load(string data, ITrace trace)\n        {\n            lock (_lock)\n            {\n                var entries = new Dictionary<string, List<ConfigCacheEntry>>(GitConfigurationKeyComparer.Instance);\n\n                var scope = new StringBuilder();\n                var key = new StringBuilder();\n                var value = new StringBuilder();\n\n                int i = 0;\n                while (i < data.Length)\n                {\n                    scope.Clear();\n                    key.Clear();\n                    value.Clear();\n\n                    // Read scope (NUL terminated)\n                    while (i < data.Length && data[i] != '\\0')\n                    {\n                        scope.Append(data[i++]);\n                    }\n\n                    if (i >= data.Length)\n                    {\n                        trace.WriteLine(\"Invalid Git configuration output. Expected null terminator (\\\\0) after scope.\");\n                        break;\n                    }\n\n                    // Skip the NUL terminator\n                    i++;\n\n                    // Read key (newline terminated)\n                    while (i < data.Length && data[i] != '\\n')\n                    {\n                        key.Append(data[i++]);\n                    }\n\n                    if (i >= data.Length)\n                    {\n                        trace.WriteLine(\"Invalid Git configuration output. Expected newline terminator (\\\\n) after key.\");\n                        break;\n                    }\n\n                    // Skip the newline terminator\n                    i++;\n\n                    // Read value (NUL terminated)\n                    while (i < data.Length && data[i] != '\\0')\n                    {\n                        value.Append(data[i++]);\n                    }\n\n                    if (i >= data.Length)\n                    {\n                        trace.WriteLine(\"Invalid Git configuration output. Expected null terminator (\\\\0) after value.\");\n                        break;\n                    }\n\n                    // Skip the NUL terminator\n                    i++;\n\n                    string keyStr = key.ToString();\n                    var entry = new ConfigCacheEntry(scope.ToString(), value.ToString());\n\n                    if (!entries.ContainsKey(keyStr))\n                    {\n                        entries[keyStr] = new List<ConfigCacheEntry>();\n                    }\n                    entries[keyStr].Add(entry);\n                }\n\n                _entries = entries;\n            }\n        }\n\n        public bool TryGet(string name, GitConfigurationLevel level, out string value)\n        {\n            lock (_lock)\n            {\n                if (_entries == null)\n                {\n                    value = null;\n                    return false;\n                }\n\n                if (!_entries.TryGetValue(name, out var entryList))\n                {\n                    value = null;\n                    return false;\n                }\n\n                // Find the last entry matching the level filter (respects Git's precedence)\n                // Git config precedence: system < global < local, so last match wins\n                ConfigCacheEntry lastMatch = null;\n                foreach (var entry in entryList)\n                {\n                    if (level == GitConfigurationLevel.All || entry.Level == level)\n                    {\n                        lastMatch = entry;\n                    }\n                }\n\n                if (lastMatch != null)\n                {\n                    value = lastMatch.Value;\n                    return true;\n                }\n\n                value = null;\n                return false;\n            }\n        }\n\n        public IEnumerable<string> GetAll(string name, GitConfigurationLevel level)\n        {\n            lock (_lock)\n            {\n                if (_entries == null || !_entries.TryGetValue(name, out var entryList))\n                {\n                    return Array.Empty<string>();\n                }\n\n                var results = new List<string>();\n                foreach (var entry in entryList)\n                {\n                    if (level == GitConfigurationLevel.All || entry.Level == level)\n                    {\n                        results.Add(entry.Value);\n                    }\n                }\n\n                return results;\n            }\n        }\n\n        public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb)\n        {\n            lock (_lock)\n            {\n                if (_entries == null)\n                    return;\n\n                foreach (var kvp in _entries)\n                {\n                    foreach (var entry in kvp.Value)\n                    {\n                        if (level == GitConfigurationLevel.All || entry.Level == level)\n                        {\n                            var configEntry = new GitConfigurationEntry(kvp.Key, entry.Value);\n                            if (!cb(configEntry))\n                            {\n                                return;\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        public void Clear()\n        {\n            lock (_lock)\n            {\n                _entries = null;\n            }\n        }\n    }\n\n    public class GitProcessConfiguration : IGitConfiguration\n    {\n        private static readonly GitVersion TypeConfigMinVersion = new GitVersion(2, 18, 0);\n        private static readonly GitVersion ConfigListTypeMinVersion = new GitVersion(2, 54, 0);\n        private static readonly GitVersion ConfigListTypeMinVfsBase = new GitVersion(2, 53, 0);\n        private static readonly GitVersion ConfigListTypeMinVfsSuffix = new GitVersion(0, 1);\n\n        private readonly ITrace _trace;\n        private readonly GitProcess _git;\n        private readonly Dictionary<GitConfigurationType, ConfigCache> _cache;\n        private readonly bool _useCache;\n\n        internal GitProcessConfiguration(ITrace trace, GitProcess git) : this(trace, git, useCache: true)\n        {\n        }\n\n        internal GitProcessConfiguration(ITrace trace, GitProcess git, bool useCache)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            EnsureArgument.NotNull(git, nameof(git));\n\n            _trace = trace;\n            _git = git;\n\n            // 'git config list --type=<X>' requires Git 2.54.0+,\n            // or microsoft/git fork 2.53.0.vfs.0.1+\n            if (useCache && !SupportsConfigListType(git))\n            {\n                trace.WriteLine($\"Git version {git.Version.OriginalString} does not support 'git config list --type'; config cache disabled\");\n                useCache = false;\n            }\n\n            _useCache = useCache;\n            _cache = useCache ? new Dictionary<GitConfigurationType, ConfigCache>() : null;\n        }\n\n        private static bool SupportsConfigListType(GitProcess git)\n        {\n            if (git.Version >= ConfigListTypeMinVersion)\n                return true;\n\n            // The microsoft/git fork fast-tracked the fix into 2.53.0.vfs.0.1.\n            // Version strings like \"2.53.0.vfs.0.1\" parse as [2,53,0] because\n            // GitVersion stops at the non-integer \"vfs\" component, so we check\n            // the original string for the \".vfs.\" marker and parse the suffix.\n            string versionStr = git.Version.OriginalString;\n            if (versionStr != null)\n            {\n                int vfsIdx = versionStr.IndexOf(\".vfs.\");\n                if (vfsIdx >= 0)\n                {\n                    var baseVersion = new GitVersion(versionStr.Substring(0, vfsIdx));\n                    var vfsSuffix = new GitVersion(versionStr.Substring(vfsIdx + 5));\n                    return baseVersion >= ConfigListTypeMinVfsBase\n                        && vfsSuffix >= ConfigListTypeMinVfsSuffix;\n                }\n            }\n\n            return false;\n        }\n\n        private void EnsureCacheLoaded(GitConfigurationType type)\n        {\n            ConfigCache cache;\n            if (!_useCache || (_cache.TryGetValue(type, out cache) && cache.IsLoaded))\n            {\n                return;\n            }\n\n            if (cache == null)\n            {\n                cache = new ConfigCache();\n                _cache[type] = cache;\n            }\n\n            string typeArg;\n\n            switch (type)\n            {\n            case GitConfigurationType.Raw:\n                typeArg = \"--no-type\";\n                break;\n\n            case GitConfigurationType.Path:\n                typeArg = \"--type=path\";\n                break;\n\n            case GitConfigurationType.Bool:\n                typeArg = \"--type=bool\";\n                break;\n\n            default:\n                return;\n            }\n\n            using (ChildProcess git = _git.CreateProcess($\"config list --show-scope -z {typeArg}\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                // To avoid deadlocks, always read the output stream first and then wait\n                string data = git.StandardOutput.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        cache.Load(data, _trace);\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to load config cache (exit={git.ExitCode})\");\n                        // Don't throw - fall back to individual commands\n                        break;\n                }\n            }\n        }\n\n        private void InvalidateCache()\n        {\n            if (_useCache)\n            {\n                foreach (ConfigCache cache in _cache.Values)\n                {\n                    cache.Clear();\n                }\n            }\n        }\n\n        public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb)\n        {\n            if (_useCache)\n            {\n                EnsureCacheLoaded(GitConfigurationType.Raw);\n\n                ConfigCache cache = _cache[GitConfigurationType.Raw];\n\n                if (cache.IsLoaded)\n                {\n                    cache.Enumerate(level, cb);\n                    return;\n                }\n            }\n\n            // Fall back to original implementation\n            string levelArg = GetLevelFilterArg(level);\n            using (ChildProcess git = _git.CreateProcess($\"config --null {levelArg} --list\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                // To avoid deadlocks, always read the output stream first and then wait\n                // TODO: don't read in all the data at once; stream it\n                string data = git.StandardOutput.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to enumerate config entries (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, \"Failed to enumerate all Git configuration entries\");\n                }\n\n                var name  = new StringBuilder();\n                var value = new StringBuilder();\n                int i = 0;\n                while (i < data.Length)\n                {\n                    name.Clear();\n                    value.Clear();\n\n                    // Read key name (LF terminated)\n                    while (i < data.Length && data[i] != '\\n')\n                    {\n                        name.Append(data[i++]);\n                    }\n\n                    if (i >= data.Length)\n                    {\n                        _trace.WriteLine(\"Invalid Git configuration output. Expected newline terminator (\\\\n) after key.\");\n                        break;\n                    }\n\n                    // Skip the LF terminator\n                    i++;\n\n                    // Read value (null terminated)\n                    while (i < data.Length && data[i] != '\\0')\n                    {\n                        value.Append(data[i++]);\n                    }\n\n                    if (i >= data.Length)\n                    {\n                        _trace.WriteLine(\"Invalid Git configuration output. Expected null terminator (\\\\0) after value.\");\n                        break;\n                    }\n\n                    // Skip the null terminator\n                    i++;\n\n                    var entry = new GitConfigurationEntry(name.ToString(), value.ToString());\n\n                    if (!cb(entry))\n                    {\n                        break;\n                    }\n                }\n            }\n        }\n\n        public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, string name, out string value)\n        {\n            if (_useCache)\n            {\n                EnsureCacheLoaded(type);\n\n                ConfigCache cache = _cache[type];\n                if (cache.IsLoaded)\n                {\n                    // Cache is loaded, use it for the result (whether found or not)\n                    return cache.TryGet(name, level, out value);\n                }\n            }\n\n            // Fall back to individual git config command if cache not available\n            string levelArg = GetLevelFilterArg(level);\n            string typeArg = GetCanonicalizeTypeArg(type);\n            using (ChildProcess git = _git.CreateProcess($\"config --null {levelArg} {typeArg} {QuoteCmdArg(name)}\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                // To avoid deadlocks, always read the output stream first and then wait\n                // TODO: don't read in all the data at once; stream it\n                string data = git.StandardOutput.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        break;\n                    case 1: // Not found\n                        value = null;\n                        return false;\n                    default: // Error\n                        _trace.WriteLine($\"Failed to read Git configuration entry '{name}'. (exit={git.ExitCode}, level={level})\");\n                        value = null;\n                        return false;\n                }\n\n                string[] entries = data.Split('\\0');\n                if (entries.Length > 0)\n                {\n                    value = entries[0];\n                    return true;\n                }\n\n                value = null;\n                return false;\n            }\n        }\n\n        public void Set(GitConfigurationLevel level, string name, string value)\n        {\n            EnsureSpecificLevel(level);\n\n            string levelArg = GetLevelFilterArg(level);\n            using (ChildProcess git = _git.CreateProcess($\"config {levelArg} {QuoteCmdArg(name)} {QuoteCmdArg(value)}\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        InvalidateCache();\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to set config entry '{name}' to value '{value}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to set Git configuration entry '{name}'\");\n                }\n            }\n        }\n\n        public void Add(GitConfigurationLevel level, string name, string value)\n        {\n            EnsureSpecificLevel(level);\n\n            string levelArg = GetLevelFilterArg(level);\n            using (ChildProcess git = _git.CreateProcess($\"config {levelArg} --add {QuoteCmdArg(name)} {QuoteCmdArg(value)}\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        InvalidateCache();\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to add config entry '{name}' with value '{value}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to add Git configuration entry '{name}'\");\n                }\n            }\n        }\n\n        public void Unset(GitConfigurationLevel level, string name)\n        {\n            EnsureSpecificLevel(level);\n\n            string levelArg = GetLevelFilterArg(level);\n            using (ChildProcess git = _git.CreateProcess($\"config {levelArg} --unset {QuoteCmdArg(name)}\"))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                    case 5: // Trying to unset a value that does not exist\n                        InvalidateCache();\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to unset config entry '{name}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to unset Git configuration entry '{name}'\");\n                }\n            }\n        }\n\n        public IEnumerable<string> GetAll(GitConfigurationLevel level, GitConfigurationType type, string name)\n        {\n            if (_useCache)\n            {\n                EnsureCacheLoaded(type);\n\n                ConfigCache cache = _cache[type];\n                if (cache.IsLoaded)\n                {\n                    var cachedValues = cache.GetAll(name, level);\n                    foreach (var val in cachedValues)\n                    {\n                        yield return val;\n                    }\n                    yield break;\n                }\n            }\n\n            // Fall back to individual git config command\n            string levelArg = GetLevelFilterArg(level);\n            string typeArg = GetCanonicalizeTypeArg(type);\n\n            var gitArgs = $\"config --null {levelArg} {typeArg} --get-all {QuoteCmdArg(name)}\";\n\n            using (ChildProcess git = _git.CreateProcess(gitArgs))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                // To avoid deadlocks, always read the output stream first and then wait\n                // TODO: don't read in all the data at once; stream it\n                string data = git.StandardOutput.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        string[] entries = data.Split('\\0');\n\n                        // Because each line terminates with the \\0 character, splitting leaves us with one\n                        // bogus blank entry at the end of the array which we should ignore\n                        for (var i = 0; i < entries.Length - 1; i++)\n                        {\n                            yield return entries[i];\n                        }\n                        break;\n\n                    case 1: // No results\n                        break;\n\n                    default:\n                        _trace.WriteLine($\"Failed to get all config entries '{name}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to get all Git configuration entries '{name}'\");\n                }\n            }\n        }\n\n        public IEnumerable<string> GetRegex(GitConfigurationLevel level, GitConfigurationType type, string nameRegex, string valueRegex)\n        {\n            string levelArg = GetLevelFilterArg(level);\n            string typeArg = GetCanonicalizeTypeArg(type);\n\n            var gitArgs = $\"config --null {levelArg} {typeArg} --get-regex {QuoteCmdArg(nameRegex)}\";\n            if (valueRegex != null)\n            {\n                gitArgs += $\" {QuoteCmdArg(valueRegex)}\";\n            }\n\n            using (ChildProcess git = _git.CreateProcess(gitArgs))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                // To avoid deadlocks, always read the output stream first and then wait\n                // TODO: don't read in all the data at once; stream it\n                string data = git.StandardOutput.ReadToEnd();\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                    case 1: // No results\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to get all multivar regex '{nameRegex}' and value regex '{valueRegex}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to get Git configuration multi-valued entries with name regex '{nameRegex}'\");\n                }\n\n                string[] entries = data.Split('\\0');\n                foreach (string entry in entries)\n                {\n                    string[] kvp = entry.Split(new[]{'\\n'}, count: 2);\n\n                    if (kvp.Length == 2)\n                    {\n                        yield return kvp[1];\n                    }\n                }\n            }\n        }\n\n        public void ReplaceAll(GitConfigurationLevel level, string name, string valueRegex, string value)\n        {\n            EnsureSpecificLevel(level);\n\n            string levelArg = GetLevelFilterArg(level);\n            var gitArgs = $\"config {levelArg} --replace-all {QuoteCmdArg(name)} {QuoteCmdArg(value)}\";\n            if (valueRegex != null)\n            {\n                gitArgs += $\" {QuoteCmdArg(valueRegex)}\";\n            }\n\n            using (ChildProcess git = _git.CreateProcess(gitArgs))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                        InvalidateCache();\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to replace all multivar '{name}' and value regex '{valueRegex}' with new value '{value}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to replace all Git configuration multi-valued entries '{name}'\");\n                }\n            }\n        }\n\n        public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex)\n        {\n            EnsureSpecificLevel(level);\n\n            string levelArg = GetLevelFilterArg(level);\n            var gitArgs = $\"config {levelArg} --unset-all {QuoteCmdArg(name)}\";\n            if (valueRegex != null)\n            {\n                gitArgs += $\" {QuoteCmdArg(valueRegex)}\";\n            }\n\n            using (ChildProcess git = _git.CreateProcess(gitArgs))\n            {\n                git.Start(Trace2ProcessClass.Git);\n                git.WaitForExit();\n\n                switch (git.ExitCode)\n                {\n                    case 0: // OK\n                    case 5: // Trying to unset a value that does not exist\n                        InvalidateCache();\n                        break;\n                    default:\n                        _trace.WriteLine($\"Failed to unset all multivar '{name}' with value regex '{valueRegex}' (exit={git.ExitCode}, level={level})\");\n                        throw GitProcess.CreateGitException(git, $\"Failed to unset all Git configuration multi-valued entries '{name}'\");\n                }\n            }\n        }\n\n        private static void EnsureSpecificLevel(GitConfigurationLevel level)\n        {\n            if (level == GitConfigurationLevel.All)\n            {\n                throw new InvalidOperationException(\"Must have a specific configuration level filter to modify values.\");\n            }\n        }\n\n        private static string GetLevelFilterArg(GitConfigurationLevel level)\n        {\n            switch (level)\n            {\n                case GitConfigurationLevel.System:\n                    return \"--system\";\n                case GitConfigurationLevel.Global:\n                    return \"--global\";\n                case GitConfigurationLevel.Local:\n                    return \"--local\";\n                case GitConfigurationLevel.Unknown:\n                default:\n                    return null;\n            }\n        }\n\n        private string GetCanonicalizeTypeArg(GitConfigurationType type)\n        {\n            if (_git.Version >= TypeConfigMinVersion)\n            {\n                return type switch\n                {\n                    GitConfigurationType.Bool   => \"--type=bool\",\n                    GitConfigurationType.Path   => \"--type=path\",\n                    _                           => null\n                };\n            }\n            else\n            {\n                return type switch\n                {\n                    GitConfigurationType.Bool   => \"--bool\",\n                    GitConfigurationType.Path   => \"--path\",\n                    _                           => null\n                };\n            }\n        }\n\n        public static string QuoteCmdArg(string str)\n        {\n            bool needsQuotes = string.IsNullOrEmpty(str);\n            var result = new StringBuilder();\n\n            for (int i = 0; i < (str?.Length ?? 0); i++)\n            {\n                switch (str![i])\n                {\n                    case '\"':\n                        result.Append(\"\\\\\\\"\");\n                        needsQuotes = true;\n                        break;\n\n                    case ' ':\n                    case '{':\n                    case '*':\n                    case '?':\n                    case '\\r':\n                    case '\\n':\n                    case '\\t':\n                    case '\\'':\n                        result.Append(str[i]);\n                        needsQuotes = true;\n                        break;\n\n                    case '\\\\':\n                        int end = i;\n\n                        // Copy all the '\\'s in this run.\n                        while (end < str.Length && str[end] == '\\\\')\n                        {\n                            result.Append('\\\\');\n                            end++;\n                        }\n\n                        // If we ended the run of '\\'s with a '\"' then we need to double up the number of '\\'s.\n                        // The '\"' will be escaped on the next pass of the loop.\n                        // Also if we have reached the end of the string, and we need to book-end the result\n                        // with double quotes ('\"') we should escape all the '\\'s to prevent ending on an\n                        // escaped '\"' in the result.\n                        if (end < str.Length && str[end] == '\"' ||\n                            end == str.Length && needsQuotes)\n                        {\n                            result.Append('\\\\', end - i);\n                        }\n\n                        // Back-off one character\n                        if (end > i)\n                        {\n                            end--;\n                        }\n\n                        i = end;\n                        break;\n\n                    default:\n                        result.Append(str[i]);\n                        break;\n                }\n            }\n\n            if (needsQuotes)\n            {\n                result.Insert(0, '\"');\n                result.Append('\"');\n            }\n\n            return result.ToString();\n        }\n    }\n\n    public static class GitConfigurationExtensions\n    {\n        /// <summary>\n        /// Enumerate all configuration entries invoking the specified callback for each entry.\n        /// </summary>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"cb\">Callback to invoke for each matching configuration entry.</param>\n        public static void Enumerate(this IGitConfiguration config, GitConfigurationEnumerationCallback cb)\n        {\n            config.Enumerate(GitConfigurationLevel.All, cb);\n        }\n\n        /// <summary>\n        /// Enumerate all configuration entries invoking the specified callback for each entry.\n        /// </summary>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"section\">Optional section name to filter; use null for any.</param>\n        /// <param name=\"property\">Optional property name to filter; use null for any.</param>\n        /// <param name=\"cb\">Callback to invoke for each matching configuration entry.</param>\n        public static void Enumerate(this IGitConfiguration config,\n            GitConfigurationLevel level, string section, string property, GitConfigurationEnumerationCallback cb)\n        {\n            config.Enumerate(level, entry =>\n            {\n                if (GitConfigurationKeyComparer.TrySplit(entry.Key, out string entrySection, out _, out string entryProperty) &&\n                    (section  is null || GitConfigurationKeyComparer.SectionComparer.Equals(section, entrySection)) &&\n                    (property is null || GitConfigurationKeyComparer.PropertyComparer.Equals(property, entryProperty)))\n                {\n                    return cb(entry);\n                }\n\n                return true;\n            });\n        }\n\n        /// <summary>\n        /// Enumerate all configuration entries invoking the specified callback for each entry.\n        /// </summary>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"section\">Optional section name to filter; use null for any.</param>\n        /// <param name=\"property\">Optional property name to filter; use null for any.</param>\n        /// <param name=\"cb\">Callback to invoke for each matching configuration entry.</param>\n        public static void Enumerate(this IGitConfiguration config, string section, string property, GitConfigurationEnumerationCallback cb)\n        {\n            Enumerate(config, GitConfigurationLevel.All, section, property, cb);\n        }\n\n        /// <summary>\n        /// Get the value of a configuration entry as a string.\n        /// </summary>\n        /// <exception cref=\"System.Collections.Generic.KeyNotFoundException\">A configuration entry with the specified key was not found.</exception>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"level\">Filter to the specific configuration level.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <returns>Configuration entry value.</returns>\n        public static string Get(this IGitConfiguration config, GitConfigurationLevel level, string name)\n        {\n            if (!config.TryGet(level, GitConfigurationType.Raw, name, out string value))\n            {\n                throw new KeyNotFoundException($\"Git configuration entry with the name '{name}' was not found.\");\n            }\n\n            return value;\n        }\n\n        /// <summary>\n        /// Get the value of a configuration entry as a string.\n        /// </summary>\n        /// <exception cref=\"System.Collections.Generic.KeyNotFoundException\">A configuration entry with the specified key was not found.</exception>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <returns>Configuration entry value.</returns>\n        public static string Get(this IGitConfiguration config, string name)\n        {\n            return Get(config, GitConfigurationLevel.All, name);\n        }\n\n        /// <summary>\n        /// Try and get the value of a configuration entry as a string.\n        /// </summary>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <param name=\"isPath\">Whether the entry should be canonicalized as a path.</param>\n        /// <param name=\"value\">Configuration entry value.</param>\n        /// <returns>True if the value was found, false otherwise.</returns>\n        public static bool TryGet(this IGitConfiguration config, string name, bool isPath, out string value)\n        {\n            return config.TryGet(GitConfigurationLevel.All,\n                isPath ? GitConfigurationType.Path : GitConfigurationType.Raw,\n                name, out value);\n        }\n\n        /// <summary>\n        /// Get all value of a multivar configuration entry.\n        /// </summary>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"name\">Configuration entry name.</param>\n        /// <returns>All values of the multivar configuration entry.</returns>\n        public static IEnumerable<string> GetAll(this IGitConfiguration config, string name)\n        {\n            return config.GetAll(GitConfigurationLevel.All, GitConfigurationType.Raw, name);\n        }\n\n        /// <summary>\n        /// Get all values of a multivar configuration entry.\n        /// </summary>\n        /// <param name=\"config\">Configuration object.</param>\n        /// <param name=\"nameRegex\">Configuration entry name regular expression.</param>\n        /// <param name=\"valueRegex\">Regular expression to filter which variables we're interested in. Use null to indicate all.</param>\n        /// <returns>All values of the multivar configuration entry.</returns>\n        public static IEnumerable<string> GetRegex(this IGitConfiguration config, string nameRegex, string valueRegex)\n        {\n            return config.GetRegex(GitConfigurationLevel.All, GitConfigurationType.Raw, nameRegex, valueRegex);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GitConfigurationEntry.cs",
    "content": "namespace GitCredentialManager\n{\n    public class GitConfigurationEntry\n    {\n        public GitConfigurationEntry(string key, string value)\n        {\n            Key = key;\n            Value = value;\n        }\n\n        public string Key { get; }\n        public string Value { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GitConfigurationKeyComparer.cs",
    "content": "using System;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents string comparison of Git configuration entry key names.\n    /// </summary>\n    /// <remarks>\n    /// Git configuration entries have the form \"section[.scope].property\", where the\n    /// scope part is optional.\n    /// <para/>\n    /// The section and property components are NOT case sensitive.\n    /// The scope component if present IS case sensitive.\n    /// </remarks>\n    public class GitConfigurationKeyComparer : StringComparer\n    {\n        public static readonly GitConfigurationKeyComparer Instance = new GitConfigurationKeyComparer();\n\n        public static readonly StringComparer SectionComparer = OrdinalIgnoreCase;\n        public static readonly StringComparer ScopeComparer = Ordinal;\n        public static readonly StringComparer PropertyComparer = OrdinalIgnoreCase;\n\n        private GitConfigurationKeyComparer() { }\n\n        public override int Compare(string x, string y)\n        {\n            TrySplit(x, out string xSection, out string xScope, out string xProperty);\n            TrySplit(y, out string ySection, out string yScope, out string yProperty);\n\n            int cmpSection = OrdinalIgnoreCase.Compare(xSection, ySection);\n            if (cmpSection != 0) return cmpSection;\n\n            int cmpProperty = OrdinalIgnoreCase.Compare(xProperty, yProperty);\n            if (cmpProperty != 0) return cmpProperty;\n\n            return Ordinal.Compare(xScope, yScope);\n        }\n\n        public override bool Equals(string x, string y)\n        {\n            if (ReferenceEquals(x, y)) return true;\n            if (x is null || y is null) return false;\n\n            TrySplit(x, out string xSection, out string xScope, out string xProperty);\n            TrySplit(y, out string ySection, out string yScope, out string yProperty);\n\n            // Section and property names are not case sensitive, but the inner 'scope' IS case sensitive!\n            return OrdinalIgnoreCase.Equals(xSection, ySection) &&\n                   OrdinalIgnoreCase.Equals(xProperty, yProperty) &&\n                   Ordinal.Equals(xScope, yScope);\n        }\n\n        public override int GetHashCode(string obj)\n        {\n            TrySplit(obj, out string section, out string scope, out string property);\n\n            int code = OrdinalIgnoreCase.GetHashCode(section) ^\n                       OrdinalIgnoreCase.GetHashCode(property);\n\n            return scope is null\n                ? code\n                : code ^ Ordinal.GetHashCode(scope);\n        }\n\n        public static bool TrySplit(string str, out string section, out string scope, out string property)\n        {\n            section = null;\n            scope = null;\n            property = null;\n\n            if (string.IsNullOrWhiteSpace(str))\n            {\n                return false;\n            }\n\n            section = str.TruncateFromIndexOf('.');\n            property = str.TrimUntilLastIndexOf('.');\n            int scopeLength = str.Length - (section.Length + property.Length + 2);\n            scope = scopeLength > 0 ? str.Substring(section.Length + 1, scopeLength) : null;\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GitStreamReader.cs",
    "content": "using System.IO;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager;\n\n/// <summary>\n/// StreamReader that does NOT consider a lone carriage-return as a new-line character,\n/// only a line-feed or carriage-return immediately followed by a line-feed.\n/// <para/>\n/// The only major operating system that uses a lone carriage-return as a new-line character\n/// is the classic Macintosh OS (before OS X), which is not supported by Git.\n/// </summary>\npublic class GitStreamReader : StreamReader\n{\n    public GitStreamReader(Stream stream, Encoding encoding) : base(stream, encoding) { }\n\n    public override string ReadLine()\n    {\n#if NETFRAMEWORK\n        return ReadLineAsync().ConfigureAwait(false).GetAwaiter().GetResult();\n#else\n        return ReadLineAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();\n#endif\n    }\n\n#if NETFRAMEWORK\n    public override async Task<string> ReadLineAsync()\n#else\n    public override async ValueTask<string> ReadLineAsync(CancellationToken cancellationToken)\n#endif\n    {\n        int nr;\n        var sb = new StringBuilder();\n        var buffer = new char[1];\n        bool lastWasCR = false;\n\n        while ((nr = await base.ReadAsync(buffer, 0, 1).ConfigureAwait(false)) > 0)\n        {\n            char c = buffer[0];\n\n            // Only treat a line-feed as a new-line character.\n            // Carriage-returns alone are NOT considered new-line characters.\n            if (c == '\\n')\n            {\n                if (lastWasCR)\n                {\n                    // If the last character was a carriage-return we should remove it from the string builder\n                    // since together with this line-feed it is considered a new-line character.\n                    sb.Length--;\n                }\n\n                // We have a new-line character, so we should stop reading.\n                break;\n            }\n\n            lastWasCR = c == '\\r';\n\n            sb.Append(c);\n        }\n\n        if (sb.Length == 0 && nr == 0)\n        {\n            return null;\n        }\n\n        return sb.ToString();\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/GitVersion.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace GitCredentialManager\n{\n    public class GitVersion : IComparable, IComparable<GitVersion>\n    {\n        private readonly string _originalString;\n        private List<int> _components;\n\n        public GitVersion(string versionString)\n        {\n            if (versionString is null)\n            {\n                _components = new List<int>();\n                return;\n            }\n\n            _originalString = versionString;\n\n            string[] splitVersion = versionString.Split('.');\n            _components = new List<int>(splitVersion.Length);\n\n            foreach (string part in splitVersion)\n            {\n                if (Int32.TryParse(part, out int component))\n                {\n                    _components.Add(component);\n                }\n                else\n                {\n                    // Exit at the first non-integer component\n                    break;\n                }\n            }\n        }\n\n        public GitVersion(params int[] components)\n        {\n            _components = components.ToList();\n        }\n\n        public override string ToString()\n        {\n            return string.Join(\".\", _components);\n        }\n\n        public string OriginalString\n        {\n            get\n            {\n                if (_originalString is null)\n                {\n                    return ToString();\n                }\n\n                return _originalString;\n            }\n        }\n\n        public int CompareTo(object obj)\n        {\n            if (obj is null)\n            {\n                return 1;\n            }\n\n            GitVersion other = obj as GitVersion;\n            if (other == null)\n            {\n                throw new ArgumentException(\"A GitVersion object is required for comparison.\", \"obj\");\n            }\n\n            return CompareTo(other);\n        }\n\n        public int CompareTo(GitVersion other)\n        {\n            if (other is null)\n            {\n                return 1;\n            }\n\n            // Compare for as many components as the two versions have in common. If a\n            // component does not exist in a components list, it is assumed to be 0.\n            int thisCount = _components.Count, otherCount = other._components.Count;\n            for (int i = 0; i < Math.Max(thisCount, otherCount); i++)\n            {\n                int thisComponent = i < thisCount ? _components[i] : 0;\n                int otherComponent = i < otherCount ? other._components[i] : 0;\n                if (thisComponent != otherComponent)\n                {\n                    return thisComponent.CompareTo(otherComponent);\n                }\n            }\n\n            // No discrepencies found in versions\n            return 0;\n        }\n\n        public static int Compare(GitVersion left, GitVersion right)\n        {\n            if (object.ReferenceEquals(left, right))\n            {\n                return 0;\n            }\n\n            if (left is null)\n            {\n                return -1;\n            }\n\n            return left.CompareTo(right);\n        }\n\n        public override bool Equals(object obj)\n        {\n            GitVersion other = obj as GitVersion;\n            if (other is null)\n            {\n                return false;\n            }\n\n            return this.CompareTo(other) == 0;\n        }\n\n        public override int GetHashCode()\n        {\n            return ToString().GetHashCode();\n        }\n\n        public static bool operator ==(GitVersion left, GitVersion right)\n        {\n            if (left is null)\n            {\n                return right is null;\n            }\n\n            return left.Equals(right);\n        }\n\n        public static bool operator !=(GitVersion left, GitVersion right)\n        {\n            return !(left == right);\n        }\n\n        public static bool operator <(GitVersion left, GitVersion right)\n        {\n            return Compare(left, right) < 0;\n        }\n\n        public static bool operator >(GitVersion left, GitVersion right)\n        {\n            return Compare(left, right) > 0;\n        }\n\n        public static bool operator <=(GitVersion left, GitVersion right)\n        {\n            return Compare(left, right) <= 0;\n        }\n\n        public static bool operator >=(GitVersion left, GitVersion right)\n        {\n            return Compare(left, right) >= 0;\n        }\n    }\n}"
  },
  {
    "path": "src/shared/Core/Gpg.cs",
    "content": "using System;\nusing System.Diagnostics;\n\nnamespace GitCredentialManager\n{\n    public interface IGpg\n    {\n        string DecryptFile(string path);\n\n        void EncryptFile(string path, string gpgId, string contents);\n    }\n\n    public class Gpg : IGpg\n    {\n        private readonly string _gpgPath;\n        private readonly ISessionManager _sessionManager;\n        private readonly IProcessManager _processManager;\n        private readonly ITrace2 _trace2;\n\n        public Gpg(string gpgPath, ISessionManager sessionManager, IProcessManager processManager, ITrace2 trace2)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(gpgPath, nameof(gpgPath));\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n            EnsureArgument.NotNull(trace2, nameof(trace2));\n\n            _gpgPath = gpgPath;\n            _sessionManager = sessionManager;\n            _processManager = processManager;\n            _trace2 = trace2;\n        }\n\n        public string DecryptFile(string path)\n        {\n            var psi = new ProcessStartInfo(_gpgPath, $\"--batch --decrypt \\\"{path}\\\"\")\n            {\n                UseShellExecute = false,\n                RedirectStandardOutput = true,\n                // Suppress verbose decryption messages\n                // Ok to redirect stderr for non-Git-related processes\n                RedirectStandardError = true,\n            };\n\n            PrepareEnvironment(psi);\n\n            using (var gpg = _processManager.CreateProcess(psi))\n            {\n                if (!gpg.Start(Trace2ProcessClass.Other))\n                {\n                    throw new Trace2Exception(_trace2, \"Failed to start gpg.\");\n                }\n\n                gpg.WaitForExit();\n\n                if (gpg.ExitCode != 0)\n                {\n                    string stdout = gpg.StandardOutput.ReadToEnd();\n                    string stderr = gpg.StandardError.ReadToEnd();\n                    var format = \"Failed to decrypt file '{0}' with gpg. exit={1}, out={2}, err={3}\";\n                    var message = string.Format(format, path, gpg.ExitCode, stdout, stderr);\n                    throw new Trace2Exception(_trace2, message, format);\n                }\n\n                return gpg.StandardOutput.ReadToEnd();\n            }\n        }\n\n        public void EncryptFile(string path, string gpgId, string contents)\n        {\n            var psi = new ProcessStartInfo(_gpgPath, $\"--encrypt --batch --recipient \\\"{gpgId}\\\" --output \\\"{path}\\\"\")\n            {\n                UseShellExecute = false,\n                RedirectStandardInput = true,\n                RedirectStandardOutput = true,\n                RedirectStandardError = true, // Ok to redirect stderr for non-git-related processes\n            };\n\n            PrepareEnvironment(psi);\n\n            using (var gpg = _processManager.CreateProcess(psi))\n            {\n                if (!gpg.Start(Trace2ProcessClass.Other))\n                {\n                    throw new Trace2Exception(_trace2, \"Failed to start gpg.\");\n                }\n\n                gpg.StandardInput.Write(contents);\n                gpg.StandardInput.Close();\n\n                gpg.WaitForExit();\n\n                if (gpg.ExitCode != 0)\n                {\n                    string stdout = gpg.StandardOutput.ReadToEnd();\n                    string stderr = gpg.StandardError.ReadToEnd();\n                    var format = \"Failed to encrypt file '{0}' with gpg. exit={1}, out={2}, err={3}\";\n                    var message = string.Format(format, path, gpg.ExitCode, stdout, stderr);\n                    throw new Trace2Exception(_trace2, message, format);\n                }\n            }\n        }\n\n        private void PrepareEnvironment(ProcessStartInfo psi)\n        {\n            // If we're in a headless environment over SSH, and we don't have a GPG_TTY\n            // explicitly set, use the SSH_TTY variable for our GPG_TTY.\n            if (!_sessionManager.IsDesktopSession &&\n                !psi.Environment.ContainsKey(\"GPG_TTY\") &&\n                psi.Environment.ContainsKey(\"SSH_TTY\"))\n            {\n                psi.Environment[\"GPG_TTY\"] = psi.Environment[\"SSH_TTY\"];\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/HostProvider.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents a particular Git hosting service and provides for the creation of credentials to access the remote.\n    /// </summary>\n    public interface IHostProvider : IDisposable\n    {\n        /// <summary>\n        /// Unique identifier of the hosting provider.\n        /// </summary>\n        string Id { get; }\n\n        /// <summary>\n        /// Name of the hosting provider.\n        /// </summary>\n        string Name { get; }\n\n        /// <summary>\n        /// Supported authority identifiers.\n        /// </summary>\n        IEnumerable<string> SupportedAuthorityIds { get; }\n\n        /// <summary>\n        /// Determine if the <see cref=\"InputArguments\"/> are recognized by this particular Git hosting provider.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        /// <returns>True if the provider supports the Git credential request, false otherwise.</returns>\n        bool IsSupported(InputArguments input);\n\n        /// <summary>\n        /// Determine if the <see cref=\"HttpResponseMessage\"/> identifies a recognized Git hosting provider.\n        /// </summary>\n        /// <param name=\"response\">Response message of an endpoint query.</param>\n        /// <returns>True if the provider supports the host provider at the endpoint, false otherwise.</returns>\n        bool IsSupported(HttpResponseMessage response);\n\n        /// <summary>\n        /// Get a credential for accessing the remote Git repository on this hosting service.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        /// <returns>A credential Git can use to authenticate to the remote repository.</returns>\n        Task<GetCredentialResult> GetCredentialAsync(InputArguments input);\n\n        /// <summary>\n        /// Store a credential for accessing the remote Git repository on this hosting service.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        Task StoreCredentialAsync(InputArguments input);\n\n        /// <summary>\n        /// Erase a stored credential for accessing the remote Git repository on this hosting service.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        Task EraseCredentialAsync(InputArguments input);\n    }\n\n    public class GetCredentialResult\n    {\n        public GetCredentialResult(ICredential credential)\n        {\n            Credential = credential;\n        }\n\n        public ICredential Credential { get; set; }\n        public IDictionary<string, string> AdditionalProperties { get; set; }\n            = new  Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n    }\n\n    /// <summary>\n    /// Represents a Git hosting provider where credentials can be stored and recalled in/from the Operating System's\n    /// secure credential store.\n    /// </summary>\n    public abstract class HostProvider : DisposableObject, IHostProvider\n    {\n        protected HostProvider(ICommandContext context)\n        {\n            Context = context;\n        }\n\n        /// <summary>\n        /// The current command execution context.\n        /// </summary>\n        protected ICommandContext Context { get; }\n\n        public abstract string Id { get; }\n\n        public abstract string Name { get; }\n\n        public virtual IEnumerable<string> SupportedAuthorityIds => Enumerable.Empty<string>();\n\n        public abstract bool IsSupported(InputArguments input);\n\n        public virtual bool IsSupported(HttpResponseMessage response)\n        {\n            return false;\n        }\n\n        /// <summary>\n        /// Return a string that uniquely identifies the service that a credential should be stored against.\n        /// </summary>\n        /// <remarks>\n        /// <para>\n        /// This key forms part of the identifier used to retrieve and store credentials from the OS secure\n        /// credential storage system. It is important the returned value is stable over time to avoid any\n        /// potential re-authentication requests.\n        /// </para>\n        /// <para>\n        /// The default implementation returns the absolute URI formed by from the <see cref=\"InputArguments\"/>\n        /// without any userinfo component. Any trailing slashes are trimmed.\n        /// </para>\n        /// </remarks>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        /// <returns>Credential service name.</returns>\n        public virtual string GetServiceName(InputArguments input)\n        {\n            // By default we assume the service name will be the absolute URI based on the\n            // input arguments from Git, without any userinfo part.\n            return input.GetRemoteUri(includeUser: false).AbsoluteUri.TrimEnd('/');\n        }\n\n        /// <summary>\n        /// Create a new credential used for accessing the remote Git repository on this hosting service.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        /// <returns>A credential Git can use to authenticate to the remote repository.</returns>\n        public abstract Task<ICredential> GenerateCredentialAsync(InputArguments input);\n\n        public virtual async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)\n        {\n            // Try and locate an existing credential in the OS credential store\n            string service = GetServiceName(input);\n            Context.Trace.WriteLine($\"Looking for existing credential in store with service={service} account={input.UserName}...\");\n\n            ICredential credential = Context.CredentialStore.Get(service, input.UserName);\n            if (credential == null)\n            {\n                Context.Trace.WriteLine(\"No existing credentials found.\");\n\n                // No existing credential was found, create a new one\n                Context.Trace.WriteLine(\"Creating new credential...\");\n                credential = await GenerateCredentialAsync(input);\n                Context.Trace.WriteLine(\"Credential created.\");\n            }\n            else\n            {\n                Context.Trace.WriteLine(\"Existing credential found.\");\n            }\n\n            return new GetCredentialResult(credential);\n        }\n\n        public virtual Task StoreCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n\n            // WIA-authentication is signaled to Git as an empty username/password pair\n            // and we will get called to 'store' these WIA credentials.\n            // We avoid storing empty credentials.\n            if (string.IsNullOrWhiteSpace(input.UserName) && string.IsNullOrWhiteSpace(input.Password))\n            {\n                Context.Trace.WriteLine(\"Not storing empty credential.\");\n            }\n            else\n            {\n                // Add or update the credential in the store.\n                Context.Trace.WriteLine($\"Storing credential with service={service} account={input.UserName}...\");\n                Context.CredentialStore.AddOrUpdate(service, input.UserName, input.Password);\n                Context.Trace.WriteLine(\"Credential was successfully stored.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public virtual Task EraseCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n\n            // Try to locate an existing credential\n            Context.Trace.WriteLine($\"Erasing stored credential in store with service={service} account={input.UserName}...\");\n            if (Context.CredentialStore.Remove(service, input.UserName))\n            {\n                Context.Trace.WriteLine(\"Credential was successfully erased.\");\n            }\n            else\n            {\n                Context.Trace.WriteLine(\"No credential was erased.\");\n            }\n\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/HostProviderRegistry.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Priority in which host providers are queried during auto-detection.\n    /// </summary>\n    public enum HostProviderPriority\n    {\n        Low = 0,\n        Normal = 1,\n        High = 2,\n    }\n\n    /// <summary>\n    /// Represents a collection of <see cref=\"IHostProvider\"/>s which are selected based on Git credential query\n    /// <see cref=\"InputArguments\"/>.\n    /// </summary>\n    /// <remarks>\n    /// All registered <see cref=\"IHostProvider\"/>s will be disposed when this <see cref=\"IHostProviderRegistry\"/> is disposed.\n    /// </remarks>\n    public interface IHostProviderRegistry : IDisposable\n    {\n        /// <summary>\n        /// Add the given <see cref=\"IHostProvider\"/> to this registry.\n        /// </summary>\n        /// <param name=\"hostProvider\">Host provider to register.</param>\n        /// <param name=\"priority\">Priority at which the provider will be considered when auto-detecting.</param>\n        /// <remarks>Providers will be disposed of when this registry instance is disposed itself.</remarks>\n        void Register(IHostProvider hostProvider, HostProviderPriority priority);\n\n        /// <summary>\n        /// Select a <see cref=\"IHostProvider\"/> that can service the Git credential query based on the\n        /// <see cref=\"InputArguments\"/>.\n        /// </summary>\n        /// <param name=\"input\">Input arguments of a Git credential query.</param>\n        /// <returns>A host provider that can service the given query.</returns>\n        Task<IHostProvider> GetProviderAsync(InputArguments input);\n    }\n\n    /// <summary>\n    /// Host provider registry where each provider is queried by priority order until the first\n    /// provider that supports the credential query or matches the endpoint query is found.\n    /// </summary>\n    public class HostProviderRegistry : IHostProviderRegistry\n    {\n        private readonly ICommandContext _context;\n        private readonly IDictionary<HostProviderPriority, ICollection<IHostProvider>> _hostProviders;\n\n        public HostProviderRegistry(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n            _hostProviders = new Dictionary<HostProviderPriority, ICollection<IHostProvider>>();\n        }\n\n        public void Register(IHostProvider hostProvider, HostProviderPriority priority)\n        {\n            EnsureArgument.NotNull(hostProvider, nameof(hostProvider));\n\n            if (StringComparer.OrdinalIgnoreCase.Equals(hostProvider.Id, Constants.ProviderIdAuto))\n            {\n                throw new ArgumentException(\n                    $\"A host provider cannot be registered with the ID '{Constants.ProviderIdAuto}'\",\n                    nameof(hostProvider));\n            }\n\n            if (hostProvider.SupportedAuthorityIds.Any(y => StringComparer.OrdinalIgnoreCase.Equals(y, Constants.AuthorityIdAuto)))\n            {\n                throw new ArgumentException(\n                    $\"A host provider cannot be registered with the legacy authority ID '{Constants.AuthorityIdAuto}'\",\n                    nameof(hostProvider));\n            }\n\n            if (!_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))\n            {\n                providers = new List<IHostProvider>();\n                _hostProviders[priority] = providers;\n            }\n\n            providers.Add(hostProvider);\n        }\n\n        public async Task<IHostProvider> GetProviderAsync(InputArguments input)\n        {\n            IHostProvider provider;\n\n            //\n            // Try and locate a specified provider\n            //\n            if (_context.Settings.ProviderOverride is string providerId)\n            {\n                _context.Trace.WriteLine($\"Host provider override was set id='{providerId}'\");\n\n                if (!StringComparer.OrdinalIgnoreCase.Equals(Constants.ProviderIdAuto, providerId))\n                {\n                    provider = _hostProviders\n                        .SelectMany(x => x.Value)\n                        .FirstOrDefault(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, providerId));\n\n                    if (provider is null)\n                    {\n                        _context.Trace.WriteLine($\"No host provider was found with ID '{providerId}'.. falling back to auto-detection.\");\n                        _context.Streams.Error.WriteLine($\"warning: a host provider override was set but no such provider '{providerId}' was found. Falling back to auto-detection.\");\n                    }\n                    else\n                    {\n                        return provider;\n                    }\n                }\n            }\n            //\n            // Try and locate a provider by supported authorities\n            //\n            else if (_context.Settings.LegacyAuthorityOverride is string authority)\n            {\n                _context.Trace.WriteLine($\"Host provider authority override was set authority='{authority}'\");\n                _context.Streams.Error.WriteLine(\"warning: the `credential.authority` and `GCM_AUTHORITY` settings are deprecated.\");\n                _context.Streams.Error.WriteLine($\"warning: see {Constants.HelpUrls.GcmAuthorityDeprecated} for more information.\");\n\n                if (!StringComparer.OrdinalIgnoreCase.Equals(Constants.AuthorityIdAuto, authority))\n                {\n                    provider = _hostProviders\n                        .SelectMany(x => x.Value)\n                        .FirstOrDefault(x => x.SupportedAuthorityIds.Contains(authority, StringComparer.OrdinalIgnoreCase));\n\n                    if (provider is null)\n                    {\n                        _context.Trace.WriteLine($\"No host provider was found with authority '{authority}'.. falling back to auto-detection.\");\n                        _context.Streams.Error.WriteLine($\"warning: a supported authority override was set but no such provider supporting authority '{authority}' was found. Falling back to auto-detection.\");\n                    }\n                    else\n                    {\n                        return provider;\n                    }\n                }\n            }\n\n            //\n            // Auto-detection\n            // Perform auto-detection network probe and remember the result\n            //\n            _context.Trace.WriteLine(\"Performing auto-detection of host provider.\");\n\n            var uri = input.GetRemoteUri();\n            if (uri is null)\n            {\n                throw new Trace2Exception(_context.Trace2, \"Unable to detect host provider without a remote URL\");\n            }\n\n            // We can only probe HTTP(S) URLs - for SMTP, IMAP, etc we cannot do network probing\n            bool canProbeUri = StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, \"http\") ||\n                               StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, \"https\");\n\n            var probeTimeout = TimeSpan.FromMilliseconds(_context.Settings.AutoDetectProviderTimeout);\n            _context.Trace.WriteLine($\"Auto-detect probe timeout is {probeTimeout.TotalSeconds} ms.\");\n\n            HttpResponseMessage probeResponse = null;\n\n            async Task<IHostProvider> MatchProviderAsync(HostProviderPriority priority, bool probe)\n            {\n                if (_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))\n                {\n                    _context.Trace.WriteLine($\"Checking against {providers.Count} host providers registered with priority '{priority}'.\");\n\n                    // Try matching using the static Git input arguments first (cheap)\n                    if (providers.TryGetFirst(x => x.IsSupported(input), out IHostProvider match))\n                    {\n                        return match;\n                    }\n\n                    // Try matching using the HTTP response from a query to the remote URL (expensive).\n                    // The user may have disabled this feature with a zero or negative timeout for performance reasons.\n                    // We only probe the remote once and reuse the same response for all providers.\n                    if (probe && probeTimeout.TotalMilliseconds > 0)\n                    {\n                        if (probeResponse is null)\n                        {\n                            _context.Trace.WriteLine(\"Querying remote URL for host provider auto-detection.\");\n\n                            using (HttpClient client = _context.HttpClientFactory.CreateClient())\n                            {\n                                client.Timeout = probeTimeout;\n\n                                try\n                                {\n                                    probeResponse = await client.HeadAsync(uri);\n                                }\n                                catch (TaskCanceledException)\n                                {\n                                    _context.Streams.Error.WriteLine($\"warning: auto-detection of host provider took too long (>{probeTimeout.TotalMilliseconds}ms)\");\n                                    _context.Streams.Error.WriteLine($\"warning: see {Constants.HelpUrls.GcmAutoDetect} for more information.\");\n                                }\n                                catch (Exception ex)\n                                {\n                                    // The auto detect probing failed for some other reason.\n                                    // We don't particular care why, but we should not crash!\n                                    _context.Streams.Error.WriteLine($\"warning: failed to probe '{uri}' to detect provider\");\n                                    _context.Streams.Error.WriteLine($\"warning: {ex.Message}\");\n                                    _context.Streams.Error.WriteLine($\"warning: see {Constants.HelpUrls.GcmAutoDetect} for more information.\");\n                                }\n                            }\n                        }\n\n                        if (providers.TryGetFirst(x => x.IsSupported(probeResponse), out match))\n                        {\n                            return match;\n                        }\n                    }\n                }\n\n                return null;\n            }\n\n            // Match providers starting with the highest priority\n            IHostProvider match = await MatchProviderAsync(HostProviderPriority.High, canProbeUri) ??\n                                  await MatchProviderAsync(HostProviderPriority.Normal, canProbeUri) ??\n                                  await MatchProviderAsync(HostProviderPriority.Low, canProbeUri) ??\n                                  throw new Exception(\"No host provider available to service this request.\");\n\n            // If we ended up making a network call then set the host provider explicitly\n            // to avoid future calls!\n            if (probeResponse != null)\n            {\n                IGitConfiguration gitConfig = _context.Git.GetConfiguration();\n                var keyName = string.Format(CultureInfo.InvariantCulture, \"{0}.{1}.{2}\",\n                    Constants.GitConfiguration.Credential.SectionName, uri.ToString().TrimEnd('/'),\n                    Constants.GitConfiguration.Credential.Provider);\n\n                try\n                {\n                    _context.Trace.WriteLine($\"Remembering host provider for '{uri}' as '{match.Id}'...\");\n                    gitConfig.Set(GitConfigurationLevel.Global, keyName, match.Id);\n                }\n                catch (Exception ex)\n                {\n                    var message = \"Failed to set host provider!\";\n                    _context.Trace.WriteLine(message);\n                    _context.Trace.WriteException(ex);\n                    _context.Trace2.WriteError(message);\n\n                    _context.Streams.Error.WriteLine(\"warning: failed to remember result of host provider detection!\");\n                    _context.Streams.Error.WriteLine($\"warning: try setting this manually: `git config --global {keyName} {match.Id}`\");\n                }\n            }\n\n            return match;\n        }\n\n        public void Dispose()\n        {\n            // Dispose of all registered providers to give them a chance to clean up and release any resources\n            foreach (IHostProvider provider in _hostProviders.Values.SelectMany(x => x))\n            {\n                provider.Dispose();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/HttpClientExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    public static class HttpClientExtensions\n    {\n        #region HeadAsync\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, string requestUri)\n        {\n            return client.HeadAsync(new Uri(requestUri));\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, Uri requestUri)\n        {\n            return client.HeadAsync(requestUri, HttpCompletionOption.ResponseHeadersRead);\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, string requestUri, HttpCompletionOption completionOption)\n        {\n            return client.HeadAsync(new Uri(requestUri), completionOption);\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, Uri requestUri, HttpCompletionOption completionOption)\n        {\n            return client.HeadAsync(requestUri, completionOption, CancellationToken.None);\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, string requestUri, CancellationToken cancellationToken)\n        {\n            return client.HeadAsync(new Uri(requestUri), cancellationToken);\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, Uri requestUri, CancellationToken cancellationToken)\n        {\n            return client.HeadAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, string requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken)\n        {\n            return client.HeadAsync(new Uri(requestUri), completionOption, cancellationToken);\n        }\n\n        public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, Uri requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken)\n        {\n            return client.SendAsync(new HttpRequestMessage(HttpMethod.Head, requestUri), completionOption, cancellationToken);\n        }\n\n        #endregion\n\n        #region SendAsync with content and headers\n\n        public static Task<HttpResponseMessage> SendAsync(this HttpClient client,\n            HttpMethod method,\n            Uri requestUri,\n            IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null,\n            HttpContent content = null)\n        {\n            var request = new HttpRequestMessage(method, requestUri)\n            {\n                Content = content\n            };\n\n            if (!(headers is null))\n            {\n                foreach (var kvp in headers)\n                {\n                    request.Headers.Add(kvp.Key, kvp.Value);\n                }\n            }\n\n            return client.SendAsync(request);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/HttpClientFactory.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Net.Security;\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Constructs <see cref=\"HttpClient\"/>s that have been configured for use in Git Credential Manager.\n    /// </summary>\n    public interface IHttpClientFactory\n    {\n        /// <summary>\n        /// Get a new instance of <see cref=\"HttpClient\"/> with default request headers set.\n        /// </summary>\n        /// <remarks>\n        /// Callers should reuse instances of <see cref=\"HttpClient\"/> returned from this method as long\n        /// as they are needed, rather than repeatably call this method to create new ones.\n        /// <para/>\n        /// Creating a new <see cref=\"HttpClient\"/> consumes one free socket which may not be released\n        /// by the Operating System until sometime after the client is disposed, leading to possible free\n        /// socket exhaustion.\n        /// <para/>\n        /// The client instance is configured for use behind a proxy using Git's http.proxy configuration\n        /// for the local repository (the current working directory), if found.\n        /// </remarks>\n        /// <returns>New client instance with default headers.</returns>\n        HttpClient CreateClient();\n    }\n\n    public class HttpClientFactory : IHttpClientFactory\n    {\n        private readonly IFileSystem _fileSystem;\n        private readonly ITrace _trace;\n        private readonly ITrace2 _trace2;\n        private readonly ISettings _settings;\n        private readonly IStandardStreams _streams;\n\n        public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ITrace2 trace2, ISettings settings, IStandardStreams streams)\n        {\n            EnsureArgument.NotNull(fileSystem, nameof(fileSystem));\n            EnsureArgument.NotNull(trace, nameof(trace));\n            EnsureArgument.NotNull(settings, nameof(settings));\n            EnsureArgument.NotNull(streams, nameof(streams));\n\n            _fileSystem = fileSystem;\n            _trace = trace;\n            _trace2 = trace2;\n            _settings = settings;\n            _streams = streams;\n        }\n\n        public HttpClient CreateClient()\n        {\n            _trace.WriteLine(\"Creating new HTTP client instance...\");\n\n            HttpClientHandler handler;\n\n            if (TryCreateProxy(out IWebProxy proxy))\n            {\n                _trace.WriteLine(\"HTTP client is using the configured proxy.\");\n\n                handler = new HttpClientHandler\n                {\n                    Proxy = proxy,\n                    UseProxy = true\n                };\n            }\n            else\n            {\n                handler = new HttpClientHandler();\n            }\n\n            // Trace Git's chosen SSL/TLS backend\n            _trace.WriteLine($\"Git's SSL/TLS backend is: {_settings.TlsBackend}\");\n\n            // Mirror Git for Windows and only send client TLS certificates automatically if we're using\n            // the schannel backend _and_ the user has opted in to sending them.\n            if (_settings.TlsBackend == TlsBackend.Schannel &&\n                _settings.AutomaticallyUseClientCertificates)\n            {\n                _trace.WriteLine(\"Configured to automatically send TLS client certificates.\");\n                handler.ClientCertificateOptions = ClientCertificateOption.Automatic;\n            }\n\n            // Configure server certificate verification and warn if we're bypassing validation\n\n            // IsCertificateVerificationEnabled takes precedence over custom TLS cert verification\n            if (!_settings.IsCertificateVerificationEnabled)\n            {\n                _trace.WriteLine(\"TLS certificate verification has been disabled.\");\n                _streams.Error.WriteLine(\"warning: ----------------- SECURITY WARNING ----------------\");\n                _streams.Error.WriteLine(\"warning: | TLS certificate verification has been disabled! |\");\n                _streams.Error.WriteLine(\"warning: ---------------------------------------------------\");\n                _streams.Error.WriteLine($\"warning: HTTPS connections may not be secure. See {Constants.HelpUrls.GcmTlsVerification} for more information.\");\n\n#if NETFRAMEWORK\n                ServicePointManager.ServerCertificateValidationCallback = (req, cert, chain, errors) => true;\n#else\n                handler.ServerCertificateCustomValidationCallback = (req, cert, chain, errors) => true;\n#endif\n            }\n            // If schannel is the TLS backend, custom certificate usage must be explicitly enabled\n            else if (!string.IsNullOrWhiteSpace(_settings.CustomCertificateBundlePath) &&\n                ((_settings.TlsBackend != TlsBackend.Schannel) || _settings.UseCustomCertificateBundleWithSchannel))\n            {\n                string certBundlePath = _settings.CustomCertificateBundlePath;\n                _trace.WriteLine($\"Custom certificate verification has been enabled with certificate bundle at {certBundlePath}\");\n\n                // Throw exception if cert bundle file not found\n                if (!_fileSystem.FileExists(certBundlePath))\n                {\n                    var format = \"Custom certificate bundle not found at path: {0}\";\n                    var message = string.Format(format, certBundlePath);\n                    throw new Trace2FileNotFoundException(_trace2, message, format, certBundlePath);\n                }\n\n                Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> validationCallback = (cert, chain, errors) =>\n                {\n                    // Fail immediately if there are non-chain issues with the remote cert\n                    if ((errors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)\n                    {\n                        return false;\n                    }\n\n                    // Import the custom certs\n                    X509Certificate2Collection certBundle = new X509Certificate2Collection();\n                    certBundle.Import(certBundlePath);\n\n                    try\n                    {\n                        // Add the certs to the chain\n                        chain.ChainPolicy.ExtraStore.AddRange(certBundle);\n\n                        // Rebuild the chain\n                        if (chain.Build(cert))\n                        {\n                            return true;\n                        }\n\n                        // Manually handle case where only error is UntrustedRoot\n                        if (chain.ChainStatus.All(status => status.Status == X509ChainStatusFlags.UntrustedRoot))\n                        {\n                            // Verify root is contained within the certBundle\n                            X509Certificate2 rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;\n                            var matchingCerts = certBundle.Find(X509FindType.FindByThumbprint, rootCert.Thumbprint, false);\n                            if (matchingCerts.Count > 0)\n                            {\n                                // Check the content of the first matching cert found (do\n                                // not try others if mismatched - mirrors OpenSSL:\n                                // https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_default_verify_paths.html#WARNINGS)\n                                return rootCert.RawData.SequenceEqual(matchingCerts[0].RawData);\n                            }\n                            else\n                            {\n                                // Untrusted root not found in custom cert bundle\n                                return false;\n                            }\n                        }\n\n                        // Fail for errors other than UntrustedRoot\n                        return false;\n                    }\n                    finally\n                    {\n                        // Dispose imported cert bundle\n                        for (int i = 0; i < certBundle.Count; i++)\n                        {\n                            certBundle[i].Dispose();\n                        }\n                    }\n                };\n\n                // Set the custom server certificate validation callback.\n                // NOTE: this is executed after the default platform server certificate validation is performed\n#if NETFRAMEWORK\n                ServicePointManager.ServerCertificateValidationCallback = (_, cert, chain, errors) =>\n                {\n                    // Fail immediately if the cert or chain isn't present\n                    if (cert is null || chain is null)\n                    {\n                        return false;\n                    }\n\n                    using (X509Certificate2 cert2 = new X509Certificate2(cert))\n                    {\n                        return validationCallback(cert2, chain, errors);\n                    }\n                };\n#else\n                handler.ServerCertificateCustomValidationCallback = (_, cert, chain, errors) => validationCallback(cert, chain, errors);\n#endif\n            }\n\n            // If CustomCookieFilePath is set, set Cookie header from cookie file, which is written by libcurl\n            if (!string.IsNullOrWhiteSpace(_settings.CustomCookieFilePath) && _fileSystem.FileExists(_settings.CustomCookieFilePath))\n            {\n                // get the filename from gitconfig\n                string cookieFilePath = _settings.CustomCookieFilePath;\n                _trace.WriteLine($\"Custom cookie file has been enabled with {cookieFilePath}\");\n\n                // get cookie from cookie file\n                string cookieFileContents = _fileSystem.ReadAllText(cookieFilePath);\n\n                var cookieParser = new CurlCookieParser(_trace);\n                var cookies = cookieParser.Parse(cookieFileContents);\n\n                // Set the cookie\n                var cookieContainer = new CookieContainer();\n                foreach (var cookie in cookies)\n                {\n                    var schema = cookie.Secure ? \"https\" : \"http\";\n                    var uri = new UriBuilder(schema, cookie.Domain.TrimStart('.')).Uri;\n                    cookieContainer.Add(uri, new Cookie(cookie.Name, cookie.Value));\n                }\n                handler.CookieContainer = cookieContainer;\n                handler.UseCookies = true;\n                \n                _trace.WriteLine(\"Configured to automatically send cookie header.\");\n\n            }\n\n            var client = new HttpClient(handler);\n\n            // Add default headers\n            client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent(_trace2));\n            client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue\n            {\n                NoCache = true\n            };\n\n            return client;\n        }\n\n        /// <summary>\n        /// Try to create an <see cref=\"IWebProxy\"/> object from the configuration specified by environment variables,\n        /// or Git configuration for the specified repository, current user, and system.\n        /// </summary>\n        /// <param name=\"proxy\">Configured <see cref=\"IWebProxy\"/>, or null if no proxy configuration was found.</param>\n        /// <returns>True if proxy configuration was found, false otherwise.</returns>\n        public bool TryCreateProxy(out IWebProxy proxy)\n        {\n            // Try to extract the proxy URI from the environment or Git config\n            ProxyConfiguration proxyConfig = _settings.GetProxyConfiguration();\n            if (proxyConfig != null)\n            {\n                // Inform the user if they are using a deprecated proxy configuration\n                if (proxyConfig.IsDeprecatedSource)\n                {\n                    _trace.WriteLine(\"Using a deprecated proxy configuration.\");\n                    _streams.Error.WriteLine($\"warning: Using a deprecated proxy configuration. See {Constants.HelpUrls.GcmHttpProxyGuide} for more information.\");\n                }\n\n                // If NO_PROXY is set to the value \"*\" then libcurl disables proxying. We should mirror this.\n                if (StringComparer.OrdinalIgnoreCase.Equals(\"*\", proxyConfig.NoProxyRaw))\n                {\n                    _trace.WriteLine(\"NO_PROXY value set to \\\"*\\\"; disabling proxy settings\");\n                    proxy = null;\n                    return false;\n                }\n\n                // Dictionary of proxy info for tracing\n                var dict = new Dictionary<string, string> {[\"address\"] = proxyConfig.Address.ToString()};\n\n                // Try to configure proxy credentials.\n                // For an empty username AND password we should use the system default credentials\n                // (for example for Windows Integrated Authentication-based proxies).\n                if (!(string.IsNullOrEmpty(proxyConfig.UserName) && string.IsNullOrEmpty(proxyConfig.Password)))\n                {\n                    proxy = new WebProxy(proxyConfig.Address)\n                    {\n                        Credentials = new NetworkCredential(proxyConfig.UserName, proxyConfig.Password),\n                    };\n\n                    // Add user/pass info to the trace dictionary\n                    dict[\"username\"] = proxyConfig.UserName;\n                    dict[\"password\"] = proxyConfig.Password;\n                }\n                else\n                {\n                    proxy = new WebProxy(proxyConfig.Address)\n                    {\n                        UseDefaultCredentials = true,\n                    };\n\n                    // Trace the use of system default credentials\n                    dict[\"useDefaultCredentials\"] = \"true\";\n                }\n\n                // Set bypass address list.\n                // The .NET WebProxy class requires that each host entry in the bypass list be a regular expression.\n                // However libcurl (that we are aiming to be compatible/co-operative with) doesn't support regexs so\n                // we must convert the libcurl-esc entries in to .NET-compatible regular expressions.\n                // If we fail at any point we shouldn't crash but write a warning to trace output.\n                if (!string.IsNullOrWhiteSpace(proxyConfig.NoProxyRaw))\n                {\n                    dict[\"noProxy\"] = proxyConfig.NoProxyRaw;\n\n                    try\n                    {\n                        string[] bypassRegexs = ProxyConfiguration.ConvertToBypassRegexArray(proxyConfig.NoProxyRaw).ToArray();\n                        dict[\"bypass\"] = string.Join(\",\", bypassRegexs);\n\n                        ((WebProxy)proxy).BypassList = bypassRegexs;\n                    }\n                    catch (Exception ex)\n                    {\n                        var message =\n                            \"Failed to convert proxy bypass hosts to regular expressions; ignoring bypass list\";\n                        _trace.WriteLine(message);\n                        _trace.WriteException(ex);\n                        _trace2.WriteError(message);\n                        dict[\"bypass\"] = \"<< failed to convert >>\";\n                    }\n                }\n\n                // Tracer out proxy info dictionary\n                _trace.WriteLine(\"Created a WebProxy instance:\");\n                _trace.WriteDictionarySecrets(dict, new[] {\"password\"});\n\n                return true;\n            }\n\n            proxy = null;\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/HttpContentExtensions.cs",
    "content": "using System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    public static class HttpContentExtensions\n    {\n        public static async Task<IDictionary<string, string>> ReadAsFormContentAsync(this HttpContent content)\n        {\n            string str = await content.ReadAsStringAsync();\n            return UriExtensions.ParseQueryString(str);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/HttpRequestExtensions.cs",
    "content": "using System;\nusing System.Globalization;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\n\nnamespace GitCredentialManager\n{\n    public static class HttpRequestExtensions\n    {\n        /// <summary>\n        /// Add a basic authentication header to the request, with the given username and password.\n        /// </summary>\n        /// <remarks>\n        /// The header value is formed by computing the base64 string from the UTF-8 string \"{<paramref name=\"userName\"/>}:{<paramref name=\"password\"/>}\".\n        /// </remarks>\n        public static void AddBasicAuthenticationHeader(this HttpRequestMessage request, string userName, string password)\n        {\n            string basicAuthValue = string.Format(CultureInfo.InvariantCulture, \"{0}:{1}\", userName, password);\n            byte[] authBytes = Encoding.UTF8.GetBytes(basicAuthValue);\n            string base64String = Convert.ToBase64String(authBytes);\n            request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBasicScheme, base64String);\n        }\n\n        /// <summary>\n        /// Add a bearer authentication header to the request, with the given bearer token.\n        /// </summary>\n        public static void AddBearerAuthenticationHeader(this HttpRequestMessage request, string bearerToken)\n        {\n            request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ICredentialStore.cs",
    "content": "using System.Collections.Generic;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents a secure storage location for <see cref=\"ICredential\"/>s.\n    /// </summary>\n    public interface ICredentialStore\n    {\n        /// <summary>\n        /// Get all accounts from the store for the given service.\n        /// </summary>\n        /// <param name=\"service\">Name of the service to match against. Use null to match all values.</param>\n        /// <returns>All accounts that match the query.</returns>\n        IList<string> GetAccounts(string service);\n\n        /// <summary>\n        /// Get the first credential from the store that matches the given query.\n        /// </summary>\n        /// <param name=\"service\">Name of the service to match against. Use null to match all values.</param>\n        /// <param name=\"account\">Account name to match against. Use null to match all values.</param>\n        /// <returns>First matching credential or null if none are found.</returns>\n        ICredential Get(string service, string account);\n\n        /// <summary>\n        /// Add or update credential in the store with the specified key.\n        /// </summary>\n        /// <param name=\"service\">Name of the service this credential is for. Use null to match all values.</param>\n        /// <param name=\"account\">Account associated with this credential. Use null to match all values.</param>\n        /// <param name=\"secret\">Secret value to store.</param>\n        void AddOrUpdate(string service, string account, string secret);\n\n        /// <summary>\n        /// Delete credential from the store that matches the given query.\n        /// </summary>\n        /// <param name=\"service\">Name of the service to match against. Use null to match all values.</param>\n        /// <param name=\"account\">Account name to match against. Use null to match all values.</param>\n        /// <returns>True if the credential was deleted, false otherwise.</returns>\n        bool Remove(string service, string account);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ISessionManager.cs",
    "content": "using System;\nusing System.Diagnostics;\n\nnamespace GitCredentialManager\n{\n    public interface ISessionManager\n    {\n        /// <summary>\n        /// Determine if the current session has access to a desktop/can display UI.\n        /// </summary>\n        /// <returns>True if the session can display UI, false otherwise.</returns>\n        bool IsDesktopSession { get; }\n\n        /// <summary>\n        /// Determine if the current session has access to a web browser.\n        /// </summary>\n        /// <returns>True if the session can display a web browser, false otherwise.</returns>\n        bool IsWebBrowserAvailable { get; }\n\n        /// <summary>\n        /// Open the system web browser to the specified URL.\n        /// </summary>\n        /// <param name=\"uri\"><see cref=\"Uri\"/> to open the browser at.</param>\n        /// <exception cref=\"InvalidOperationException\">Thrown if <see cref=\"IsWebBrowserAvailable\"/> is false.</exception>\n        void OpenBrowser(Uri uri);\n    }\n\n    public static class SessionManagerExtensions\n    {\n        public static void OpenBrowser(this ISessionManager sm, string url)\n        {\n            if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))\n            {\n                throw new ArgumentException($\"Not a valid URI: '{url}'\");\n            }\n\n            sm.OpenBrowser(uri);\n        }\n    }\n    \n    public abstract class SessionManager : ISessionManager\n    {\n        protected ITrace Trace { get; }\n        protected IEnvironment Environment { get; }\n        protected IFileSystem FileSystem { get; }\n\n        protected SessionManager(ITrace trace, IEnvironment env, IFileSystem fs)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            EnsureArgument.NotNull(env, nameof(env));\n            EnsureArgument.NotNull(fs, nameof(fs));\n\n            Trace = trace;\n            Environment = env;\n            FileSystem = fs;\n        }\n        \n        public abstract bool IsDesktopSession { get; }\n\n        public virtual bool IsWebBrowserAvailable => IsDesktopSession;\n\n        public void OpenBrowser(Uri uri)\n        {\n            if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&\n                !uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))\n            {\n                throw new ArgumentException(\"Can only open HTTP/HTTPS URIs\", nameof(uri));\n            }\n\n            OpenBrowserInternal(uri.ToString());\n        }\n\n        protected virtual void OpenBrowserInternal(string url)\n        {\n            Trace.WriteLine(\"Opening browser using framework shell-execute: \" + url);\n            var psi = new ProcessStartInfo(url) { UseShellExecute = true };\n            Process.Start(psi);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ISystemPrompts.cs",
    "content": "\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents native system UI prompts.\n    /// </summary>\n    public interface ISystemPrompts\n    {\n        /// <summary>\n        /// The parent window handle or ID. Used for correctly positioning and parenting system dialogs.\n        /// </summary>\n        /// <remarks>This value is platform specific.</remarks>\n        object ParentWindowId { get; set; }\n\n        /// <summary>\n        /// Show a basic credential prompt using native system UI.\n        /// </summary>\n        /// <param name=\"resource\">The name or URL of the resource to collect credentials for.</param>\n        /// <param name=\"userName\">Optional pre-filled username.</param>\n        /// <param name=\"credential\">The captured basic credential.</param>\n        /// <returns>True if the user completes the dialog, false otherwise.</returns>\n        bool ShowCredentialPrompt(string resource, string userName, out ICredential credential);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ITerminal.cs",
    "content": "using System;\nusing GitCredentialManager.Interop;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents a terminal (TTY) interface.\n    /// </summary>\n    public interface ITerminal\n    {\n        /// <summary>\n        /// Write a message to the terminal screen.\n        /// </summary>\n        /// <param name=\"format\">Format message to print to the terminal.</param>\n        /// <param name=\"args\">Format argument values.</param>\n        /// <exception cref=\"InteropException\">Throw if an error occurs interacting with the native terminal device.</exception>\n        void WriteLine(string format, params object[] args);\n\n        /// <summary>\n        /// Prompt for user input.\n        /// </summary>\n        /// <param name=\"prompt\">Prompt message.</param>\n        /// <returns>User input.</returns>\n        /// <exception cref=\"InteropException\">Throw if an error occurs interacting with the native terminal device.</exception>\n        string Prompt(string prompt);\n\n        /// <summary>\n        /// Prompt for secret user input.\n        /// </summary>\n        /// <remarks>\n        /// Typed user input is masked or hidden.\n        /// </remarks>\n        /// <param name=\"prompt\">Prompt message.</param>\n        /// <returns>Secret user input.</returns>\n        /// <exception cref=\"InteropException\">Throw if an error occurs interacting with the native terminal device.</exception>\n        string PromptSecret(string prompt);\n    }\n\n    public static class TerminalExtensions\n    {\n        /// <summary>\n        /// Write a blank line to the terminal screen.\n        /// </summary>\n        public static void WriteLine(this ITerminal terminal)\n        {\n            terminal.WriteLine(Environment.NewLine);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ITrace2Writer.cs",
    "content": "using System;\nusing System.Text;\n\nnamespace GitCredentialManager;\n\n/// <summary>\n/// The different format targets supported in the TRACE2 tracing\n/// system.\n/// </summary>\npublic enum Trace2FormatTarget\n{\n    Event,\n    Normal,\n    Performance\n}\n\npublic interface ITrace2Writer : IDisposable\n{\n    bool Failed { get; }\n\n    void Write(Trace2Message message);\n}\n\npublic class Trace2Writer : DisposableObject, ITrace2Writer\n{\n    private readonly Trace2FormatTarget _formatTarget;\n\n    public bool Failed { get; protected set; }\n\n    protected Trace2Writer(Trace2FormatTarget formatTarget)\n    {\n        _formatTarget = formatTarget;\n    }\n\n    protected string Format(Trace2Message message)\n    {\n        EnsureArgument.NotNull(message, nameof(message));\n        var sb = new StringBuilder();\n\n        switch (_formatTarget)\n        {\n            case Trace2FormatTarget.Event:\n                sb.Append(message.ToJson());\n                break;\n            case Trace2FormatTarget.Normal:\n                sb.Append(message.ToNormalString());\n                break;\n            case Trace2FormatTarget.Performance:\n                sb.Append(message.ToPerformanceString());\n                break;\n            default:\n                Console.WriteLine($\"warning: unrecognized format target '{_formatTarget}', disabling TRACE2 tracing.\");\n                Failed = true;\n                break;\n        }\n\n        sb.Append('\\n');\n        return sb.ToString();\n    }\n\n    public virtual void Write(Trace2Message message)\n    { }\n}\n"
  },
  {
    "path": "src/shared/Core/IniFile.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Text.RegularExpressions;\n\nnamespace GitCredentialManager\n{\n    public class IniFile\n    {\n        public IniFile()\n        {\n            Sections = new Dictionary<IniSectionName, IniSection>();\n        }\n\n        public IDictionary<IniSectionName, IniSection> Sections { get; }\n\n        public bool TryGetSection(string name, string subName, out IniSection section)\n        {\n            return Sections.TryGetValue(new IniSectionName(name, subName), out section);\n        }\n\n        public bool TryGetSection(string name, out IniSection section)\n        {\n            return Sections.TryGetValue(new IniSectionName(name), out section);\n        }\n    }\n\n    [DebuggerDisplay(\"{DebuggerDisplay}\")]\n    public readonly struct IniSectionName : IEquatable<IniSectionName>\n    {\n        public IniSectionName(string name, string subName = null)\n        {\n            Name = name;\n            SubName = string.IsNullOrEmpty(subName) ? null : subName;\n        }\n\n        public string Name { get; }\n\n        public string SubName { get; }\n\n        public bool Equals(IniSectionName other)\n        {\n            // Main section name is case-insensitive, but subsection name IS case-sensitive!\n            return StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name) &&\n                   StringComparer.Ordinal.Equals(SubName, other.SubName);\n        }\n\n        public override bool Equals(object obj)\n        {\n            return obj is IniSectionName other && Equals(other);\n        }\n\n        public override int GetHashCode()\n        {\n            unchecked\n            {\n                return ((Name != null ? Name.ToLowerInvariant().GetHashCode() : 0) * 397) ^\n                       (SubName != null ? SubName.GetHashCode() : 0);\n            }\n        }\n\n        private string DebuggerDisplay => SubName is null ? Name : $\"{Name} \\\"{SubName}\\\"\";\n    }\n\n    [DebuggerDisplay(\"{DebuggerDisplay}\")]\n    public class IniSection\n    {\n        public IniSection(IniSectionName name)\n        {\n            Name = name;\n            Properties = new List<IniProperty>();\n        }\n\n        public IniSectionName Name { get; }\n\n        public IList<IniProperty> Properties { get; }\n\n        public bool TryGetProperty(string name, out string value)\n        {\n            if (TryGetMultiProperty(name, out IEnumerable<string> values))\n            {\n                value = values.Last();\n                return true;\n            }\n\n            value = null;\n            return false;\n        }\n\n        public bool TryGetMultiProperty(string name, out IEnumerable<string> values)\n        {\n            IniProperty[] props = Properties\n                .Where(x => StringComparer.OrdinalIgnoreCase.Equals(x.Name, name))\n                .ToArray();\n\n            if (props.Length == 0)\n            {\n                values = Array.Empty<string>();\n                return false;\n            }\n\n            values = props.Select(x => x.Value);\n            return true;\n        }\n\n        private string DebuggerDisplay => Name.SubName is null\n            ? $\"{Name.Name} [Properties: {Properties.Count}]\"\n            : $\"{Name.Name} \\\"{Name.SubName}\\\" [Properties: {Properties.Count}]\";\n    }\n\n    [DebuggerDisplay(\"{DebuggerDisplay}\")]\n    public class IniProperty\n    {\n        public IniProperty(string name, string value)\n        {\n            Name = name;\n            Value = value;\n        }\n\n        public string Name { get; }\n        public string Value { get; }\n\n        private string DebuggerDisplay => $\"{Name}={Value}\";\n    }\n\n    public static class IniSerializer\n    {\n        private static readonly Regex SectionRegex =\n            new Regex(@\"^\\[[^\\S#]*(?'name'[^\\s#\\]]*?)(?:\\s+\"\"(?'sub'.+)\"\")?\\s*\\]\", RegexOptions.Compiled);\n\n        private static readonly Regex PropertyRegex =\n            new Regex(@\"^[^\\S#]*?(?'name'[^\\s#]+)\\s*=(?'value'.*)?$\", RegexOptions.Compiled);\n\n        public static IniFile Deserialize(IFileSystem fs, string path)\n        {\n            IEnumerable<string> lines = fs.ReadAllLines(path).Select(x => x.Trim());\n\n            var iniFile = new IniFile();\n            IniSection section = null;\n\n            foreach (string line in lines)\n            {\n                Match match = SectionRegex.Match(line);\n                if (match.Success)\n                {\n                    string mainName = match.Groups[\"name\"].Value;\n                    string subName = match.Groups[\"sub\"].Value;\n\n                    // Skip empty-named sections\n                    if (string.IsNullOrWhiteSpace(mainName))\n                    {\n                        continue;\n                    }\n\n                    if (!iniFile.TryGetSection(mainName, subName, out section))\n                    {\n                        var sectionName = new IniSectionName(mainName, subName);\n                        section = new IniSection(sectionName);\n                        iniFile.Sections[sectionName] = section;\n                    }\n\n                    continue;\n                }\n\n                match = PropertyRegex.Match(line);\n                if (match.Success)\n                {\n                    if (section is null)\n                    {\n                        throw new Exception(\"Missing section header\");\n                    }\n\n                    string propName = match.Groups[\"name\"].Value;\n                    string propValue = match.Groups[\"value\"].Value.Trim();\n\n                    // Trim trailing comments\n                    int firstDQuote = propValue.IndexOf('\"');\n                    int lastDQuote = propValue.LastIndexOf('\"');\n                    int commentIdx = propValue.LastIndexOf('#');\n                    if (commentIdx > -1)\n                    {\n                        bool insideDQuotes = firstDQuote > -1 && lastDQuote > -1 &&\n                                             (firstDQuote < commentIdx && commentIdx < lastDQuote);\n\n                        if (!insideDQuotes)\n                        {\n                            propValue = propValue.Substring(0, commentIdx).Trim();\n                        }\n                    }\n\n                    // Trim book-ending double quotes: \"foo\" => foo\n                    if (propValue.Length > 1 && propValue[0] == '\"' &&\n                        propValue[propValue.Length - 1] == '\"')\n                    {\n                        propValue = propValue.Substring(1, propValue.Length - 2);\n                    }\n\n                    var property = new IniProperty(propName, propValue);\n                    section.Properties.Add(property);\n                }\n            }\n\n            return iniFile;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/InputArguments.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Linq;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents the input for a Git credential query such as get, erase, or store.\n    /// </summary>\n    /// <remarks>\n    /// This class surfaces the input that is streamed over standard in from Git which provides\n    /// the credential helper the remote repository information, including the protocol, host,\n    /// and remote repository path.\n    /// </remarks>\n    public class InputArguments\n    {\n        private readonly IReadOnlyDictionary<string, IList<string>> _dict;\n\n        public InputArguments(IDictionary<string, string> dict)\n        {\n            EnsureArgument.NotNull(dict, nameof(dict));\n\n            // Transform input from 1:1 to 1:n and store as readonly\n            _dict = new ReadOnlyDictionary<string, IList<string>>(\n                dict.ToDictionary(x => x.Key, x => (IList<string>)new[] { x.Value })\n            );\n        }\n\n        public InputArguments(IDictionary<string, IList<string>> dict)\n        {\n            EnsureArgument.NotNull(dict, nameof(dict));\n\n            // Wrap the dictionary internally as readonly\n            _dict = new ReadOnlyDictionary<string, IList<string>>(dict);\n        }\n\n        #region Common Arguments\n\n        public string Protocol => GetArgumentOrDefault(\"protocol\");\n        public string Host     => GetArgumentOrDefault(\"host\");\n        public string Path     => GetArgumentOrDefault(\"path\");\n        public string UserName => GetArgumentOrDefault(\"username\");\n        public string Password => GetArgumentOrDefault(\"password\");\n        public IList<string> WwwAuth => GetMultiArgumentOrDefault(\"wwwauth\");\n\n        #endregion\n\n        #region Public Methods\n\n        public string this[string key]\n        {\n            get => GetArgumentOrDefault(key);\n        }\n\n        public string GetArgumentOrDefault(string key)\n        {\n            return TryGetArgument(key, out string value) ? value : null;\n        }\n\n        public IList<string> GetMultiArgumentOrDefault(string key)\n        {\n            return TryGetMultiArgument(key, out IList<string> values) ? values : Array.Empty<string>();\n        }\n\n        public bool TryGetArgument(string key, out string value)\n        {\n            if (_dict.TryGetValue(key, out IList<string> values))\n            {\n                value = values.FirstOrDefault();\n                return value != null;\n            }\n\n            value = null;\n            return false;\n        }\n\n        public bool TryGetMultiArgument(string key, out IList<string> value)\n        {\n            if (_dict.TryGetValue(key, out IList<string> values))\n            {\n                value = values;\n                return true;\n            }\n\n            value = null;\n            return false;\n        }\n\n        public bool TryGetHostAndPort(out string host, out int? port)\n        {\n            host = null;\n            port = null;\n\n            if (Host is null)\n            {\n                return false;\n            }\n\n            // Split port number and hostname from host input argument\n            string[] hostParts = Host.Split(':');\n            if (hostParts.Length > 0)\n            {\n                host = hostParts[0];\n            }\n\n            if (hostParts.Length > 1)\n            {\n                if (!int.TryParse(hostParts[1], out int portInt))\n                {\n                    return false;\n                }\n\n                port = portInt;\n            }\n\n            return true;\n        }\n\n        public Uri GetRemoteUri(bool includeUser = false)\n        {\n            if (Protocol is null || Host is null)\n            {\n                return null;\n            }\n\n            string[] hostParts = Host.Split(':');\n            if (hostParts.Length > 0)\n            {\n                var ub = new UriBuilder(Protocol, hostParts[0]);\n\n                if (hostParts.Length > 1 && int.TryParse(hostParts[1], out int port))\n                {\n                    ub.Port = port;\n                }\n\n                if (includeUser && !string.IsNullOrEmpty(UserName))\n                {\n                    ub.UserName = Uri.EscapeDataString(UserName);\n                }\n\n                if (Path != null)\n                {\n                    string[] pathParts = Path.Split('?', '#');\n                    // We know the first piece is the path\n                    ub.Path = pathParts[0];\n\n                    switch (pathParts.Length)\n                    {\n                        // If we have 3 items, that means path, query, and fragment\n                        case 3:\n                            ub.Query = pathParts[1];\n                            ub.Fragment = pathParts[2];\n                            break;\n                        // If we have 2 items, we must distinguish between query and fragment\n                        case 2 when Path.Contains('?'):\n                            ub.Query = pathParts[1];\n                            break;\n                        case 2 when Path.Contains('#'):\n                            ub.Fragment = pathParts[1];\n                            break;\n                    }\n                }\n                return ub.Uri;\n            }\n\n            return null;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/InternalsVisibleTo.cs",
    "content": "using System.Runtime.CompilerServices;\n\n[assembly: InternalsVisibleTo(\"Core.Tests\")]\n[assembly: InternalsVisibleTo(\"GitHub.Tests\")]\n[assembly: InternalsVisibleTo(\"Atlassian.Bitbucket.Tests\")]\n"
  },
  {
    "path": "src/shared/Core/Interop/InteropException.cs",
    "content": "using System;\nusing System.ComponentModel;\nusing System.Diagnostics;\n\nnamespace GitCredentialManager.Interop\n{\n    /// <summary>\n    /// An unexpected error occurred in interop-code.\n    /// </summary>\n    [DebuggerDisplay(\"{DebuggerDisplay}\")]\n    public class InteropException : Exception\n    {\n        public InteropException()\n            : base() { }\n\n        public InteropException(string message, int errorCode)\n            : base(message)\n        {\n            ErrorCode = errorCode;\n        }\n\n        public InteropException(string message, int errorCode, Exception innerException)\n            : base(message, innerException)\n        {\n            ErrorCode = errorCode;\n        }\n\n        public InteropException(string message, Win32Exception w32Exception)\n            : base(message, w32Exception)\n        {\n            ErrorCode = w32Exception.NativeErrorCode;\n        }\n\n        /// <summary>\n        /// Native error code.\n        /// </summary>\n        public int ErrorCode { get; }\n\n        private string DebuggerDisplay => $\"{Message} [0x{ErrorCode:x}]\";\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/InteropUtils.cs",
    "content": "using System;\nusing System.Linq;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop\n{\n    internal static class InteropUtils\n    {\n        public static byte[] ToByteArray(IntPtr ptr, long count)\n        {\n            var destination = new byte[count];\n            Marshal.Copy(ptr, destination, 0, destination.Length);\n            return destination;\n        }\n\n        public static bool AreEqual(byte[] bytes, IntPtr ptr, uint length)\n        {\n            if (bytes.Length == 0 && (ptr == IntPtr.Zero || length == 0))\n            {\n                return true;\n            }\n\n            if (bytes.Length != length)\n            {\n                return false;\n            }\n\n            byte[] ptrBytes = ToByteArray(ptr, length);\n            return bytes.SequenceEqual(ptrBytes);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/LinuxConfigParser.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text.RegularExpressions;\n\nnamespace GitCredentialManager.Interop.Linux;\n\npublic class LinuxConfigParser\n{\n#if NETFRAMEWORK\n    private const string SQ = \"'\";\n    private const string DQ = \"\\\"\";\n    private const string Hash = \"#\";\n#else\n    private const char SQ = '\\'';\n    private const char DQ = '\"';\n    private const char Hash = '#';\n#endif\n\n    private static readonly Regex LineRegex = new(@\"^\\s*(?<key>[a-zA-Z0-9\\.-]+)\\s*=\\s*(?<value>.+?)\\s*(?:#.*)?$\");\n\n    private readonly ITrace _trace;\n\n    public LinuxConfigParser(ITrace trace)\n    {\n        EnsureArgument.NotNull(trace, nameof(trace));\n\n        _trace = trace;\n    }\n\n    public IDictionary<string, string> Parse(string content)\n    {\n        var result = new Dictionary<string, string>(GitConfigurationKeyComparer.Instance);\n\n        IEnumerable<string> lines = content.Split(new char[] { '\\n' }, StringSplitOptions.RemoveEmptyEntries);\n\n        foreach (string line in lines)\n        {\n            // Ignore empty lines or full-line comments\n            var trimmedLine = line.Trim();\n            if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(Hash))\n                continue;\n\n            var match = LineRegex.Match(trimmedLine);\n            if (!match.Success)\n            {\n                _trace.WriteLine($\"Invalid config line format: {line}\");\n                continue;\n            }\n\n            string key = match.Groups[\"key\"].Value;\n            string value = match.Groups[\"value\"].Value;\n\n            // Remove enclosing quotes from the value, if any\n            if ((value.StartsWith(DQ) && value.EndsWith(DQ)) || (value.StartsWith(SQ) && value.EndsWith(SQ)))\n                value = value.Substring(1, value.Length - 2);\n\n            result[key] = value;\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/LinuxFileSystem.cs",
    "content": "using System;\nusing System.IO;\nusing GitCredentialManager.Interop.Posix;\n\nnamespace GitCredentialManager.Interop.Linux\n{\n    public class LinuxFileSystem : PosixFileSystem\n    {\n        public override bool IsSamePath(string a, string b)\n        {\n            if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))\n            {\n                return false;\n            }\n\n            // Normalize paths\n            a = Path.GetFullPath(a);\n            b = Path.GetFullPath(b);\n\n            // Resolve symbolic links\n            a = ResolveSymbolicLinks(a);\n            b = ResolveSymbolicLinks(b);\n\n            return StringComparer.Ordinal.Equals(a, b);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/LinuxSessionManager.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing GitCredentialManager.Interop.Posix;\n\nnamespace GitCredentialManager.Interop.Linux;\n\npublic class LinuxSessionManager : PosixSessionManager\n{\n    private bool? _isWebBrowserAvailable;\n\n    public LinuxSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs)\n    {\n        PlatformUtils.EnsureLinux();\n    }\n    \n    public override bool IsWebBrowserAvailable\n    {\n        get\n        {\n            return _isWebBrowserAvailable ??= GetWebBrowserAvailable();\n        }\n    }\n\n    protected override void OpenBrowserInternal(string url)\n    {\n        //\n        // On Linux, 'shell execute' utilities like xdg-open launch a process without\n        // detaching from the standard in/out descriptors. Some applications (like\n        // Chromium) write messages to stdout, which is currently hooked up and being\n        // consumed by Git, and cause errors.\n        //\n        // Sadly, the Framework does not allow us to redirect standard streams if we\n        // set ProcessStartInfo::UseShellExecute = true, so we must manually launch\n        // these utilities and redirect the standard streams manually.\n        //\n        // We try and use the same 'shell execute' utilities as the Framework does,\n        // searching for them in the same order until we find one.\n        //\n        if (!TryGetShellExecuteHandler(Environment, out string shellExecPath))\n        {\n            throw new Exception(\"Failed to locate a utility to launch the default web browser.\");\n        }\n\n        Trace.WriteLine($\"Opening browser using '{shellExecPath}: {url}\");\n\n        var psi = new ProcessStartInfo(shellExecPath, url)\n        {\n            RedirectStandardOutput = true,\n            // Ok to redirect stderr for non-git-related processes\n            RedirectStandardError = true\n        };\n\n        Process.Start(psi);\n    }\n\n    private bool GetWebBrowserAvailable()\n    {\n        // We need a shell execute handler to be able to launch to browser\n        if (!TryGetShellExecuteHandler(Environment, out _))\n        {\n            Trace.WriteLine(\"Could not locate a shell execute handler for Linux - browser is not available.\");\n            return false;\n        }\n\n        // If this is a Windows Subsystem for Linux distribution we may\n        // be able to launch the web browser of the host Windows OS, but\n        // there are further checks to do on the Windows host's session.\n        //\n        // If we are in Windows logon session 0 then the user can never interact,\n        // even in the WinSta0 window station. This is typical when SSH-ing into a\n        // Windows 10+ machine using the default OpenSSH Server configuration,\n        // which runs in the 'services' session 0.\n        //\n        // If we're in any other session, and in the WinSta0 window station then\n        // the user can possibly interact. However, since it's hard to determine\n        // the window station from PowerShell cmdlets (we'd need to write P/Invoke\n        // code and that's just messy and too many levels of indirection quite\n        // frankly!) we just assume any non session 0 is interactive.\n        //\n        // This assumption doesn't hold true if the user has changed the user that\n        // the OpenSSH Server service runs as (not a built-in NT service) *AND*\n        // they've SSH-ed into the Windows host (and then started a WSL shell).\n        // This feels like a very small subset of users...\n        //\n        if (WslUtils.IsWslDistribution(Environment, FileSystem, out _))\n        {\n            if (WslUtils.GetWindowsSessionId(FileSystem) == 0)\n            {\n                Trace.WriteLine(\"This is a WSL distribution, but Windows session 0 was detected - browser is not available.\");\n                return false;\n            }\n\n            // Not on session 0 - we assume the user can interact with browser on Windows.\n            Trace.WriteLine(\"This is a WSL distribution - browser is available.\");\n            return true;\n        }\n\n        //\n        // We may also be able to launch a browser if we're inside a Visual Studio Code remote session.\n        // VSCode overrides the BROWSER environment variable to a script that allows the user to open\n        // the browser on their client machine.\n        //\n        // Even though we can start a browser, one piece of critical functionality we need is the ability\n        // to have that browser be able to connect back to GCM over localhost. There are several types\n        // of VSCode remote session, and only some of them automatically forward ports in such a way that\n        // the client browser can automatically connect back to GCM over localhost.\n        //\n        // * SSH [OK]\n        //   Connection over SSH to a remote machine.\n        //\n        // * Dev Containers [OK]\n        //   Connection to a container.\n        //\n        // * Dev Tunnels [Not OK - forwarded ports not accessible on the client via localhost]\n        //   Connection to a remote machine over the Internet using Microsoft Dev Tunnels.\n        //\n        // * WSL [Ignored - already handled above]\n        //\n        if (Environment.Variables.ContainsKey(\"VSCODE_IPC_HOOK_CLI\") &&\n            Environment.Variables.ContainsKey(\"BROWSER\"))\n        {\n            // Looking for SSH_CONNECTION tells us we're connected via SSH.\n            // HOWEVER, we may also see SSH_CONNECTION in a Dev Tunnel session if the tunnel server\n            // process was started within an SSH session (and the SSH_CONNECTION environment variable\n            // was inherited).\n            // We therefore check for the absence of the SSH_TTY variable, which gets unset\n            // in Dev Tunnel sessions but is always still set in regular SSH sessions.\n            if (Environment.Variables.ContainsKey(\"SSH_CONNECTION\") &&\n                !Environment.Variables.ContainsKey(\"SSH_TTY\"))\n            {\n                Trace.WriteLine(\"VSCode (Remote SSH) detected - browser is available.\");\n                return true;\n            }\n\n            if (Environment.Variables.ContainsKey(\"REMOTE_CONTAINERS\"))\n            {\n                Trace.WriteLine(\"VSCode (Dev Containers) detected - browser is available.\");\n                return true;\n            }\n\n            Trace.WriteLine(\"VSCode (Remote Tunnel) detected - browser is not available.\");\n            return false;\n        }\n\n        // We need a desktop session to be able to launch the browser in the general case\n        return IsDesktopSession;\n    }\n\n    private static bool TryGetShellExecuteHandler(IEnvironment env, out string shellExecPath)\n    {\n        // One additional 'shell execute' utility we also attempt to use over the Framework\n        // is `wslview` that is commonly found on WSL (Windows Subsystem for Linux) distributions\n        // that opens the browser on the Windows host.\n        string[] shellHandlers = { \"xdg-open\", \"gnome-open\", \"kfmclient\", WslUtils.WslViewShellHandlerName };\n        foreach (string shellExec in shellHandlers)\n        {\n            if (env.TryLocateExecutable(shellExec, out shellExecPath))\n            {\n                return true;\n            }\n        }\n\n        shellExecPath = null;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/LinuxSettings.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing Avalonia.Markup.Xaml.MarkupExtensions;\n\nnamespace GitCredentialManager.Interop.Linux;\n\npublic class LinuxSettings : Settings\n{\n    private readonly ITrace _trace;\n    private readonly IFileSystem _fs;\n\n    private IDictionary<string, string> _extConfigCache;\n\n    /// <summary>\n    /// Reads settings from Git configuration, environment variables, and defaults from the\n    /// /etc/git-credential-manager.d app configuration directory.\n    /// </summary>\n    public LinuxSettings(IEnvironment environment, IGit git, ITrace trace, IFileSystem fs)\n        : base(environment, git)\n    {\n        EnsureArgument.NotNull(trace, nameof(trace));\n        EnsureArgument.NotNull(fs, nameof(fs));\n\n        _trace = trace;\n        _fs = fs;\n\n        PlatformUtils.EnsureLinux();\n    }\n\n    protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)\n    {\n        value = null;\n\n        _extConfigCache ??= ReadExternalConfiguration();\n\n        if (_extConfigCache is null)\n            return false; // No external config found (or failed to read)\n\n        string name = string.IsNullOrWhiteSpace(scope)\n            ? $\"{section}.{property}\"\n            : $\"{section}.{scope}.{property}\";\n\n        // Check if the setting exists in the configuration\n        if (!_extConfigCache.TryGetValue(name, out value))\n        {\n            // No property exists\n            return false;\n        }\n\n        _trace.WriteLine($\"Default setting found in app configuration directory: {name}={value}\");\n        return true;\n    }\n\n    private IDictionary<string, string> ReadExternalConfiguration()\n    {\n        try\n        {\n            // Check for system-wide config files in /etc/git-credential-manager/config.d and concatenate them together\n            // in alphabetical order to form a single configuration.\n            const string configDir = Constants.LinuxAppDefaultsDirectoryPath;\n            if (!_fs.DirectoryExists(configDir))\n            {\n                // No configuration directory exists\n                return null;\n            }\n\n            // Get all the files in the configuration directory\n            IEnumerable<string> files = _fs.EnumerateFiles(configDir, \"*\");\n\n            // Read the contents of each file and concatenate them together\n            var combinedFile = new StringBuilder();\n            foreach (string file in files)\n            {\n                using Stream stream = _fs.OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);\n                using var reader = new StreamReader(stream);\n                string contents = reader.ReadToEnd();\n                combinedFile.Append(contents);\n                combinedFile.Append('\\n');\n            }\n\n            var parser = new LinuxConfigParser(_trace);\n\n            return parser.Parse(combinedFile.ToString());\n        }\n        catch (Exception ex)\n        {\n            // Reading defaults is not critical to the operation of the application\n            // so we can ignore any errors and just log the failure.\n            _trace.WriteLine(\"Failed to read default setting from app configuration directory.\");\n            _trace.WriteException(ex);\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/LinuxTerminal.cs",
    "content": "using System;\nusing GitCredentialManager.Interop.Linux.Native;\nusing GitCredentialManager.Interop.Posix;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager.Interop.Linux\n{\n    public class LinuxTerminal : PosixTerminal\n    {\n        public LinuxTerminal(ITrace trace, ITrace2 trace2)\n            : base(trace, trace2) { }\n\n        protected override IDisposable CreateTtyContext(int fd, bool echo)\n        {\n            return new TtyContext(Trace, Trace2, fd, echo);\n        }\n\n        private class TtyContext : IDisposable\n        {\n            private readonly ITrace _trace;\n            private readonly int _fd;\n\n            private termios_Linux _originalTerm;\n            private bool _isDisposed;\n\n            public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo)\n            {\n                EnsureArgument.NotNull(trace, nameof(trace));\n                EnsureArgument.PositiveOrZero(fd, nameof(fd));\n\n                _trace = trace;\n                _fd = fd;\n\n                int error = 0;\n\n                // Capture current terminal settings so we can restore them later\n                if ((error = Termios_Linux.tcgetattr(_fd, out termios_Linux t)) != 0)\n                {\n                    throw new Trace2InteropException(trace2, \"Failed to get initial terminal settings\", error);\n                }\n\n                _originalTerm = t;\n\n                // Set desired echo state\n                _trace.WriteLine($\"Setting terminal echo state to '{echo}'\");\n                if (echo)\n                    t.c_lflag |= LocalFlags.ECHO;\n                else\n                    t.c_lflag &= ~LocalFlags.ECHO;\n\n                if ((error = Termios_Linux.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0)\n                {\n                    throw new Trace2InteropException(trace2, \"Failed to set terminal settings\", error);\n                }\n            }\n\n            public void Dispose()\n            {\n                if (_isDisposed)\n                {\n                    return;\n                }\n\n                int error = 0;\n\n                // Restore original terminal settings\n                if ((error = Termios_Linux.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref _originalTerm)) != 0)\n                {\n                    _trace.WriteLine($\"Failed to get restore terminal settings (error: {error:x}\");\n                }\n\n                _isDisposed = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/Native/Glib.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Linux.Native\n{\n    public static class Glib\n    {\n        private const string LibraryName = \"libglib-2.0.so.0\";\n\n        public struct GHashTable { /* transparent */ }\n\n        [StructLayout(LayoutKind.Sequential)]\n        public struct GList\n        {\n            public IntPtr data;\n            public IntPtr next;\n            public IntPtr prev;\n        }\n\n        [StructLayout(LayoutKind.Sequential)]\n        public struct GError\n        {\n            public int domain;\n            public int code;\n            public IntPtr message;\n        }\n\n        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n        public delegate uint GHashFunc(IntPtr key);\n\n        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n        public delegate bool GEqualFunc(IntPtr a, IntPtr b);\n\n        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n        public delegate void GDestroyNotify(IntPtr data);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern uint g_str_hash(IntPtr key);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern bool g_str_equal(IntPtr a, IntPtr b);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe GHashTable* g_hash_table_new(GHashFunc hash_func, GEqualFunc key_equal_func);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe GHashTable* g_hash_table_new_full(\n            GHashFunc hash_func,\n            GEqualFunc key_equal_func,\n            GDestroyNotify key_destroy_func,\n            GDestroyNotify value_destroy_func);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe void g_hash_table_destroy(GHashTable* hash_table);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe bool g_hash_table_insert(GHashTable* hash_table, IntPtr key, IntPtr value);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe IntPtr g_hash_table_lookup(GHashTable* hash_table, IntPtr key);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe void g_list_free_full(GList* list, GDestroyNotify free_func);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe void g_hash_table_unref(GHashTable* hash_table);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe void g_error_free(GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void g_free(IntPtr mem);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/Native/Gobject.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Linux.Native\n{\n    public static class Gobject\n    {\n        private const string LibraryName = \"libgobject-2.0.so.0\";\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void g_object_ref(IntPtr @object);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void g_object_unref(IntPtr @object);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/Native/Libsecret.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Linux.Native\n{\n    public static class Libsecret\n    {\n        private const string LibraryName = \"libsecret-1.so.0\";\n\n        public enum SecretSchemaAttributeType\n        {\n            SECRET_SCHEMA_ATTRIBUTE_STRING = 0,\n            SECRET_SCHEMA_ATTRIBUTE_INTEGER = 1,\n            SECRET_SCHEMA_ATTRIBUTE_BOOLEAN = 2,\n        }\n\n        [StructLayout(LayoutKind.Sequential)]\n        public struct SecretSchemaAttribute\n        {\n            public string name;\n            public SecretSchemaAttributeType type;\n        }\n\n        [Flags]\n        public enum SecretSchemaFlags\n        {\n            SECRET_SCHEMA_NONE = 0,\n            SECRET_SCHEMA_DONT_MATCH_NAME = 1 << 1,\n        }\n\n        [StructLayout(LayoutKind.Sequential)]\n        public struct SecretSchema\n        {\n            public string name;\n            public SecretSchemaFlags flags;\n\n            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]\n            public SecretSchemaAttribute[] attributes;\n\n            int reserved;\n            IntPtr reserved1;\n            IntPtr reserved2;\n            IntPtr reserved3;\n            IntPtr reserved4;\n            IntPtr reserved5;\n            IntPtr reserved6;\n            IntPtr reserved7;\n        }\n        public struct SecretService { /* transparent */ }\n\n        [Flags]\n        public enum SecretServiceFlags\n        {\n            SECRET_SERVICE_NONE             = 0,\n            SECRET_SERVICE_OPEN_SESSION     = 1 << 1,\n            SECRET_SERVICE_LOAD_COLLECTIONS = 1 << 2,\n        }\n\n        [Flags]\n        public enum SecretSearchFlags\n        {\n            SECRET_SEARCH_NONE         = 0,\n            SECRET_SEARCH_ALL          = 1 << 1,\n            SECRET_SEARCH_UNLOCK       = 1 << 2,\n            SECRET_SEARCH_LOAD_SECRETS = 1 << 3,\n        }\n\n        public struct SecretItem { /* transparent */ }\n\n        public struct SecretValue { /* transparent */ }\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe SecretService* secret_service_get_sync(\n            SecretServiceFlags flags,\n            IntPtr cancellable,\n            out Glib.GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe Glib.GList* secret_service_search_sync(\n            SecretService* service,\n            ref SecretSchema schema,\n            Glib.GHashTable* attributes,\n            SecretSearchFlags flags,\n            IntPtr cancellable,\n            out Glib.GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe bool secret_service_store_sync(\n            SecretService* service,\n            ref SecretSchema schema,\n            Glib.GHashTable *attributes,\n            string collection,\n            string label,\n            SecretValue *value,\n            IntPtr cancellable,\n            out Glib.GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe bool secret_service_clear_sync(\n            SecretService* service,\n            ref SecretSchema schema,\n            Glib.GHashTable *attributes,\n            IntPtr cancellable,\n            out Glib.GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern string secret_item_get_label(IntPtr self);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe Glib.GHashTable* secret_item_get_attributes(SecretItem* item);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe void secret_item_load_secret_sync(\n            SecretItem* self,\n            IntPtr cancellable,\n            out Glib.GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe int secret_service_unlock_sync(SecretService* service,\n            Glib.GList* objects,\n            IntPtr cancellable,\n            out Glib.GList* unlocked,\n            out Glib.GError* error);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe bool secret_item_get_locked(SecretItem *self);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe SecretValue* secret_item_get_secret(SecretItem* item);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe SecretValue* secret_value_new(\n            byte[] secret,\n            int length,\n            string content_type);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe IntPtr secret_value_get(SecretValue* value, out int length);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe void secret_value_unref(SecretValue* value);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe IntPtr secret_value_unref_to_password(SecretValue *value, out int length);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void secret_password_free(IntPtr password);\n\n        [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe bool secret_item_delete_sync(SecretItem* self, IntPtr cancellable, out Glib.GError* error);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/Native/termios_Linux.cs",
    "content": "using System.Runtime.InteropServices;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager.Interop.Linux.Native\n{\n    public static class Termios_Linux\n    {\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int tcgetattr(int fd, out termios_Linux termios);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int tcsetattr(int fd, SetActionFlags optActions, ref termios_Linux termios);\n    }\n\n    [StructLayout(LayoutKind.Explicit)]\n    public struct termios_Linux\n    {\n        // Linux has an array of 32 elements\n        private const int NCCS = 32;\n\n        // Linux uses unsigned 32-bit sized flags\n        [FieldOffset(0)]  public InputFlags   c_iflag;\n        [FieldOffset(4)]  public OutputFlags  c_oflag;\n        [FieldOffset(8)]  public ControlFlags c_cflag;\n        [FieldOffset(12)] public LocalFlags   c_lflag;\n\n        [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)]\n        [FieldOffset(16)] public byte[] c_cc;\n\n        [FieldOffset(16 + NCCS)]     public uint c_ispeed;\n        [FieldOffset(16 + NCCS + 4)] public uint c_ospeed;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/SecretServiceCollection.cs",
    "content": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Runtime.InteropServices;\r\nusing System.Text;\r\nusing static GitCredentialManager.Interop.Linux.Native.Gobject;\r\nusing static GitCredentialManager.Interop.Linux.Native.Glib;\r\nusing static GitCredentialManager.Interop.Linux.Native.Libsecret;\r\nusing static GitCredentialManager.Interop.Linux.Native.Libsecret.SecretSchemaAttributeType;\r\nusing static GitCredentialManager.Interop.Linux.Native.Libsecret.SecretSchemaFlags;\r\n\r\nnamespace GitCredentialManager.Interop.Linux\r\n{\r\n    public class SecretServiceCollection : ICredentialStore\r\n    {\r\n        private const string SchemaName = \"com.microsoft.GitCredentialManager\";\r\n        private const string ServiceAttributeName = \"service\";\r\n        private const string AccountAttributeName = \"account\";\r\n        private const string PlainTextContentType = \"plain/text\";\r\n\r\n        private readonly string _namespace;\r\n\r\n        #region Constructors\r\n\r\n        /// <summary>\r\n        /// Open the default secret collection for the current user.\r\n        /// </summary>\r\n        /// <param name=\"namespace\">Optional namespace to scope credential operations.</param>\r\n        /// <returns>Default secret collection.</returns>\r\n        public SecretServiceCollection(string @namespace)\r\n        {\r\n            PlatformUtils.EnsureLinux();\r\n            _namespace = @namespace;\r\n        }\r\n\r\n        #endregion\r\n\r\n        #region ICredentialStore\r\n\r\n        public IList<string> GetAccounts(string service)\r\n        {\r\n            return Enumerate(service, null).Select(x => x.Account).Distinct().ToList();\r\n        }\r\n\r\n        public ICredential Get(string service, string account)\r\n        {\r\n            return Enumerate(service, account).FirstOrDefault();\r\n        }\r\n\r\n        private unsafe IEnumerable<ICredential> Enumerate(string service, string account)\r\n        {\r\n            GHashTable* queryAttrs = null;\r\n            GList* results = null;\r\n            GError* error = null;\r\n\r\n            try\r\n            {\r\n                SecretService* secService = GetSecretService();\r\n\r\n                queryAttrs = CreateSearchQuery(service, account);\r\n\r\n                SecretSchema schema = GetSchema();\r\n\r\n                // Execute search query and return all results\r\n                results = secret_service_search_sync(\r\n                    secService,\r\n                    ref schema,\r\n                    queryAttrs,\r\n                    SecretSearchFlags.SECRET_SEARCH_UNLOCK | SecretSearchFlags.SECRET_SEARCH_ALL,\r\n                    IntPtr.Zero,\r\n                    out error);\r\n\r\n                if (error != null)\r\n                {\r\n                    int code = error->code;\r\n                    string message = Marshal.PtrToStringAuto(error->message)!;\r\n                    throw new InteropException(\"Failed to search for credentials\", code, new Exception(message));\r\n                }\r\n\r\n                var credentials = new List<ICredential>();\r\n\r\n                GList* itemPtr = results;\r\n                while (itemPtr != null && itemPtr->data != IntPtr.Zero)\r\n                {\r\n                    SecretItem* item = (SecretItem*) itemPtr->data;\r\n\r\n                    // Although we've unlocked the collection during the search call,\r\n                    // an item can also be individually locked within a collection.\r\n                    // If the item is locked we should try and unlock it.\r\n                    if (secret_item_get_locked(item))\r\n                    {\r\n                        var toUnlockList = new GList\r\n                        {\r\n                            data = (IntPtr) item,\r\n                            next = IntPtr.Zero,\r\n                            prev = IntPtr.Zero\r\n                        };\r\n\r\n                        int numUnlocked = secret_service_unlock_sync(\r\n                            secService,\r\n                            &toUnlockList,\r\n                            IntPtr.Zero,\r\n                            out _,\r\n                            out error\r\n                        );\r\n\r\n                        if (numUnlocked != 1)\r\n                        {\r\n                            throw new InteropException(\"Failed to unlock item\", numUnlocked);\r\n                        }\r\n                    }\r\n\r\n                    credentials.Add(CreateCredentialFromItem(item));\r\n\r\n                    itemPtr = (GList*)itemPtr->next;\r\n                }\r\n\r\n                return credentials;\r\n            }\r\n            finally\r\n            {\r\n                if (queryAttrs != null) g_hash_table_destroy(queryAttrs);\r\n                if (error != null) g_error_free(error);\r\n                if (results != null) g_list_free_full(results, g_object_unref);\r\n            }\r\n        }\r\n\r\n        public unsafe void AddOrUpdate(string service, string account, string secret)\r\n        {\r\n            GHashTable* attributes = null;\r\n            SecretValue* secretValue = null;\r\n            GError *error = null;\r\n\r\n            // If there is an existing credential that matches the same account and password\r\n            // then don't bother writing out anything because they're the same!\r\n            ICredential existingCred = Get(service, account);\r\n            if (existingCred != null &&\r\n                StringComparer.Ordinal.Equals(existingCred.Account, account) &&\r\n                StringComparer.Ordinal.Equals(existingCred.Password, secret))\r\n            {\r\n                return;\r\n            }\r\n\r\n            try\r\n            {\r\n                SecretService* secService = GetSecretService();\r\n\r\n                // Create attributes for the key and user\r\n                attributes = g_hash_table_new_full(g_str_hash, g_str_equal,\r\n                    Marshal.FreeHGlobal, Marshal.FreeHGlobal);\r\n\r\n                string fullServiceName = CreateServiceName(service);\r\n                IntPtr serviceKeyPtr = Marshal.StringToHGlobalAnsi(ServiceAttributeName);\r\n                IntPtr serviceValuePtr = Marshal.StringToHGlobalAnsi(fullServiceName);\r\n                g_hash_table_insert(attributes, serviceKeyPtr, serviceValuePtr);\r\n\r\n                if (!string.IsNullOrWhiteSpace(account))\r\n                {\r\n                    IntPtr accountKeyPtr = Marshal.StringToHGlobalAnsi(AccountAttributeName);\r\n                    IntPtr accountValuePtr = Marshal.StringToHGlobalAnsi(account);\r\n                    g_hash_table_insert(attributes, accountKeyPtr, accountValuePtr);\r\n                }\r\n\r\n                // Create the secret value object from the secret string\r\n                byte[] secretBytes = Encoding.UTF8.GetBytes(secret);\r\n                secretValue = secret_value_new(secretBytes, secretBytes.Length, PlainTextContentType);\r\n\r\n                SecretSchema schema = GetSchema();\r\n\r\n                // Store the secret with the associated attributes\r\n                bool result = secret_service_store_sync(\r\n                    secService,\r\n                    ref schema,\r\n                    attributes,\r\n                    null,\r\n                    fullServiceName, // Use full service name as label\r\n                    secretValue,\r\n                    IntPtr.Zero,\r\n                    out error);\r\n\r\n                if (error != null)\r\n                {\r\n                    int code = error->code;\r\n                    string message = Marshal.PtrToStringAuto(error->message)!;\r\n                    throw new InteropException(\"Failed to store credentials\", code, new Exception(message));\r\n                }\r\n\r\n                if (!result)\r\n                {\r\n                    throw new InteropException(\"Failed to store credentials\", -1);\r\n                }\r\n            }\r\n            finally\r\n            {\r\n                if (attributes != null) g_hash_table_destroy(attributes);\r\n                if (secretValue != null) secret_value_unref(secretValue);\r\n                if (error != null) g_error_free(error);\r\n            }\r\n        }\r\n\r\n        public unsafe bool Remove(string service, string account)\r\n        {\r\n            GHashTable* attributes = null;\r\n            GError* error = null;\r\n\r\n            try\r\n            {\r\n                SecretService* secService = GetSecretService();\r\n\r\n                // Create search query\r\n                attributes = CreateSearchQuery(service, account);\r\n\r\n                SecretSchema schema = GetSchema();\r\n\r\n                // Erase the secret with the specified key\r\n                bool result = secret_service_clear_sync(\r\n                    secService,\r\n                    ref schema,\r\n                    attributes,\r\n                    IntPtr.Zero,\r\n                    out error);\r\n\r\n                if (error != null)\r\n                {\r\n                    int code = error->code;\r\n                    string message = Marshal.PtrToStringAuto(error->message)!;\r\n                    throw new InteropException(\"Failed to erase credentials\", code, new Exception(message));\r\n                }\r\n\r\n                return result;\r\n            }\r\n            finally\r\n            {\r\n                if (attributes != null) g_hash_table_destroy(attributes);\r\n                if (error != null) g_error_free(error);\r\n            }\r\n        }\r\n\r\n        #endregion\r\n\r\n        private unsafe GHashTable* CreateSearchQuery(string service, string account)\r\n        {\r\n            // Build search query\r\n            GHashTable* queryAttrs = g_hash_table_new_full(\r\n                g_str_hash, g_str_equal,\r\n                Marshal.FreeHGlobal, Marshal.FreeHGlobal);\r\n\r\n            // If we've be given a service then filter on the service attribute\r\n            if (!string.IsNullOrWhiteSpace(service))\r\n            {\r\n                string fullServiceName = CreateServiceName(service);\r\n                IntPtr keyPtr = Marshal.StringToHGlobalAnsi(ServiceAttributeName);\r\n                IntPtr valuePtr = Marshal.StringToHGlobalAnsi(fullServiceName);\r\n                g_hash_table_insert(queryAttrs, keyPtr, valuePtr);\r\n            }\r\n\r\n            // If we've be given a username then filter on the account attribute\r\n            if (!string.IsNullOrWhiteSpace(account))\r\n            {\r\n                IntPtr keyPtr = Marshal.StringToHGlobalAnsi(AccountAttributeName);\r\n                IntPtr valuePtr = Marshal.StringToHGlobalAnsi(account);\r\n                g_hash_table_insert(queryAttrs, keyPtr, valuePtr);\r\n            }\r\n\r\n            return queryAttrs;\r\n        }\r\n\r\n        private static unsafe ICredential CreateCredentialFromItem(SecretItem* item)\r\n        {\r\n            GHashTable* secretAttrs = null;\r\n            IntPtr serviceKeyPtr = IntPtr.Zero;\r\n            IntPtr accountKeyPtr = IntPtr.Zero;\r\n            SecretValue* value = null;\r\n            IntPtr passwordPtr = IntPtr.Zero;\r\n            GError* error = null;\r\n\r\n            try\r\n            {\r\n                secretAttrs = secret_item_get_attributes(item);\r\n\r\n                // Extract the service attribute\r\n                serviceKeyPtr = Marshal.StringToHGlobalAnsi(ServiceAttributeName);\r\n                IntPtr serviceValuePtr = g_hash_table_lookup(secretAttrs, serviceKeyPtr);\r\n                string service = Marshal.PtrToStringAuto(serviceValuePtr);\r\n\r\n                // Extract the account attribute\r\n                accountKeyPtr = Marshal.StringToHGlobalAnsi(AccountAttributeName);\r\n                IntPtr accountValuePtr = g_hash_table_lookup(secretAttrs, accountKeyPtr);\r\n                string account = Marshal.PtrToStringAuto(accountValuePtr);\r\n\r\n                // Load the secret value\r\n                secret_item_load_secret_sync(item, IntPtr.Zero, out error);\r\n                value = secret_item_get_secret(item);\r\n                if (value == null)\r\n                {\r\n                    throw new InteropException(\"Failed to load secret\", -1);\r\n                }\r\n\r\n                // Extract the secret/password\r\n                passwordPtr = secret_value_get(value, out int passwordLength);\r\n                string password = Marshal.PtrToStringAuto(passwordPtr, passwordLength);\r\n\r\n                return new SecretServiceCredential(service, account, password);\r\n            }\r\n            finally\r\n            {\r\n                if (secretAttrs != null) g_hash_table_unref(secretAttrs);\r\n                if (accountKeyPtr != IntPtr.Zero) Marshal.FreeHGlobal(accountKeyPtr);\r\n                if (serviceKeyPtr != IntPtr.Zero) Marshal.FreeHGlobal(serviceKeyPtr);\r\n                if (value != null) secret_value_unref(value);\r\n                if (error != null) g_error_free(error);\r\n            }\r\n        }\r\n\r\n        private string CreateServiceName(string service)\r\n        {\r\n            var sb = new StringBuilder();\r\n            if (!string.IsNullOrWhiteSpace(_namespace))\r\n            {\r\n                sb.AppendFormat(\"{0}:\", _namespace);\r\n            }\r\n\r\n            sb.Append(service);\r\n            return sb.ToString();\r\n        }\r\n\r\n        private static unsafe SecretService* GetSecretService()\r\n        {\r\n            // Get a handle to the default secret service, open a session,\r\n            // and load all collections\r\n            SecretService* service = secret_service_get_sync(\r\n                SecretServiceFlags.SECRET_SERVICE_OPEN_SESSION | SecretServiceFlags.SECRET_SERVICE_LOAD_COLLECTIONS,\r\n                IntPtr.Zero, out GError* error);\r\n\r\n            if (error != null)\r\n            {\r\n                int code = error->code;\r\n                string message = Marshal.PtrToStringAuto(error->message)!;\r\n                g_error_free(error);\r\n                throw new InteropException(\"Failed to open secret service session\", code, new Exception(message));\r\n            }\r\n\r\n            return service;\r\n        }\r\n\r\n        private static SecretSchema GetSchema()\r\n        {\r\n            var schema = new SecretSchema\r\n            {\r\n                name = SchemaName,\r\n                flags = SECRET_SCHEMA_DONT_MATCH_NAME,\r\n                attributes = new SecretSchemaAttribute[32]\r\n            };\r\n\r\n            schema.attributes[0] = new SecretSchemaAttribute\r\n            {\r\n                name = ServiceAttributeName,\r\n                type = SECRET_SCHEMA_ATTRIBUTE_STRING\r\n            };\r\n\r\n            schema.attributes[1] = new SecretSchemaAttribute\r\n            {\r\n                name = AccountAttributeName,\r\n                type = SECRET_SCHEMA_ATTRIBUTE_STRING\r\n            };\r\n\r\n            return schema;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/shared/Core/Interop/Linux/SecretServiceCredential.cs",
    "content": "using System.Diagnostics;\n\nnamespace GitCredentialManager.Interop.Linux\n{\n    [DebuggerDisplay(\"{DebuggerDisplay}\")]\n    public class SecretServiceCredential : ICredential\n    {\n        internal SecretServiceCredential(string service, string account, string password)\n        {\n            Service = service;\n            Account = account;\n            Password = password;\n        }\n\n        public string Service { get; }\n\n        public string Account { get; }\n\n        public string Password { get; }\n\n        private string DebuggerDisplay => $\"[Service: {Service}, Account: {Account}]\";\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSEnvironment.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading;\nusing GitCredentialManager.Interop.Posix;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    public class MacOSEnvironment : PosixEnvironment\n    {\n        private ICollection<string> _pathsToIgnore;\n\n        public MacOSEnvironment(IFileSystem fileSystem)\n            : base(fileSystem) { }\n\n        internal MacOSEnvironment(IFileSystem fileSystem, IReadOnlyDictionary<string, string> variables)\n            : base(fileSystem, variables) { }\n\n        public override bool TryLocateExecutable(string program, out string path)\n        {\n            if (_pathsToIgnore is null)\n            {\n                _pathsToIgnore = new List<string>();\n                if (Variables.TryGetValue(\"HOMEBREW_PREFIX\", out string homebrewPrefix))\n                {\n                    string homebrewGit = Path.Combine(homebrewPrefix, \"Homebrew/Library/Homebrew/shims/shared/git\");\n                    _pathsToIgnore.Add(homebrewGit);\n                }\n            }\n            return TryLocateExecutable(program, _pathsToIgnore, out path);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSFileSystem.cs",
    "content": "using System;\nusing System.IO;\nusing GitCredentialManager.Interop.Posix;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    public class MacOSFileSystem : PosixFileSystem\n    {\n        public override bool IsSamePath(string a, string b)\n        {\n            if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))\n            {\n                return false;\n            }\n\n            // Normalize paths\n            a = Path.GetFullPath(a);\n            b = Path.GetFullPath(b);\n\n            // Resolve symbolic links\n            a = ResolveSymbolicLinks(a);\n            b = ResolveSymbolicLinks(b);\n\n            // TODO: determine if file system is case-sensitive\n            // By default HFS+/APFS is NOT case-sensitive...\n            return StringComparer.OrdinalIgnoreCase.Equals(a, b);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSKeychain.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing GitCredentialManager.Interop.MacOS.Native;\nusing static GitCredentialManager.Interop.MacOS.Native.CoreFoundation;\nusing static GitCredentialManager.Interop.MacOS.Native.SecurityFramework;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    public class MacOSKeychain : ICredentialStore\n    {\n        private readonly string _namespace;\n\n        #region Constructors\n\n        /// <summary>\n        /// Open the default keychain (current user's login keychain).\n        /// </summary>\n        /// <param name=\"namespace\">Optional namespace to scope credential operations.</param>\n        /// <returns>Default keychain.</returns>\n        public MacOSKeychain(string @namespace = null)\n        {\n            PlatformUtils.EnsureMacOS();\n            _namespace = @namespace;\n        }\n\n        #endregion\n\n        #region ICredentialStore\n\n        public IList<string> GetAccounts(string service)\n        {\n            IntPtr query = IntPtr.Zero;\n            IntPtr resultPtr = IntPtr.Zero;\n            IntPtr servicePtr = IntPtr.Zero;\n            IntPtr accountPtr = IntPtr.Zero;\n\n            try\n            {\n                query = CFDictionaryCreateMutable(\n                    IntPtr.Zero,\n                    0,\n                    IntPtr.Zero, IntPtr.Zero);\n\n                CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword);\n                CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);\n                CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);\n\n                if (!string.IsNullOrWhiteSpace(service))\n                {\n                    string fullService = CreateServiceName(service);\n                    servicePtr = CreateCFStringUtf8(fullService);\n                    CFDictionaryAddValue(query, kSecAttrService, servicePtr);\n                }\n\n                int searchResult = SecItemCopyMatching(query, out resultPtr);\n\n                switch (searchResult)\n                {\n                    case OK:\n                        int typeId = CFGetTypeID(resultPtr);\n                        Debug.Assert(typeId == CFArrayGetTypeID(), \"Returned unknown item from account query\");\n                        if (typeId == CFArrayGetTypeID())\n                        {\n                            int len = (int)CFArrayGetCount(resultPtr);\n                            var accounts = new HashSet<string>(len);\n                            for (int i = 0; i < len; i++)\n                            {\n                                IntPtr dict = CFArrayGetValueAtIndex(resultPtr, i);\n                                string account = GetStringAttribute(dict, kSecAttrAccount);\n                                accounts.Add(account);\n                            }\n\n                            return accounts.ToList();\n                        }\n\n                        throw new InteropException($\"Unknown keychain search result type CFTypeID: {typeId}.\", -1);\n\n                    case ErrorSecItemNotFound:\n                        return Array.Empty<string>();\n\n                    default:\n                        ThrowIfError(searchResult);\n                        return null;\n                }\n            }\n            finally\n            {\n                if (query != IntPtr.Zero) CFRelease(query);\n                if (servicePtr != IntPtr.Zero) CFRelease(servicePtr);\n                if (accountPtr != IntPtr.Zero) CFRelease(accountPtr);\n                if (resultPtr != IntPtr.Zero) CFRelease(resultPtr);\n            }\n        }\n\n        public ICredential Get(string service, string account)\n        {\n            IntPtr query = IntPtr.Zero;\n            IntPtr resultPtr = IntPtr.Zero;\n            IntPtr servicePtr = IntPtr.Zero;\n            IntPtr accountPtr = IntPtr.Zero;\n\n            try\n            {\n                query = CFDictionaryCreateMutable(\n                    IntPtr.Zero,\n                    0,\n                    IntPtr.Zero, IntPtr.Zero);\n\n                CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword);\n                CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne);\n                CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue);\n                CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);\n\n                if (!string.IsNullOrWhiteSpace(service))\n                {\n                    string fullService = CreateServiceName(service);\n                    servicePtr = CreateCFStringUtf8(fullService);\n                    CFDictionaryAddValue(query, kSecAttrService, servicePtr);\n                }\n\n                if (!string.IsNullOrWhiteSpace(account))\n                {\n                    accountPtr = CreateCFStringUtf8(account);\n                    CFDictionaryAddValue(query, kSecAttrAccount, accountPtr);\n                }\n\n                int searchResult = SecItemCopyMatching(query, out resultPtr);\n\n                switch (searchResult)\n                {\n                    case OK:\n                        int typeId = CFGetTypeID(resultPtr);\n                        Debug.Assert(typeId != CFArrayGetTypeID(), \"Returned more than one keychain item in search\");\n                        if (typeId == CFDictionaryGetTypeID())\n                        {\n                            return CreateCredentialFromAttributes(resultPtr);\n                        }\n\n                        throw new InteropException($\"Unknown keychain search result type CFTypeID: {typeId}.\", -1);\n\n                    case ErrorSecItemNotFound:\n                        return null;\n\n                    default:\n                        ThrowIfError(searchResult);\n                        return null;\n                }\n            }\n            finally\n            {\n                if (query != IntPtr.Zero) CFRelease(query);\n                if (servicePtr != IntPtr.Zero) CFRelease(servicePtr);\n                if (accountPtr != IntPtr.Zero) CFRelease(accountPtr);\n                if (resultPtr != IntPtr.Zero) CFRelease(resultPtr);\n            }\n        }\n\n        public void AddOrUpdate(string service, string account, string secret)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(service, nameof(service));\n\n            byte[] secretBytes = Encoding.UTF8.GetBytes(secret);\n\n            IntPtr passwordData = IntPtr.Zero;\n            IntPtr itemRef = IntPtr.Zero;\n\n            string serviceName = CreateServiceName(service);\n\n            uint serviceNameLength = (uint) serviceName.Length;\n            uint accountLength = (uint) (account?.Length ?? 0);\n\n            try\n            {\n                // Check if an entry already exists in the keychain\n                int findResult = SecKeychainFindGenericPassword(\n                    IntPtr.Zero, serviceNameLength, serviceName, accountLength, account,\n                    out uint passwordDataLength, out passwordData, out itemRef);\n\n                switch (findResult)\n                {\n                    // Update existing entry only if the password/secret is different\n                    case OK when !InteropUtils.AreEqual(secretBytes, passwordData, passwordDataLength):\n                        ThrowIfError(\n                            SecKeychainItemModifyAttributesAndData(itemRef, IntPtr.Zero, (uint) secretBytes.Length, secretBytes),\n                            \"Could not update existing item\"\n                        );\n                        break;\n\n                    // Create new entry\n                    case ErrorSecItemNotFound:\n                        ThrowIfError(\n                            SecKeychainAddGenericPassword(IntPtr.Zero, serviceNameLength, serviceName, accountLength,\n                                account, (uint) secretBytes.Length, secretBytes, out itemRef),\n                            \"Could not create new item\"\n                        );\n                        break;\n\n                    default:\n                        ThrowIfError(findResult);\n                        break;\n                }\n            }\n            finally\n            {\n                if (passwordData != IntPtr.Zero)\n                {\n                    SecKeychainItemFreeContent(IntPtr.Zero, passwordData);\n                }\n\n                if (itemRef != IntPtr.Zero)\n                {\n                    CFRelease(itemRef);\n                }\n            }\n        }\n\n        public bool Remove(string service, string account)\n        {\n            IntPtr query = IntPtr.Zero;\n            IntPtr itemRefPtr = IntPtr.Zero;\n            IntPtr servicePtr = IntPtr.Zero;\n            IntPtr accountPtr = IntPtr.Zero;\n\n            try\n            {\n                query = CFDictionaryCreateMutable(\n                    IntPtr.Zero,\n                    0,\n                    IntPtr.Zero, IntPtr.Zero);\n\n                CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword);\n                CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne);\n                CFDictionaryAddValue(query, kSecReturnRef, kCFBooleanTrue);\n\n                if (!string.IsNullOrWhiteSpace(service))\n                {\n                    string fullService = CreateServiceName(service);\n                    servicePtr = CreateCFStringUtf8(fullService);\n                    CFDictionaryAddValue(query, kSecAttrService, servicePtr);\n                }\n\n                if (!string.IsNullOrWhiteSpace(account))\n                {\n                    accountPtr = CreateCFStringUtf8(account);\n                    CFDictionaryAddValue(query, kSecAttrAccount, accountPtr);\n                }\n\n                // Search for the credential to delete and get the SecKeychainItem ref.\n                int searchResult = SecItemCopyMatching(query, out itemRefPtr);\n                switch (searchResult)\n                {\n                    case OK:\n                        // Delete the item\n                        ThrowIfError(\n                            SecKeychainItemDelete(itemRefPtr)\n                        );\n                        return true;\n\n                    case ErrorSecItemNotFound:\n                        return false;\n\n                    default:\n                        ThrowIfError(searchResult);\n                        return false;\n                }\n            }\n            finally\n            {\n                if (query != IntPtr.Zero) CFRelease(query);\n                if (itemRefPtr != IntPtr.Zero) CFRelease(itemRefPtr);\n                if (servicePtr != IntPtr.Zero) CFRelease(servicePtr);\n                if (accountPtr != IntPtr.Zero) CFRelease(accountPtr);\n            }\n        }\n\n        #endregion\n\n        private static IntPtr CreateCFStringUtf8(string str)\n        {\n            byte[] bytes = Encoding.UTF8.GetBytes(str);\n            return CFStringCreateWithBytes(IntPtr.Zero,\n                bytes, bytes.Length, CFStringEncoding.kCFStringEncodingUTF8, false);\n        }\n\n        private static ICredential CreateCredentialFromAttributes(IntPtr attributes)\n        {\n            string service = GetStringAttribute(attributes, kSecAttrService);\n            string account = GetStringAttribute(attributes, kSecAttrAccount);\n            string password = GetStringAttribute(attributes, kSecValueData);\n            string label = GetStringAttribute(attributes, kSecAttrLabel);\n            return new MacOSKeychainCredential(service, account, password, label);\n        }\n\n        private static string GetStringAttribute(IntPtr dict, IntPtr key)\n        {\n            if (dict == IntPtr.Zero)\n            {\n                return null;\n            }\n\n            if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero)\n            {\n                if (CFGetTypeID(value) == CFStringGetTypeID())\n                {\n                    return CFStringToString(value);\n                }\n\n                if (CFGetTypeID(value) == CFDataGetTypeID())\n                {\n                    int length = CFDataGetLength(value);\n                    IntPtr ptr = CFDataGetBytePtr(value);\n                    return Marshal.PtrToStringAuto(ptr, length);\n                }\n            }\n\n            return null;\n        }\n\n        private string CreateServiceName(string service)\n        {\n            var sb = new StringBuilder();\n            if (!string.IsNullOrWhiteSpace(_namespace))\n            {\n                sb.AppendFormat(\"{0}:\", _namespace);\n            }\n\n            sb.Append(service);\n            return sb.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSKeychainCredential.cs",
    "content": "using System.Diagnostics;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    [DebuggerDisplay(\"{DebuggerDisplay}\")]\n    public class MacOSKeychainCredential : ICredential\n    {\n        internal MacOSKeychainCredential(string service, string account, string password, string label)\n        {\n            Service = service;\n            Account = account;\n            Password = password;\n            Label = label;\n        }\n\n        public string Service  { get; }\n\n        public string Account { get; }\n\n        public string Label { get; }\n\n        public string Password { get; }\n\n        private string DebuggerDisplay => $\"{Label} [Service: {Service}, Account: {Account}]\";\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSPreferences.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing GitCredentialManager.Interop.MacOS.Native;\nusing static GitCredentialManager.Interop.MacOS.Native.CoreFoundation;\n\nnamespace GitCredentialManager.Interop.MacOS;\n\npublic class MacOSPreferences\n{\n    private readonly string _appId;\n\n    public MacOSPreferences(string appId)\n    {\n        EnsureArgument.NotNull(appId, nameof(appId));\n\n        _appId = appId;\n    }\n\n    /// <summary>\n    /// Return a <see cref=\"string\"/> typed value from the app preferences.\n    /// </summary>\n    /// <param name=\"key\">Preference name.</param>\n    /// <exception cref=\"InvalidOperationException\">Thrown if the preference is not a string.</exception>\n    /// <returns>\n    /// <see cref=\"string\"/> or null if the preference with the given key does not exist.\n    /// </returns>\n    public string GetString(string key)\n    {\n        return TryGet(key, CFStringToString, out string value)\n            ? value\n            : null;\n    }\n\n    /// <summary>\n    /// Return a <see cref=\"int\"/> typed value from the app preferences.\n    /// </summary>\n    /// <param name=\"key\">Preference name.</param>\n    /// <exception cref=\"InvalidOperationException\">Thrown if the preference is not an integer.</exception>\n    /// <returns>\n    /// <see cref=\"int\"/> or null if the preference with the given key does not exist.\n    /// </returns>\n    public int? GetInteger(string key)\n    {\n        return TryGet(key, CFNumberToInt32, out int value)\n            ? value\n            : null;\n    }\n\n    /// <summary>\n    /// Return a <see cref=\"IDictionary{TKey,TValue}\"/> typed value from the app preferences.\n    /// </summary>\n    /// <param name=\"key\">Preference name.</param>\n    /// <exception cref=\"InvalidOperationException\">Thrown if the preference is not a dictionary.</exception>\n    /// <returns>\n    /// <see cref=\"IDictionary{TKey,TValue}\"/> or null if the preference with the given key does not exist.\n    /// </returns>\n    public IDictionary<string, string> GetDictionary(string key)\n    {\n        return TryGet(key, CFDictionaryToDictionary, out IDictionary<string, string> value)\n            ? value\n            : null;\n    }\n\n    private bool TryGet<T>(string key, Func<IntPtr, T> converter, out T value)\n    {\n        IntPtr cfValue = IntPtr.Zero;\n        IntPtr keyPtr = IntPtr.Zero;\n        IntPtr appIdPtr = CreateAppIdPtr();\n\n        try\n        {\n            keyPtr = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.kCFStringEncodingUTF8);\n            cfValue = CFPreferencesCopyAppValue(keyPtr, appIdPtr);\n\n            if (cfValue == IntPtr.Zero)\n            {\n                value = default;\n                return false;\n            }\n\n            value = converter(cfValue);\n            return true;\n        }\n        finally\n        {\n            if (cfValue != IntPtr.Zero) CFRelease(cfValue);\n            if (keyPtr != IntPtr.Zero) CFRelease(keyPtr);\n            if (appIdPtr != IntPtr.Zero) CFRelease(appIdPtr);\n        }\n    }\n\n    private IntPtr CreateAppIdPtr()\n    {\n        return CFStringCreateWithCString(IntPtr.Zero, _appId, CFStringEncoding.kCFStringEncodingUTF8);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSSessionManager.cs",
    "content": "using GitCredentialManager.Interop.MacOS.Native;\nusing GitCredentialManager.Interop.Posix;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    public class MacOSSessionManager : PosixSessionManager\n    {\n        public MacOSSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs)\n        {\n            PlatformUtils.EnsureMacOS();\n        }\n\n        public override bool IsDesktopSession\n        {\n            get\n            {\n                // Get information about the current session\n                int error = SecurityFramework.SessionGetInfo(SecurityFramework.CallerSecuritySession, out int id, out var sessionFlags);\n\n                // Check if the session supports Quartz\n                if (error == 0 && (sessionFlags & SessionAttributeBits.SessionHasGraphicAccess) != 0)\n                {\n                    return true;\n                }\n\n                // Fall-through and check if X11 is available on macOS\n                return base.IsDesktopSession;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSSettings.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    /// <summary>\n    /// Reads settings from Git configuration, environment variables, and defaults from the system.\n    /// </summary>\n    public class MacOSSettings : Settings\n    {\n        private readonly ITrace _trace;\n\n        public MacOSSettings(IEnvironment environment, IGit git, ITrace trace)\n            : base(environment, git)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            _trace = trace;\n\n            PlatformUtils.EnsureMacOS();\n        }\n\n        protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)\n        {\n            value = null;\n\n            try\n            {\n                // Check for app default preferences for our bundle ID.\n                // Defaults can be deployed system administrators via device management profiles.\n                var prefs = new MacOSPreferences(Constants.MacOSBundleId);\n                IDictionary<string, string> dict = prefs.GetDictionary(\"configuration\");\n\n                if (dict is null)\n                {\n                    // No configuration key exists\n                    return false;\n                }\n\n                // Wrap the raw dictionary in one configured with the Git configuration key comparer.\n                // This means we can use the same key comparison rules as Git in our configuration plist dict,\n                // That is, sections and names are insensitive to case, but the scope is case-sensitive.\n                var config = new Dictionary<string, string>(dict, GitConfigurationKeyComparer.Instance);\n\n                string name = string.IsNullOrWhiteSpace(scope)\n                    ? $\"{section}.{property}\"\n                    : $\"{section}.{scope}.{property}\";\n\n                if (!config.TryGetValue(name, out value))\n                {\n                    // No property exists\n                    return false;\n                }\n\n                _trace.WriteLine($\"Default setting found in app preferences: {name}={value}\");\n                return true;\n            }\n            catch (Exception ex)\n            {\n                // Reading defaults is not critical to the operation of the application\n                // so we can ignore any errors and just log the failure.\n                _trace.WriteLine(\"Failed to read default setting from app preferences.\");\n                _trace.WriteException(ex);\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/MacOSTerminal.cs",
    "content": "using System;\nusing GitCredentialManager.Interop.MacOS.Native;\nusing GitCredentialManager.Interop.Posix;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager.Interop.MacOS\n{\n    public class MacOSTerminal : PosixTerminal\n    {\n        public MacOSTerminal(ITrace trace, ITrace2 trace2)\n            : base(trace, trace2) { }\n\n        protected override IDisposable CreateTtyContext(int fd, bool echo)\n        {\n            return new TtyContext(Trace, Trace2, fd, echo);\n        }\n\n        private class TtyContext : IDisposable\n        {\n            private readonly ITrace _trace;\n            private readonly int _fd;\n\n            private termios_MacOS _originalTerm;\n            private bool _isDisposed;\n\n            public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo)\n            {\n                EnsureArgument.NotNull(trace, nameof(trace));\n                EnsureArgument.PositiveOrZero(fd, nameof(fd));\n\n                _trace = trace;\n                _fd = fd;\n\n                int error = 0;\n\n                // Capture current terminal settings so we can restore them later\n                if ((error = Termios_MacOS.tcgetattr(_fd, out termios_MacOS t)) != 0)\n                {\n                    throw new Trace2InteropException(trace2, \"Failed to get initial terminal settings\", error);\n                }\n\n                _originalTerm = t;\n\n                // Set desired echo state\n                _trace.WriteLine($\"Setting terminal echo state to '{echo}'\");\n                if (echo)\n                    t.c_lflag |= LocalFlags.ECHO;\n                else\n                    t.c_lflag &= ~LocalFlags.ECHO;\n\n                if ((error = Termios_MacOS.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0)\n                {\n                    throw new Trace2InteropException(trace2, \"Failed to set terminal settings\", error);\n                }\n            }\n\n            public void Dispose()\n            {\n                if (_isDisposed)\n                {\n                    return;\n                }\n\n                int error = 0;\n\n                // Restore original terminal settings\n                if ((error = Termios_MacOS.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref _originalTerm)) != 0)\n                {\n                    _trace.WriteLine($\"Failed to get restore terminal settings (error: {error:x}\");\n                }\n\n                _isDisposed = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing static GitCredentialManager.Interop.MacOS.Native.LibSystem;\n\nnamespace GitCredentialManager.Interop.MacOS.Native\n{\n    public static class CoreFoundation\n    {\n        private const string CoreFoundationFrameworkLib = \"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation\";\n\n        public static readonly IntPtr Handle;\n        public static readonly IntPtr kCFBooleanTrue;\n        public static readonly IntPtr kCFBooleanFalse;\n\n        static CoreFoundation()\n        {\n            Handle = dlopen(CoreFoundationFrameworkLib, 0);\n\n            kCFBooleanTrue  = GetGlobal(Handle, \"kCFBooleanTrue\");\n            kCFBooleanFalse = GetGlobal(Handle, \"kCFBooleanFalse\");\n        }\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFArrayCreateMutable(IntPtr allocator, long capacity, IntPtr callbacks);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void CFArrayInsertValueAtIndex(IntPtr theArray, long idx, IntPtr value);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern long CFArrayGetCount(IntPtr theArray);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFArrayGetValueAtIndex(IntPtr theArray, long idx);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFDictionaryCreateMutable(\n            IntPtr allocator,\n            long capacity,\n            IntPtr keyCallBacks,\n            IntPtr valueCallBacks);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void CFDictionaryAddValue(\n            IntPtr theDict,\n            IntPtr key,\n            IntPtr value);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFDictionaryGetValue(IntPtr theDict, IntPtr key);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern bool CFDictionaryGetValueIfPresent(IntPtr theDict, IntPtr key, out IntPtr value);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, long numBytes,\n            CFStringEncoding encoding, bool isExternalRepresentation);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string cStr, CFStringEncoding encoding);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern long CFStringGetLength(IntPtr theString);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern bool CFStringGetCString(IntPtr theString, IntPtr buffer, long bufferSize, CFStringEncoding encoding);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void CFRetain(IntPtr cf);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern void CFRelease(IntPtr cf);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFGetTypeID(IntPtr cf);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFStringGetTypeID();\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFDataGetTypeID();\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFDictionaryGetTypeID();\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFArrayGetTypeID();\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFNumberGetTypeID();\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFDataGetBytePtr(IntPtr theData);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int CFDataGetLength(IntPtr theData);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFPreferencesCopyAppValue(IntPtr key, IntPtr appID);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern bool CFNumberGetValue(IntPtr number, CFNumberType theType, out IntPtr valuePtr);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr CFDictionaryGetKeysAndValues(IntPtr theDict, IntPtr[] keys, IntPtr[] values);\n\n        [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern long CFDictionaryGetCount(IntPtr theDict);\n\n        public static string CFStringToString(IntPtr cfString)\n        {\n            if (cfString == IntPtr.Zero)\n            {\n                throw new ArgumentNullException(nameof(cfString));\n            }\n\n            if (CFGetTypeID(cfString) != CFStringGetTypeID())\n            {\n                throw new InvalidOperationException(\"Object is not a CFString.\");\n            }\n\n            long length = CFStringGetLength(cfString);\n            IntPtr buffer = Marshal.AllocHGlobal((int)length + 1);\n\n            try\n            {\n                if (!CFStringGetCString(cfString, buffer, length + 1, CFStringEncoding.kCFStringEncodingUTF8))\n                {\n                    throw new InvalidOperationException(\"Failed to convert CFString to C string.\");\n                }\n\n                return Marshal.PtrToStringAnsi(buffer);\n            }\n            finally\n            {\n                Marshal.FreeHGlobal(buffer);\n            }\n        }\n\n        public static int CFNumberToInt32(IntPtr cfNumber)\n        {\n            if (cfNumber == IntPtr.Zero)\n            {\n                throw new ArgumentNullException(nameof(cfNumber));\n            }\n\n            if (CFGetTypeID(cfNumber) != CFNumberGetTypeID())\n            {\n                throw new InvalidOperationException(\"Object is not a CFNumber.\");\n            }\n\n            if (!CFNumberGetValue(cfNumber, CFNumberType.kCFNumberIntType, out IntPtr valuePtr))\n            {\n                throw new InvalidOperationException(\"Failed to convert CFNumber to Int32.\");\n            }\n\n            return valuePtr.ToInt32();\n        }\n\n        public static IDictionary<string, string> CFDictionaryToDictionary(IntPtr cfDict)\n        {\n            if (cfDict == IntPtr.Zero)\n            {\n                throw new ArgumentNullException(nameof(cfDict));\n            }\n\n            if (CFGetTypeID(cfDict) != CFDictionaryGetTypeID())\n            {\n                throw new InvalidOperationException(\"Object is not a CFDictionary.\");\n            }\n\n            int count = (int)CFDictionaryGetCount(cfDict);\n            var keys = new IntPtr[count];\n            var values = new IntPtr[count];\n\n            CFDictionaryGetKeysAndValues(cfDict, keys, values);\n\n            var dict = new Dictionary<string, string>(capacity: count);\n            for (int i = 0; i < count; i++)\n            {\n                string keyStr = CFStringToString(keys[i])!;\n                string valueStr = CFStringToString(values[i]);\n\n                dict[keyStr] = valueStr;\n            }\n\n            return dict;\n        }\n    }\n\n    public enum CFStringEncoding\n    {\n        kCFStringEncodingUTF8 = 0x08000100,\n    }\n\n    public enum CFNumberType\n    {\n        kCFNumberSInt8Type = 1,\n        kCFNumberSInt16Type = 2,\n        kCFNumberSInt32Type = 3,\n        kCFNumberSInt64Type = 4,\n        kCFNumberFloat32Type = 5,\n        kCFNumberFloat64Type = 6,\n        kCFNumberCharType = 7,\n        kCFNumberShortType = 8,\n        kCFNumberIntType = 9,\n        kCFNumberLongType = 10,\n        kCFNumberLongLongType = 11,\n        kCFNumberFloatType = 12,\n        kCFNumberDoubleType = 13,\n        kCFNumberCFIndexType = 14,\n        kCFNumberNSIntegerType = 15,\n        kCFNumberCGFloatType = 16\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/Native/LibC.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.MacOS.Native\n{\n    public static class LibC\n    {\n        private const string LibCLib = \"libc\";\n\n        [DllImport(LibCLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int _NSGetExecutablePath(IntPtr buf, out int bufsize);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/Native/LibSystem.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.MacOS.Native\n{\n    public static class LibSystem\n    {\n        private const string LibSystemLib = \"/System/Library/Frameworks/System.framework/System\";\n\n        [DllImport(LibSystemLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr dlopen(string name, int flags);\n\n        [DllImport(LibSystemLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern IntPtr dlsym(IntPtr handle, string symbol);\n\n        public static IntPtr GetGlobal(IntPtr handle, string symbol)\n        {\n            IntPtr ptr = dlsym(handle, symbol);\n            return Marshal.PtrToStructure<IntPtr>(ptr);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/Native/SecurityFramework.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing static GitCredentialManager.Interop.MacOS.Native.LibSystem;\n\nnamespace GitCredentialManager.Interop.MacOS.Native\n{\n    // https://developer.apple.com/documentation/security/keychain_services/keychain_items\n    public static class SecurityFramework\n    {\n        private const string SecurityFrameworkLib = \"/System/Library/Frameworks/Security.framework/Security\";\n\n        public static readonly IntPtr Handle;\n        public static readonly IntPtr kSecClass;\n        public static readonly IntPtr kSecMatchLimit;\n        public static readonly IntPtr kSecReturnAttributes;\n        public static readonly IntPtr kSecReturnRef;\n        public static readonly IntPtr kSecReturnPersistentRef;\n        public static readonly IntPtr kSecClassGenericPassword;\n        public static readonly IntPtr kSecMatchLimitAll;\n        public static readonly IntPtr kSecMatchLimitOne;\n        public static readonly IntPtr kSecMatchItemList;\n        public static readonly IntPtr kSecAttrLabel;\n        public static readonly IntPtr kSecAttrAccount;\n        public static readonly IntPtr kSecAttrService;\n        public static readonly IntPtr kSecValueRef;\n        public static readonly IntPtr kSecValueData;\n        public static readonly IntPtr kSecReturnData;\n\n        static SecurityFramework()\n        {\n            Handle = dlopen(SecurityFrameworkLib, 0);\n\n            kSecClass                = GetGlobal(Handle, \"kSecClass\");\n            kSecMatchLimit           = GetGlobal(Handle, \"kSecMatchLimit\");\n            kSecReturnAttributes     = GetGlobal(Handle, \"kSecReturnAttributes\");\n            kSecReturnRef            = GetGlobal(Handle, \"kSecReturnRef\");\n            kSecReturnPersistentRef  = GetGlobal(Handle, \"kSecReturnPersistentRef\");\n            kSecClassGenericPassword = GetGlobal(Handle, \"kSecClassGenericPassword\");\n            kSecMatchLimitAll        = GetGlobal(Handle, \"kSecMatchLimitAll\");\n            kSecMatchLimitOne        = GetGlobal(Handle, \"kSecMatchLimitOne\");\n            kSecMatchItemList        = GetGlobal(Handle, \"kSecMatchItemList\");\n            kSecAttrLabel            = GetGlobal(Handle, \"kSecAttrLabel\");\n            kSecAttrAccount          = GetGlobal(Handle, \"kSecAttrAccount\");\n            kSecAttrService          = GetGlobal(Handle, \"kSecAttrService\");\n            kSecValueRef             = GetGlobal(Handle, \"kSecValueRef\");\n            kSecValueData            = GetGlobal(Handle, \"kSecValueData\");\n            kSecReturnData           = GetGlobal(Handle, \"kSecReturnData\");\n        }\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SessionGetInfo(int session, out int sessionId, out SessionAttributeBits attributes);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecAccessCreate(IntPtr descriptor, IntPtr trustedList, out IntPtr accessRef);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemCreateFromContent(IntPtr itemClass, IntPtr attrList, uint length,\n            IntPtr data, IntPtr keychainRef, IntPtr initialAccess, out IntPtr itemRef);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainAddGenericPassword(\n            IntPtr keychain,\n            uint serviceNameLength,\n            string serviceName,\n            uint accountNameLength,\n            string accountName,\n            uint passwordLength,\n            byte[] passwordData,\n            out IntPtr itemRef);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainFindGenericPassword(\n            IntPtr keychainOrArray,\n            uint serviceNameLength,\n            string serviceName,\n            uint accountNameLength,\n            string accountName,\n            out uint passwordLength,\n            out IntPtr passwordData,\n            out IntPtr itemRef);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern unsafe int SecKeychainItemCopyAttributesAndData(\n            IntPtr itemRef,\n            IntPtr info,\n            IntPtr itemClass,\n            SecKeychainAttributeList** attrList,\n            uint* dataLength,\n            void** data);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemModifyAttributesAndData(\n            IntPtr itemRef,\n            IntPtr attrList, // SecKeychainAttributeList*\n            uint length,\n            byte[] data);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemDelete(\n            IntPtr itemRef);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemFreeContent(\n            IntPtr attrList, // SecKeychainAttributeList*\n            IntPtr data);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemFreeAttributesAndData(\n            IntPtr attrList, // SecKeychainAttributeList*\n            IntPtr data);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecItemCopyMatching(IntPtr query, out IntPtr result);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemCopyFromPersistentReference(IntPtr persistentItemRef, out IntPtr itemRef);\n\n        [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]\n        public static extern int SecKeychainItemCopyContent(IntPtr itemRef, IntPtr itemClass, IntPtr attrList,\n            out uint length, out IntPtr outData);\n\n        public const int CallerSecuritySession = -1;\n\n        // https://developer.apple.com/documentation/security/1542001-security_framework_result_codes\n        public const int OK = 0;\n        public const int ErrorSecNoSuchKeychain = -25294;\n        public const int ErrorSecInvalidKeychain = -25295;\n        public const int ErrorSecAuthFailed = -25293;\n        public const int ErrorSecDuplicateItem = -25299;\n        public const int ErrorSecItemNotFound = -25300;\n        public const int ErrorSecInteractionNotAllowed = -25308;\n        public const int ErrorSecInteractionRequired = -25315;\n        public const int ErrorSecNoSuchAttr = -25303;\n\n        public static void ThrowIfError(int error, string defaultErrorMessage = \"Unknown error.\")\n        {\n            switch (error)\n            {\n                case OK:\n                    return;\n                case ErrorSecNoSuchKeychain:\n                    throw new InteropException(\"The keychain does not exist.\", error);\n                case ErrorSecInvalidKeychain:\n                    throw new InteropException(\"The keychain is not valid.\", error);\n                case ErrorSecAuthFailed:\n                    throw new InteropException(\"Authorization/Authentication failed.\", error);\n                case ErrorSecDuplicateItem:\n                    throw new InteropException(\"The item already exists.\", error);\n                case ErrorSecItemNotFound:\n                    throw new InteropException(\"The item cannot be found.\", error);\n                case ErrorSecInteractionNotAllowed:\n                    throw new InteropException(\"Interaction with the Security Server is not allowed.\", error);\n                case ErrorSecInteractionRequired:\n                    throw new InteropException(\"User interaction is required.\", error);\n                case ErrorSecNoSuchAttr:\n                    throw new InteropException(\"The attribute does not exist.\", error);\n                default:\n                    throw new InteropException(defaultErrorMessage, error);\n            }\n        }\n    }\n\n    [Flags]\n    public enum SessionAttributeBits\n    {\n        SessionIsRoot = 0x0001,\n        SessionHasGraphicAccess = 0x0010,\n        SessionHasTty = 0x0020,\n        SessionIsRemote = 0x1000,\n    }\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct SecKeychainAttributeInfo\n    {\n        public uint Count;\n        public IntPtr Tag; // uint* (SecKeychainAttrType*)\n        public IntPtr Format; // uint* (CssmDbAttributeFormat*)\n    }\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct SecKeychainAttributeList\n    {\n        public uint Count;\n        public IntPtr Attributes; // SecKeychainAttribute*\n    }\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct SecKeychainAttribute\n    {\n        public SecKeychainAttrType Tag;\n        public uint Length;\n        public IntPtr Data;\n    }\n\n    public enum CssmDbAttributeFormat : uint\n    {\n        String = 0,\n        SInt32 = 1,\n        UInt32 = 2,\n        BigNum = 3,\n        Real = 4,\n        TimeDate = 5,\n        Blob = 6,\n        MultiUInt32 = 7,\n        Complex = 8\n    };\n\n    public enum SecKeychainAttrType : uint\n    {\n        // https://developer.apple.com/documentation/security/secitemattr/accountitemattr\n        AccountItem = 1633903476,\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/MacOS/Native/termios_MacOS.cs",
    "content": "using System.Runtime.InteropServices;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager.Interop.MacOS.Native\n{\n    public static class Termios_MacOS\n    {\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int tcgetattr(int fd, out termios_MacOS termios);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int tcsetattr(int fd, SetActionFlags optActions, ref termios_MacOS termios);\n    }\n\n    [StructLayout(LayoutKind.Explicit)]\n    public struct termios_MacOS\n    {\n        // macOS has an array of 20 elements\n        private const int NCCS = 20;\n\n        // macOS uses unsigned 64-bit sized flags\n        [FieldOffset(0)]  public InputFlags   c_iflag;\n        [FieldOffset(8)]  public OutputFlags  c_oflag;\n        [FieldOffset(16)] public ControlFlags c_cflag;\n        [FieldOffset(24)] public LocalFlags   c_lflag;\n\n        [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)]\n        [FieldOffset(32)] public byte[] c_cc;\n\n        [FieldOffset(32 + NCCS)]     public ulong c_ispeed;\n        [FieldOffset(32 + NCCS + 8)] public ulong c_ospeed;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/GpgPassCredentialStore.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\n\nnamespace GitCredentialManager.Interop.Posix\n{\n    public class GpgPassCredentialStore : PlaintextCredentialStore\n    {\n        public const string PasswordStoreDirEnvar = \"PASSWORD_STORE_DIR\";\n\n        private readonly IGpg _gpg;\n\n        public GpgPassCredentialStore(IFileSystem fileSystem, IGpg gpg, string storeRoot, string @namespace = null)\n            : base(fileSystem, storeRoot, @namespace)\n        {\n            PlatformUtils.EnsurePosix();\n            EnsureArgument.NotNull(gpg, nameof(gpg));\n            _gpg = gpg;\n        }\n\n        protected override string CredentialFileExtension => \".gpg\";\n\n        private string GetGpgId(string credentialFullPath)\n        {\n            // Walk up from the credential's directory to the store root, looking for a .gpg-id file.\n            // This mimics the behaviour of GNU Pass, which uses the nearest .gpg-id in the directory hierarchy.\n            string dir = Path.GetDirectoryName(credentialFullPath);\n            while (dir != null)\n            {\n                string gpgIdPath = Path.Combine(dir, \".gpg-id\");\n                if (FileSystem.FileExists(gpgIdPath))\n                {\n                    using (var stream = FileSystem.OpenFileStream(gpgIdPath, FileMode.Open, FileAccess.Read, FileShare.Read))\n                    using (var reader = new StreamReader(stream))\n                    {\n                        return reader.ReadLine();\n                    }\n                }\n\n                // Stop after checking the store root\n                if (FileSystem.IsSamePath(dir, StoreRoot))\n                {\n                    break;\n                }\n\n                dir = Path.GetDirectoryName(dir);\n            }\n\n            throw new Exception($\"Cannot find GPG ID in password store at '{StoreRoot}'; run `pass init <gpg-id>` to initialize the store.\");\n        }\n\n        protected override bool TryDeserializeCredential(string path, out FileCredential credential)\n        {\n            string text = _gpg.DecryptFile(path);\n\n            int line1Idx = text.IndexOf(Environment.NewLine, StringComparison.OrdinalIgnoreCase);\n            if (line1Idx > 0)\n            {\n                // Password is the first line\n                string password = text.Substring(0, line1Idx);\n\n                // All subsequent lines are metadata/attributes\n                string attrText = text.Substring(line1Idx + Environment.NewLine.Length);\n                using var attrReader = new StringReader(attrText);\n                IDictionary<string, string> attrs = attrReader.ReadDictionary(StringComparer.OrdinalIgnoreCase);\n\n                // Account is optional\n                attrs.TryGetValue(\"account\", out string account);\n\n                // Service is required\n                if (attrs.TryGetValue(\"service\", out string service))\n                {\n                    credential = new FileCredential(path, service, account, password);\n                    return true;\n                }\n            }\n\n            credential = null;\n            return false;\n        }\n\n        protected override void SerializeCredential(FileCredential credential)\n        {\n            string gpgId = GetGpgId(credential.FullPath);\n\n            var sb = new StringBuilder(credential.Password);\n            sb.AppendFormat(\"{1}service={0}{1}\", credential.Service, Environment.NewLine);\n            sb.AppendFormat(\"account={0}{1}\", credential.Account, Environment.NewLine);\n            string fileContents = sb.ToString();\n\n            // Ensure the parent directory exists\n            string parentDir = Path.GetDirectoryName(credential.FullPath);\n            if (!FileSystem.DirectoryExists(parentDir))\n            {\n                FileSystem.CreateDirectory(parentDir);\n            }\n\n            // Delete any existing file\n            if (FileSystem.FileExists(credential.FullPath))\n            {\n                FileSystem.DeleteFile(credential.FullPath);\n            }\n\n            // Encrypt!\n            _gpg.EncryptFile(credential.FullPath, gpgId, fileContents);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Fcntl.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Posix.Native\n{\n    public static class Fcntl\n    {\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int open(string pathname, OpenFlags flags);\n    }\n\n    [Flags]\n    public enum OpenFlags\n    {\n        O_RDONLY    = 0,\n        O_WRONLY    = 1,\n        O_RDWR      = 2,\n        O_CREAT     = 64,\n        O_EXCL      = 128,\n        O_NOCTTY    = 256,\n        O_TRUNC     = 512,\n        O_APPEND    = 1024,\n        O_NONBLOCK  = 2048,\n        O_SYNC      = 4096,\n        O_NOFOLLOW  = 131072,\n        O_DIRECTORY = 65536,\n        O_DIRECT    = 16384,\n        O_ASYNC     = 8192,\n        O_LARGEFILE = 32768,\n        O_CLOEXEC   = 524288,\n        O_PATH      = 2097152,\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Signal.cs",
    "content": "using System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Posix.Native\n{\n    public static class Signal\n    {\n        /// <summary>\n        /// Interrupt.\n        /// </summary>\n        public const int SIGINT  = 2;\n\n        /// <summary>\n        /// Quit.\n        /// </summary>\n        public const int SIGQUIT = 3;\n\n        /// <summary>\n        /// Abort.\n        /// </summary>\n        public const int SIGABRT = 6;\n\n        /// <summary>\n        /// Kill (cannot be caught or ignored).\n        /// </summary>\n        public const int SIGKILL = 9;\n\n        /// <summary>\n        /// Software termination signal from kill.\n        /// </summary>\n        public const int SIGTERM = 15;\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern void kill(int pid, int sig);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Stat.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Posix.Native\n{\n    public static class Stat\n    {\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int chmod(string path, NativeFileMode mode);\n    }\n    [Flags]\n    public enum NativeFileMode\n    {\n        NONE = 0,\n\n        // Default permissions (RW for owner, RW for group, RW for other)\n        DEFAULT = S_IWOTH | S_IROTH | S_IWGRP | S_IRGRP | S_IWUSR | S_IRUSR,\n\n        // All file access permissions (RWX for owner, group, and other)\n        ACCESSPERMS = S_IRWXO | S_IRWXU | S_IRWXG,\n\n        // Read for owner (0000400)\n        S_IRUSR = 0x100,\n        // Write for owner (0000200)\n        S_IWUSR = 0x080,\n        // Execute for owner (0000100)\n        S_IXUSR = 0x040,\n        // Access permissions for owner\n        S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR,\n\n        // Read for group (0000040)\n        S_IRGRP = 0x020,\n        // Write for group (0000020)\n        S_IWGRP = 0x010,\n        // Execute for group (0000010)\n        S_IXGRP = 0x008,\n        // Access permissions for group\n        S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP,\n\n        // Read for other (0000004)\n        S_IROTH = 0x004,\n        // Write for other (0000002)\n        S_IWOTH = 0x002,\n        // Execute for other (0000001)\n        S_IXOTH = 0x001,\n        // Access permissions for other\n        S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH,\n\n        // Set user ID on execution (0004000)\n        S_ISUID = 0x800,\n        // Set group ID on execution (0002000)\n        S_ISGID = 0x400,\n        // Sticky bit (0001000)\n        S_ISVTX = 0x200,\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Stdio.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing System.Text;\n\nnamespace GitCredentialManager.Interop.Posix.Native\n{\n    public static class Stdio\n    {\n        public const int EOF = -1;\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern IntPtr fgets(StringBuilder sb, int size, IntPtr stream);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int fputc(int c, IntPtr stream);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int fprintf(IntPtr stream, string format, string message);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int fgetc(IntPtr stream);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int fputs(string str, IntPtr stream);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern void setbuf(IntPtr stream, int size);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern IntPtr fdopen(int fd, string mode);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Stdlib.cs",
    "content": "using System.Runtime.InteropServices;\r\n\r\nnamespace GitCredentialManager.Interop.Posix.Native\r\n{\r\n    public static class Stdlib\r\n    {\r\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\r\n        public static extern unsafe byte* realpath(byte* path, byte* resolved_path);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Termios.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Interop.Posix.Native\n{\n    [Flags]\n    public enum SetActionFlags\n    {\n        /// <summary>\n        /// Make change immediately.\n        /// </summary>\n        TCSANOW = 0,\n\n        /// <summary>\n        /// Drain output, then change.\n        /// </summary>\n        TCSADRAIN = 1,\n\n        /// <summary>\n        /// Drain output, flush input.\n        /// </summary>\n        TCSAFLUSH = 2,\n    }\n\n    [Flags]\n    public enum InputFlags : uint\n    {\n        IGNBRK  = 0x00000001, // ignore BREAK condition\n        BRKINT  = 0x00000002, // map BREAK to SIGINTR\n        IGNPAR  = 0x00000004, // ignore (discard) parity errors\n        PARMRK  = 0x00000008, // mark parity and framing errors\n        INPCK   = 0x00000010, // enable checking of parity errors\n        ISTRIP  = 0x00000020, // strip 8th bit off chars\n        INLCR   = 0x00000040, // map NL into CR\n        IGNCR   = 0x00000080, // ignore CR\n        ICRNL   = 0x00000100, // map CR to NL (ala CRMOD)\n        IXON    = 0x00000200, // enable output flow control\n        IXOFF   = 0x00000400, // enable input flow control\n        IXANY   = 0x00000800, // any char will restart after stop\n        IMAXBEL = 0x00002000, // ring bell on input queue full\n        IUTF8   = 0x00004000, // maintain state for UTF-8 VERASE\n    }\n\n    [Flags]\n    public enum OutputFlags : uint\n    {\n        OPOST  = 0x00000001, // enable following output processing\n        ONLCR  = 0x00000002, // map NL to CR-NL (ala CRMOD)\n        OXTABS = 0x00000004, // expand tabs to spaces\n        ONOEOT = 0x00000008, // discard EOT's (^D) on output)\n        OCRNL  = 0x00000010, // map CR to NL on output\n        ONOCR  = 0x00000020, // no CR output at column 0\n        ONLRET = 0x00000040, // NL performs CR function\n        OFILL  = 0x00000080, // use fill characters for delay\n        NLDLY  = 0x00000300, // \\n delay\n        TABDLY = 0x00000c04, // horizontal tab delay\n        CRDLY  = 0x00003000, // \\r delay\n        FFDLY  = 0x00004000, // form feed delay\n        BSDLY  = 0x00008000, // \\b delay\n        VTDLY  = 0x00010000, // vertical tab delay\n        OFDEL  = 0x00020000, // fill is DEL, else NUL\n    }\n\n    [Flags]\n    public enum ControlFlags : uint\n    {\n        CIGNORE    = 0x00000001, // ignore control flags\n        CSIZE      = 0x00000300, // character size mask\n        CS5        = 0x00000000, // 5 bits (pseudo)\n        CS6        = 0x00000100, // 6 bits\n        CS7        = 0x00000200, // 7 bits\n        CS8        = 0x00000300, // 8 bits\n        CSTOPB     = 0x00000400, // send 2 stop bits\n        CREAD      = 0x00000800, // enable receiver\n        PARENB     = 0x00001000, // parity enable\n        PARODD     = 0x00002000, // odd parity, else even\n        HUPCL      = 0x00004000, // hang up on last close\n        CLOCAL     = 0x00008000, // ignore modem status lines\n        CCTS_OFLOW = 0x00010000, // CTS flow control of output\n        CRTSCTS    = (CCTS_OFLOW | CRTS_IFLOW),\n        CRTS_IFLOW = 0x00020000, // RTS flow control of input\n        CDTR_IFLOW = 0x00040000, // DTR flow control of input\n        CDSR_OFLOW = 0x00080000, // DSR flow control of output\n        CCAR_OFLOW = 0x00100000, // DCD flow control of output\n        MDMBUF     = 0x00100000, // old name for CCAR_OFLOW\n    }\n\n    [Flags]\n    public enum LocalFlags : uint\n    {\n        ECHOKE     = 0x00000001, // visual erase for line kill\n        ECHOE      = 0x00000002, // visually erase chars\n        ECHOK      = 0x00000004, // echo NL after line kill\n        ECHO       = 0x00000008, // enable echoing\n        ECHONL     = 0x00000010, // echo NL even if ECHO is off\n        ECHOPRT    = 0x00000020, // visual erase mode for hardcopy\n        ECHOCTL    = 0x00000040, // echo control chars as ^(Char)\n        ISIG       = 0x00000080, // enable signals INTR, QUIT, [D]SUSP\n        ICANON     = 0x00000100, // canonicalize input lines\n        ALTWERASE  = 0x00000200, // use alternate WERASE algorithm\n        IEXTEN     = 0x00000400, // enable DISCARD and LNEXT\n        EXTPROC    = 0x00000800, // external processing\n        TOSTOP     = 0x00400000, // stop background jobs from output\n        FLUSHO     = 0x00800000, // output being flushed (state)\n        NOKERNINFO = 0x02000000, // no kernel output from VSTATUS\n        PENDIN     = 0x20000000, // XXX retype pending input (state)\n        NOFLSH     = 0x80000000, // don't flush after interrupt\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/Native/Unistd.cs",
    "content": "using System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Posix.Native\n{\n    public static class Unistd\n    {\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int read(int fd, byte[] buf, int count);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int write(int fd, byte[] buf, int size);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int close(int fd);\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int getpid();\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int getppid();\n\n        [DllImport(\"libc\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\n        public static extern int geteuid();\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/PosixEnvironment.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace GitCredentialManager.Interop.Posix\n{\n    public class PosixEnvironment : EnvironmentBase\n    {\n        public PosixEnvironment(IFileSystem fileSystem)\n            : base(fileSystem) { }\n\n        internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary<string, string> variables)\n            : base(fileSystem, variables) { }\n\n        #region EnvironmentBase\n\n        public override void AddDirectoryToPath(string directoryPath, EnvironmentVariableTarget target)\n        {\n            throw new NotImplementedException();\n        }\n\n        public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target)\n        {\n            throw new NotImplementedException();\n        }\n\n        protected override string[] SplitPathVariable(string value)\n        {\n            return value.Split(':');\n        }\n\n        #endregion\n\n        protected override IReadOnlyDictionary<string, string> GetCurrentVariables()\n        {\n            var dict = new Dictionary<string, string>();\n            var variables = Environment.GetEnvironmentVariables();\n\n            foreach (var key in variables.Keys)\n            {\n                if (key is string name && variables[key] is string value)\n                {\n                    dict[name] = value;\n                }\n            }\n\n            return dict;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/PosixFileDescriptor.cs",
    "content": "using System;\nusing System.Text;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager.Interop.Posix\n{\n    /// <summary>\n    /// Represents a thin wrapper over a POSIX file descriptor.\n    /// </summary>\n    public class PosixFileDescriptor : DisposableObject\n    {\n        private readonly int _fd;\n\n        private PosixFileDescriptor()\n        {\n            PlatformUtils.EnsurePosix();\n        }\n\n        public PosixFileDescriptor(string filename, OpenFlags mode) : this()\n        {\n            _fd = Fcntl.open(filename, mode);\n        }\n\n        /// <summary>\n        /// True if the file descriptor is invalid (-1), false otherwise.\n        /// </summary>\n        public bool IsInvalid => _fd == -1;\n\n        public static implicit operator int(PosixFileDescriptor fd)\n        {\n            return fd._fd;\n        }\n\n        /// <summary>\n        /// Read <paramref name=\"count\"/> number of bytes into the buffer <paramref name=\"buf\"/> from the file.\n        /// </summary>\n        /// <param name=\"buf\">Buffer into which to read bytes will be placed.</param>\n        /// <param name=\"count\">Maximum number of bytes to read.</param>\n        /// <returns>Number of bytes actually read. A value of -1 indicates failure.</returns>\n        public int Read(byte[] buf, int count)\n        {\n            ThrowIfDisposed();\n            ThrowIfInvalid();\n            return Unistd.read(_fd, buf, count);\n        }\n\n        /// <summary>\n        /// Write <paramref name=\"size\"/> number of bytes from the buffer <paramref name=\"buf\"/> to the file.\n        /// </summary>\n        /// <param name=\"buf\">Buffer into which to read bytes will be placed.</param>\n        /// <param name=\"size\">Number of bytes from buffer <paramref name=\"buf\"/> to write.</param>\n        /// <returns>Number of bytes actually written. A value of -1 indicates failure.</returns>\n        public int Write(byte[] buf, int size)\n        {\n            ThrowIfDisposed();\n            ThrowIfInvalid();\n            return Unistd.write(_fd, buf, size);\n        }\n\n        /// <summary>\n        /// Write <paramref name=\"str\"/> as a UTF8 encoded string to the file.\n        /// </summary>\n        /// <param name=\"str\">String value to write to the file.</param>\n        /// <returns>Number of UTF8 bytes written. A value of -1 indicates failure.</returns>\n        public int Write(string str)\n        {\n            byte[] buf = Encoding.UTF8.GetBytes(str);\n            return Write(buf, buf.Length);\n        }\n\n        protected override void ReleaseUnmanagedResources()\n        {\n            if (!IsInvalid)\n            {\n                Unistd.close(_fd);\n            }\n\n            base.ReleaseUnmanagedResources();\n        }\n\n        private void ThrowIfInvalid()\n        {\n            if (IsInvalid)\n            {\n                throw new InvalidOperationException(\"File descriptor is invalid\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/PosixFileSystem.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace GitCredentialManager.Interop.Posix\n{\n    public abstract class PosixFileSystem : FileSystem\n    {\n        /// <summary>\n        /// Recursively resolve a symbolic link.\n        /// </summary>\n        /// <param name=\"path\">Path to resolve.</param>\n        /// <returns>Resolved symlink, or original path if not a link.</returns>\n        /// <exception cref=\"ArgumentException\">Path is not absolute.</exception>\n        protected internal static string ResolveSymbolicLinks(string path)\n        {\n#if NETFRAMEWORK\n            // Support for symlinks only exists in .NET 6+.\n            // Since we're still targeting .NET Framework on Windows it\n            // doesn't matter if we don't resolve symlinks for POSIX here\n            // (unless we're running on Mono.. but why do that?)\n            return path;\n#else\n            if (!Path.IsPathRooted(path))\n            {\n                throw new ArgumentException(\"Path must be absolute\", nameof(path));\n            }\n\n            // If the file or directory doesn't actually exist we cannot resolve\n            // any symlinks, so just return the original input path.\n            if (!File.Exists(path) && !Directory.Exists(path))\n            {\n                return path;\n            }\n\n            // If the file is a symlink then resolve it!\n            string realPath = TryResolveFileLink(path, out string resolvedFile)\n                ? resolvedFile\n                : path;\n\n            // Work backwards from the file name resolving directories symlinks\n            string partialPath = Path.GetFileName(realPath);\n            string dirPath = Path.GetDirectoryName(realPath);\n            while (dirPath != null)\n            {\n                // Try to resolve directory symlinks\n                if (TryResolveDirectoryLink(dirPath, out string resolvedDir))\n                {\n                    dirPath = resolvedDir;\n                }\n\n                string dirName = Path.GetFileName(dirPath);\n                partialPath = Path.Combine(dirName, partialPath);\n                dirPath = Path.GetDirectoryName(dirPath);\n            }\n\n            return Path.Combine(\"/\", partialPath);\n#endif\n        }\n\n#if !NETFRAMEWORK\n        private static bool TryResolveFileLink(string path, out string target)\n        {\n            FileSystemInfo fsi = File.ResolveLinkTarget(path, true);\n            target = fsi?.FullName;\n            return fsi != null;\n        }\n\n        private static bool TryResolveDirectoryLink(string path, out string target)\n        {\n            FileSystemInfo fsi = Directory.ResolveLinkTarget(path, true);\n            target = fsi?.FullName;\n            return fsi != null;\n        }\n#endif\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/PosixSessionManager.cs",
    "content": "namespace GitCredentialManager.Interop.Posix\n{\n    public abstract class PosixSessionManager : SessionManager\n    {\n        protected PosixSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs)\n        {\n            PlatformUtils.EnsurePosix();\n        }\n\n        // Check if we have an X11 or Wayland display environment available\n        public override bool IsDesktopSession =>\n            !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(\"DISPLAY\")) ||\n            !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(\"WAYLAND_DISPLAY\"));\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Posix/PosixTerminal.cs",
    "content": "using System;\nusing System.Text;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager.Interop.Posix\n{\n    /// <summary>\n    /// Represents a thin wrapper around the POSIX TTY device (/dev/tty).\n    /// </summary>\n    public abstract class PosixTerminal : ITerminal\n    {\n        private const string TtyDeviceName = \"/dev/tty\";\n        private const byte DeleteChar = 127;\n\n        protected readonly ITrace Trace;\n        protected readonly ITrace2 Trace2;\n\n        public PosixTerminal(ITrace trace, ITrace2 trace2)\n        {\n            PlatformUtils.EnsurePosix();\n            EnsureArgument.NotNull(trace, nameof(trace));\n\n            Trace = trace;\n        }\n\n        public void WriteLine(string format, params object[] args)\n        {\n            using (var fd = new PosixFileDescriptor(TtyDeviceName, OpenFlags.O_RDWR))\n            {\n                if (fd.IsInvalid)\n                {\n                    Trace.WriteLine(\"Not a TTY, abandoning write line.\");\n                    return;\n                }\n\n                fd.Write(string.Format(format, args));\n                fd.Write(\"\\n\");\n            }\n        }\n\n        public string Prompt(string prompt)\n        {\n            return Prompt(prompt, echo: true);\n        }\n\n        public string PromptSecret(string prompt)\n        {\n            return Prompt(prompt, echo: false);\n        }\n\n        protected abstract IDisposable CreateTtyContext(int fd, bool echo);\n\n        private string Prompt(string prompt, bool echo)\n        {\n            using (var fd = new PosixFileDescriptor(TtyDeviceName, OpenFlags.O_RDWR))\n            {\n                if (fd.IsInvalid)\n                {\n                    Trace.WriteLine(\"Not a TTY, abandoning prompt.\");\n                    return null;\n                }\n\n                fd.Write($\"{prompt}: \");\n\n                var sb = new StringBuilder();\n\n                using (CreateTtyContext(fd, echo))\n                {\n                    var readBuf = new byte[1];\n                    bool eol = false;\n                    while (!eol)\n                    {\n                        int nr;\n                        // Read one byte at a time\n                        if ((nr = fd.Read(readBuf, 1)) != 1)\n                        {\n                            // Either we reached end of file or an error occured.\n                            // We don't care which so let's just trace and terminate further reading.\n                            Trace.WriteLine($\"Exiting POSIX terminal prompt read-loop unexpectedly (nr={nr})\");\n                            eol = true;\n                            break;\n                        }\n\n                        int c = readBuf[0];\n                        switch (c)\n                        {\n                            case 3: // CTRL + C\n                                // Since `read` is a blocking call we must manually raise the SIGINT signal\n                                // when the user types CTRL+C into the terminal window.\n                                int pid = Unistd.getpid();\n                                Trace.WriteLine($\"Intercepted SIGINT during terminal prompt read-loop - sending SIGINT to self (pid={pid})\");\n                                Signal.kill(pid, Signal.SIGINT);\n                                break;\n\n                            case '\\n':\n                                eol = true;\n                                // Only need to echo the newline to move the terminal cursor down when\n                                // echo is disabled. When echo is enabled the newline is written for us.\n                                if (!echo)\n                                {\n                                    fd.Write(\"\\n\");\n                                }\n                                break;\n\n                            case '\\b':\n                            case DeleteChar:\n                                if (sb.Length > 0)\n                                {\n                                    sb.Remove(sb.Length - 1, 1);\n                                    fd.Write(\"\\b \\b\");\n                                }\n                                break;\n\n                            default:\n                                sb.Append((char) c);\n                                break;\n                        }\n                    }\n                    return sb.ToString();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/U8StringConverter.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing System.Text;\n\nnamespace GitCredentialManager.Interop\n{\n    /// <summary>\n    /// Conversion utilities to convert between .NET strings (UTF-16) and byte arrays (UTF-8).\n    /// </summary>\n    public static class U8StringConverter\n    {\n        private const byte NULL = (byte) '\\0';\n\n        // Throw only on invalid bytes when converting from managed to native.\n        // We shouldn't have invalid managed strings so this would be an error condition.\n        // We continue to accept potentially malformed native strings however, because they may come from Git which\n        // doesn't technically care about Unicode encoding format compliance.\n        private static readonly Encoding NativeEncoding = new UTF8Encoding(false, throwOnInvalidBytes: true);\n        private static readonly Encoding ManagedEncoding = new UTF8Encoding(false, throwOnInvalidBytes: false);\n\n        public static unsafe IntPtr ToNative(string str)\n        {\n            if (str == null)\n            {\n                return IntPtr.Zero;\n            }\n\n            int length = NativeEncoding.GetByteCount(str);\n\n            // +1 for the null terminator byte\n            var buffer = (byte*)Marshal.AllocHGlobal(length + 1).ToPointer();\n\n            if (length > 0)\n            {\n                fixed (char* pValue = str)\n                {\n                    NativeEncoding.GetBytes(pValue, str.Length, buffer, length);\n                }\n            }\n            buffer[length] = NULL;\n\n            return new IntPtr(buffer);\n        }\n\n        public static unsafe string ToManaged(byte* buf)\n        {\n            byte* end = buf;\n\n            if (buf == null)\n            {\n                return null;\n            }\n\n            if (*buf == NULL)\n            {\n                return string.Empty;\n            }\n\n            while (*end != NULL)\n            {\n                end++;\n            }\n\n            return new string((sbyte*)buf, 0, (int)(end - buf), ManagedEncoding);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/U8StringMarshaler.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop\n{\n    /// <summary>\n    /// Marshaler for converting between .NET strings (UTF-16) and byte arrays (UTF-8).\n    /// Uses <seealso cref=\"U8StringConverter\"/> internally.\n    /// </summary>\n    public class U8StringMarshaler : ICustomMarshaler\n    {\n        // We need to clean up strings that we marshal to native, but should not clean up strings that\n        // we marshal to managed.\n        private static readonly U8StringMarshaler NativeInstance = new U8StringMarshaler(true);\n        private static readonly U8StringMarshaler ManagedInstance = new U8StringMarshaler(false);\n\n        private readonly bool _cleanup;\n\n        public const string NativeCookie  = \"U8StringMarshaler.Native\";\n        public const string ManagedCookie = \"U8StringMarshaler.Managed\";\n\n        public static ICustomMarshaler GetInstance(string cookie)\n        {\n            switch (cookie)\n            {\n                case NativeCookie:\n                    return NativeInstance;\n                case ManagedCookie:\n                    return ManagedInstance;\n                default:\n                    throw new ArgumentException(\"Invalid marshaler cookie\");\n            }\n        }\n\n        private U8StringMarshaler(bool cleanup)\n        {\n            _cleanup = cleanup;\n        }\n\n        public int GetNativeDataSize()\n        {\n            return -1;\n        }\n\n        public IntPtr MarshalManagedToNative(object value)\n        {\n            switch (value)\n            {\n                case null:\n                    return IntPtr.Zero;\n                case string str:\n                    return U8StringConverter.ToNative(str);\n                default:\n                    throw new MarshalDirectiveException(\"Cannot marshal a non-string\");\n            }\n        }\n\n        public unsafe object MarshalNativeToManaged(IntPtr ptr)\n        {\n            return U8StringConverter.ToManaged((byte*) ptr);\n        }\n\n        public void CleanUpManagedData(object value)\n        {\n        }\n\n        public virtual void CleanUpNativeData(IntPtr ptr)\n        {\n            if (ptr != IntPtr.Zero && _cleanup)\n            {\n                Marshal.FreeHGlobal(ptr);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/DpapiCredentialStore.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Security.Cryptography;\nusing System.Text;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class DpapiCredentialStore : PlaintextCredentialStore\n    {\n        public DpapiCredentialStore(IFileSystem fileSystem, string storeRoot, string @namespace = null)\n            : base(fileSystem, storeRoot, @namespace)\n        {\n            PlatformUtils.EnsureWindows();\n        }\n\n        protected override bool TryDeserializeCredential(string path, out FileCredential credential)\n        {\n            string text;\n            using (var stream = FileSystem.OpenFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))\n            using (var reader = new StreamReader(stream))\n            {\n                text = reader.ReadToEnd();\n            }\n\n            int line1Idx = text.IndexOf(Environment.NewLine, StringComparison.OrdinalIgnoreCase);\n            if (line1Idx > 0)\n            {\n                // The first line is a base64 encoded set of bytes that need to be decrypted by DPAPI\n                string cryptoBase64 = text.Substring(0, line1Idx);\n                byte[] cryptoBytes = Convert.FromBase64String(cryptoBase64);\n                byte[] plainBytes = ProtectedData.Unprotect(\n                    cryptoBytes, null, DataProtectionScope.CurrentUser);\n                string password = Encoding.UTF8.GetString(plainBytes);\n\n                // All subsequent lines are metadata/attributes\n                string attrText = text.Substring(line1Idx + Environment.NewLine.Length);\n                using var attrReader = new StringReader(attrText);\n                IDictionary<string, string> attrs = attrReader.ReadDictionary(StringComparer.OrdinalIgnoreCase);\n\n                // Account is optional\n                attrs.TryGetValue(\"account\", out string account);\n\n                // Service is required\n                if (attrs.TryGetValue(\"service\", out string service))\n                {\n                    credential = new FileCredential(path, service, account, password);\n                    return true;\n                }\n            }\n\n            credential = null;\n            return false;\n        }\n\n        protected override void SerializeCredential(FileCredential credential)\n        {\n            // Ensure the parent directory exists\n            string parentDir = Path.GetDirectoryName(credential.FullPath);\n            if (!FileSystem.DirectoryExists(parentDir))\n            {\n                FileSystem.CreateDirectory(parentDir);\n            }\n\n            // Use DPAPI to encrypt the password value, and then store the base64 encoding of the resulting bytes\n            byte[] plainBytes = Encoding.UTF8.GetBytes(credential.Password);\n            byte[] cryptoBytes = ProtectedData.Protect(\n                plainBytes, null, DataProtectionScope.CurrentUser);\n            string cryptoBase64 = Convert.ToBase64String(cryptoBytes);\n\n            using (var stream = FileSystem.OpenFileStream(credential.FullPath, FileMode.Create, FileAccess.Write, FileShare.None))\n            using (var writer = new StreamWriter(stream))\n            {\n                writer.WriteLine(cryptoBase64);\n                writer.WriteLine(\"service={0}\", credential.Service);\n                writer.WriteLine(\"account={0}\", credential.Account);\n                writer.Flush();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/Advapi32.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;\n\nnamespace GitCredentialManager.Interop.Windows.Native\n{\n    // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/\n    public static class Advapi32\n    {\n        private const string LibraryName = \"advapi32.dll\";\n\n        [DllImport(LibraryName, EntryPoint = \"CredReadW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern bool CredRead(\n            string target,\n            CredentialType type,\n            int reserved,\n            out IntPtr credential);\n\n        [DllImport(LibraryName, EntryPoint = \"CredWriteW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern bool CredWrite(\n            ref Win32Credential credential,\n            int flags);\n\n        [DllImport(LibraryName, EntryPoint = \"CredDeleteW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern bool CredDelete(\n            string target,\n            CredentialType type,\n            int flags);\n\n        [DllImport(LibraryName, EntryPoint = \"CredFree\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern void CredFree(\n            IntPtr credential);\n\n        [DllImport(LibraryName, EntryPoint = \"CredEnumerateW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern bool CredEnumerate(\n            string filter,\n            CredentialEnumerateFlags flags,\n            out int count,\n            out IntPtr credentialsList);\n\n        [DllImport(LibraryName, EntryPoint = \"CredGetSessionTypes\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern bool CredGetSessionTypes(\n            uint maximumPersistCount,\n            [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] CredentialPersist[] maximumPersist);\n\n        // Values from wincred.h\n        public const uint CRED_TYPE_MAXIMUM = 7;\n        public const uint CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000;\n    }\n\n    // Enum values from wincred.h\n    public enum CredentialType\n    {\n        Generic = 1,\n        DomainPassword = 2,\n        DomainCertificate = 3,\n        DomainVisiblePassword = 4,\n        GenericCertificate = 5,\n        DomainExtended = 6\n    }\n\n    public enum CredentialPersist\n    {\n        None = 0,\n        Session = 1,\n        LocalMachine = 2,\n        Enterprise = 3,\n    }\n\n    [Flags]\n    public enum CredentialEnumerateFlags\n    {\n        None = 0,\n        AllCredentials = 0x1\n    }\n\n    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\n    public struct Win32Credential\n    {\n        public int Flags;\n        public CredentialType Type;\n        [MarshalAs(UnmanagedType.LPWStr)]\n        public string TargetName;\n        [MarshalAs(UnmanagedType.LPWStr)]\n        public string Comment;\n        public FILETIME LastWritten;\n        public int CredentialBlobSize;\n        public IntPtr CredentialBlob;\n        public CredentialPersist Persist;\n        public int AttributeCount;\n        public IntPtr Attributes;\n        [MarshalAs(UnmanagedType.LPWStr)]\n        public string TargetAlias;\n        [MarshalAs(UnmanagedType.LPWStr)]\n        public string UserName;\n\n        public string GetCredentialBlobAsString()\n        {\n            if (CredentialBlobSize != 0 && CredentialBlob != IntPtr.Zero)\n            {\n                byte[] passwordBytes = InteropUtils.ToByteArray(CredentialBlob, CredentialBlobSize);\n                return Encoding.Unicode.GetString(passwordBytes);\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/CredUi.cs",
    "content": "using System;\r\nusing System.Runtime.InteropServices;\r\nusing System.Text;\r\n\r\nnamespace GitCredentialManager.Interop.Windows.Native\r\n{\r\n    // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/\r\n    public static class CredUi\r\n    {\r\n        private const string LibraryName = \"credui.dll\";\r\n\r\n        [Flags]\r\n        public enum CredentialPackFlags : uint\r\n        {\r\n            None = 0,\r\n            ProtectedCredentials = 0x1,\r\n            WowBuffer = 0x2,\r\n            GenericCredentials = 0x4,\r\n        }\r\n\r\n        [Flags]\r\n        public enum CredentialUiFlags : uint\r\n        {\r\n            None = 0,\r\n            IncorrectPassword = 0x1,\r\n            DoNoPersist = 0x2,\r\n            RequestAdministrator = 0x4,\r\n            ExcludeCertificates = 0x8,\r\n            RequireCertificates = 0x10,\r\n            ShowSaveCheckbox = 0x40,\r\n            AlwaysShowUi = 0x80,\r\n            RequireSmartCard = 0x100,\r\n            PasswordOnlyOk = 0x200,\r\n            ValidateUsername = 0x400,\r\n            CompleteUsername = 0x800,\r\n            Persist = 0x1000,\r\n            ServerCredential = 0x4000,\r\n            ExpectConfirmation = 0x20000,\r\n            GenericCredentials = 0x40000,\r\n            UsernameTargetCredentials = 0x80000,\r\n            KeepUsername = 0x100000,\r\n        }\r\n\r\n        public enum CredentialUiResult : uint\r\n        {\r\n            Success = 0,\r\n            Cancelled = 1223,\r\n            NoSuchLogonSession = 1312,\r\n            NotFound = 1168,\r\n            InvalidAccountName = 1315,\r\n            InsufficientBuffer = 122,\r\n            InvalidParameter = 87,\r\n            InvalidFlags = 1004,\r\n        }\r\n\r\n        [Flags]\r\n        public enum CredentialUiWindowsFlags : uint\r\n        {\r\n            None = 0,\r\n\r\n            /// <summary>\r\n            /// The caller is requesting that the credential provider return the user name and password in plain text.\r\n            /// </summary>\r\n            Generic = 0x0001,\r\n\r\n            /// <summary>\r\n            /// The Save check box is displayed in the dialog box.\r\n            /// </summary>\r\n            Checkbox = 0x0002,\r\n\r\n            /// <summary>\r\n            /// Only credential providers that support the authentication package specified by the `authPackage` parameter\r\n            /// should be enumerated.\r\n            /// </summary>\r\n            AuthPackageOnly = 0x0010,\r\n\r\n            /// <summary>\r\n            /// Only the credentials specified by the `inAuthBuffer` parameter for the authentication package specified\r\n            /// by the `authPackage` parameter should be enumerated.\r\n            /// <para/>\r\n            /// If this flag is set, and the `inAuthBuffer` parameter is `null`, the function fails.\r\n            /// </summary>\r\n            InCredOnly = 0x0020,\r\n\r\n            /// <summary>\r\n            /// Credential providers should enumerate only administrators.\r\n            /// <para/>\r\n            /// This value is intended for User Account Control (UAC) purposes only.\r\n            /// <para/>\r\n            /// We recommend that external callers not set this flag.\r\n            /// </summary>\r\n            EnumerateAdmins = 0x0100,\r\n\r\n            /// <summary>\r\n            /// Only the incoming credentials for the authentication package specified by the `authPackage` parameter\r\n            /// should be enumerated.\r\n            /// </summary>\r\n            EnumerateCurrentUser = 0x0200,\r\n\r\n            /// <summary>\r\n            /// The credential dialog box should be displayed on the secure desktop.\r\n            /// <para/>\r\n            /// This value cannot be combined with <see cref=\"Generic\"/>.\r\n            /// </summary>\r\n            SecurePrompt = 0x1000,\r\n\r\n            /// <summary>\r\n            /// The credential dialog box is invoked by the SspiPromptForCredentials function, and the client is prompted\r\n            /// before a prior handshake.\r\n            /// <para/>\r\n            /// If SSPIPFC_NO_CHECKBOX is passed in the `inAuthBuffer` parameter, then the credential provider should\r\n            /// not display the check box.\r\n            /// </summary>\r\n            Preprompting = 0x2000,\r\n\r\n            /// <summary>\r\n            /// The credential provider should align the credential BLOB pointed to by the `outAuthBuffer` parameter to\r\n            /// a 32-bit boundary, even if the provider is running on a 64-bit system.\r\n            /// </summary>\r\n            Pack32Wow = 0x10000000,\r\n        }\r\n\r\n        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\r\n        public struct CredentialUiInfo\r\n        {\r\n            [MarshalAs(UnmanagedType.U4)]\r\n            public int Size;\r\n\r\n            [MarshalAs(UnmanagedType.SysInt)]\r\n            public IntPtr Parent;\r\n\r\n            [MarshalAs(UnmanagedType.LPWStr)]\r\n            public string MessageText;\r\n\r\n            [MarshalAs(UnmanagedType.LPWStr)]\r\n            public string CaptionText;\r\n\r\n            [MarshalAs(UnmanagedType.SysInt)]\r\n            public IntPtr BannerArt;\r\n        }\r\n\r\n        [DllImport(LibraryName, EntryPoint = \"CredUIPromptForWindowsCredentialsW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\r\n        public static extern int CredUIPromptForWindowsCredentials(\r\n            ref CredentialUiInfo credInfo,\r\n            uint authError,\r\n            ref uint authPackage,\r\n            IntPtr inAuthBuffer,\r\n            uint inAuthBufferSize,\r\n            out IntPtr outAuthBuffer,\r\n            out uint outAuthBufferSize,\r\n            ref bool saveCredentials,\r\n            CredentialUiWindowsFlags flags);\r\n\r\n        [DllImport(LibraryName, EntryPoint = \"CredPackAuthenticationBufferW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\r\n        public static extern bool CredPackAuthenticationBuffer(\r\n            CredentialPackFlags flags,\r\n            string username,\r\n            string password,\r\n            IntPtr packedCredentials,\r\n            ref int packedCredentialsSize);\r\n\r\n        [DllImport(LibraryName, EntryPoint = \"CredUnPackAuthenticationBufferW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\r\n        public static extern bool CredUnPackAuthenticationBuffer(\r\n            CredentialPackFlags flags,\r\n            IntPtr authBuffer,\r\n            uint authBufferSize,\r\n            StringBuilder username,\r\n            ref int maxUsernameLen,\r\n            StringBuilder domainName,\r\n            ref int maxDomainNameLen,\r\n            StringBuilder password,\r\n            ref int maxPasswordLen);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/Kernel32.cs",
    "content": "\nusing System;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing Microsoft.Win32.SafeHandles;\n\nnamespace GitCredentialManager.Interop.Windows.Native\n{\n    public static class Kernel32\n    {\n        private const string LibraryName = \"kernel32.dll\";\n\n        /// <summary>\n        /// Creates or opens a file or I/O device.\n        /// <para/>\n        /// The most commonly used I/O devices are as follows: file, file stream, directory, physical disk, volume,\n        /// console buffer, tape drive, communications resource, mailslot, and pipe.\n        /// <para/>\n        /// The function returns a handle that can be used to access the file or device for various types of I/O\n        /// depending on the file or device and the flags and attributes specified.\n        /// <para/>\n        /// Return a handle to the file created.\n        /// </summary>\n        /// <param name=\"fileName\">\n        /// The name of the file or device to be created or opened. You may use either forward slashes (/) or\n        /// backslashes (\\) in this name.\n        /// <para/>\n        /// In the ANSI version of this function, the name is limited to `MAX_PATH` characters.\n        /// <para/>\n        /// To extend this limit to 32,767 wide characters, call the Unicode version of the function and prepend\n        /// \"\\\\?\\\" to the path.\n        /// </param>\n        /// <param name=\"desiredAccess\">\n        /// The requested access to the file or device, which can be summarized as read, write, both or neither zero).\n        /// <para/>\n        /// If this parameter is zero, the application can query certain metadata such as file, directory, or device\n        /// attributes without accessing that file or device, even if `<see cref=\"FileAccess.GenericRead\"/>` access\n        /// would have been denied.\n        /// <para/>\n        /// You cannot request an access mode that conflicts with the sharing mode that is specified by the\n        /// `<paramref name=\"shareMode\"/>` parameter in an open request that already has an open handle.\n        /// </param>\n        /// <param name=\"shareMode\">\n        /// The requested sharing mode of the file or device, which can be read, write, both, delete, all of these,\n        /// or none (refer to the following table).\n        /// <para/>\n        /// Access requests to attributes or extended attributes are not affected by this flag.\n        /// <para/>\n        /// If this parameter is zero and CreateFile succeeds, the file or device cannot be shared and cannot be opened\n        /// again until the handle to the file or device is closed.\n        /// <para/>\n        /// You cannot request a sharing mode that conflicts with the access mode that is specified in an existing\n        /// request that has an open handle.\n        /// <para/>\n        /// CreateFile would fail and the `<see cref=\"Marshal.GetLastWin32Error\"/>` function would return\n        /// `<see cref=\"Win32Error.SharingViloation\"/>`.\n        /// <para/>\n        /// To enable a process to share a file or device while another process has the file or device open, use a\n        /// compatible combination of one or more of the following values.\n        /// </param>\n        /// <param name=\"securityAttributes\">\n        /// This parameter should be `<see cref=\"IntPtr.Zero\"/>`.\n        /// </param>\n        /// <param name=\"creationDisposition\">\n        /// An action to take on a file or device that exists or does not exist.\n        /// <para/>\n        /// For devices other than files, this parameter is usually set to\n        /// `<see cref=\"FileCreationDisposition.OpenExisting\"/>`.\n        /// </param>\n        /// <param name=\"flagsAndAttributes\">\n        /// The file or device attributes and flags, `<see cref=\"FileAttributes.Normal\"/>` being the most common\n        /// default value for files.\n        /// <para/>\n        /// This parameter can include any combination of `<see cref=\"FileAttributes\"/>`.\n        /// <para/>\n        /// All other file attributes override `<see cref=\"FileAttributes.Normal\"/>`.\n        /// </param>\n        /// <param name=\"templateFile\">\n        /// This parameter should be `<see cref=\"IntPtr.Zero\"/>`.\n        /// </param>\n        [DllImport(LibraryName, EntryPoint = \"CreateFileW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern SafeFileHandle CreateFile(\n            [In, MarshalAs(UnmanagedType.LPWStr)] string fileName,\n            [In, MarshalAs(UnmanagedType.U4)] FileAccess desiredAccess,\n            [In, MarshalAs(UnmanagedType.U4)] FileShare shareMode,\n            [In, MarshalAs(UnmanagedType.SysInt)] IntPtr securityAttributes,\n            [In, MarshalAs(UnmanagedType.U4)] FileCreationDisposition creationDisposition,\n            [In, MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,\n            [In, MarshalAs(UnmanagedType.SysInt)] IntPtr templateFile);\n\n        /// <summary>\n        /// Reads character input from the console input buffer and removes it from the buffer.\n        /// <para/>\n        /// Returns `<see langword=\"true\"/>` if successful; otherwise `<see langword=\"false\"/>`.\n        /// </summary>\n        /// <param name=\"consoleInputHandle\">\n        /// A handle to the console input buffer. The handle must have the `<see cref=\"FileAccess.GenericRead\"/>`\n        /// access right.\n        /// </param>\n        /// <param name=\"buffer\">\n        /// A pointer to a buffer that receives the data read from the console input buffer.\n        /// <para/>\n        /// The storage for this buffer is allocated from a shared heap for the process that is 64 KB in size.\n        /// <para/>\n        /// The maximum size of the buffer will depend on heap usage.\n        /// </param>\n        /// <param name=\"numberOfCharsToRead\">\n        /// The number of characters to be read.\n        /// <para/>\n        /// The size of the buffer pointed to by the `<paramref name=\"buffer\"/>` parameter should be at least `<paramref name=\"NumberofCharsToRead\"/>` * `<see langword=\"sizeof\"/>(<see langword=\"char\"/>)` bytes.\n        /// </param>\n        /// <param name=\"numberOfCharsRead\">\n        /// A pointer to a variable that receives the number of characters actually read.\n        /// </param>\n        /// <param name=\"reserved\">\n        /// Reserved; must be `<see cref=\"IntPtr.Zero\"/>`.\n        /// </param>\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Usage\", \"CA2205:UseManagedEquivalentsOfWin32Api\")]\n        [DllImport(LibraryName, EntryPoint = \"ReadConsoleW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        [return: MarshalAs(UnmanagedType.Bool)]\n        public static extern bool ReadConsole(\n            [In] SafeFileHandle consoleInputHandle,\n            [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder buffer,\n            [In, MarshalAs(UnmanagedType.U4)] uint numberOfCharsToRead,\n            [Out, MarshalAs(UnmanagedType.U4)] out uint numberOfCharsRead,\n            [In, MarshalAs(UnmanagedType.SysInt)] IntPtr reserved);\n\n        /// <summary>\n        /// Writes a character string to a console screen buffer beginning at the current cursor location.\n        /// <para/>\n        /// Returns `<see langword=\"true\"/>` if successful; otherwise `<see langword=\"false\"/>`.\n        /// </summary>\n        /// <param name=\"consoleOutputHandle\">\n        /// A handle to the console screen buffer.\n        /// <para/>\n        /// The handle must have the `<see cref=\"FileAccess.GenericWrite\"/>` access right.\n        /// </param>\n        /// <param name=\"buffer\">\n        /// A pointer to a buffer that contains characters to be written to the console screen buffer.\n        /// <para/>\n        /// The storage for this buffer is allocated from a shared heap for the process that is 64 KB in size.\n        /// <para/>\n        /// The maximum size of the buffer will depend on heap usage.\n        /// </param>\n        /// <param name=\"numberOfCharsToWrite\">\n        /// The number of characters to be written.\n        /// <para/>\n        /// If the total size of the specified number of characters exceeds the available heap, the function fails\n        /// with `<see cref=\"Win32Error.NotEnoughMemory\"/>`.\n        /// </param>\n        /// <param name=\"numberOfCharsWritten\">\n        /// A pointer to a variable that receives the number of characters actually written.\n        /// </param>\n        /// <param name=\"reserved\">\n        /// Reserved; must be `<see cref=\"IntPtr.Zero\"/>`.\n        /// </param>\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Usage\", \"CA2205:UseManagedEquivalentsOfWin32Api\")]\n        [DllImport(LibraryName, EntryPoint = \"WriteConsoleW\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        [return: MarshalAs(UnmanagedType.Bool)]\n        public static extern bool WriteConsole(\n            [In] SafeHandle consoleOutputHandle,\n            [In, MarshalAs(UnmanagedType.LPWStr)] StringBuilder buffer,\n            [In, MarshalAs(UnmanagedType.U4)] uint numberOfCharsToWrite,\n            [Out, MarshalAs(UnmanagedType.U4)] out uint numberOfCharsWritten,\n            [In, MarshalAs(UnmanagedType.SysInt)] IntPtr reserved);\n\n        /// <summary>\n        /// Retrieves the current input mode of a console's input buffer or the current output mode of a console screen\n        /// buffer.\n        /// <para/>\n        /// Returns `<see langword=\"true\"/>` if successful; otherwise `<see langword=\"false\"/>`.\n        /// </summary>\n        /// <param name=\"consoleHandle\">\n        /// A handle to the console input buffer or the console screen buffer. The handle must have the\n        /// <see cref=\"FileAccess.GenericRead\"/> access right.\n        /// </param>\n        /// <param name=\"consoleMode\">\n        /// A pointer to a variable that receives the current mode of the specified buffer.\n        /// <para/>\n        /// If the `<paramref name=\"consoleHandle\"/>` parameter is an input handle, the mode can be one or more of the\n        /// following values.\n        /// <para/>\n        /// When a console is created, all input modes except `<see cref=\"ConsoleMode.WindowInput\"/>` are enabled by\n        /// default.\n        /// <para/>\n        /// If the `<paramref name=\"consoleHandle\"/>` parameter is a screen buffer handle, the mode can be one or more\n        /// of the following values.\n        /// <para/>\n        /// When a screen buffer is created, both output modes are enabled by default.\n        /// </param>\n        [DllImport(LibraryName, EntryPoint = \"GetConsoleMode\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        [return: MarshalAs(UnmanagedType.Bool)]\n        public static extern bool GetConsoleMode(\n            [In] SafeFileHandle consoleHandle,\n            [Out, MarshalAs(UnmanagedType.U4)] out ConsoleMode consoleMode);\n\n        /// <summary>\n        /// Sets the input mode of a console's input buffer or the output mode of a console screen buffer.\n        /// <para/>\n        /// Returns `<see langword=\"true\"/>` if successful; otherwise `<see langword=\"false\"/>`.\n        /// </summary>\n        /// <param name=\"consoleHandle\">\n        /// A handle to the console input buffer or a console screen buffer.\n        /// <para/>\n        /// The handle must have the `<see cref=\"FileAccess.GenericRead\"/>` access right.\n        /// </param>\n        /// <param name=\"consoleMode\">\n        /// <para>\n        /// The input or output mode to be set.\n        /// <para/>\n        /// If the `<paramref name=\"consoleHandle\"/>` parameter is an input handle, the mode can be one or more of the\n        /// following values.\n        /// <para/>\n        /// When a console is created, all input modes except `<see cref=\"ConsoleMode.WindowInput\"/>` are enabled by\n        /// default.\n        /// </para>\n        /// <para>\n        /// If the `<paramref name=\"consoleHandle\"/>` parameter is a screen buffer handle, the mode can be one or more\n        /// of the following values.\n        /// <para/>\n        /// When a screen buffer is created, both output modes are enabled by default.\n        /// </para>\n        /// </param>\n        [DllImport(LibraryName, EntryPoint = \"SetConsoleMode\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        [return: MarshalAs(UnmanagedType.Bool)]\n        public static extern bool SetConsoleMode(\n            [In] SafeFileHandle consoleHandle,\n            [In, MarshalAs(UnmanagedType.U4)] ConsoleMode consoleMode);\n\n        /// <summary>\n        /// Retrieves the command-line string for the current process.\n        /// </summary>\n        /// <returns>The return value is the command-line string for the current process.</returns>\n        [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern IntPtr GetCommandLine();\n\n        /// <summary>\n        /// Frees the specified local memory object and invalidates its handle.\n        /// </summary>\n        /// <param name=\"ptr\">\n        /// A handle to the local memory object.\n        /// This handle is returned by either the LocalAlloc or LocalReAlloc function.\n        /// It is not safe to free memory allocated with GlobalAlloc.\n        /// </param>\n        /// <returns>\n        /// If the function succeeds, the return value is NULL.\n        /// <para/>\n        /// If the function fails, the return value is equal to a handle to the local memory object.\n        /// <para/>\n        /// To get extended error information, call GetLastError.\n        /// </returns>\n        [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern IntPtr LocalFree(IntPtr ptr);\n\n        /// <summary>\n        /// Retrieves the window handle used by the console associated with the calling process.\n        /// </summary>\n        /// <returns>\n        /// The return value is a handle to the window used by the console associated with the calling process or\n        /// NULL if there is no such associated console.\n        /// </returns>\n        [DllImport(\"kernel32.dll\", SetLastError = true)]\n        public static extern IntPtr GetConsoleWindow();\n    }\n\n    [Flags]\n    public enum FileAccess : uint\n    {\n        GenericRead = 0x80000000,\n        GenericWrite = 0x40000000,\n        GenericExecute = 0x20000000,\n        GenericAll = 0x10000000,\n    }\n\n    [Flags]\n    public enum FileAttributes : uint\n    {\n        /// <summary>\n        /// The file is read only.\n        /// <para/>\n        /// Applications can read the file, but cannot write to or delete it.\n        /// </summary>\n        Readonly = 0x00000001,\n\n        /// <summary>\n        /// The file is hidden. Do not include it in an ordinary directory listing.\n        /// </summary>\n        Hidden = 0x00000002,\n\n        /// <summary>\n        /// The file is part of or used exclusively by an operating system.\n        /// </summary>\n        System = 0x00000004,\n\n        Directory = 0x00000010,\n\n        /// <summary>\n        /// The file should be archived.\n        /// <para/>\n        /// Applications use this attribute to mark files for backup\n        /// or removal.\n        /// </summary>\n        Archive = 0x00000020,\n\n        Device = 0x00000040,\n\n        /// <summary>\n        /// The file does not have other attributes set.\n        /// <para/>\n        /// This attribute is valid only if used alone.\n        /// </summary>\n        Normal = 0x00000080,\n\n        /// <summary>\n        /// The file is being used for temporary storage.\n        /// </summary>\n        Temporary = 0x00000100,\n\n        SparseFile = 0x00000200,\n\n        ReparsePoint = 0x00000400,\n\n        Compressed = 0x00000800,\n\n        /// <summary>\n        /// The data of a file is not immediately available. This attribute indicates that file data is physically\n        /// moved to offline storage.\n        /// <para/>\n        /// This attribute is used by Remote Storage, the hierarchical storage management software.\n        /// <para/>\n        /// Applications should not arbitrarily change this attribute.\n        /// </summary>\n        Offline = 0x00001000,\n\n        NotContentIndexed = 0x00002000,\n\n        /// <summary>\n        /// The file or directory is encrypted.\n        /// <para/>For a file, this means that all data in the file is encrypted.\n        /// <para/>\n        /// For a directory, this means that encryption is the default for newly created files and subdirectories.\n        /// <para/>\n        /// This flag has no effect if <see cref=\"Archive\"/> is also specified.\n        /// <para>\n        /// This flag is not supported on Home, Home Premium, Starter, or ARM editions of Windows.\n        /// </summary>\n        Encrypted = 0x00004000,\n\n        FirstPipeInstance = 0x00080000,\n\n        /// <summary>\n        /// The file data is requested, but it should continue to be located in remote storage.\n        /// <para/>\n        /// It should not be transported back to local storage.\n        /// <para/>\n        /// This flag is for use by remote storage systems.\n        /// </summary>\n        OpenNoRecall = 0x00100000,\n\n        /// <summary>\n        /// Normal reparse point processing will not occur;\n        /// `<see cref=\"CreateFile(string, FileAccess, FileShare, IntPtr, FileCreationDisposition, FileAttributes, IntPtr)\"/>`\n        /// will attempt to open the reparse point. When a file is opened, a file handle is returned, whether or not\n        /// the filter that controls the reparse point is operational.\n        /// <para/>\n        /// This flag cannot be used with the `<see cref=\"FileCreationDisposition.CreateAlways\"/>` flag.\n        /// <para/>\n        /// If the file is not a reparse point, then this flag is ignored.\n        /// </summary>\n        OpenReparsePoint = 0x00200000,\n\n        /// <summary>\n        /// The file or device is being opened with session awareness.\n        /// <para/>\n        /// If this flag is not specified, then per-session devices (such as a redirected USB device) cannot be opened\n        /// by processes running in session 0.\n        /// <para/>\n        /// This flag has no effect for callers not in session 0.\n        /// <para/>\n        /// This flag is supported only on server editions of Windows.\n        /// </summary>\n        SessionAware = 0x00800000,\n\n        /// <summary>\n        /// Access will occur according to POSIX rules.\n        /// <para/>\n        /// This includes allowing multiple files with names, differing only in case, for file systems that support\n        /// that naming.\n        /// <para/>\n        /// Use care when using this option, because files created with this flag may not be accessible by applications\n        /// that are written for MS-DOS or 16-bit Windows.\n        /// </summary>\n        PosixSemantics = 0x01000000,\n\n        /// <summary>\n        /// The file is being opened or created for a backup or restore operation.\n        /// <para/>\n        /// The system ensures that the calling process overrides file security checks when the process has\n        /// SE_BACKUP_NAME and SE_RESTORE_NAME privileges.\n        /// <para/>\n        /// You must set this flag to obtain a handle to a directory.\n        /// <para/>\n        /// A directory handle can be passed to some functions instead of a file handle.\n        /// </summary>\n        BackupSemantics = 0x02000000,\n\n        /// <summary>\n        /// The file is to be deleted immediately after all of its handles are closed, which includes the specified\n        /// handle and any other open or duplicated handles.\n        /// <para/>\n        /// If there are existing open handles to a file, the call fails unless they were all opened with the\n        /// `<see cref=\"FileShare.Delete\"/>` share mode.\n        /// <para/>\n        /// Subsequent open requests for the file fail, unless the `<see cref=\"FileShare.Delete\"/>` share mode is\n        /// specified.\n        /// </summary>\n        DeleteOnClose = 0x04000000,\n\n        /// <summary>\n        /// Access is intended to be sequential from beginning to end. The system can use this as a hint to optimize\n        /// file caching.\n        /// <para/>\n        /// This flag should not be used if read-behind (that is, reverse scans) will be used.\n        /// <para/>\n        /// This flag has no effect if the file system does not support cached I/O and `<see cref=\"NoBuffering\"/>`.\n        /// </summary>\n        SequentialScan = 0x08000000,\n\n        /// <summary>\n        /// Access is intended to be random. The system can use this as a hint to optimize file caching.\n        /// <para/>\n        /// This flag has no effect if the file system does not support cached I/O and `<see cref=\"NoBuffering\"/>`.\n        /// </summary>\n        RandomAccess = 0x10000000,\n\n        /// <summary>\n        /// The file or device is being opened with no system caching for data reads and writes.\n        /// <para/>\n        /// This flag does not affect hard disk caching or memory mapped files.\n        /// <para/>\n        /// There are strict requirements for successfully working with files opened with\n        /// `<see cref=\"CreateFile(string, FileAccess, FileShare, IntPtr, FileCreationDisposition, FileAttributes, IntPtr)\"/>`\n        /// using the `<see cref=\"NoBuffering\"/>` flag.\n        /// </summary>\n        NoBuffering = 0x20000000,\n\n        /// <summary>\n        /// The file or device is being opened or created for asynchronous I/O.\n        /// <para/>\n        /// When subsequent I/O operations are completed on this handle, the event specified in the OVERLAPPED\n        /// structure will be set to the signaled state.\n        /// <para/>\n        /// If this flag is specified, the file can be used for simultaneous read and write operations.\n        /// <para/>\n        /// If this flag is not specified, then I/O operations are serialized, even if the calls to the read and write\n        /// functions specify an OVERLAPPED structure.\n        /// </summary>\n        Overlapped = 0x40000000,\n\n        /// <summary>\n        /// Write operations will not go through any intermediate cache, they will go directly to disk.\n        /// </summary>\n        WriteThrough = 0x80000000,\n    }\n\n    public enum FileCreationDisposition : uint\n    {\n        /// <summary>\n        /// Creates a new file, only if it does not already exist.\n        /// <para/>\n        /// If the specified file exists, the function fails and the last-error code is set to <see cref=\"Win32Error.FileExists\"/>.\n        /// <para/>\n        /// If the specified file does not exist and is a valid path to a writable location, a new file is created.\n        /// </summary>\n        New = 1,\n\n        /// <summary>\n        /// Creates a new file, always.\n        /// <para/>\n        /// If the specified file exists and is writable, the function overwrites the file, the function succeeds, and\n        /// last-error code is set to <see cref=\"Win32Error.AlreadExists\"/>.\n        /// <para/>\n        /// If the specified file does not exist and is a valid path, a new file is created, the function succeeds, and\n        /// the last-error code is set to zero.\n        /// </summary>\n        CreateAlways = 2,\n\n        /// <summary>\n        /// Opens a file, always.\n        /// <para/>\n        /// If the specified file exists, the function succeeds and the last-error code is set to\n        /// <see cref=\"Win32Error.AlreadExists\"/>.\n        /// <para/>\n        /// If the specified file does not exist and is a valid path to a writable location, the function creates a\n        /// file and the last-error code is set to zero.\n        /// </summary>\n        OpenExisting = 3,\n\n        /// <summary>\n        /// Opens a file or device, only if it exists.\n        /// <para/>\n        /// If the specified file or device does not exist, the function fails and the last-error code is set to\n        /// <see cref=\"Win32Error.FileNotFound\"/>.\n        /// </summary>\n        OpenAlways = 4,\n\n        /// <summary>\n        /// Opens a file and truncates it so that its size is zero bytes, only if it exists.\n        /// <para/>\n        /// If the specified file does not exist, the function fails and the last-error code is\n        /// set to <see cref=\"Win32Error.FileNotFound\"/>.\n        /// <para/>\n        /// The calling process must open the file with <see cref=\"FileAccess.GenericWrite\"/>.\n        /// </summary>\n        TruncateExisting = 5\n    }\n\n    [Flags]\n    public enum FileShare : uint\n    {\n        /// <summary>\n        /// Prevents other processes from opening a file or device if they request delete, read, or write access.\n        /// </summary>\n        None = 0x00000000,\n\n        /// <summary>\n        /// Enables subsequent open operations on an object to request read access.\n        /// <para/>\n        /// Otherwise, other processes cannot open the object if they request read access.\n        /// <para/>\n        /// If this flag is not specified, but the object has been opened for read access, the function fails.\n        /// </summary>\n        Read = 0x00000001,\n\n        /// <summary>\n        /// Enables subsequent open operations on an object to request write access.\n        /// <para/>\n        /// Otherwise, other processes cannot open the object if they request write access.\n        /// <para/>\n        /// If this flag is not specified, but the object has been opened for write access, the\n        /// function fails.\n        /// </summary>\n        Write = 0x00000002,\n\n        /// <summary>\n        /// Enables subsequent open operations on an object to request delete access.\n        /// <para/>\n        /// Otherwise, other processes cannot open the object if they request delete access.\n        /// <para/>\n        /// If this flag is not specified, but the object has been opened for delete access, the function fails.\n        /// </summary>\n        Delete = 0x00000004\n    }\n\n    [Flags]\n        public enum ConsoleMode : uint\n        {\n            /// <summary>\n            /// CTRL+C is processed by the system and is not placed in the input buffer.\n            /// <para/>\n            /// If the input buffer is being read by\n            /// `<see cref=\"ReadConsole(SafeFileHandle, StringBuilder, uint, out uint, IntPtr)\"/>`, other control\n            /// keys are processed by the system and are not returned in the ReadConsole buffer.\n            /// <para/>\n            /// If the <see cref=\"LineInput\"/> mode is also enabled, backspace, carriage return, and line feed\n            /// characters are handled by the system.\n            /// </summary>\n            ProcessedInput = 0x0001,\n\n            /// <summary>\n            /// The `<see cref=\"ReadConsole(SafeFileHandle, StringBuilder, uint, out uint, IntPtr)\"/>` function\n            /// returns only when a carriage return character is read.\n            /// <para/>\n            /// If this mode is disabled, the functions return when one or more characters are available.\n            /// </summary>\n            LineInput = 0x0002,\n\n            /// <summary>\n            /// Characters read by the `<see cref=\"ReadConsole(SafeFileHandle, StringBuilder, uint, out uint, IntPtr)\"/>`\n            /// function are written to the active screen buffer as they are read.\n            /// <para/>\n            /// This mode can be used only if the <see cref=\"LineInput\"/> mode is also enabled.\n            /// </summary>\n            EchoInput = 0x0004,\n\n            /// <summary>\n            /// User interactions that change the size of the console screen buffer are reported in the\n            /// console's input buffer.\n            /// <para/>\n            /// Information about these events can be read from the input buffer by applications using the\n            /// ReadConsoleInput function, but not by those using `<see cref=\"ReadConsole(SafeFileHandle, StringBuilder, uint, out uint, IntPtr)\"/>`.\n            /// </summary>\n            WindowInput = 0x0008,\n\n            /// <summary>\n            /// If the mouse pointer is within the borders of the console window and the window has the keyboard focus,\n            /// mouse events generated by mouse movement and button presses are placed in the input buffer.\n            /// <para/>\n            /// These events are discarded by\n            /// `<see cref=\"ReadConsole(SafeFileHandle, StringBuilder, uint, out uint, IntPtr)\"/>`, even when this\n            /// mode is enabled.\n            /// </summary>\n            MouseInput = 0x0010,\n\n            /// <summary>\n            /// When enabled, text entered in a console window will be inserted at the current cursor location and all\n            /// text following that location will not be overwritten.\n            /// <para/>\n            /// When disabled, all following text will be overwritten.\n            /// </summary>\n            InsertMode = 0x0020,\n\n            /// <summary>\n            /// This flag enables the user to use the mouse to select and edit text.\n            /// </summary>\n            QuickEdit = 0x0040,\n\n            /// <summary>\n            /// Characters written by the `<see cref=\"WriteConsole(SafeHandle, StringBuilder, uint, out uint, IntPtr)\"/>`\n            /// function or echoed by the ReadFile or ReadConsole function are parsed for ASCII control sequences, and\n            /// the correct action is performed.\n            /// <para/>\n            /// Backspace, tab, bell, carriage return, and line feed characters are processed.\n            /// </summary>\n            ProcessedOuput = 0x0001,\n\n            /// <summary>\n            /// When writing with `<see cref=\"WriteConsole(SafeHandle, StringBuilder, uint, out uint, IntPtr)\"/>` or\n            /// echoing with ReadFile or ReadConsole, the cursor moves to the beginning of the next row when it reaches\n            /// the end of the current row.\n            /// <para/>\n            /// This causes the rows displayed in the console window to scroll up automatically when the cursor advances\n            /// beyond the last row in the window.\n            /// <para/>\n            /// It also causes the contents of the console screen buffer to scroll up (discarding the top row of the\n            /// console screen buffer) when the cursor advances beyond the last row in the console screen buffer.\n            /// <para/>\n            /// If this mode is disabled, the last character in the row is overwritten with any subsequent characters.\n            /// </summary>\n            WrapAtEolOutput = 0x0002,\n\n            AllFlags = ProcessedInput\n                     | LineInput\n                     | EchoInput\n                     | WindowInput\n                     | MouseInput\n                     | InsertMode\n                     | QuickEdit\n                     | ProcessedOuput\n                     | WrapAtEolOutput,\n        }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/Ole32.cs",
    "content": "﻿using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Windows.Native\n{\n    public static class Ole32\n    {\n        private const string LibraryName = \"ole32.dll\";\n\n        public const uint RPC_E_TOO_LATE = 0x80010119;\n\n        [DllImport(LibraryName)]\n        public static extern int CoInitializeSecurity(\n            IntPtr pVoid,\n            int cAuthSvc,\n            IntPtr asAuthSvc,\n            IntPtr pReserved1,\n            RpcAuthnLevel level,\n            RpcImpLevel impers,\n            IntPtr pAuthList,\n            EoAuthnCap dwCapabilities,\n            IntPtr pReserved3);\n\n        public enum RpcAuthnLevel\n        {\n            Default = 0,\n            None = 1,\n            Connect = 2,\n            Call = 3,\n            Pkt = 4,\n            PktIntegrity = 5,\n            PktPrivacy = 6\n        }\n\n        public enum RpcImpLevel\n        {\n            Default = 0,\n            Anonymous = 1,\n            Identify = 2,\n            Impersonate = 3,\n            Delegate = 4\n        }\n\n        public enum EoAuthnCap\n        {\n            None = 0x00,\n            MutualAuth = 0x01,\n            StaticCloaking = 0x20,\n            DynamicCloaking = 0x40,\n            AnyAuthority = 0x80,\n            MakeFullSIC = 0x100,\n            Default = 0x800,\n            SecureRefs = 0x02,\n            AccessControl = 0x04,\n            AppID = 0x08,\n            Dynamic = 0x10,\n            RequireFullSIC = 0x200,\n            AutoImpersonate = 0x400,\n            NoCustomMarshal = 0x2000,\n            DisableAAA = 0x1000\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/Shell32.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Windows.Native\n{\n    public static class Shell32\n    {\n        private const string LibraryName = \"shell32.dll\";\n\n        /// <summary>\n        /// Parses a Unicode command line string and returns an array of pointers\n        /// to the command line arguments, along with a count of such arguments,\n        /// in a way that is similar to the standard C run-time argv and argc values.\n        /// </summary>\n        /// <param name=\"lpCmdLine\">\n        /// Pointer to a null-terminated Unicode string that contains the full command line.\n        /// If this parameter is an empty string the function returns the path to the current executable file.\n        /// </param>\n        /// <param name=\"pNumArgs\">\n        /// Pointer to an int that receives the number of array elements returned, similar to argc.\n        /// </param>\n        /// <returns>A pointer to an array of LPWSTR values, similar to argv.</returns>\n        [DllImport(\"Shell32.dll\", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]\n        public static extern IntPtr CommandLineToArgvW(IntPtr lpCmdLine, out int pNumArgs);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/User32.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Windows.Native\n{\n    public static class User32\n    {\n        private const string LibraryName = \"user32.dll\";\n\n        public const int UOI_FLAGS = 1;\n        public const int WSF_VISIBLE = 0x0001;\n\n        [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, SetLastError = true)]\n        public static extern IntPtr GetProcessWindowStation();\n\n        [DllImport(LibraryName, EntryPoint = \"GetUserObjectInformation\", CallingConvention = CallingConvention.StdCall, SetLastError = true)]\n        public static extern unsafe bool GetUserObjectInformation(IntPtr hObj, int nIndex, void* pvBuffer, uint nLength, ref uint lpnLengthNeeded);\n\n        [DllImport(LibraryName, EntryPoint=\"SetWindowLong\", SetLastError = true)]\n        private static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr value);\n\n        [DllImport(LibraryName, EntryPoint=\"SetWindowLongPtr\", SetLastError = true)]\n        private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr value);\n\n        public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr value)\n        {\n            if (IntPtr.Size == 8)\n                return SetWindowLongPtr64(hWnd, nIndex, value);\n            else\n                return SetWindowLongPtr32(hWnd, nIndex, value);\n        }\n\n        [DllImport(LibraryName, SetLastError = true)]\n        public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);\n\n        [DllImport(LibraryName, SetLastError = true)]\n        public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);\n\n        /// <summary>\n        /// Retrieves the handle to the ancestor of the specified window.\n        /// </summary>\n        /// <param name=\"hwnd\">\n        /// A handle to the window whose ancestor is to be retrieved.\n        /// If this parameter is the desktop window, the function returns NULL.\n        /// </param>\n        /// <param name=\"flags\">The ancestor to be retrieved.</param>\n        /// <returns>The return value is the handle to the ancestor window.</returns>\n        [DllImport(\"user32.dll\", SetLastError = true)]\n        public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags);\n    }\n\n    public enum GetAncestorFlags\n    {\n        /// <summary>\n        /// Retrieves the parent window. This does not include the owner, as it does with the GetParent function.\n        /// </summary>\n        GetParent = 1,\n\n        /// <summary>\n        /// Retrieves the root window by walking the chain of parent windows.\n        /// </summary>\n        GetRoot = 2,\n\n        /// <summary>\n        /// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent.\n        /// </summary>\n        GetRootOwner = 3\n    }\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct USEROBJECTFLAGS\n    {\n        public int fInherit;\n        public int fReserved;\n        public int dwFlags;\n    }\n\n    public enum WindowLongParam\n    {\n        GWL_WNDPROC = -4,\n        GWL_HINSTANCE = -6,\n        GWL_HWNDPARENT = -8,\n        GWL_ID = -12,\n        GWL_STYLE = -16,\n        GWL_EXSTYLE = -20,\n        GWL_USERDATA = -21\n    }\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct RECT\n    {\n        public int left;\n        public int top;\n        public int right;\n        public int bottom;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/Native/Win32Error.cs",
    "content": "using System.ComponentModel;\nusing System.Runtime.InteropServices;\n\nnamespace GitCredentialManager.Interop.Windows.Native\n{\n    // https://docs.microsoft.com/en-gb/windows/desktop/Debug/system-error-codes\n\n    /// <summary>\n    /// The System Error Codes are very broad.\n    /// <para/>\n    /// Each one can occur in one of many hundreds of locations in the system.\n    /// <para/>\n    /// Consequently the descriptions of these codes cannot be very specific.\n    /// <para/>\n    /// Use of these codes requires some amount of investigation and analysis.\n    /// <para/>\n    /// You need to note both the programmatic and the run-time context in which these errors occur.\n    /// <para/>\n    /// Because these codes are defined in WinError.h for anyone to use, sometimes the codes are returned by non-system software.\n    /// <para/>\n    /// Sometimes the code is returned by a function deep in the stack and far removed from your code that is handling the error.\n    /// </summary>\n    internal static class Win32Error\n    {\n        /// <summary>\n        /// The operation completed successfully.\n        /// </summary>\n        public const int Success = 0;\n\n        /// <summary>\n        /// The system cannot find the file specified.\n        /// </summary>\n        public const int FileNotFound = 2;\n\n        /// <summary>\n        /// The handle is invalid.\n        /// </summary>\n        public const int InvalidHandle = 6;\n\n        /// <summary>\n        /// Not enough storage is available to process this command.\n        /// </summary>\n        public const int NotEnoughMemory = 8;\n\n        /// <summary>\n        /// A device attached to the system is not functioning.\n        /// </summary>\n        public const int GenericFailure = 31;\n\n        /// <summary>\n        /// The process cannot access the file because it is being used by another process.\n        /// </summary>\n        public const int SharingViolation = 32;\n\n        /// <summary>\n        /// The file exists.\n        /// </summary>\n        public const int FileExists = 80;\n\n        /// <summary>\n        /// The data area passed to a system call is too small.\n        /// </summary>\n        public const int InsufficientBuffer = 122;\n\n        /// <summary>\n        /// Cannot create a file when that file already exists.\n        /// </summary>\n        public const int AlreadyExists = 183;\n\n        /// <summary>\n        /// The implementation is not capable of performing the request.\n        /// </summary>\n        public const int NotCapable = 775;\n\n        /// <summary>\n        /// Element not found.\n        /// </summary>\n        public const int NotFound = 1168;\n\n        /// <summary>\n        /// The operation was canceled by the user.\n        /// </summary>\n        public const int Cancelled = 1223;\n\n        /// <summary>\n        /// A specified logon session does not exist. It may already have been terminated.\n        /// </summary>\n        public const int NoSuchLogonSession = 1312;\n\n        public static int GetLastError(bool success)\n        {\n            if (success)\n            {\n                return Success;\n            }\n\n            return Marshal.GetLastWin32Error();\n        }\n\n        /// <summary>\n        /// Throw an <see cref=\"InteropException\"/> if <paramref name=\"succeeded\"/> is not true.\n        /// </summary>\n        /// <param name=\"trace2\">The application's TRACE2 tracer.</param>\n        /// <param name=\"succeeded\">Windows API return code.</param>\n        /// <param name=\"defaultErrorMessage\">Default error message.</param>\n        /// <exception cref=\"InteropException\">Throw if <paramref name=\"succeeded\"/> is not true.</exception>\n        public static void ThrowIfError(ITrace2 trace2, bool succeeded, string defaultErrorMessage = \"Unknown error.\")\n        {\n            ThrowIfError(GetLastError(succeeded), defaultErrorMessage, trace2);\n        }\n\n        /// <summary>\n        /// Throw an <see cref=\"InteropException\"/> if <paramref name=\"succeeded\"/> is not true.\n        /// </summary>\n        /// <param name=\"succeeded\">Windows API return code.</param>\n        /// <param name=\"defaultErrorMessage\">Default error message.</param>\n        /// <exception cref=\"InteropException\">Throw if <paramref name=\"succeeded\"/> is not true.</exception>\n        public static void ThrowIfError(bool succeeded, string defaultErrorMessage = \"Unknown error.\")\n        {\n            ThrowIfError(GetLastError(succeeded), defaultErrorMessage);\n        }\n\n        /// <summary>\n        /// Throw an <see cref=\"InteropException\"/> if <paramref name=\"error\"/> is not <see cref=\"Success\"/>.\n        /// </summary>\n        /// <param name=\"error\">Windows API error code.</param>\n        /// <param name=\"defaultErrorMessage\">Default error message.</param>\n        /// <param name=\"trace2\">The application's TRACE2 tracer.</param>\n        /// <exception cref=\"InteropException\">Throw if <paramref name=\"error\"/> is not <see cref=\"Success\"/>.</exception>\n        public static void ThrowIfError(int error, string defaultErrorMessage = \"Unknown error.\", ITrace2 trace2 = null)\n        {\n            switch (error)\n            {\n                case Success:\n                    return;\n                default:\n                    // The Win32Exception constructor will automatically get the human-readable\n                    // message for the error code.\n                    if (trace2 != null)\n                        throw new Trace2InteropException(trace2, defaultErrorMessage, new Win32Exception(error));\n                    throw new InteropException(defaultErrorMessage, new Win32Exception(error));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsCredential.cs",
    "content": "\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class WindowsCredential : ICredential\n    {\n        public WindowsCredential(string service, string userName, string password, string targetName)\n        {\n            Service = service;\n            UserName = userName;\n            Password = password;\n            TargetName = targetName;\n        }\n\n        public string Service { get; }\n\n        public string UserName { get; }\n\n        public string Password { get; }\n\n        public string TargetName { get; }\n\n        string ICredential.Account => UserName;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsCredentialManager.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing GitCredentialManager.Interop.Windows.Native;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class WindowsCredentialManager : ICredentialStore\n    {\n        internal const string TargetNameLegacyGenericPrefix = \"LegacyGeneric:target=\";\n\n        private readonly string _namespace;\n\n        /// <summary>\n        /// Open the Windows Credential Manager vault for the current user.\n        /// </summary>\n        /// <param name=\"namespace\">Optional namespace to scope credential operations.</param>\n        /// <returns>Current user's Credential Manager vault.</returns>\n        public WindowsCredentialManager(string @namespace = null)\n        {\n            PlatformUtils.EnsureWindows();\n            _namespace = @namespace;\n        }\n\n        public IList<string> GetAccounts(string service)\n        {\n            return Enumerate(service, null).Select(x => x.UserName).Distinct().ToList();\n        }\n\n        public ICredential Get(string service, string account)\n        {\n            return Enumerate(service, account).FirstOrDefault();\n        }\n\n        public void AddOrUpdate(string service, string account, string secret)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(service, nameof(service));\n\n            IntPtr existingCredPtr = IntPtr.Zero;\n            IntPtr credBlob = IntPtr.Zero;\n\n            try\n            {\n                // Determine if we need to update an existing credential, which might have\n                // a target name that does not include the account name.\n                //\n                // We first check for the presence of a credential with an account-less\n                // target name.\n                //\n                //  - If such credential exists and *has the same account* then we will\n                //    update that entry.\n                //  - If such credential exists and does *not* have the same account then\n                //    we must create a new entry with the account in the target name.\n                //  - If no such credential exists then we create a new entry with the\n                //    account-less target name.\n                //\n                string targetName = CreateTargetName(service, account: null);\n                if (Advapi32.CredRead(targetName, CredentialType.Generic, 0, out existingCredPtr))\n                {\n                    var existingCred = Marshal.PtrToStructure<Win32Credential>(existingCredPtr);\n                    if (!StringComparer.Ordinal.Equals(existingCred.UserName, account))\n                    {\n                        // Create new entry with the account in the target name\n                        targetName = CreateTargetName(service, account);\n                    }\n                    else\n                    {\n                        // No need to write out credential if the account and secret/password are the same\n                        string existingSecret = existingCred.GetCredentialBlobAsString();\n                        if (StringComparer.Ordinal.Equals(existingSecret, secret))\n                        {\n                            return;\n                        }\n                    }\n                }\n\n                byte[] secretBytes = Encoding.Unicode.GetBytes(secret);\n                credBlob = Marshal.AllocHGlobal(secretBytes.Length);\n                Marshal.Copy(secretBytes, 0, credBlob, secretBytes.Length);\n\n                var newCred = new Win32Credential\n                {\n                    Type = CredentialType.Generic,\n                    TargetName = targetName,\n                    CredentialBlobSize = secretBytes.Length,\n                    CredentialBlob = credBlob,\n                    Persist = CredentialPersist.LocalMachine,\n                    UserName = account,\n                };\n\n                int result = Win32Error.GetLastError(\n                    Advapi32.CredWrite(ref newCred, 0)\n                );\n\n                Win32Error.ThrowIfError(result, \"Failed to write item to store.\");\n            }\n            finally\n            {\n                if (credBlob != IntPtr.Zero)\n                {\n                    Marshal.FreeHGlobal(credBlob);\n                }\n\n                if (existingCredPtr != IntPtr.Zero)\n                {\n                    Advapi32.CredFree(existingCredPtr);\n                }\n            }\n        }\n\n        public bool Remove(string service, string account)\n        {\n            WindowsCredential credential = Enumerate(service, account).FirstOrDefault();\n\n            if (credential != null)\n            {\n                int result = Win32Error.GetLastError(\n                    Advapi32.CredDelete(credential.TargetName, CredentialType.Generic, 0)\n                );\n\n                switch (result)\n                {\n                    case Win32Error.Success:\n                        return true;\n\n                    case Win32Error.NotFound:\n                        return false;\n\n                    default:\n                        Win32Error.ThrowIfError(result);\n                        return false;\n                }\n            }\n\n            return false;\n        }\n\n        /// <summary>\n        /// Check if we can persist credentials to for the current process and logon session.\n        /// </summary>\n        /// <returns>True if persistence is possible, false otherwise.</returns>\n        public static bool CanPersist()\n        {\n            uint count = Advapi32.CRED_TYPE_MAXIMUM;\n            var arr = new CredentialPersist[count];\n\n            int result = Win32Error.GetLastError(\n                Advapi32.CredGetSessionTypes(count, arr)\n            );\n\n            CredentialPersist persist = CredentialPersist.None;\n            if (result == Win32Error.Success)\n            {\n                persist = arr[(int)CredentialType.Generic];\n            }\n\n            // If the maximum allowed is anything less than \"local machine\" then cannot persist credentials.\n            return persist >= CredentialPersist.LocalMachine;\n        }\n\n        private IEnumerable<WindowsCredential> Enumerate(string service, string account)\n        {\n            IntPtr credList = IntPtr.Zero;\n\n            try\n            {\n                int result = Win32Error.GetLastError(\n                    Advapi32.CredEnumerate(\n                        null,\n                        CredentialEnumerateFlags.AllCredentials,\n                        out int count,\n                        out credList)\n                );\n\n                switch (result)\n                {\n                    case Win32Error.Success:\n                        int ptrSize = Marshal.SizeOf<IntPtr>();\n                        for (int i = 0; i < count; i++)\n                        {\n                            IntPtr credPtr = Marshal.ReadIntPtr(credList, i * ptrSize);\n                            Win32Credential credential = Marshal.PtrToStructure<Win32Credential>(credPtr);\n\n                            if (!IsMatch(service, account, credential))\n                            {\n                                continue;\n                            }\n\n                            yield return CreateCredentialFromStructure(credential);\n                        }\n                        break;\n\n                    case Win32Error.NotFound:\n                        yield break;\n\n                    default:\n                        Win32Error.ThrowIfError(result, \"Failed to enumerate credentials.\");\n                        yield break;\n                }\n            }\n            finally\n            {\n                if (credList != IntPtr.Zero)\n                {\n                    Advapi32.CredFree(credList);\n                }\n            }\n        }\n\n        private WindowsCredential CreateCredentialFromStructure(Win32Credential credential)\n        {\n            string password = credential.GetCredentialBlobAsString();\n\n            // Recover the target name we gave from the internal (raw) target name\n            string targetName = credential.TargetName.TrimUntilIndexOf(TargetNameLegacyGenericPrefix);\n\n            // Recover the service name from the target name\n            string serviceName = targetName;\n            if (!string.IsNullOrWhiteSpace(_namespace))\n            {\n                serviceName = serviceName.TrimUntilIndexOf($\"{_namespace}:\");\n            }\n\n            // Strip any userinfo component from the service name\n            serviceName = RemoveUriUserInfo(serviceName);\n\n            return new WindowsCredential(serviceName, credential.UserName, password, targetName);\n        }\n\n        public /* for testing */ static string RemoveUriUserInfo(string url)\n        {\n            // To remove the userinfo component we must search for the end of the :// scheme\n            // delimiter, and the start of the @ userinfo delimiter. We don't want to match\n            // any other '@' character however (such as one in the URI path).\n            // To ensure this we only consider an '@' character that exists before the first\n            // '/' character after the scheme delimiter - that is to say the authority-path\n            // separator.\n            //\n            //                authority\n            //              |-----------|\n            //     scheme://userinfo@host/path\n            //\n            int schemeDelimIdx = url.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal);\n            if (schemeDelimIdx > 0)\n            {\n                int authorityIdx = schemeDelimIdx + Uri.SchemeDelimiter.Length;\n                int slashIdx = url.IndexOf(\"/\", authorityIdx, StringComparison.Ordinal);\n                int atIdx = url.IndexOf(\"@\", StringComparison.Ordinal);\n\n                // No path component or trailing slash; use end of string\n                if (slashIdx < 0)\n                {\n                    slashIdx = url.Length - 1;\n                }\n\n                // Only if the '@' is before the first slash is this the userinfo delimiter\n                if (0 < atIdx && atIdx < slashIdx)\n                {\n                    return url.Substring(0, authorityIdx) + url.Substring(atIdx + 1);\n                }\n            }\n\n            return url;\n        }\n\n        internal /* for testing */ bool IsMatch(string service, string account, Win32Credential credential)\n        {\n            // Match against the username first\n            if (!string.IsNullOrWhiteSpace(account) &&\n                !StringComparer.Ordinal.Equals(account, credential.UserName))\n            {\n                return false;\n            }\n\n            // Trim the \"LegacyGeneric\" prefix Windows adds\n            string targetName = credential.TargetName.TrimUntilIndexOf(TargetNameLegacyGenericPrefix);\n            \n            // Only match credentials with the namespace we have been configured with (if any)\n            if (!string.IsNullOrWhiteSpace(_namespace))\n            {\n                string nsPrefix = $\"{_namespace}:\";\n                if (!targetName.StartsWith(nsPrefix, StringComparison.Ordinal))\n                {\n                    return false;\n                }\n\n                targetName = targetName.Substring(nsPrefix.Length);\n            }\n\n            // If the target name matches the service name exactly then return 'match'\n            if (StringComparer.Ordinal.Equals(service, targetName))\n            {\n                return true;\n            }\n\n            // Try matching the target and service as URIs\n            if (Uri.TryCreate(service, UriKind.Absolute, out Uri serviceUri) &&\n                Uri.TryCreate(targetName, UriKind.Absolute, out Uri targetUri))\n            {\n                // Match scheme/protocol\n                if (!StringComparer.OrdinalIgnoreCase.Equals(serviceUri.Scheme, targetUri.Scheme))\n                {\n                    return false;\n                }\n\n                // Match host name\n                if (!StringComparer.OrdinalIgnoreCase.Equals(serviceUri.Host, targetUri.Host))\n                {\n                    return false;\n                }\n\n                // Match port number\n                if (serviceUri.Port != targetUri.Port)\n                {\n                    return false;\n                }\n\n                // Match path\n                if (!string.IsNullOrWhiteSpace(serviceUri.AbsolutePath) &&\n                    !StringComparer.OrdinalIgnoreCase.Equals(serviceUri.AbsolutePath, targetUri.AbsolutePath))\n                {\n                    return false;\n                }\n\n                // URLs match\n                return true;\n            }\n\n            // Unable to match\n            return false;\n        }\n\n        internal /* for testing */ string CreateTargetName(string service, string account)\n        {\n            var serviceUri = new Uri(service, UriKind.Absolute);\n            var sb = new StringBuilder();\n\n            if (!string.IsNullOrWhiteSpace(_namespace))\n            {\n                sb.AppendFormat(\"{0}:\", _namespace);\n            }\n\n            if (!string.IsNullOrWhiteSpace(serviceUri.Scheme))\n            {\n                sb.AppendFormat(\"{0}://\", serviceUri.Scheme);\n            }\n\n            if (!string.IsNullOrWhiteSpace(account))\n            {\n                string escapedAccount = account.Replace('@', '_');\n                sb.AppendFormat(\"{0}@\", escapedAccount);\n            }\n\n            if (!string.IsNullOrWhiteSpace(serviceUri.Host))\n            {\n                sb.Append(serviceUri.Host);\n            }\n\n            if (!serviceUri.IsDefaultPort)\n            {\n                sb.AppendFormat(\":{0}\", serviceUri.Port);\n            }\n\n            string trimmedPath = serviceUri.AbsolutePath.TrimEnd('/');\n            if (!string.IsNullOrWhiteSpace(trimmedPath))\n            {\n                sb.Append(trimmedPath);\n            }\n\n            return sb.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsEnvironment.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class WindowsEnvironment : EnvironmentBase\n    {\n        public WindowsEnvironment(IFileSystem fileSystem)\n            : base(fileSystem) { }\n\n        internal WindowsEnvironment(IFileSystem fileSystem, IReadOnlyDictionary<string, string> variables)\n            : base(fileSystem, variables) { }\n\n        #region EnvironmentBase\n\n        protected override string[] SplitPathVariable(string value)\n        {\n            // Ensure we don't return empty values here - callers may use this as the base\n            // path for `Path.Combine(..)`, for which an empty value means 'current directory'.\n            // We only ever want to use the current directory for path resolution explicitly.\n            return value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);\n        }\n\n        public override void AddDirectoryToPath(string directoryPath, EnvironmentVariableTarget target)\n        {\n            // Read the current PATH variable, not the cached one\n            string currentValue = Environment.GetEnvironmentVariable(\"PATH\", target) ?? string.Empty;\n\n            // Append directory to the end\n            var sb = new StringBuilder();\n            sb.Append(currentValue);\n            if (!currentValue.EndsWith(\";\"))\n            {\n                sb.Append(';');\n            }\n            sb.Append(directoryPath);\n\n            string newValue = sb.ToString();\n\n            // Update the real system immediately\n            Environment.SetEnvironmentVariable(\"PATH\", newValue, target);\n\n            // Update the cached PATH variable to the latest value (as well as all other variables)\n            Refresh();\n        }\n\n        public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target)\n        {\n            // Read the current PATH variable, not the cached one\n            string currentValue = Environment.GetEnvironmentVariable(\"PATH\", target) ?? string.Empty;\n\n            // Only need to update PATH if it does indeed contain the parent directory\n            if (directoryPath != null && currentValue.IndexOf(directoryPath, StringComparison.OrdinalIgnoreCase) > -1)\n            {\n                // Cut out the directory path\n                string newValue = currentValue.TrimMiddle(directoryPath, StringComparison.OrdinalIgnoreCase);\n\n                // Update the real system immediately\n                Environment.SetEnvironmentVariable(\"PATH\", newValue, target);\n\n                // Update the cached PATH variable to the latest value (as well as all other variables)\n                Refresh();\n            }\n        }\n\n        #endregion\n\n        protected override IReadOnlyDictionary<string, string> GetCurrentVariables()\n        {\n            // On Windows it is technically possible to get env vars which differ only by case\n            // even though the general assumption is that they are case insensitive on Windows.\n            // For example, some of the standard .NET types like System.Diagnostics.Process\n            // will fail to start a process on Windows if given duplicate environment variables.\n            // See this issue for more information: https://github.com/dotnet/corefx/issues/13146\n\n            // We should de-duplicate by setting the string comparer to OrdinalIgnoreCase.\n            var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n\n            var variables = Environment.GetEnvironmentVariables();\n\n            foreach (var key in variables.Keys)\n            {\n                if (key is string name && variables[key] is string value)\n                {\n                    dict[name] = value;\n                }\n            }\n\n            return dict;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsFileSystem.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class WindowsFileSystem : FileSystem\n    {\n        public override bool IsSamePath(string a, string b)\n        {\n            if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))\n            {\n                return false;\n            }\n\n            a = Path.GetFullPath(a);\n            b = Path.GetFullPath(b);\n\n            // Note: we do not resolve or handle symlinks on Windows\n            // because they require administrator permissions to even create!\n\n            return StringComparer.OrdinalIgnoreCase.Equals(a, b);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsProcessManager.cs",
    "content": "namespace GitCredentialManager.Interop.Windows;\n\npublic class WindowsProcessManager : ProcessManager\n{\n    public WindowsProcessManager(ITrace2 trace2) : base(trace2)\n    {\n        PlatformUtils.EnsureWindows();\n    }\n\n    public override ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)\n    {\n        // If we're asked to start a WSL executable we must launch via the wsl.exe command tool\n        if (!useShellExecute && WslUtils.IsWslPath(path))\n        {\n            string wslPath = WslUtils.ConvertToDistroPath(path, out string distro);\n            return WslUtils.CreateWslProcess(distro, $\"{wslPath} {args}\", Trace2, workingDirectory);\n        }\n\n        return base.CreateProcess(path, args, useShellExecute, workingDirectory);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsSessionManager.cs",
    "content": "using System;\nusing GitCredentialManager.Interop.Windows.Native;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class WindowsSessionManager : SessionManager\n    {\n        public WindowsSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs)\n        {\n            PlatformUtils.EnsureWindows();\n        }\n\n        public override unsafe bool IsDesktopSession\n        {\n            get\n            {\n                // Environment.UserInteractive is hard-coded to return true for POSIX and Windows platforms on .NET Core 2.x and 3.x.\n                // In .NET 5 the implementation on Windows has been 'fixed', but still POSIX versions always return true.\n                //\n                // This code is lifted from the .NET 5 targeting dotnet/runtime implementation for Windows:\n                // https://github.com/dotnet/runtime/blob/cf654f08fb0078a96a4e414a0d2eab5e6c069387/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs#L125-L145\n\n                // Per documentation of GetProcessWindowStation, this handle should not be closed\n                IntPtr handle = User32.GetProcessWindowStation();\n                if (handle != IntPtr.Zero)\n                {\n                    USEROBJECTFLAGS flags = default;\n                    uint dummy = 0;\n                    if (User32.GetUserObjectInformation(handle, User32.UOI_FLAGS, &flags,\n                        (uint) sizeof(USEROBJECTFLAGS), ref dummy))\n                    {\n                        return (flags.dwFlags & User32.WSF_VISIBLE) != 0;\n                    }\n                }\n\n                // If we can't determine, return true optimistically\n                // This will include cases like Windows Nano which do not expose WindowStations\n                return true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsSettings.cs",
    "content": "\nnamespace GitCredentialManager.Interop.Windows\n{\n    /// <summary>\n    /// Reads settings from Git configuration, environment variables, and defaults from the Windows Registry.\n    /// </summary>\n    public class WindowsSettings : Settings\n    {\n        private readonly ITrace _trace;\n\n        public WindowsSettings(IEnvironment environment, IGit git, ITrace trace)\n            : base(environment, git)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            _trace = trace;\n\n            PlatformUtils.EnsureWindows();\n        }\n\n        protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value)\n        {\n            value = null;\n\n#if NETFRAMEWORK\n            // Check for machine (HKLM) registry keys that match the Git configuration name.\n            // These can be set by system administrators via Group Policy, so make useful defaults.\n            using (Microsoft.Win32.RegistryKey configKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(Constants.WindowsRegistry.HKConfigurationPath))\n            {\n                if (configKey is null)\n                {\n                    // No configuration key exists\n                    return false;\n                }\n\n                string name = string.IsNullOrWhiteSpace(scope)\n                    ? $\"{section}.{property}\"\n                    : $\"{section}.{scope}.{property}\";\n\n                object registryValue = configKey.GetValue(name);\n                if (registryValue is null)\n                {\n                    // No property exists\n                    return false;\n                }\n\n                value = registryValue.ToString();\n                _trace.WriteLine($\"Default setting found in registry: {name}={value}\");\n\n                return true;\n            }\n#else\n            return base.TryGetExternalDefault(section, scope, property, out value);\n#endif\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsSystemPrompts.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing GitCredentialManager.Interop.Windows.Native;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    public class WindowsSystemPrompts : ISystemPrompts\n    {\n        private IntPtr _parentHwd = IntPtr.Zero;\n\n        public object ParentWindowId\n        {\n            get => _parentHwd.ToString();\n            set => _parentHwd = ConvertUtils.TryToInt32(value, out int ptr) ? new IntPtr(ptr) : IntPtr.Zero;\n        }\n\n        public bool ShowCredentialPrompt(string resource, string userName, out ICredential credential)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));\n\n            string message = $\"Enter credentials for '{resource}'\";\n\n            var credUiInfo = new CredUi.CredentialUiInfo\n            {\n                BannerArt = IntPtr.Zero,\n                CaptionText = \"Git Credential Manager\", // TODO: make this a parameter?\n                Parent = _parentHwd,\n                MessageText = message,\n                Size = Marshal.SizeOf(typeof(CredUi.CredentialUiInfo))\n            };\n\n            var packFlags = CredUi.CredentialPackFlags.None;\n            var uiFlags = CredUi.CredentialUiWindowsFlags.Generic;\n            if (!string.IsNullOrEmpty(userName))\n            {\n                // If we are given a username, pre-populate the dialog with the given value\n                uiFlags |= CredUi.CredentialUiWindowsFlags.InCredOnly;\n            }\n\n            IntPtr inBufferPtr = IntPtr.Zero;\n            uint inBufferSize;\n\n            try\n            {\n                CreateCredentialInfoBuffer(userName, packFlags, out inBufferSize, out inBufferPtr);\n\n                return DisplayCredentialPrompt(ref credUiInfo, ref packFlags, inBufferPtr, inBufferSize, false, uiFlags, out credential);\n            }\n            finally\n            {\n                if (inBufferPtr != IntPtr.Zero)\n                {\n                    Marshal.FreeHGlobal(inBufferPtr);\n                }\n            }\n        }\n\n        private static void CreateCredentialInfoBuffer(string userName, CredUi.CredentialPackFlags flags, out uint inBufferSize, out IntPtr inBufferPtr)\n        {\n            // Windows Credential API calls require at least an empty string; not null\n            userName = userName ?? string.Empty;\n\n            int desiredBufSize = 0;\n\n            // Execute with a null packed credentials pointer to determine the required buffer size.\n            // This method always returns false when determining the buffer size so we only fail if the size is not strictly positive.\n            CredUi.CredPackAuthenticationBuffer(flags, userName, string.Empty, IntPtr.Zero, ref desiredBufSize);\n            Win32Error.ThrowIfError(desiredBufSize > 0, \"Unable to determine credential buffer size.\");\n\n            // Create a buffer of the desired size and pass the pointer and size back to the caller\n            inBufferSize = (uint) desiredBufSize;\n            inBufferPtr = Marshal.AllocHGlobal(desiredBufSize);\n\n            Win32Error.ThrowIfError(\n                CredUi.CredPackAuthenticationBuffer(flags, userName, string.Empty, inBufferPtr, ref desiredBufSize),\n                \"Unable to write to credential buffer.\"\n            );\n        }\n\n        private static bool DisplayCredentialPrompt(\n            ref CredUi.CredentialUiInfo credUiInfo,\n            ref CredUi.CredentialPackFlags packFlags,\n            IntPtr inBufferPtr,\n            uint inBufferSize,\n            bool saveCredentials,\n            CredUi.CredentialUiWindowsFlags uiFlags,\n            out ICredential credential)\n        {\n            uint authPackage = 0;\n            IntPtr outBufferPtr = IntPtr.Zero;\n            uint outBufferSize;\n\n            try\n            {\n                // Open a standard Windows authentication dialog to acquire username and password credentials\n                int error = CredUi.CredUIPromptForWindowsCredentials(\n                    ref credUiInfo,\n                    0,\n                    ref authPackage,\n                    inBufferPtr,\n                    inBufferSize,\n                    out outBufferPtr,\n                    out outBufferSize,\n                    ref saveCredentials,\n                    uiFlags);\n\n                switch (error)\n                {\n                    case Win32Error.Cancelled:\n                        credential = null;\n                        return false;\n                    default:\n                        Win32Error.ThrowIfError(error, \"Failed to show credential prompt.\");\n                        break;\n                }\n\n                int maxUserLength = 512;\n                int maxPassLength = 512;\n                int maxDomainLength = 256;\n                var usernameBuffer = new StringBuilder(maxUserLength);\n                var domainBuffer = new StringBuilder(maxDomainLength);\n                var passwordBuffer = new StringBuilder(maxPassLength);\n\n                // Unpack the result\n                Win32Error.ThrowIfError(\n                    CredUi.CredUnPackAuthenticationBuffer(\n                        packFlags,\n                        outBufferPtr,\n                        outBufferSize,\n                        usernameBuffer,\n                        ref maxUserLength,\n                        domainBuffer,\n                        ref maxDomainLength,\n                        passwordBuffer,\n                        ref maxPassLength),\n                    \"Failed to unpack credential buffer.\"\n                );\n\n                // Return the plaintext credential strings to the caller\n                string userName = usernameBuffer.ToString();\n                string password = passwordBuffer.ToString();\n\n                credential = new GitCredential(userName, password);\n                return true;\n            }\n            finally\n            {\n                if (outBufferPtr != IntPtr.Zero)\n                {\n                    Marshal.FreeHGlobal(outBufferPtr);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Interop/Windows/WindowsTerminal.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing GitCredentialManager.Interop.Windows.Native;\nusing Microsoft.Win32.SafeHandles;\n\nnamespace GitCredentialManager.Interop.Windows\n{\n    /// <summary>\n    /// Represents a thin wrapper around the Windows console device.\n    /// </summary>\n    public class WindowsTerminal : ITerminal\n    {\n        // ReadConsole 32768 fail, 32767 OK @linquize [https://github.com/Microsoft/Git-Credential-Manager-for-Windows/commit/a62b9a19f430d038dcd85a610d97e5f763980f85]\n        private const int BufferReadSize = 16 * 1024;\n        private const string ConsoleInName = \"CONIN$\";\n        private const string ConsoleOutName = \"CONOUT$\";\n\n        private readonly ITrace _trace;\n        private readonly ITrace2 _trace2;\n\n        public WindowsTerminal(ITrace trace, ITrace2 trace2)\n        {\n            PlatformUtils.EnsureWindows();\n\n            _trace = trace;\n            _trace2 = trace2;\n        }\n\n        public void WriteLine(string format, params object[] args)\n        {\n            var fileAccessFlags = FileAccess.GenericRead\n                                | FileAccess.GenericWrite;\n            var fileAttributes = FileAttributes.Normal;\n            var fileCreationDisposition = FileCreationDisposition.OpenExisting;\n            var fileShareFlags = FileShare.Read\n                               | FileShare.Write;\n\n            using (SafeFileHandle stdout = Kernel32.CreateFile(fileName: ConsoleOutName,\n                                                          desiredAccess: fileAccessFlags,\n                                                              shareMode: fileShareFlags,\n                                                     securityAttributes: IntPtr.Zero,\n                                                    creationDisposition: fileCreationDisposition,\n                                                     flagsAndAttributes: fileAttributes,\n                                                           templateFile: IntPtr.Zero))\n            {\n                if (stdout.IsInvalid)\n                {\n                    _trace.WriteLine(\"Not a TTY, abandoning write line.\");\n                    return;\n                }\n\n                var sb = new StringBuilder();\n                sb.AppendFormat(format, args);\n                sb.AppendLine();\n\n                if (!Kernel32.WriteConsole(buffer: sb,\n                              consoleOutputHandle: stdout,\n                             numberOfCharsToWrite: (uint) sb.Length,\n                             numberOfCharsWritten: out uint written,\n                                         reserved: IntPtr.Zero))\n                {\n                    Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), trace2: _trace2);\n                }\n            }\n        }\n\n        public string Prompt(string prompt)\n        {\n            return Prompt(prompt, echo: true);\n        }\n\n        public string PromptSecret(string prompt)\n        {\n            return Prompt(prompt, echo: false);\n        }\n\n        private string Prompt(string prompt, bool echo)\n        {\n            var fileAccessFlags = FileAccess.GenericRead\n                                | FileAccess.GenericWrite;\n            var fileAttributes = FileAttributes.Normal;\n            var fileCreationDisposition = FileCreationDisposition.OpenExisting;\n            var fileShareFlags = FileShare.Read\n                               | FileShare.Write;\n\n            using (SafeFileHandle stdout = Kernel32.CreateFile(fileName: ConsoleOutName,\n                                                          desiredAccess: fileAccessFlags,\n                                                              shareMode: fileShareFlags,\n                                                     securityAttributes: IntPtr.Zero,\n                                                    creationDisposition: fileCreationDisposition,\n                                                     flagsAndAttributes: fileAttributes,\n                                                           templateFile: IntPtr.Zero))\n            using (SafeFileHandle stdin  = Kernel32.CreateFile(fileName: ConsoleInName,\n                                                          desiredAccess: fileAccessFlags,\n                                                              shareMode: fileShareFlags,\n                                                     securityAttributes: IntPtr.Zero,\n                                                    creationDisposition: fileCreationDisposition,\n                                                     flagsAndAttributes: fileAttributes,\n                                                           templateFile: IntPtr.Zero))\n            {\n                string input;\n                var sb = new StringBuilder(BufferReadSize);\n                uint read = 0;\n                uint written = 0;\n\n                if (stdin.IsInvalid || stdout.IsInvalid)\n                {\n                    _trace.WriteLine(\"Not a TTY, abandoning prompt.\");\n                    return null;\n                }\n\n                // Prompt the user\n                sb.Append($\"{prompt}: \");\n                if (!Kernel32.WriteConsole(buffer: sb,\n                              consoleOutputHandle: stdout,\n                             numberOfCharsToWrite: (uint) sb.Length,\n                             numberOfCharsWritten: out written,\n                                         reserved: IntPtr.Zero))\n                {\n                    Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), \"Failed to write prompt text\", _trace2);\n                }\n\n                sb.Clear();\n\n                // Read input from the user\n                using (new TtyContext(_trace, _trace2, stdin, echo))\n                {\n                    if (!Kernel32.ReadConsole(buffer: sb,\n                                  consoleInputHandle: stdin,\n                                 numberOfCharsToRead: BufferReadSize,\n                                   numberOfCharsRead: out read,\n                                            reserved: IntPtr.Zero))\n                    {\n                        Win32Error.ThrowIfError(Marshal.GetLastWin32Error(),\n                            \"Unable to read prompt input from standard input\", _trace2);\n                    }\n\n                    // Record input from the user into local storage, stripping any EOL chars\n                    input = sb.ToString(0, (int) read);\n                    input = input.Trim('\\n', '\\r').Trim('\\n');\n\n                    sb.Clear();\n                }\n\n                // Write the final newline to stdout manually if we had disabled echo\n                if (!echo)\n                {\n                    sb.Append(Environment.NewLine);\n                    if (!Kernel32.WriteConsole(buffer: sb,\n                                  consoleOutputHandle: stdout,\n                                 numberOfCharsToWrite: (uint) sb.Length,\n                                 numberOfCharsWritten: out written,\n                                             reserved: IntPtr.Zero))\n                    {\n                        Win32Error.ThrowIfError(Marshal.GetLastWin32Error(),\n                            \"Failed to write final newline in secret prompting\", _trace2);\n                    }\n                }\n\n                return input;\n            }\n        }\n\n        private class TtyContext : IDisposable\n        {\n            private readonly ITrace _trace;\n            private readonly ITrace2 _trace2;\n            private readonly SafeFileHandle _stream;\n\n            private ConsoleMode _originalMode;\n            private bool _isDisposed;\n\n            public TtyContext(ITrace trace, ITrace2 trace2, SafeFileHandle stream, bool echo)\n            {\n                EnsureArgument.NotNull(stream, nameof(stream));\n\n                _trace = trace;\n                _trace2 = trace2;\n                _stream = stream;\n\n                // Capture current console mode so we can restore it later\n                ConsoleMode consoleMode;\n                if (!Kernel32.GetConsoleMode(consoleMode: out consoleMode, consoleHandle: stream))\n                {\n                    Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), \"Failed to get initial console mode\", trace2);\n                }\n\n                _originalMode = consoleMode;\n\n                // Set desired echo state\n                _trace.WriteLine($\"Setting console echo state to '{echo}'\");\n                if (!echo)\n                {\n                    ConsoleMode newConsoleMode = consoleMode ^ ConsoleMode.EchoInput;\n                    if (!Kernel32.SetConsoleMode(consoleMode: newConsoleMode, consoleHandle: _stream))\n                    {\n                        Win32Error.ThrowIfError(\n                            Marshal.GetLastWin32Error(), \"Failed to set console mode\", trace2);\n                    }\n                }\n            }\n\n            public void Dispose()\n            {\n                if (_isDisposed)\n                {\n                    return;\n                }\n\n                // Restore original console mode\n                if (!Kernel32.SetConsoleMode(consoleMode: _originalMode, consoleHandle: _stream))\n                {\n                    _trace.WriteLine(\"Failed to restore console mode\");\n                }\n\n                _isDisposed = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/NameValueCollectionExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Collections.Specialized;\n\nnamespace GitCredentialManager\n{\n    public static class NameValueCollectionExtensions\n    {\n        public static IDictionary<string, string> ToDictionary(this NameValueCollection collection, IEqualityComparer<string> comparer = null)\n        {\n            var dict = new Dictionary<string, string>(comparer ?? StringComparer.Ordinal);\n\n            foreach (string key in collection.AllKeys)\n            {\n                dict[key] = collection[key];\n            }\n\n            return dict;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/NullCredentialStore.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace GitCredentialManager;\n\n/// <summary>\n/// Credential store that does nothing. This is useful when you want to disable internal credential storage\n/// and only use another helper configured in Git to store credentials.\n/// </summary>\npublic class NullCredentialStore : ICredentialStore\n{\n    public IList<string> GetAccounts(string service) => Array.Empty<string>();\n\n    public ICredential Get(string service, string account) => null;\n\n    public void AddOrUpdate(string service, string account, string secret) { }\n\n    public bool Remove(string service, string account) => false;\n}\n"
  },
  {
    "path": "src/shared/Core/PlaintextCredentialStore.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\n\nnamespace GitCredentialManager\n{\n    public class PlaintextCredentialStore : ICredentialStore\n    {\n        public PlaintextCredentialStore(IFileSystem fileSystem, string storeRoot, string @namespace = null)\n        {\n            EnsureArgument.NotNull(fileSystem, nameof(fileSystem));\n            EnsureArgument.NotNullOrWhiteSpace(storeRoot, nameof(storeRoot));\n\n            FileSystem = fileSystem;\n            StoreRoot = storeRoot;\n            Namespace = @namespace;\n        }\n\n        protected IFileSystem FileSystem { get; }\n        protected string StoreRoot { get; }\n        protected string Namespace { get; }\n        protected virtual string CredentialFileExtension => \".credential\";\n\n        public IList<string> GetAccounts(string service)\n        {\n            return Enumerate(service, null).Select(x => x.Account).Distinct().ToList();\n        }\n\n        public ICredential Get(string service, string account)\n        {\n            return Enumerate(service, account).FirstOrDefault();\n        }\n\n        public void AddOrUpdate(string service, string account, string secret)\n        {\n            // Ensure the store root exists and permissions are set\n            EnsureStoreRoot();\n\n            FileCredential existingCredential = Enumerate(service, account).FirstOrDefault();\n\n            // No need to update existing credential if nothing has changed\n            if (existingCredential != null &&\n                StringComparer.Ordinal.Equals(account, existingCredential.Account) &&\n                StringComparer.Ordinal.Equals(secret, existingCredential.Password))\n            {\n                return;\n            }\n\n            string serviceSlug = CreateServiceSlug(service);\n            string servicePath = Path.Combine(StoreRoot, serviceSlug);\n\n            if (!FileSystem.DirectoryExists(servicePath))\n            {\n                FileSystem.CreateDirectory(servicePath);\n            }\n\n            string fullPath = Path.Combine(servicePath, $\"{account}{CredentialFileExtension}\");\n            var credential = new FileCredential(fullPath, service, account, secret);\n            SerializeCredential(credential);\n        }\n\n        public bool Remove(string service, string account)\n        {\n            foreach (FileCredential credential in Enumerate(service, account))\n            {\n                // Only delete the first match\n                FileSystem.DeleteFile(credential.FullPath);\n                return true;\n            }\n\n            return false;\n        }\n\n        protected virtual bool TryDeserializeCredential(string path, out FileCredential credential)\n        {\n            string text;\n            using (var stream = FileSystem.OpenFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))\n            using (var reader = new StreamReader(stream))\n            {\n                text = reader.ReadToEnd();\n            }\n\n            int line1Idx = text.IndexOf(Environment.NewLine, StringComparison.OrdinalIgnoreCase);\n            if (line1Idx > 0)\n            {\n                // Password is the first line\n                string password = text.Substring(0, line1Idx);\n\n                // All subsequent lines are metadata/attributes\n                string attrText = text.Substring(line1Idx + Environment.NewLine.Length);\n                using var attrReader = new StringReader(attrText);\n                IDictionary<string, string> attrs = attrReader.ReadDictionary(StringComparer.OrdinalIgnoreCase);\n\n                // Account is optional\n                attrs.TryGetValue(\"account\", out string account);\n\n                // Service is required\n                if (attrs.TryGetValue(\"service\", out string service))\n                {\n                    credential = new FileCredential(path, service, account, password);\n                    return true;\n                }\n            }\n\n            credential = null;\n            return false;\n        }\n\n        protected virtual void SerializeCredential(FileCredential credential)\n        {\n            // Ensure the parent directory exists\n            string parentDir = Path.GetDirectoryName(credential.FullPath);\n            if (!FileSystem.DirectoryExists(parentDir))\n            {\n                FileSystem.CreateDirectory(parentDir);\n            }\n\n            using (var stream = FileSystem.OpenFileStream(credential.FullPath, FileMode.Create, FileAccess.Write, FileShare.None))\n            using (var writer = new StreamWriter(stream))\n            {\n                writer.WriteLine(credential.Password);\n                writer.WriteLine(\"service={0}\", credential.Service);\n                writer.WriteLine(\"account={0}\", credential.Account);\n                writer.Flush();\n            }\n        }\n\n        private IEnumerable<FileCredential> Enumerate(string service, string account)\n        {\n            string serviceSlug = CreateServiceSlug(service);\n            string searchPath = Path.Combine(StoreRoot, serviceSlug);\n            bool anyAccount = string.IsNullOrWhiteSpace(account);\n\n            if (!FileSystem.DirectoryExists(searchPath))\n            {\n                yield break;\n            }\n\n            IEnumerable<string> allFiles = FileSystem.EnumerateFiles(searchPath, $\"*{CredentialFileExtension}\");\n\n            foreach (string fullPath in allFiles)\n            {\n                string accountFile = Path.GetFileNameWithoutExtension(fullPath);\n                if (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, accountFile))\n                {\n                    // Validate the credential metadata also matches our search\n                    if (TryDeserializeCredential(fullPath, out FileCredential credential) &&\n                        StringComparer.OrdinalIgnoreCase.Equals(service, credential.Service) &&\n                        (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, credential.Account)))\n                    {\n                        yield return credential;\n                    }\n                }\n            }\n        }\n\n        /// <summary>\n        /// Ensure the store root directory exists. If it does not, create a new directory with\n        /// permissions that only permit the owner to read/write/execute. Permissions on an existing\n        /// directory are not modified.\n        /// </summary>\n        private void EnsureStoreRoot()\n        {\n            if (FileSystem.DirectoryExists(StoreRoot))\n            {\n                // Don't touch the permissions on the existing directory\n                return;\n            }\n\n            FileSystem.CreateDirectory(StoreRoot);\n\n            // We only set file system permissions on POSIX platforms\n            if (!PlatformUtils.IsPosix())\n            {\n                return;\n            }\n\n            // Set store root permissions such that only the owner can read/write/execute\n            var mode = Interop.Posix.Native.NativeFileMode.S_IRUSR |\n                       Interop.Posix.Native.NativeFileMode.S_IWUSR |\n                       Interop.Posix.Native.NativeFileMode.S_IXUSR;\n\n            // Ignore the return code.. this is a best effort only\n            Interop.Posix.Native.Stat.chmod(StoreRoot, mode);\n        }\n\n        private string CreateServiceSlug(string service)\n        {\n            var sb = new StringBuilder();\n            char sep = Path.DirectorySeparatorChar;\n\n            if (!string.IsNullOrWhiteSpace(Namespace))\n            {\n                sb.AppendFormat(\"{0}{1}\", Namespace, sep);\n            }\n\n            if (Uri.TryCreate(service, UriKind.Absolute, out Uri serviceUri))\n            {\n                sb.AppendFormat(\"{0}{1}\", serviceUri.Scheme, sep);\n                sb.AppendFormat(\"{0}\", serviceUri.Host);\n\n                if (!serviceUri.IsDefaultPort)\n                {\n                    sb.Append(PlatformUtils.IsWindows() ? '-' : ':');\n                    sb.Append(serviceUri.Port);\n                }\n\n                sb.Append(serviceUri.AbsolutePath.Replace('/', sep));\n            }\n            else\n            {\n                sb.Append(service);\n            }\n\n            return sb.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/PlatformUtils.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.InteropServices;\nusing GitCredentialManager.Interop.Posix.Native;\n\nnamespace GitCredentialManager\n{\n    public static class PlatformUtils\n    {\n        /// <summary>\n        /// Get information about the current platform (OS and CLR details).\n        /// </summary>\n        /// <returns>Platform information.</returns>\n        public static PlatformInformation GetPlatformInformation(ITrace2 trace2)\n        {\n            string osType = GetOSType();\n            string osVersion = GetOSVersion(trace2);\n            string cpuArch = GetCpuArchitecture();\n            string clrVersion = RuntimeInformation.FrameworkDescription;\n\n            return new PlatformInformation(osType, osVersion, cpuArch, clrVersion);\n        }\n\n        public static bool IsDevBox()\n        {\n            if (!IsWindows())\n            {\n                return false;\n            }\n\n#if NETFRAMEWORK\n            // Check for machine (HKLM) registry keys for Cloud PC indicators\n            // Note that the keys are only found in the 64-bit registry view\n            using (Microsoft.Win32.RegistryKey hklm64 = Microsoft.Win32.RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64))\n            using (Microsoft.Win32.RegistryKey w365Key = hklm64.OpenSubKey(Constants.WindowsRegistry.HKWindows365Path))\n            {\n                if (w365Key is null)\n                {\n                    // No Windows365 key exists\n                    return false;\n                }\n\n                object w365Value = w365Key.GetValue(Constants.WindowsRegistry.IsW365EnvironmentKeyName);\n                string partnerValue = w365Key.GetValue(Constants.WindowsRegistry.W365PartnerIdKeyName)?.ToString();\n\n                return w365Value is not null && Guid.TryParse(partnerValue, out Guid partnerId) && partnerId == Constants.DevBoxPartnerId;\n            }\n#else\n            return false;\n#endif\n        }\n\n        /// <summary>\n        /// Returns true if the current process is running on an ARM processor.\n        /// </summary>\n        /// <returns>True if ARM(v6,hf) or ARM64, false otherwise</returns>\n        public static bool IsArm()\n        {\n            switch (RuntimeInformation.OSArchitecture)\n            {\n                case Architecture.Arm:\n                case Architecture.Arm64:\n                    return true;\n                default:\n                    return false;\n            }\n        }\n\n        public static bool IsWindowsBrokerSupported()\n        {\n            if (!IsWindows())\n            {\n                return false;\n            }\n\n            // Implementation of version checking was taken from:\n            // https://github.com/dotnet/runtime/blob/6578f257e3be2e2144a65769706e981961f0130c/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs#L110-L122\n            //\n            // Note that we cannot use Environment.OSVersion in .NET Framework (or Core versions less than 5.0) as\n            // the implementation in those versions \"lies\" about Windows versions > 8.1 if there is no application manifest.\n            if (RtlGetVersionEx(out RTL_OSVERSIONINFOEX osvi) != 0)\n            {\n                return false;\n            }\n\n            // Windows major version 10 is required for WAM\n            if (osvi.dwMajorVersion < 10)\n            {\n                return false;\n            }\n\n            // Specific minimum build number is different between Windows Server and Client SKUs\n            const int minClientBuildNumber = 15063;\n            const int minServerBuildNumber = 17763; // Server 2019\n\n            switch (osvi.wProductType)\n            {\n                case VER_NT_WORKSTATION:\n                    return osvi.dwBuildNumber >= minClientBuildNumber;\n\n                case VER_NT_SERVER:\n                case VER_NT_DOMAIN_CONTROLLER:\n                    return osvi.dwBuildNumber >= minServerBuildNumber;\n            }\n\n            return false;\n        }\n\n        /// <summary>\n        /// Check if the current Operating System is macOS.\n        /// </summary>\n        /// <returns>True if running on macOS, false otherwise.</returns>\n        public static bool IsMacOS()\n        {\n            return RuntimeInformation.IsOSPlatform(OSPlatform.OSX);\n        }\n\n        /// <summary>\n        /// Check if the current Operating System is Windows.\n        /// </summary>\n        /// <returns>True if running on Windows, false otherwise.</returns>\n        public static bool IsWindows()\n        {\n            return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);\n        }\n\n        /// <summary>\n        /// Check if the current Operating System is Linux-based.\n        /// </summary>\n        /// <returns>True if running on a Linux distribution, false otherwise.</returns>\n        public static bool IsLinux()\n        {\n            return RuntimeInformation.IsOSPlatform(OSPlatform.Linux);\n        }\n\n        /// <summary>\n        /// Check if the current Operating System is POSIX-compliant.\n        /// </summary>\n        /// <returns>True if running on a POSIX-compliant Operating System, false otherwise.</returns>\n        public static bool IsPosix()\n        {\n            return IsMacOS() || IsLinux();\n        }\n\n        /// <summary>\n        /// Ensure the current Operating System is macOS, fail otherwise.\n        /// </summary>\n        /// <exception cref=\"PlatformNotSupportedException\">Thrown if the current OS is not macOS.</exception>\n        public static void EnsureMacOS()\n        {\n            if (!IsMacOS())\n            {\n                throw new PlatformNotSupportedException();\n            }\n        }\n\n        /// <summary>\n        /// Ensure the current Operating System is Windows, fail otherwise.\n        /// </summary>\n        /// <exception cref=\"PlatformNotSupportedException\">Thrown if the current OS is not Windows.</exception>\n        public static void EnsureWindows()\n        {\n            if (!IsWindows())\n            {\n                throw new PlatformNotSupportedException();\n            }\n        }\n\n        /// <summary>\n        /// Ensure the current Operating System is Linux-based, fail otherwise.\n        /// </summary>\n        /// <exception cref=\"PlatformNotSupportedException\">Thrown if the current OS is not Linux-based.</exception>\n        public static void EnsureLinux()\n        {\n            if (!IsLinux())\n            {\n                throw new PlatformNotSupportedException();\n            }\n        }\n\n        /// <summary>\n        /// Ensure the current Operating System is POSIX-compliant, fail otherwise.\n        /// </summary>\n        /// <exception cref=\"PlatformNotSupportedException\">Thrown if the current OS is not POSIX-compliant.</exception>\n        public static void EnsurePosix()\n        {\n            if (!IsPosix())\n            {\n                throw new PlatformNotSupportedException();\n            }\n        }\n\n        public static bool IsElevatedUser()\n        {\n            if (IsWindows())\n            {\n#if NETFRAMEWORK\n                var identity = System.Security.Principal.WindowsIdentity.GetCurrent();\n                var principal = new System.Security.Principal.WindowsPrincipal(identity);\n                return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);\n#endif\n            }\n            else if (IsPosix())\n            {\n                return Unistd.geteuid() == 0;\n            }\n\n            return false;\n        }\n\n        #region Platform Entry Path Utils\n\n        /// <summary>\n        /// Get the native entry executable absolute path.\n        /// </summary>\n        /// <returns>Entry absolute path or null if there was an error.</returns>\n        public static string GetNativeEntryPath()\n        {\n            try\n            {\n                if (IsWindows())\n                {\n                    return GetWindowsEntryPath();\n                }\n\n                if (IsMacOS())\n                {\n                    return GetMacOSEntryPath();\n                }\n\n                if (IsLinux())\n                {\n                    return GetLinuxEntryPath();\n                }\n            }\n            catch\n            {\n                // If there are any issues getting the native entry path\n                // we should not throw, and certainly not crash!\n                // Just return null instead.\n            }\n\n            return null;\n        }\n\n        private static string GetLinuxEntryPath()\n        {\n            // Try to extract our native argv[0] from the original cmdline\n            string cmdline = File.ReadAllText(\"/proc/self/cmdline\");\n            string argv0 = cmdline.Split('\\0')[0];\n\n            // argv[0] is an absolute file path\n            if (Path.IsPathRooted(argv0))\n            {\n                return argv0;\n            }\n\n            string path = Path.GetFullPath(\n                Path.Combine(Environment.CurrentDirectory, argv0)\n            );\n\n            // argv[0] is relative to current directory (./app) or a relative\n            // name resolved from the current directory (subdir/app).\n            // Note that we do NOT want to consider the case when it is just\n            // a simple filename (argv[0] == \"app\") because that would actually\n            // have been resolved from the $PATH instead (handled below)!\n            if ((argv0.StartsWith(\"./\") || argv0.IndexOf('/') > 0) && File.Exists(path))\n            {\n                return path;\n            }\n\n            // argv[0] is a name that was resolved from the $PATH\n            string pathVar = Environment.GetEnvironmentVariable(\"PATH\");\n            if (pathVar != null)\n            {\n                string[] paths = pathVar.Split(':');\n                foreach (string pathBase in paths)\n                {\n                    path = Path.Combine(pathBase, argv0);\n                    if (File.Exists(path))\n                    {\n                        return path;\n                    }\n                }\n            }\n\n#if NETFRAMEWORK\n            return null;\n#else\n            //\n            // We cannot determine the absolute file path from argv[0]\n            // (how we were launched), so let's now try to extract the\n            // fully resolved executable path from /proc/self/exe.\n            // Note that this means we may miss if we've been invoked\n            // via a symlink, but it's better than nothing at this point!\n            //\n            FileSystemInfo fsi = File.ResolveLinkTarget(\"/proc/self/exe\", returnFinalTarget: false);\n            return fsi?.FullName;\n#endif\n        }\n\n        private static string GetMacOSEntryPath()\n        {\n            // Determine buffer size by passing NULL initially\n            Interop.MacOS.Native.LibC._NSGetExecutablePath(IntPtr.Zero, out int size);\n\n            IntPtr bufPtr = Marshal.AllocHGlobal(size);\n            int result = Interop.MacOS.Native.LibC._NSGetExecutablePath(bufPtr, out size);\n\n            // buf is null-byte terminated\n            string name = result == 0 ? Marshal.PtrToStringAuto(bufPtr) : null;\n            Marshal.FreeHGlobal(bufPtr);\n\n            return name;\n        }\n\n        private static string GetWindowsEntryPath()\n        {\n            IntPtr argvPtr = Interop.Windows.Native.Shell32.CommandLineToArgvW(\n                Interop.Windows.Native.Kernel32.GetCommandLine(), out _);\n            IntPtr argv0Ptr = Marshal.ReadIntPtr(argvPtr);\n            string argv0 = Marshal.PtrToStringAuto(argv0Ptr);\n            Interop.Windows.Native.Kernel32.LocalFree(argvPtr);\n\n            // If this isn't absolute then we should return null to prevent any\n            // caller that expect only an absolute path from mis-using this result.\n            // They will have to fall-back to other mechanisms for getting the entry path.\n            return Path.IsPathRooted(argv0) ? argv0 : null;\n        }\n\n        #endregion\n\n        #region Platform information helper methods\n\n        private static string GetOSType()\n        {\n            if (IsWindows())\n            {\n                return \"Windows\";\n            }\n\n            if (IsMacOS())\n            {\n                return \"macOS\";\n            }\n\n            if (IsLinux())\n            {\n                return \"Linux\";\n            }\n\n            return \"Unknown\";\n        }\n\n        private static string _linuxDistroVersion;\n\n        private static string GetOSVersion(ITrace2 trace2)\n        {\n            //\n            // Since .NET 5 we can use Environment.OSVersion because it was updated to\n            // return the correct version on Windows & macOS, rather than the manifested\n            // version for Windows or the kernel version for macOS.\n            // https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version\n            //\n            // However, we still need to use the old method for Windows on .NET Framework\n            // and call into the Win32 API to get the correct version (regardless of app\n            // compatibility settings).\n#if NETFRAMEWORK\n            if (IsWindows() && RtlGetVersionEx(out RTL_OSVERSIONINFOEX osvi) == 0)\n            {\n                return $\"{osvi.dwMajorVersion}.{osvi.dwMinorVersion} (build {osvi.dwBuildNumber})\";\n            }\n#endif\n            if (IsWindows() || IsMacOS())\n            {\n                return Environment.OSVersion.Version.ToString();\n            }\n\n            if (IsLinux())\n            {\n                return _linuxDistroVersion ??= GetLinuxDistroVersion();\n\n                string GetLinuxDistroVersion()\n                {\n                    // Let's first try to get the distribution information from /etc/os-release\n                    // (or /usr/lib/os-release) which is required in systemd distributions.\n                    // https://www.freedesktop.org/software/systemd/man/os-release.html\n                    foreach (string osReleasePath in new[] { \"/etc/os-release\", \"/usr/lib/os-release\" })\n                    {\n                        if (!File.Exists(osReleasePath))\n                        {\n                            continue;\n                        }\n\n                        var props = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n                        char[] split = { '=' };\n                        string[] lines = File.ReadAllLines(osReleasePath);\n                        foreach (string line in lines)\n                        {\n                            // Each line is a key=\"value\" pair\n                            string[] kvp = line.Split(split, count: 2);\n                            if (kvp.Length != 2)\n                            {\n                                continue;\n                            }\n\n                            props[kvp[0]] = kvp[1].Trim('\"');\n                        }\n\n                        // Try to get the PRETTY_NAME first which is a user-friendly description\n                        // including the distro name and version.\n                        if (props.TryGetValue(\"PRETTY_NAME\", out string prettyName))\n                        {\n                            return prettyName;\n                        }\n\n                        // Fall-back to (NAME || ID) + (VERSION || VERSION_ID || VERSION_CODENAME)?\n                        if (props.TryGetValue(\"NAME\", out string distro) ||\n                            props.TryGetValue(\"ID\", out distro))\n                        {\n                            if (props.TryGetValue(\"VERSION\", out string version) ||\n                                props.TryGetValue(\"VERSION_ID\", out version) ||\n                                props.TryGetValue(\"VERSION_CODENAME\", out version))\n                            {\n                                return $\"{distro} {version}\";\n                            }\n\n                            // Return just the distro name if we don't have a version\n                            return distro;\n                        }\n                    }\n\n                    // If we couldn't get the distribution information from /etc/os-release\n                    // (for example if we're running on a non-systemd distribution), then let's\n                    // use `uname -a` to get at least some information.\n                    var psi = new ProcessStartInfo\n                    {\n                        FileName = \"uname\",\n                        Arguments = \"-a\",\n                        RedirectStandardOutput = true\n                    };\n\n                    using (var uname = new ChildProcess(trace2, psi))\n                    {\n                        uname.Start(Trace2ProcessClass.Other);\n                        uname.Process.WaitForExit();\n\n                        if (uname.ExitCode == 0)\n                        {\n                            return uname.StandardOutput.ReadToEnd().Trim();\n                        }\n                    }\n\n                    return \"Unknown-Linux\";\n                }\n            }\n\n            return \"Unknown\";\n        }\n\n        private static string GetCpuArchitecture()\n        {\n            switch (RuntimeInformation.OSArchitecture)\n            {\n                case Architecture.Arm:\n                    return \"ARM32\";\n                case Architecture.Arm64:\n                    return \"ARM64\";\n                case Architecture.X64:\n                    return \"x86-64\";\n                case Architecture.X86:\n                    return \"x86\";\n                default:\n                    return RuntimeInformation.OSArchitecture.ToString();\n            }\n        }\n\n        #endregion\n\n        #region Windows Native Version APIs\n\n        // Interop code sourced from the .NET Runtime as of version 5.0:\n        // https://github.com/dotnet/runtime/blob/6578f257e3be2e2144a65769706e981961f0130c/src/libraries/Common/src/Interop/Windows/NtDll/Interop.RtlGetVersion.cs\n\n        [DllImport(\"ntdll.dll\", ExactSpelling = true)]\n        private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation);\n\n        private static unsafe int RtlGetVersionEx(out RTL_OSVERSIONINFOEX osvi)\n        {\n            osvi = default;\n            osvi.dwOSVersionInfoSize = (uint)sizeof(RTL_OSVERSIONINFOEX);\n            return RtlGetVersion(ref osvi);\n        }\n\n        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\n        private unsafe struct RTL_OSVERSIONINFOEX\n        {\n            internal uint dwOSVersionInfoSize;\n            internal uint dwMajorVersion;\n            internal uint dwMinorVersion;\n            internal uint dwBuildNumber;\n            internal uint dwPlatformId;\n            internal fixed char szCSDVersion[128];\n            internal ushort wServicePackMajor;\n            internal ushort wServicePackMinor;\n            internal short wSuiteMask;\n            internal byte wProductType;\n            internal byte wReserved;\n        }\n\n        /// <summary>\n        /// The operating system is Windows client.\n        /// </summary>\n        private const byte VER_NT_WORKSTATION = 0x0000001;\n\n        /// <summary>\n        /// The system is a domain controller and the operating system is Windows Server.\n        /// </summary>\n        private const byte VER_NT_DOMAIN_CONTROLLER = 0x0000002;\n\n        /// <summary>\n        /// The operating system is Windows Server.\n        /// </summary>\n        /// <remarks>\n        /// A server that is also a domain controller is reported as VER_NT_DOMAIN_CONTROLLER, not VER_NT_SERVER.\n        /// </remarks>\n        private const byte VER_NT_SERVER = 0x0000003;\n\n        #endregion\n    }\n\n    public struct PlatformInformation\n    {\n        public PlatformInformation(string osType, string osVersion, string cpuArch, string clrVersion)\n        {\n            OperatingSystemType = osType;\n            OperatingSystemVersion = osVersion;\n            CpuArchitecture = cpuArch;\n            ClrVersion = clrVersion;\n        }\n\n        public readonly string OperatingSystemType;\n        public readonly string OperatingSystemVersion;\n        public readonly string CpuArchitecture;\n        public readonly string ClrVersion;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/ProcessManager.cs",
    "content": "using System;\nusing System.Diagnostics;\n\nnamespace GitCredentialManager;\n\npublic interface IProcessManager\n{\n    /// <summary>\n    /// Create a process ready to start.\n    /// </summary>\n    /// <param name=\"path\">Absolute file path of executable or command to start.</param>\n    /// <param name=\"args\">Command line arguments to pass to executable.</param>\n    /// <param name=\"useShellExecute\">\n    ///     True to resolve <paramref name=\"path\"/> using the OS shell, false to use as an absolute file path.\n    /// </param>\n    /// <param name=\"workingDirectory\">Working directory for the new process.</param>\n    /// <returns><see cref=\"Process\"/> object ready to start.</returns>\n    ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory);\n\n    /// <summary>\n    /// Create a process ready to start.\n    /// </summary>\n    /// <param name=\"psi\">Process start info.</param>\n    /// <returns><see cref=\"Process\"/> object ready to start.</returns>\n    ChildProcess CreateProcess(ProcessStartInfo psi);\n}\n\npublic class ProcessManager : IProcessManager\n{\n    private const string SidEnvar = \"GIT_TRACE2_PARENT_SID\";\n\n    protected readonly ITrace2 Trace2;\n\n    public static string Sid { get; internal set; }\n\n    public static int Depth { get; internal set; }\n\n    public ProcessManager(ITrace2 trace2)\n    {\n        EnsureArgument.NotNull(trace2, nameof(trace2));\n\n        Trace2 = trace2;\n    }\n\n    public virtual ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)\n    {\n        var psi = new ProcessStartInfo(path, args)\n        {\n            RedirectStandardInput = true,\n            RedirectStandardOutput = true,\n            RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled\n            UseShellExecute = useShellExecute,\n            WorkingDirectory = workingDirectory ?? string.Empty\n        };\n\n        return CreateProcess(psi);\n    }\n\n    public virtual ChildProcess CreateProcess(ProcessStartInfo psi)\n    {\n        return new ChildProcess(Trace2, psi);\n    }\n\n    /// <summary>\n    /// Create a TRACE2 \"session id\" (sid) for this process.\n    /// </summary>\n    public static void CreateSid()\n    {\n        Sid = Environment.GetEnvironmentVariable(SidEnvar);\n\n        if (!string.IsNullOrEmpty(Sid))\n        {\n            // Use trim to ensure no accidental leading or trailing slashes\n            Sid = $\"{Sid.Trim('/')}/{Guid.NewGuid():D}\";\n            // Only check for process depth if there is a parent.\n            // If there is not a parent, depth defaults to 0.\n            Depth = GetProcessDepth();\n        }\n        else\n        {\n            // We are the root process; create our own 'root' SID\n            Sid = Guid.NewGuid().ToString(\"D\");\n        }\n\n        Environment.SetEnvironmentVariable(SidEnvar, Sid);\n    }\n\n    /// <summary>\n    /// Get \"depth\" of current process relative to top-level GCM process.\n    /// </summary>\n    /// <returns>Depth of current process.</returns>\n    internal static int GetProcessDepth()\n    {\n        char processSeparator = '/';\n\n        int count = 0;\n        // Use AsSpan() for slight performance bump over traditional foreach loop.\n        foreach (var c in Sid.AsSpan())\n        {\n            if (c == processSeparator)\n                count++;\n        }\n\n        return count;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Settings.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing KnownEnvars = GitCredentialManager.Constants.EnvironmentVariables;\nusing KnownGitCfg = GitCredentialManager.Constants.GitConfiguration;\nusing GitCredCfg  = GitCredentialManager.Constants.GitConfiguration.Credential;\nusing GitHttpCfg  = GitCredentialManager.Constants.GitConfiguration.Http;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Component that represents settings for Git Credential Manager as found from the environment and Git configuration.\n    /// Setting values from Git configuration may be cached for performance reasons.\n    /// </summary>\n    public interface ISettings : IDisposable\n    {\n        /// <summary>\n        /// Try and get the value of a specified setting as specified in the environment and Git configuration,\n        /// with the environment taking precedence over Git.\n        /// </summary>\n        /// <param name=\"envarName\">Optional environment variable name.</param>\n        /// <param name=\"section\">Optional Git configuration section name.</param>\n        /// <param name=\"property\">Git configuration property name. Required if <paramref name=\"section\"/> is set, optional otherwise.</param>\n        /// <param name=\"value\">Value of the requested setting.</param>\n        /// <returns>True if a setting value was found, false otherwise.</returns>\n        bool TryGetSetting(string envarName, string section, string property, out string value);\n\n        /// <summary>\n        /// Try and get the value of a specified setting as specified in the environment and Git configuration,\n        /// with the environment taking precedence over Git. If the value is pulled from the Git configuration,\n        /// it is returned as a canonical path.\n        /// </summary>\n        /// <param name=\"envarName\">Optional environment variable name.</param>\n        /// <param name=\"section\">Optional Git configuration section name.</param>\n        /// <param name=\"property\">Git configuration property name. Required if <paramref name=\"section\"/> is set, optional otherwise.</param>\n        /// <param name=\"value\">Value of the requested setting as a canonical path.</param>\n        /// <returns>True if a setting value was found, false otherwise.</returns>\n        bool TryGetPathSetting(string envarName, string section, string property, out string value);\n\n        /// <summary>\n        /// Try and get the all values of a specified setting as specified in the environment and Git configuration,\n        /// in the correct order or precedence.\n        /// </summary>\n        /// <param name=\"envarName\">Optional environment variable name.</param>\n        /// <param name=\"section\">Optional Git configuration section name.</param>\n        /// <param name=\"property\">Git configuration property name. Required if <paramref name=\"section\"/> is set, optional otherwise.</param>\n        /// <param name=\"isPath\">Whether the returned values should be transformed into canonical paths.</param>\n        /// <returns>All values for the specified setting, in order of precedence, or an empty collection if no such values are set.</returns>\n        IEnumerable<string> GetSettingValues(string envarName, string section, string property, bool isPath);\n\n        /// <summary>\n        /// Git remote address that setting lookup is scoped to, or null if no remote URL has been discovered.\n        /// </summary>\n        Uri RemoteUri { get; set; }\n\n        /// <summary>\n        /// True if debugging is enabled, false otherwise.\n        /// </summary>\n        bool IsDebuggingEnabled { get; }\n\n        /// <summary>\n        /// True if terminal prompting is enabled, false otherwise.\n        /// </summary>\n        bool IsTerminalPromptsEnabled { get; }\n\n        /// <summary>\n        /// True if GUI prompts are enabled, false otherwise.\n        /// </summary>\n        /// <remarks>\n        /// If GUI prompts are disabled but an equivalent terminal prompt is available, the latter will be used instead.\n        /// </remarks>\n        bool IsGuiPromptsEnabled { get; set; }\n\n        /// <summary>\n        /// True if it is permitted to interact with the user, false otherwise.\n        /// </summary>\n        /// <remarks>\n        /// If this value is false but interactivity is required to continue execution, the caller should\n        /// abort the operation and report failure.\n        /// </remarks>\n        bool IsInteractionAllowed { get; }\n\n        /// <summary>\n        /// Get if tracing has been enabled, returning trace setting value in the out parameter.\n        /// </summary>\n        /// <param name=\"value\">Trace setting value.</param>\n        /// <returns>True if tracing is enabled, false otherwise.</returns>\n        bool GetTracingEnabled(out string value);\n\n        /// <summary>\n        /// True if tracing of secrets and sensitive information is enabled, false otherwise.\n        /// </summary>\n        bool IsSecretTracingEnabled { get; }\n\n        /// <summary>\n        /// True if MSAL tracing is enabled, false otherwise.\n        /// </summary>\n        bool IsMsalTracingEnabled { get; }\n\n        /// <summary>\n        /// Get the host provider configured to override auto-detection if set, null otherwise.\n        /// </summary>\n        string ProviderOverride { get; }\n\n        /// <summary>\n        /// Get the authority name configured to override host provider auto-detection if set, null otherwise.\n        /// </summary>\n        string LegacyAuthorityOverride { get; }\n\n        /// <summary>\n        /// True if Windows Integrated Authentication (NTLM, Kerberos) should be detected and used if available, false otherwise.\n        /// </summary>\n        bool IsWindowsIntegratedAuthenticationEnabled { get; }\n\n        /// <summary>\n        /// True if certificate verification should occur, false otherwise.\n        /// </summary>\n        bool IsCertificateVerificationEnabled { get; }\n\n        /// <summary>\n        /// Automatically send client TLS certificates.\n        /// </summary>\n        bool AutomaticallyUseClientCertificates { get; }\n\n        /// <summary>\n        /// Get the proxy setting if configured, or null otherwise.\n        /// </summary>\n        /// <returns>Proxy setting, or null if not configured.</returns>\n        ProxyConfiguration GetProxyConfiguration();\n\n        /// <summary>\n        /// The parent window handle/ID. Used to correctly position and parent dialogs generated by GCM.\n        /// </summary>\n        /// <remarks>This value is platform specific.</remarks>\n        string ParentWindowId { get; }\n\n        /// <summary>\n        /// Credential storage namespace prefix.\n        /// </summary>\n        /// <remarks>The default value is \"git\" if unset.</remarks>\n        string CredentialNamespace { get; }\n\n        /// <summary>\n        /// Credential backing store override.\n        /// </summary>\n        string CredentialBackingStore { get; }\n\n        /// <summary>\n        /// Optional path to a file containing one or more certificates that should\n        /// be used *exclusively* when verifying server certificate chains.\n        /// </summary>\n        /// <remarks>The default value is null if unset.</remarks>\n        string CustomCertificateBundlePath { get; }\n\n        /// <summary>\n        // Optional path to a file containing one or more cookies.\n        /// </summary>\n        /// <remarks>The default value is null if unset.</remarks>\n        string CustomCookieFilePath { get; }\n\n        /// <summary>\n        /// The SSL/TLS backend.\n        /// </summary>\n        TlsBackend TlsBackend { get; }\n\n        /// <summary>\n        /// True if, when using an schannel backend, using certificates from the\n        /// CustomCertificateBundlePath is allowed.\n        /// </summary>\n        /// <remarks>The default value is false if unset.</remarks>\n        bool UseCustomCertificateBundleWithSchannel { get; }\n\n        /// <summary>\n        /// Maximum number of milliseconds to wait for a network response when probing a remote URL for the purpose\n        /// of host provider auto-detection. Use a zero or negative value to disable probing.\n        /// </summary>\n        int AutoDetectProviderTimeout { get; }\n\n        /// <summary>\n        /// Automatically use the default/current operating system account if no other account information is given\n        /// for Microsoft Authentication.\n        /// </summary>\n        bool UseMsAuthDefaultAccount { get; }\n\n        /// <summary>\n        /// True if software rendering should be used for graphical user interfaces, false otherwise.\n        /// </summary>\n        bool UseSoftwareRendering { get; }\n\n        /// <summary>\n        /// Permit the use of unsafe remotes URLs such as regular HTTP.\n        /// </summary>\n        bool AllowUnsafeRemotes { get; }\n\n        /// <summary>\n        /// Get TRACE2 settings.\n        /// </summary>\n        /// <returns>TRACE2 settings object.</returns>\n        Trace2Settings GetTrace2Settings();\n    }\n\n    public class ProxyConfiguration\n    {\n        public ProxyConfiguration(\n            Uri proxyAddress,\n            string userName = null,\n            string password = null,\n            string noProxyRaw = null,\n            bool isDeprecatedSource = false)\n        {\n            Address = proxyAddress;\n            UserName = userName;\n            Password = password;\n            NoProxyRaw = noProxyRaw;\n            IsDeprecatedSource = isDeprecatedSource;\n        }\n\n        /// <summary>\n        /// True if the proxy configuration method is deprecated, false otherwise.\n        /// </summary>\n        public bool IsDeprecatedSource { get; }\n\n        /// <summary>\n        /// Configured proxy URI (proxy server address and optional user authentication information).\n        /// </summary>\n        public Uri Address { get; }\n\n        /// <summary>\n        /// User name to use to authenticate to the proxy address.\n        /// </summary>\n        public string UserName { get; }\n\n        /// <summary>\n        /// Password to use to authenticate to the proxy address.\n        /// </summary>\n        public string Password { get; }\n\n        /// <summary>\n        /// List of host names that should not be proxied.\n        /// </summary>\n        /// <remarks>\n        /// This is the raw value from the NO_PROXY setting. Values are expected to be in a libcurl compatible format.\n        /// <para/>\n        /// To convert the string in to a set of .NET regular expressions for use with proxy settings,\n        /// use the <see cref=\"ConvertToBypassRegexArray\"/> method.\n        /// </remarks>\n        public string NoProxyRaw { get; }\n\n        /// <summary>\n        /// Convert a libcurl-format NO_PROXY string in to a set of equivalent .NET regular expressions.\n        /// </summary>\n        /// <param name=\"noProxy\">NO_PROXY value in a libcurl-compatible format.</param>\n        /// <returns>Array of regular expressions.</returns>\n        public static IEnumerable<string> ConvertToBypassRegexArray(string noProxy)\n        {\n            if (string.IsNullOrWhiteSpace(noProxy))\n            {\n                yield break;\n            }\n\n            string[] split = noProxy.Split(new[] { \",\", \" \" }, StringSplitOptions.RemoveEmptyEntries);\n\n            var normalized = new StringBuilder();\n            var regex = new StringBuilder();\n            foreach (string str in split)\n            {\n                // Normalize the domain search value\n                normalized.Clear();\n                normalized.Append(str);\n\n                // Strip leading subdomain wildcards: *.example.com => example.com\n                if (normalized.Length > 1 && normalized[0] == '*' && normalized[1] == '.')\n                {\n                    normalized.Remove(0, 2);\n                }\n\n                // Strip all leading dots: .example.com => example.com\n                while (normalized.Length > 0 && normalized[0] == '.')\n                {\n                    normalized.Remove(0, 1);\n                }\n\n                // Build the regular expression\n                regex.Clear();\n\n                // Only match (sub-)domains, not partial domain names.\n                // For example: \"example.com\" should match \"http://example.com\" and\n                // \"http://www.example.com\" but not \"http://notanexample.com\".\n                regex.Append(@\"(\\.|\\:\\/\\/)\");\n\n                // Add the escaped domain search value\n                regex.Append(Regex.Escape(normalized.ToString()));\n\n                // Ensure we only match the specified port and TLD\n                regex.Append('$');\n\n                yield return regex.ToString();\n            }\n        }\n    }\n\n    public enum TlsBackend\n    {\n        OpenSsl,\n        Schannel,\n        Other,\n    }\n\n    public class Settings : ISettings\n    {\n        private readonly IEnvironment _environment;\n        private readonly IGit _git;\n\n        private Dictionary<string,string> _configEntries;\n\n        public Settings(IEnvironment environment, IGit git)\n        {\n            EnsureArgument.NotNull(environment, nameof(environment));\n            EnsureArgument.NotNull(git, nameof(git));\n\n            _environment = environment;\n            _git = git;\n        }\n\n        public bool TryGetSetting(string envarName, string section, string property, out string value)\n        {\n            IEnumerable<string> allValues = GetSettingValues(envarName, section, property, false);\n\n            value = allValues.FirstOrDefault();\n\n            return value != null;\n        }\n\n        public bool TryGetPathSetting(string envarName, string section, string property, out string value)\n        {\n            IEnumerable<string> allValues = GetSettingValues(envarName, section, property, true);\n\n            value = allValues.FirstOrDefault();\n\n            return value != null;\n        }\n\n        public IEnumerable<string> GetSettingValues(string envarName, string section, string property, bool isPath)\n        {\n            string value;\n\n            if (envarName != null)\n            {\n                if (_environment.Variables.TryGetValue(envarName, out value))\n                {\n                    yield return value;\n                }\n            }\n\n            if (section != null && property != null)\n            {\n                IGitConfiguration config = _git.GetConfiguration();\n\n                //\n                // Enumerate all configuration entries for all sections and property names and make a\n                // local copy of them here to avoid needing to call `TryGetValue` on the IGitConfiguration\n                // object multiple times in a loop below.\n                //\n                // This is a performance optimisation to avoid calling `TryGet` on the IGitConfiguration\n                // object multiple times in a loop below, or each time this entire method is called.\n                // The assumption is that the configuration entries will not change during a single invocation\n                // of Git Credential Manager, which is reasonable given process lifetime is typically less\n                // than a few seconds. For some entries (type=path), we still need to ask Git in order to\n                // expand the path correctly.\n                //\n                if (_configEntries is null)\n                {\n                    _configEntries = new Dictionary<string, string>(GitConfigurationKeyComparer.Instance);\n                    config.Enumerate(entry =>\n                    {\n                        _configEntries[entry.Key] = entry.Value;\n\n                        // Continue the enumeration\n                        return true;\n                    });\n                }\n\n                if (RemoteUri != null)\n                {\n                    /*\n                     * Look for URL scoped \"section\" configuration entries, starting from the most specific\n                     * down to the least specific (stopping before the TLD).\n                     *\n                     * In a divergence from standard Git configuration rules, we also consider matching URL scopes\n                     * without a scheme (\"protocol://\").\n                     *\n                     * For each level of scope, we look for an entry with the scheme included (the default), and then\n                     * also one without it specified. This allows you to have one configuration scope for both \"http\" and\n                     * \"https\" without needing to repeat yourself, for example.\n                     *\n                     * For example, starting with \"https://foo.example.com/bar/buzz\" we have:\n                     *\n                     *   1a. [section \"https://foo.example.com/bar/buzz\"]\n                     *          property = value\n                     *\n                     *   1b. [section \"foo.example.com/bar/buzz\"]\n                     *          property = value\n                     *\n                     *   2a. [section \"https://foo.example.com/bar\"]\n                     *          property = value\n                     *\n                     *   2b. [section \"foo.example.com/bar\"]\n                     *          property = value\n                     *\n                     *   3a. [section \"https://foo.example.com\"]\n                     *          property = value\n                     *\n                     *   3b. [section \"foo.example.com\"]\n                     *          property = value\n                     *\n                     *   4a. [section \"https://example.com\"]\n                     *          property = value\n                     *\n                     *   4b. [section \"example.com\"]\n                     *          property = value\n                     *\n                     * It is also important to note that although the section and property names are NOT case\n                     * sensitive, the \"scope\" part IS case sensitive! We must be careful when searching to ensure\n                     * we follow Git's rules.\n                     *\n                     */\n\n                    foreach (string scope in RemoteUri.GetGitConfigurationScopes())\n                    {\n                        string queryName = $\"{section}.{scope}.{property}\";\n                        // Look for a scoped entry that includes the scheme \"protocol://example.com\" first as\n                        // this is more specific. If `isPath` is true, then re-get the value from the\n                        // `GitConfiguration` with `isPath` specified.\n                        if ((isPath && config.TryGet(queryName, true, out value)) ||\n                            _configEntries.TryGetValue(queryName, out value))\n                        {\n                            yield return value;\n                        }\n\n                        // Now look for a scoped entry that omits the scheme \"example.com\" second as this is less\n                        // specific. As above, if `isPath` is true, get the configuration setting again with\n                        // `isPath` specified.\n                        string scopeWithoutScheme = scope.TrimUntilIndexOf(Uri.SchemeDelimiter);\n                        string queryWithSchemeName = $\"{section}.{scopeWithoutScheme}.{property}\";\n                        if ((isPath && config.TryGet(queryWithSchemeName, true, out value)) ||\n                            _configEntries.TryGetValue(queryWithSchemeName, out value))\n                        {\n                            yield return value;\n                        }\n\n                        // Check for an externally specified default value\n                        if (TryGetExternalDefault(section, scope, property, out value))\n                        {\n                            yield return value;\n                        }\n                    }\n                }\n\n                /*\n                 * Try to look for an un-scoped \"section\" property setting:\n                 *\n                 *    [section]\n                 *        property = value\n                 *\n                 */\n                string name = $\"{section}.{property}\";\n                if ((isPath && config.TryGet(name, true, out value)) ||\n                    _configEntries.TryGetValue(name, out value))\n                {\n                    yield return value;\n                }\n\n                // Check for an externally specified default value without a scope\n                if (TryGetExternalDefault(section, null, property, out value))\n                {\n                    yield return value;\n                }\n            }\n        }\n\n        /// <summary>\n        /// Try to get the default value for a configuration setting.\n        /// This may come from external policies or the Operating System.\n        /// </summary>\n        /// <param name=\"section\">Configuration section name.</param>\n        /// <param name=\"scope\">Optional configuration scope.</param>\n        /// <param name=\"property\">Configuration property name.</param>\n        /// <param name=\"value\">Value of the configuration setting, or null.</param>\n        /// <returns>True if a default setting has been set, false otherwise.</returns>\n        protected internal virtual bool TryGetExternalDefault(string section, string scope, string property, out string value)\n        {\n            value = null;\n            return false;\n        }\n\n        public Uri RemoteUri { get; set; }\n\n        public bool IsDebuggingEnabled =>\n            TryGetSetting(KnownEnvars.GcmDebug,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.Debug,\n                out string str) && str.IsTruthy();\n\n        public bool IsTerminalPromptsEnabled => _environment.Variables.GetBooleanyOrDefault(KnownEnvars.GitTerminalPrompts, true);\n\n        public bool IsGuiPromptsEnabled\n        {\n            get\n            {\n                const bool defaultValue = true;\n\n                if (TryGetSetting(\n                        KnownEnvars.GcmGuiPromptsEnabled,\n                        KnownGitCfg.Credential.SectionName,\n                        KnownGitCfg.Credential.GuiPromptsEnabled,\n                        out string str))\n                {\n                    return str.ToBooleanyOrDefault(defaultValue);\n                }\n\n                return defaultValue;\n            }\n            set => _environment.SetEnvironmentVariable(KnownEnvars.GcmGuiPromptsEnabled, value ? bool.TrueString : bool.FalseString);\n        }\n\n        public bool IsInteractionAllowed\n        {\n            get\n            {\n                const bool defaultValue = true;\n                if (TryGetSetting(KnownEnvars.GcmInteractive, GitCredCfg.SectionName, GitCredCfg.Interactive, out string value))\n                {\n                    /*\n                     * COMPAT: In the previous GCM we accepted the values 'auto', 'never', and 'always'.\n                     *\n                     * We've slightly changed the behaviour of this setting in GCM to essentially\n                     * remove the 'always' option. The table below outlines the changes:\n                     *\n                     * -------------------------------------------------------------\n                     * | Value(s) | Old meaning               | New meaning        |\n                     * |-----------------------------------------------------------|\n                     * | auto     | Prompt if required        | [unchanged]        |\n                     * |-----------------------------------------------------------|\n                     * | never    | Never prompt ─ fail if    | [unchanged]        |\n                     * | false    | interaction is required   |                    |\n                     * |-----------------------------------------------------------|\n                     * | always   | Always prompt ─ don't use | Prompt if required |\n                     * | force    | cached credentials        |                    |\n                     * | true     |                           |                    |\n                     * -------------------------------------------------------------\n                     */\n                    if (StringComparer.OrdinalIgnoreCase.Equals(\"never\", value))\n                    {\n                        return false;\n                    }\n\n                    return value.ToBooleanyOrDefault(defaultValue);\n                }\n\n                return defaultValue;\n            }\n        }\n\n        public bool GetTracingEnabled(out string value) =>\n            TryGetSetting(KnownEnvars.GcmTrace,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.Trace,\n                out value) && !value.IsFalsey();\n\n        public bool UseSoftwareRendering\n        {\n            get\n            {\n                // WORKAROUND: Some Windows ARM devices have a graphics driver issue that causes transparent windows\n                // when using hardware rendering. Until this is fixed, we will default to software rendering on these\n                // devices. Users can always override this setting back to HW-accelerated rendering if they wish.\n                bool defaultValue = PlatformUtils.IsWindows() && PlatformUtils.IsArm();\n\n                return TryGetSetting(KnownEnvars.GcmGuiSoftwareRendering,\n                    KnownGitCfg.Credential.SectionName,\n                    KnownGitCfg.Credential.GuiSoftwareRendering,\n                    out string str) ? str.ToBooleanyOrDefault(defaultValue) : defaultValue;\n            }\n        }\n\n        public bool AllowUnsafeRemotes =>\n            TryGetSetting(KnownEnvars.GcmAllowUnsafeRemotes,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.AllowUnsafeRemotes,\n                out string str) && str.ToBooleanyOrDefault(false);\n\n        public Trace2Settings GetTrace2Settings()\n        {\n            var settings = new Trace2Settings();\n\n            if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Event, KnownGitCfg.Trace2.SectionName,\n                    Constants.GitConfiguration.Trace2.EventTarget, out string value))\n            {\n                settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Event, value);\n            }\n\n            if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Normal, KnownGitCfg.Trace2.SectionName,\n                    Constants.GitConfiguration.Trace2.NormalTarget, out value))\n            {\n                settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Normal, value);\n            }\n\n            if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Performance, KnownGitCfg.Trace2.SectionName,\n                    Constants.GitConfiguration.Trace2.PerformanceTarget, out value))\n            {\n                settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Performance, value);\n            }\n\n            return settings;\n        }\n\n        public bool IsSecretTracingEnabled =>\n            TryGetSetting(KnownEnvars.GcmTraceSecrets,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.TraceSecrets,\n                out string str) && str.IsTruthy();\n\n        public bool IsMsalTracingEnabled =>\n            TryGetSetting(KnownEnvars.GcmTraceMsAuth,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.TraceMsAuth,\n                out string str) && str.IsTruthy();\n\n        public string ProviderOverride =>\n            TryGetSetting(KnownEnvars.GcmProvider, GitCredCfg.SectionName, GitCredCfg.Provider, out string providerId) ? providerId : null;\n\n        public string LegacyAuthorityOverride =>\n            TryGetSetting(KnownEnvars.GcmAuthority, GitCredCfg.SectionName, GitCredCfg.Authority, out string authority) ? authority : null;\n\n        public bool IsWindowsIntegratedAuthenticationEnabled =>\n            !TryGetSetting(KnownEnvars.GcmAllowWia, GitCredCfg.SectionName, GitCredCfg.AllowWia, out string value) || value.ToBooleanyOrDefault(true);\n\n        public bool IsCertificateVerificationEnabled\n        {\n            get\n            {\n                // Prefer environment variable\n                if (_environment.Variables.TryGetValue(KnownEnvars.GitSslNoVerify, out string envarValue))\n                {\n                    return !envarValue.ToBooleanyOrDefault(false);\n                }\n\n                // Next try the equivalent Git configuration option\n                if (TryGetSetting(null, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SslVerify, out string cfgValue))\n                {\n                    return cfgValue.ToBooleanyOrDefault(true);\n                }\n\n                // Safe default\n                return true;\n            }\n        }\n\n        public bool AutomaticallyUseClientCertificates =>\n            TryGetSetting(null, KnownGitCfg.Credential.SectionName, KnownGitCfg.Http.SslAutoClientCert, out string value) && value.ToBooleanyOrDefault(false);\n\n        public string CustomCertificateBundlePath =>\n            TryGetPathSetting(KnownEnvars.GitSslCaInfo, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SslCaInfo, out string value) ? value : null;\n\n        public string CustomCookieFilePath =>\n            TryGetPathSetting(null, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.CookieFile, out string value) ? value : null;\n\n        public TlsBackend TlsBackend =>\n            TryGetSetting(null, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SslBackend, out string config)\n                ? (Enum.TryParse(config, true, out TlsBackend backend) ? backend : GitCredentialManager.TlsBackend.Other)\n                : default(TlsBackend);\n\n        public bool UseCustomCertificateBundleWithSchannel =>\n            TryGetSetting(null, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SchannelUseSslCaInfo, out string schannelUseSslCaInfo) &&\n                schannelUseSslCaInfo.ToBooleanyOrDefault(false);\n\n        public int AutoDetectProviderTimeout\n        {\n            get\n            {\n                if (TryGetSetting(KnownEnvars.GcmAutoDetectTimeout,\n                        KnownGitCfg.Credential.SectionName,\n                        KnownGitCfg.Credential.AutoDetectTimeout,\n                        out string valueStr) &&\n                    ConvertUtils.TryToInt32(valueStr, out int value))\n                {\n                    return value;\n                }\n\n                return Constants.DefaultAutoDetectProviderTimeoutMs;\n            }\n        }\n\n        public ProxyConfiguration GetProxyConfiguration()\n        {\n            bool TryGetUriSetting(string envarName, string section, string property, out Uri uri)\n            {\n                IEnumerable<string> allValues = GetSettingValues(envarName, section, property, false);\n\n                foreach (var value in allValues)\n                {\n                    if (Uri.TryCreate(value, UriKind.Absolute, out uri))\n                    {\n                        return true;\n                    }\n                    else if (string.IsNullOrWhiteSpace(value))\n                    {\n                        // An empty string value means \"no proxy\"\n                        return false;\n                    }\n                }\n\n                uri = null;\n                return false;\n            }\n\n            char[] bypassListSeparators = {',', ' '};\n\n            ProxyConfiguration CreateConfiguration(Uri uri, bool isLegacy = false)\n            {\n                // Strip the userinfo, query, and fragment parts of the Uri retaining only the scheme, host, port, and path\n                Uri address = new UriBuilder(uri)\n                {\n                    UserName = string.Empty,\n                    Password = string.Empty,\n                    Query    = string.Empty,\n                    Fragment = string.Empty,\n                }.Uri;\n\n                // Extract the username and password from the URI if present\n                uri.TryGetUserInfo(out string userName, out string password);\n\n                // Get the proxy bypass host names\n                // Check both lowercase \"no_proxy\" and uppercase \"NO_PROXY\" variants (preferring the former)\n                if (!_environment.Variables.TryGetValue(KnownEnvars.CurlNoProxy, out string noProxyStr))\n                {\n                    _environment.Variables.TryGetValue(KnownEnvars.CurlNoProxyUpper, out noProxyStr);\n                }\n\n                return new ProxyConfiguration(address, userName, password, noProxyStr, isLegacy);\n            }\n\n            /*\n             * There are several different ways we support the configuration of a proxy.\n             *\n             * In order of preference:\n             *\n             *   1. GCM proxy Git configuration (deprecated)\n             *        credential.httpsProxy\n             *        credential.httpProxy\n             *\n             *   2. Standard Git configuration\n             *        http.proxy\n             *\n             *   3. cURL environment variables\n             *        https_proxy\n             *        HTTPS_PROXY\n             *        http_proxy (note that uppercase HTTP_PROXY is not supported by libcurl)\n             *        all_proxy\n             *        ALL_PROXY\n             *\n             *   4. GCM proxy environment variable (deprecated)\n             *        GCM_HTTP_PROXY\n             *\n             * If the remote URI is HTTPS we check the HTTPS variants first, and fallback to the\n             * non-secure HTTP options if not found.\n             *\n             * For HTTP URIs we only check the HTTP variants.\n             *\n             * We also support the cURL \"no_proxy\" / \"NO_PROXY\" environment variables in conjunction with any\n             * of the above supported proxy address configurations. This comma separated list of\n             * host names (or host name wildcards) should be respected and the proxy should NOT\n             * be used for these addresses.\n             *\n             */\n\n            bool isHttps = StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttps, RemoteUri?.Scheme);\n\n            Uri proxyUri;\n\n            // 1. GCM proxy Git configuration (deprecated)\n            if (isHttps && TryGetUriSetting(null, GitCredCfg.SectionName, GitCredCfg.HttpsProxy, out proxyUri) ||\n                TryGetUriSetting(null, GitCredCfg.SectionName, GitCredCfg.HttpProxy, out proxyUri))\n            {\n                return CreateConfiguration(proxyUri, isLegacy: true);\n            }\n\n            // 2. Standard Git configuration\n            if (TryGetUriSetting(null, GitHttpCfg.SectionName, GitHttpCfg.Proxy, out proxyUri))\n            {\n                return CreateConfiguration(proxyUri);\n            }\n\n            // 3. cURL environment variables (both lower- and uppercase variants)\n            // Prefer the lowercase variants as these are quasi-standard.\n            if (isHttps && TryGetUriSetting(KnownEnvars.CurlHttpsProxy, null, null, out proxyUri) ||\n                isHttps && TryGetUriSetting(KnownEnvars.CurlHttpsProxyUpper, null, null, out proxyUri) ||\n                TryGetUriSetting(KnownEnvars.CurlHttpProxy, null, null, out proxyUri) ||\n                // Note that the uppercase HTTP_PROXY is not recognized by libcurl\n                TryGetUriSetting(KnownEnvars.CurlAllProxy, null, null, out proxyUri) ||\n                TryGetUriSetting(KnownEnvars.CurlAllProxyUpper, null, null, out proxyUri))\n            {\n                return CreateConfiguration(proxyUri);\n            }\n\n            // 4. GCM proxy environment variable (deprecated)\n            if (TryGetUriSetting(KnownEnvars.GcmHttpProxy, null, null, out proxyUri))\n            {\n                return CreateConfiguration(proxyUri, isLegacy: true);\n            }\n\n            return null;\n        }\n\n        public string ParentWindowId => _environment.Variables.TryGetValue(KnownEnvars.GcmParentWindow, out string parentWindowId) ? parentWindowId : null;\n\n        public string CredentialNamespace =>\n            TryGetSetting(KnownEnvars.GcmCredNamespace,\n                KnownGitCfg.Credential.SectionName, KnownGitCfg.Credential.CredNamespace,\n                out string @namespace)\n                ? @namespace\n                : Constants.DefaultCredentialNamespace;\n\n        public string CredentialBackingStore =>\n            TryGetSetting(\n                KnownEnvars.GcmCredentialStore,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.CredentialStore,\n                out string credStore)\n                ? credStore\n                : null;\n\n        public bool UseMsAuthDefaultAccount =>\n            TryGetSetting(\n                KnownEnvars.MsAuthUseDefaultAccount,\n                KnownGitCfg.Credential.SectionName,\n                KnownGitCfg.Credential.MsAuthUseDefaultAccount,\n                out string str)\n            ? str.IsTruthy()\n            : PlatformUtils.IsDevBox(); // default to true in DevBox environment\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            // Do nothing\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/StandardStreams.cs",
    "content": "using System;\nusing System.IO;\nusing System.Text;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents the standard I/O streams (input, output, error) of a process.\n    /// </summary>\n    public interface IStandardStreams\n    {\n        /// <summary>\n        /// The standard input text stream from the calling process, typically Git.\n        /// </summary>\n        TextReader In { get; }\n\n        /// <summary>\n        /// The standard output text stream connected back to the calling process, typically Git.\n        /// </summary>\n        TextWriter Out { get; }\n\n        /// <summary>\n        /// The standard error text stream connected back to the calling process, typically Git.\n        /// </summary>\n        TextWriter Error { get; }\n    }\n\n    public class StandardStreams : IStandardStreams\n    {\n        private const string LineFeed  = \"\\n\";\n\n        private TextReader _stdIn;\n        private TextWriter _stdOut;\n        private TextWriter _stdErr;\n\n        public TextReader In\n        {\n            get\n            {\n                if (_stdIn == null)\n                {\n                    _stdIn = new GitStreamReader(Console.OpenStandardInput(), EncodingEx.UTF8NoBom);\n                }\n\n                return _stdIn;\n            }\n        }\n\n        public TextWriter Out\n        {\n            get\n            {\n                if (_stdOut == null)\n                {\n                    _stdOut = new StreamWriter(Console.OpenStandardOutput(), EncodingEx.UTF8NoBom)\n                    {\n                        AutoFlush = true,\n                        NewLine = LineFeed,\n                    };\n                }\n\n                return _stdOut;\n            }\n        }\n\n        public TextWriter Error\n        {\n            get\n            {\n                if (_stdErr == null)\n                {\n                    _stdErr = new StreamWriter(Console.OpenStandardError(), EncodingEx.UTF8NoBom)\n                    {\n                        AutoFlush = true,\n                        NewLine = LineFeed,\n                    };\n                }\n\n                return _stdErr;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/StreamExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager\n{\n    public static class StreamExtensions\n    {\n        /// <summary>\n        /// Read a dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>.\n        /// </summary>\n        /// <remarks>\n        /// Uses the <see cref=\"StringComparer.Ordinal\"/> comparer for dictionary keys.\n        /// <para/>\n        /// Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).\n        /// </remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static IDictionary<string, string> ReadDictionary(this TextReader reader) =>\n            ReadDictionary(reader, StringComparer.Ordinal);\n\n        /// <summary>\n        /// Read a dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>,\n        /// with the specified <see cref=\"StringComparer\"/> used to compare dictionary keys.\n        /// </summary>\n        /// <remarks>Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).</remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <param name=\"comparer\">Comparer to use when comparing dictionary keys.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static IDictionary<string, string> ReadDictionary(this TextReader reader, StringComparer comparer)\n        {\n            var dict = new Dictionary<string, string>(comparer);\n\n            string line;\n            while ((line = reader.ReadLine()) != null && !string.IsNullOrWhiteSpace(line))\n            {\n                ParseLine(dict, line);\n            }\n\n            return dict;\n        }\n\n        /// <summary>\n        /// Read a multi-dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>.\n        /// </summary>\n        /// <remarks>\n        /// Uses the <see cref=\"StringComparer.Ordinal\"/> comparer for dictionary keys.\n        /// <para/>\n        /// Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).\n        /// </remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static IDictionary<string, IList<string>> ReadMultiDictionary(this TextReader reader) =>\n            ReadMultiDictionary(reader, StringComparer.Ordinal);\n\n        /// <summary>\n        /// Read a multi-dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>,\n        /// with the specified <see cref=\"StringComparer\"/> used to compare dictionary keys.\n        /// </summary>\n        /// <remarks>Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).</remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <param name=\"comparer\">Comparer to use when comparing dictionary keys.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static IDictionary<string, IList<string>> ReadMultiDictionary(this TextReader reader, StringComparer comparer)\n        {\n            var dict = new Dictionary<string, IList<string>>(comparer);\n\n            string line;\n            while ((line = reader.ReadLine()) != null && !string.IsNullOrWhiteSpace(line))\n            {\n                ParseMultiLine(dict, line);\n            }\n\n            return dict;\n        }\n\n        /// <summary>\n        /// Asynchronously read a dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>.\n        /// </summary>\n        /// <remarks>\n        /// Uses the <see cref=\"StringComparer.Ordinal\"/> comparer for dictionary keys.\n        /// <para/>\n        /// Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).\n        /// </remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static Task<IDictionary<string, string>> ReadDictionaryAsync(this TextReader reader) =>\n            ReadDictionaryAsync(reader, StringComparer.Ordinal);\n\n        /// <summary>\n        /// Asynchronously read a multi-dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>.\n        /// </summary>\n        /// <remarks>\n        /// Uses the <see cref=\"StringComparer.Ordinal\"/> comparer for dictionary keys.\n        /// <para/>\n        /// Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).\n        /// </remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static Task<IDictionary<string, IList<string>>> ReadMultiDictionaryAsync(this TextReader reader) =>\n            ReadMultiDictionaryAsync(reader, StringComparer.Ordinal);\n\n        /// <summary>\n        /// Asynchronously read a dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>,\n        /// with the specified <see cref=\"StringComparer\"/> used to compare dictionary keys.\n        /// </summary>\n        /// <remarks>Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).</remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <param name=\"comparer\">Comparer to use when comparing dictionary keys.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static async Task<IDictionary<string, string>> ReadDictionaryAsync(this TextReader reader, StringComparer comparer)\n        {\n            var dict = new Dictionary<string, string>(comparer);\n\n            string line;\n            while ((line = await reader.ReadLineAsync()) != null && !string.IsNullOrWhiteSpace(line))\n            {\n                ParseLine(dict, line);\n            }\n\n            return dict;\n        }\n\n        /// <summary>\n        /// Asynchronously read a multi-dictionary in the form `key1=value\\nkey2=value\\n\\n` from the specified <see cref=\"TextReader\"/>,\n        /// with the specified <see cref=\"StringComparer\"/> used to compare dictionary keys.\n        /// </summary>\n        /// <remarks>Also accepts dictionary lines terminated using \\r\\n (CR LF) as well as \\n (LF).</remarks>\n        /// <param name=\"reader\">Text reader to read a dictionary from.</param>\n        /// <param name=\"comparer\">Comparer to use when comparing dictionary keys.</param>\n        /// <returns>Dictionary read from the text reader.</returns>\n        public static async Task<IDictionary<string, IList<string>>> ReadMultiDictionaryAsync(this TextReader reader, StringComparer comparer)\n        {\n            var dict = new Dictionary<string, IList<string>>(comparer);\n\n            string line;\n            while ((line = await reader.ReadLineAsync()) != null && !string.IsNullOrWhiteSpace(line))\n            {\n                ParseMultiLine(dict, line);\n            }\n\n            return dict;\n        }\n\n        /// <summary>\n        /// Write a dictionary in the form `key1=value\\nkey2=value\\n\\n` to the specified <see cref=\"TextWriter\"/>,\n        /// where \\n is the configured new-line (see <see cref=\"TextWriter.NewLine\"/>).\n        /// </summary>\n        /// <remarks>The output dictionary new-lines are determined by the <see cref=\"TextWriter.NewLine\"/> property.</remarks>\n        /// <param name=\"writer\">Text writer to write a dictionary to.</param>\n        /// <param name=\"dict\">Dictionary to write to the text writer.</param>\n        public static void WriteDictionary(this TextWriter writer, IDictionary<string, string> dict)\n        {\n            foreach (var kvp in dict)\n            {\n                writer.WriteLine($\"{kvp.Key}={kvp.Value}\");\n            }\n\n            // Write terminating line\n            writer.WriteLine();\n        }\n\n        /// <summary>\n        /// Write a dictionary in the form `key1=value\\nkey2=value\\n\\n` to the specified <see cref=\"TextWriter\"/>,\n        /// where \\n is the configured new-line (see <see cref=\"TextWriter.NewLine\"/>).\n        /// </summary>\n        /// <remarks>The output dictionary new-lines are determined by the <see cref=\"TextWriter.NewLine\"/> property.</remarks>\n        /// <param name=\"writer\">Text writer to write a dictionary to.</param>\n        /// <param name=\"dict\">Dictionary to write to the text writer.</param>\n        public static void WriteDictionary(this TextWriter writer, IDictionary<string, IList<string>> dict)\n        {\n            foreach (var kvp in dict)\n            {\n                IList<string> values = GetNormalizedValueList(kvp.Value);\n                switch (values.Count)\n                {\n                    case 0:\n                        break;\n\n                    case 1:\n                        writer.WriteLine($\"{kvp.Key}={kvp.Value[0]}\");\n                        break;\n\n                    default:\n                        foreach (string value in values)\n                        {\n                            writer.WriteLine($\"{kvp.Key}[]={value}\");\n                        }\n                        break;\n                }\n            }\n\n            // Write terminating line\n            writer.WriteLine();\n        }\n\n        /// <summary>\n        /// Asynchronously write a dictionary in the form `key1=value\\nkey2=value\\n\\n` to the specified <see cref=\"TextWriter\"/>,\n        /// where \\n is the configured new-line (see <see cref=\"TextWriter.NewLine\"/>).\n        /// </summary>\n        /// <remarks>The output dictionary new-lines are determined by the <see cref=\"TextWriter.NewLine\"/> property.</remarks>\n        /// <param name=\"writer\">Text writer to write a dictionary to.</param>\n        /// <param name=\"dict\">Dictionary to write to the text writer.</param>\n        public static async Task WriteDictionaryAsync(this TextWriter writer, IDictionary<string, string> dict)\n        {\n            foreach (var kvp in dict)\n            {\n                await writer.WriteLineAsync($\"{kvp.Key}={kvp.Value}\");\n            }\n\n            // Write terminating line\n            await writer.WriteLineAsync();\n        }\n\n        private static void ParseLine(IDictionary<string, string> dict, string line)\n        {\n            int splitIndex = line.IndexOf('=');\n            if (splitIndex > 0)\n            {\n                string key = line.Substring(0, splitIndex);\n                string value = line.Substring(splitIndex + 1);\n\n                dict[key] = value;\n            }\n        }\n\n        private static void ParseMultiLine(IDictionary<string, IList<string>> dict, string line)\n        {\n            int splitIndex = line.IndexOf('=');\n            if (splitIndex > 0)\n            {\n                string key = line.Substring(0, splitIndex);\n                string value = line.Substring(splitIndex + 1);\n\n                bool multi = key.EndsWith(\"[]\");\n                if (multi)\n                {\n                    key = key.Substring(0, key.Length - 2);\n                }\n\n                if (!dict.TryGetValue(key, out IList<string> list))\n                {\n                    list = new List<string>();\n                    dict[key] = list;\n                }\n\n                // Only allow one value for non-multi/array entries (\"key=value\")\n                // and reset the array of a multi-entry if the value is empty (\"key[]=<empty>\")\n                bool emptyValue = string.IsNullOrEmpty(value);\n\n                if (!multi || emptyValue)\n                {\n                    list.Clear();\n                }\n\n                if (multi && emptyValue)\n                {\n                    return;\n                }\n\n                list.Add(value);\n            }\n        }\n\n        private static IList<string> GetNormalizedValueList(IEnumerable<string> values)\n        {\n            var result = new List<string>();\n\n            foreach (string value in values)\n            {\n                if (string.IsNullOrWhiteSpace(value))\n                {\n                    result.Clear();\n                }\n                else\n                {\n                    result.Add(value);\n                }\n            }\n\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/StringExtensions.cs",
    "content": "using System;\nusing System.Text;\n\nnamespace GitCredentialManager\n{\n    public static class StringExtensions\n    {\n        /// <summary>\n        /// Check if the string is considered to be 'truthy' (a value considered equivalent to 'true').\n        /// </summary>\n        /// <remarks>\n        /// Git considers several different values to be equivalent to 'true'; we try to be consistent with this\n        /// behavior.\n        /// <para/>\n        /// See the following Git documentation for a list of values considered to be equivalent to 'true':\n        /// https://git-scm.com/docs/git-config#git-config-boolean\n        /// </remarks>\n        /// <param name=\"str\">String value to check.</param>\n        /// <returns>True if the value is 'truthy', false otherwise.</returns>\n        public static bool IsTruthy(this string str)\n        {\n            return StringComparer.OrdinalIgnoreCase.Equals(str, bool.TrueString) ||\n                   StringComparer.OrdinalIgnoreCase.Equals(str, \"1\") ||\n                   StringComparer.OrdinalIgnoreCase.Equals(str, \"on\") ||\n                   StringComparer.OrdinalIgnoreCase.Equals(str, \"yes\");\n        }\n\n        /// <summary>\n        /// Check if the string is considered to be 'falsey' (a value considered equivalent to 'false').\n        /// </summary>\n        /// <remarks>\n        /// Git considers several different values to be equivalent to 'false'; we try to be consistent with this\n        /// behavior.\n        /// <para/>\n        /// See the following Git documentation for a list of values considered to be equivalent to 'false':\n        /// https://git-scm.com/docs/git-config#git-config-boolean\n        /// </remarks>\n        /// <param name=\"str\">String value to check.</param>\n        /// <returns>True if the value is 'falsey', false otherwise.</returns>\n        public static bool IsFalsey(this string str)\n        {\n            return StringComparer.OrdinalIgnoreCase.Equals(str, bool.FalseString) ||\n                   StringComparer.OrdinalIgnoreCase.Equals(str, \"0\") ||\n                   StringComparer.OrdinalIgnoreCase.Equals(str, \"off\") ||\n                   StringComparer.OrdinalIgnoreCase.Equals(str, \"no\");\n        }\n\n        /// <summary>\n        /// Check if the string is considered to be either 'truthy' or 'falsey' (values considered equivalent to 'true' and 'false').\n        /// </summary>\n        /// <remarks>\n        /// Git considers several different values to be equivalent to 'true' and 'false'; we try to be consistent with this\n        /// behavior.\n        /// <para/>\n        /// See the following Git documentation for a list of values considered to be equivalent to 'true' and 'false':\n        /// https://git-scm.com/docs/git-config#git-config-boolean\n        /// </remarks>\n        /// <param name=\"str\">String value to check.</param>\n        /// <returns>True if the value is 'truthy', 'false' if the value is 'falsey', null otherwise.</returns>\n        public static bool? ToBooleany(this string str)\n        {\n            if (IsTruthy(str))\n            {\n                return true;\n            }\n\n            if (IsFalsey(str))\n            {\n                return false;\n            }\n\n            return null;\n        }\n\n        /// <summary>\n        /// Check if the string is considered to be either 'truthy' or 'falsey' (values considered equivalent to 'true' and 'false').\n        /// </summary>\n        /// <remarks>\n        /// Git considers several different values to be equivalent to 'true' and 'false'; we try to be consistent with this\n        /// behavior.\n        /// <para/>\n        /// See the following Git documentation for a list of values considered to be equivalent to 'true' and 'false':\n        /// https://git-scm.com/docs/git-config#git-config-boolean\n        /// </remarks>\n        /// <param name=\"str\">String value to check.</param>\n        /// <param name=\"defaultValue\">Default value to return if the string is neither 'truthy', 'falsey', or has not been set.</param>\n        /// <returns>True if the value is 'truthy', 'false' if the value is 'falsey', <paramref name=\"defaultValue\"/> otherwise.</returns>\n        public static bool ToBooleanyOrDefault(this string str, bool defaultValue)\n        {\n            return ToBooleany(str) ?? defaultValue;\n        }\n\n        /// <summary>\n        /// Truncate the string from the first index of the given character, also removing the indexed character.\n        /// </summary>\n        /// <param name=\"str\">String to truncate.</param>\n        /// <param name=\"c\">Character to locate the index of.</param>\n        /// <returns>Truncated string.</returns>\n        public static string TruncateFromIndexOf(this string str, char c)\n        {\n            EnsureArgument.NotNull(str, nameof(str));\n\n            int last = str.IndexOf(c);\n            if (last > -1)\n            {\n                return str.Substring(0, last);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Truncate the string from the last index of the given character, also removing the indexed character.\n        /// </summary>\n        /// <param name=\"str\">String to truncate.</param>\n        /// <param name=\"c\">Character to locate the index of.</param>\n        /// <returns>Truncated string.</returns>\n        public static string TruncateFromLastIndexOf(this string str, char c)\n        {\n            EnsureArgument.NotNull(str, nameof(str));\n\n            int last = str.LastIndexOf(c);\n            if (last > -1)\n            {\n                return str.Substring(0, last);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Trim all characters at the start of the string until the first index of the given character,\n        /// also removing the indexed character.\n        /// </summary>\n        /// <param name=\"str\">String to trim.</param>\n        /// <param name=\"c\">Character to locate the index of.</param>\n        /// <returns>Trimmed string.</returns>\n        public static string TrimUntilIndexOf(this string str, char c)\n        {\n            EnsureArgument.NotNull(str, nameof(str));\n\n            int first = str.IndexOf(c);\n            if (first > -1)\n            {\n                return str.Substring(first + 1, str.Length - first - 1);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Trim all characters at the start of the string until the first index of the given string,\n        /// also removing the indexed character.\n        /// </summary>\n        /// <param name=\"str\">String to trim.</param>\n        /// <param name=\"value\">String to locate the index of.</param>\n        /// <param name=\"comparisonType\">Comparison rule for locating the string.</param>\n        /// <returns>Trimmed string.</returns>\n        public static string TrimUntilIndexOf(this string str, string value, StringComparison comparisonType = StringComparison.Ordinal)\n        {\n            EnsureArgument.NotNull(str, nameof(str));\n\n            int first = str.IndexOf(value, comparisonType);\n            if (first > -1)\n            {\n                return str.Substring(first + value.Length, str.Length - first - value.Length);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Trim all characters at the start of the string until the last index of the given character,\n        /// also removing the indexed character.\n        /// </summary>\n        /// <param name=\"str\">String to trim.</param>\n        /// <param name=\"c\">Character to locate the index of.</param>\n        /// <returns>Trimmed string.</returns>\n        public static string TrimUntilLastIndexOf(this string str, char c)\n        {\n            EnsureArgument.NotNull(str, nameof(str));\n\n            int first = str.LastIndexOf(c);\n            if (first > -1)\n            {\n                return str.Substring(first + 1, str.Length - first - 1);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Trim all characters at the start of the string until the last index of the given string,\n        /// also removing the indexed character.\n        /// </summary>\n        /// <param name=\"str\">String to trim.</param>\n        /// <param name=\"value\">String to locate the index of.</param>\n        /// <param name=\"comparisonType\">Comparison rule for locating the string.</param>\n        /// <returns>Trimmed string.</returns>\n        public static string TrimUntilLastIndexOf(this string str, string value, StringComparison comparisonType = StringComparison.Ordinal)\n        {\n            EnsureArgument.NotNull(str, nameof(str));\n\n            int first = str.LastIndexOf(value, comparisonType);\n            if (first > -1)\n            {\n                return str.Substring(first + value.Length, str.Length - first - value.Length);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Trim the matching value from the middle of the string.\n        /// </summary>\n        /// <param name=\"str\">String to trim.</param>\n        /// <param name=\"value\">String to locate and remove.</param>\n        /// <param name=\"comparisonType\">Comparison rule for locating the string.</param>\n        /// <returns>Trimmed string.</returns>\n        public static string TrimMiddle(this string str, string value, StringComparison comparisonType = StringComparison.Ordinal)\n        {\n            int index = str.IndexOf(value, comparisonType);\n\n            if (index > -1)\n            {\n                return str.Substring(0, index) +\n                       str.Substring(index + value.Length);\n            }\n\n            return str;\n        }\n\n        /// <summary>\n        /// Check whether string contains a specified substring.\n        /// </summary>\n        /// <param name=\"str\">String to check.</param>\n        /// <param name=\"value\">String to locate.</param>\n        /// <param name=\"comparisonType\">Comparison rule for comparing the strings.</param>\n        /// <returns>True if the string contains the substring, false if not.</returns>\n        public static bool Contains(this string str, string value, StringComparison comparisonType)\n        {\n            return str?.IndexOf(value, comparisonType) >= 0;\n        }\n\n        /// <summary>\n        /// Convert string to snake case.\n        /// </summary>\n        /// <param name=\"str\">String to convert.</param>\n        /// <returns>Input string converted to snake case.</returns>\n        public static string ToSnakeCase(this string str)\n        {\n            int len = str.Length;\n            var sb = new StringBuilder(2*len);\n            for (int i = 0; i < len; i++)\n            {\n                if (i > 0 && char.IsUpper(str[i]) &&\n                    (char.IsLower(str[i - 1]) || i < len - 1 && char.IsLower(str[i + 1])))\n                {\n                    sb.Append('_');\n\n                }\n                sb.Append(char.ToLower(str[i]));\n            }\n\n            return sb.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/TerminalMenu.cs",
    "content": "using System.Collections;\nusing System.Collections.Generic;\n\nnamespace GitCredentialManager\n{\n    public class TerminalMenu : IEnumerable<TerminalMenuItem>\n    {\n        private readonly ITerminal _terminal;\n        private readonly IList<TerminalMenuItem> _items = new List<TerminalMenuItem>();\n\n        public TerminalMenu(ITerminal terminal, string title = null)\n        {\n            _terminal = terminal;\n            Title = title;\n        }\n\n        public string Title { get; set; }\n\n        public TerminalMenuItem Add(string name)\n        {\n            var item = new TerminalMenuItem(name);\n            _items.Add(item);\n            return item;\n        }\n\n        public TerminalMenuItem Show(int? defaultOption = null)\n        {\n            bool hasDefault = defaultOption.HasValue && defaultOption.Value > -1;\n            if (hasDefault)\n            {\n                EnsureArgument.InRange(defaultOption.Value, nameof(defaultOption), 0, _items.Count, upperInclusive: false);\n            }\n\n            string title = $\"{Title ?? \"Select an option\"}:\";\n\n            string promptDefaultTag = hasDefault ? \" (enter for default)\" : string.Empty;\n            string prompt = $\"option{promptDefaultTag}\";\n\n            while (true)\n            {\n                _terminal.WriteLine(title);\n\n                for (var i = 0; i < _items.Count; i++)\n                {\n                    string itemDefaultTag = i == defaultOption ? \" (default)\" : string.Empty;\n\n                    // Use 1-based numbers for the UI\n                    _terminal.WriteLine($\"  {i + 1}. {_items[i].Name}{itemDefaultTag}\");\n                }\n\n                string optionStr = _terminal.Prompt(prompt);\n\n                if (string.IsNullOrWhiteSpace(optionStr))\n                {\n                    if (hasDefault)\n                    {\n                        return _items[defaultOption.Value];\n                    }\n\n                    _terminal.WriteLine(\"No default option is configured.\\n\");\n                    continue;\n                }\n\n                if (!int.TryParse(optionStr, out int option))\n                {\n                    _terminal.WriteLine($\"Invalid option '{optionStr}'. Expected a number.\\n\");\n                    continue;\n                }\n\n                // The option as the user enters it is using a 1-based index\n                // so we must subtract one to get to the 0-based index we use here.\n                option--;\n\n                if (option < 0 || option >= _items.Count)\n                {\n                    _terminal.WriteLine($\"Invalid option '{optionStr}'.\\n\");\n                    continue;\n                }\n\n                return _items[option];\n            }\n        }\n\n        public IEnumerator<TerminalMenuItem> GetEnumerator()\n        {\n            return _items.GetEnumerator();\n        }\n\n        IEnumerator IEnumerable.GetEnumerator()\n        {\n            return ((IEnumerable) _items).GetEnumerator();\n        }\n    }\n\n    public class TerminalMenuItem\n    {\n        public TerminalMenuItem(string name)\n        {\n            Name = name;\n        }\n\n        public string Name { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Trace.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Represents the application's tracing system.\n    /// </summary>\n    public interface ITrace : IDisposable\n    {\n        /// <summary>\n        /// True if any listeners have been added to the tracing system.\n        /// </summary>\n        bool HasListeners { get; }\n\n        /// <summary>\n        /// Get or set whether or not sensitive information such as secrets and credentials should be\n        /// output to attached trace listeners.\n        /// </summary>\n        bool IsSecretTracingEnabled { get; set; }\n\n        /// <summary>\n        /// Add a listener to the trace writer.\n        /// </summary>\n        /// <param name=\"listener\">The listener to add.</param>\n        void AddListener(TextWriter listener);\n\n        /// <summary>\n        /// Forces any pending trace messages to be written to any listeners.\n        /// </summary>\n        void Flush();\n\n        /// <summary>\n        /// Writes an exception as a message to the trace writer.\n        /// <para/>\n        /// Expands exceptions' inner exceptions into additional trace lines.\n        /// </summary>\n        /// <param name=\"exception\">The exception to write.</param>\n        /// <param name=\"filePath\">Path of the file this method is called from.</param>\n        /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n        /// <param name=\"memberName\">Name of the member in which this method is called.</param>\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        void WriteException(\n            Exception exception,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\");\n\n        /// <summary>\n        /// Write the contents of a dictionary to the trace writer.\n        /// <para/>\n        /// Calls <see cref=\"object.ToString\"/> on all keys and values.\n        /// </summary>\n        /// <param name=\"dictionary\">The dictionary to write.</param>\n        /// <param name=\"filePath\">Path of the file this method is called from.</param>\n        /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n        /// <param name=\"memberName\">Name of the member in which this method is called.</param>\n        void WriteDictionary<TKey, TValue>(\n            IDictionary<TKey, TValue> dictionary,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\");\n\n        /// <summary>\n        /// Write the contents of a dictionary that contains sensitive information to the trace writer.\n        /// <para/>\n        /// Calls <see cref=\"object.ToString\"/> on all keys and values, except keys specified as secret.\n        /// </summary>\n        /// <param name=\"dictionary\">The dictionary to write.</param>\n        /// <param name=\"secretKeys\">Dictionary keys that contain secrets/sensitive information.</param>\n        /// <param name=\"keyComparer\">Comparer to use for <paramref name=\"secretKeys\"/>.</param>\n        /// <param name=\"filePath\">Path of the file this method is called from.</param>\n        /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n        /// <param name=\"memberName\">Name of the member in which this method is called.</param>\n        void WriteDictionarySecrets<TKey, TValue>(\n            IDictionary<TKey, TValue> dictionary,\n            TKey[] secretKeys,\n            IEqualityComparer<TKey> keyComparer = null,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\");\n\n        /// <summary>\n        /// Writes a message to the trace writer followed by a line terminator.\n        /// </summary>\n        /// <param name=\"message\">The message to write.</param>\n        /// <param name=\"filePath\">Path of the file this method is called from.</param>\n        /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n        /// <param name=\"memberName\">Name of the member in which this method is called.</param>\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        void WriteLine(\n            string message,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\");\n\n        /// <summary>\n        /// Writes a message containing sensitive information to the trace writer followed by a line terminator.\n        /// <para/>\n        /// Attached listeners will only receive the fully formatted message if <see cref=\"IsSecretTracingEnabled\"/> is set\n        /// to true, otherwise the secret arguments will be masked.\n        /// </summary>\n        /// <param name=\"format\">The format string to write.</param>\n        /// <param name=\"secrets\">Sensitive/secret arguments for the format string.</param>\n        /// <param name=\"filePath\">Path of the file this method is called from.</param>\n        /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n        /// <param name=\"memberName\">Name of the member in which this method is called.</param>\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        void WriteLineSecrets(\n            string format,\n            object[] secrets,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\");\n    }\n\n    public class Trace : DisposableObject, ITrace\n    {\n        private const string SecretMask = \"********\";\n\n        private readonly object _writersLock = new object();\n        private readonly List<TextWriter> _writers = new List<TextWriter>();\n\n        public bool HasListeners\n        {\n            get\n            {\n                lock (_writersLock)\n                {\n                    return _writers.Any();\n                }\n            }\n        }\n\n        public bool IsSecretTracingEnabled { get; set; }\n\n        public void AddListener(TextWriter listener)\n        {\n            ThrowIfDisposed();\n\n            lock (_writersLock)\n            {\n                // Try not to add the same listener more than once\n                if (_writers.Contains(listener))\n                    return;\n\n                _writers.Add(listener);\n            }\n        }\n\n        public void Flush()\n        {\n            ThrowIfDisposed();\n\n            lock (_writersLock)\n            {\n                foreach (var writer in _writers)\n                {\n                    try\n                    {\n                        writer?.Flush();\n                    }\n                    catch\n                    { /* squelch */ }\n                }\n            }\n        }\n\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        public void WriteException(\n            Exception exception,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\")\n        {\n            // Exception being null probably won't happen, but we shouldn't die because we failed to trace it.\n            if (exception is null)\n                return;\n\n            WriteLine($\"! error: '{exception.Message}'.\", filePath, lineNumber, memberName);\n\n            while ((exception = exception.InnerException) != null)\n            {\n                WriteLine($\"       > '{exception.Message}'.\", filePath, lineNumber, memberName);\n            }\n        }\n\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        public void WriteDictionary<TKey, TValue>(\n            IDictionary<TKey, TValue> dictionary,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\")\n        {\n            foreach (KeyValuePair<TKey, TValue> entry in dictionary)\n            {\n                WriteLine($\"\\t{entry.Key}={entry.Value}\", filePath, lineNumber, memberName);\n            }\n        }\n\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        public void WriteDictionarySecrets<TKey, TValue>(\n            IDictionary<TKey, TValue> dictionary,\n            TKey[] secretKeys,\n            IEqualityComparer<TKey> keyComparer = null,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\")\n        {\n            foreach (KeyValuePair<TKey, TValue> entry in dictionary)\n            {\n                bool isSecretEntry = !(secretKeys is null) &&\n                                     secretKeys.Contains(entry.Key, keyComparer ?? EqualityComparer<TKey>.Default);\n\n                void WriteSecretLine(string keySuffix, object value)\n                {\n                    var message = isSecretEntry && !IsSecretTracingEnabled\n                        ? $\"\\t{entry.Key}{keySuffix}={SecretMask}\"\n                        : $\"\\t{entry.Key}{keySuffix}={value}\";\n                    WriteLine(message, filePath, lineNumber, memberName);\n                }\n\n                if (entry.Value is IEnumerable<string> values)\n                {\n                    List<string> valueList = values.ToList();\n                    foreach (string value in valueList)\n                    {\n                        WriteSecretLine(valueList.Count > 1 ? \"[]\" : string.Empty, value);\n                    }\n                }\n                else\n                {\n                    WriteSecretLine(string.Empty, entry.Value);\n                }\n            }\n        }\n\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        public void WriteLine(\n            string message,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\")\n        {\n            ThrowIfDisposed();\n\n            lock (_writersLock)\n            {\n                if (_writers.Count == 0)\n                {\n                    return;\n                }\n\n                string text = FormatText(message, filePath, lineNumber, memberName);\n\n                foreach (var writer in _writers)\n                {\n                    try\n                    {\n                        writer?.Write(text);\n                        writer?.Write('\\n');\n                        writer?.Flush();\n                    }\n                    catch { /* squelch */ }\n                }\n            }\n        }\n\n        [System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Design\", \"CA1026:DefaultParametersShouldNotBeUsed\")]\n        public void WriteLineSecrets(\n            string format,\n            object[] secrets,\n            [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n            [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0,\n            [System.Runtime.CompilerServices.CallerMemberName] string memberName = \"\")\n        {\n            string message = this.IsSecretTracingEnabled\n                           ? string.Format(format, secrets)\n                           : string.Format(format, secrets.Select(_ => (object)SecretMask).ToArray());\n\n            WriteLine(message, filePath, lineNumber, memberName);\n        }\n\n        protected override void ReleaseManagedResources()\n        {\n            lock (_writersLock)\n            {\n                try\n                {\n                    for (int i = 0; i < _writers.Count; i += 1)\n                    {\n                        using (var writer = _writers[i])\n                        {\n                            _writers.Remove(writer);\n                        }\n                    }\n                }\n                catch\n                { /* squelch */ }\n            }\n\n            base.ReleaseManagedResources();\n        }\n\n        private static string FormatText(string message, string filePath, int lineNumber, string memberName)\n        {\n            const int sourceColumnMaxWidth = 23;\n\n            EnsureArgument.NotNull(message, nameof(message));\n            EnsureArgument.NotNull(filePath, nameof(filePath));\n            EnsureArgument.PositiveOrZero(lineNumber, nameof(lineNumber));\n            EnsureArgument.NotNull(memberName, nameof(memberName));\n\n            // Source column format is file:line\n            string source = $\"{filePath}:{lineNumber}\";\n\n            if (source.Length > sourceColumnMaxWidth)\n            {\n                source = TraceUtils.FormatSource(source, sourceColumnMaxWidth);\n            }\n\n            // Git's trace format is \"{timestamp,-15} {source,-23} trace: {details}\"\n            string text = $\"{DateTime.Now:HH:mm:ss.ffffff} {source,-23} trace: [{memberName}] {message}\";\n\n            return text;\n        }\n    }\n\n    public class DebugTraceWriter : TextWriter\n    {\n        public override Encoding Encoding => Encoding.UTF8;\n\n        public override void Write(char value) => Debug.Write(value);\n\n        public override void Write(string value) => Debug.Write(value);\n\n        public override void WriteLine(string value) => Debug.WriteLine(value);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Trace2.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Pipes;\nusing System.Text;\nusing System.Threading;\n\nnamespace GitCredentialManager;\n\n/// <summary>\n/// The different event types tracked in the TRACE2 tracing\n/// system.\n/// </summary>\npublic enum Trace2Event\n{\n    Version = 0,\n    Start = 1,\n    Exit = 2,\n    ChildStart = 3,\n    ChildExit = 4,\n    Error = 5,\n    RegionEnter = 6,\n    RegionLeave = 7,\n}\n\n/// <summary>\n/// Classifications of processes invoked by GCM.\n/// </summary>\npublic enum Trace2ProcessClass\n{\n    None = 0,\n    UIHelper = 1,\n    Git = 2,\n    Other = 3\n}\n\n/// <summary>\n/// Stores various TRACE2 format targets user has enabled.\n/// Check <see cref=\"Trace2FormatTarget\"/> for supported formats.\n/// </summary>\npublic class Trace2Settings\n{\n    public IDictionary<Trace2FormatTarget, string> FormatTargetsAndValues { get; set; } =\n        new Dictionary<Trace2FormatTarget, string>();\n}\n\n/// <summary>\n/// Specifies a \"text span\" (i.e. space between two pipes) for the performance format target.\n/// </summary>\npublic class PerformanceFormatSpan\n{\n    public int Size { get; set; }\n\n    public int BeginPadding { get; set; }\n\n    public int EndPadding { get; set; }\n}\n\n/// <summary>\n/// Class that manages regions.\n/// </summary>\npublic class Region : DisposableObject\n{\n    private readonly ITrace2 _trace2;\n    private readonly string _category;\n    private readonly string _label;\n    private readonly string _filePath;\n    private readonly int _lineNumber;\n    private readonly string _message;\n    private readonly DateTimeOffset _startTime;\n\n    public Region(ITrace2 trace2, string category, string label, string filePath, int lineNumber, string message = \"\")\n    {\n        _trace2 = trace2;\n        _category = category;\n        _label = label;\n        _filePath = filePath;\n        _lineNumber = lineNumber;\n        _message = message;\n\n        _startTime = DateTimeOffset.UtcNow;\n\n        _trace2.WriteRegionEnter(_category, _label, _message, _filePath, _lineNumber);\n    }\n\n    protected override void ReleaseManagedResources()\n    {\n        double relativeTime = (DateTimeOffset.UtcNow - _startTime).TotalSeconds;\n        _trace2.WriteRegionLeave(relativeTime, _category, _label, _message, _filePath, _lineNumber);\n    }\n}\n\n/// <summary>\n/// Represents the application's TRACE2 tracing system.\n/// </summary>\npublic interface ITrace2 : IDisposable\n{\n    /// <summary>\n    /// Initialize TRACE2 tracing by initializing multi-use fields and setting up any configured target formats.\n    /// </summary>\n    /// <param name=\"startTime\">Approximate time calling application began executing.</param>\n    void Initialize(DateTimeOffset startTime);\n\n    /// <summary>\n    /// Write Version and Start events.\n    /// </summary>\n    /// <param name=\"appPath\">The path to the application.</param>\n    /// <param name=\"args\">Args passed to the application (if applicable).</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void Start(string appPath,\n        string[] args,\n        [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0);\n\n    /// <summary>\n    /// Write Exit event and dispose of writers.\n    /// </summary>\n    /// <param name=\"exitCode\">The exit code of the GCM application.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void Stop(int exitCode,\n        [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0);\n\n    /// <summary>\n    /// Writes information related to startup of child process to trace writer.\n    /// </summary>\n    /// <param name=\"startTime\">Time at which child process began executing.</param>\n    /// <param name=\"processClass\">Process classification.</param>\n    /// <param name=\"useShell\">Specifies whether or not OS shell was used to start the process.</param>\n    /// <param name=\"appName\">Name of application running in child process.</param>\n    /// <param name=\"argv\">Arguments specific to the child process.</param>\n    /// <param name=\"sid\">The child process's session id.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void WriteChildStart(DateTimeOffset startTime,\n        Trace2ProcessClass processClass,\n        bool useShell,\n        string appName,\n        string argv,\n        [System.Runtime.CompilerServices.CallerFilePath]\n        string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber]\n        int lineNumber = 0);\n\n    /// <summary>\n    /// Writes information related to exit of child process to trace writer.\n    /// </summary>\n    /// <param name=\"relativeTime\">Runtime of child process.</param>\n    /// <param name=\"pid\">Id of exiting process.</param>\n    /// <param name=\"code\">Process exit code.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void WriteChildExit(\n        double relativeTime,\n        int pid,\n        int code,\n        [System.Runtime.CompilerServices.CallerFilePath]\n        string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber]\n        int lineNumber = 0);\n\n    /// <summary>\n    /// Writes an error as a message to the trace writer.\n    /// </summary>\n    /// <param name=\"errorMessage\">The error message to write.</param>\n    /// <param name=\"parameterizedMessage\">The error format string.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void WriteError(\n        string errorMessage,\n        string parameterizedMessage = null,\n        [System.Runtime.CompilerServices.CallerFilePath]\n        string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber]\n        int lineNumber = 0);\n\n    /// <summary>\n    /// Creates a region and manages entry/leaving.\n    /// </summary>\n    /// <param name=\"category\">Category of region.</param>\n    /// <param name=\"label\">Description of region.</param>\n    /// <param name=\"message\">Message associated with entering region.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    Region CreateRegion(\n        string category,\n        string label,\n        string message = \"\",\n        [System.Runtime.CompilerServices.CallerFilePath]\n        string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber]\n        int lineNumber = 0);\n\n    /// <summary>\n    /// Writes a region enter message to the trace writer.\n    /// </summary>\n    /// <param name=\"category\">Category of region.</param>\n    /// <param name=\"label\">Description of region.</param>\n    /// <param name=\"message\">Message associated with entering region.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void WriteRegionEnter(\n        string category,\n        string label,\n        string message = \"\",\n        [System.Runtime.CompilerServices.CallerFilePath]\n        string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber]\n        int lineNumber = 0);\n\n    /// <summary>\n    /// Writes a region leave message to the trace writer.\n    /// </summary>\n    /// <param name=\"relativeTime\">Time of region execution.</param>\n    /// <param name=\"category\">Category of region.</param>\n    /// <param name=\"label\">Description of region.</param>\n    /// <param name=\"message\">Message associated with entering region.</param>\n    /// <param name=\"filePath\">Path of the file this method is called from.</param>\n    /// <param name=\"lineNumber\">Line number of file this method is called from.</param>\n    void WriteRegionLeave(\n        double relativeTime,\n        string category,\n        string label,\n        string message = \"\",\n        [System.Runtime.CompilerServices.CallerFilePath]\n        string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber]\n        int lineNumber = 0);\n}\n\npublic class Trace2 : DisposableObject, ITrace2\n{\n    private readonly ICommandContext _commandContext;\n    private readonly object _writersLock = new object();\n    private readonly Encoding _utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);\n    private readonly List<ITrace2Writer> _writers = new List<ITrace2Writer>();\n\n    private const string GitSidVariable = \"GIT_TRACE2_PARENT_SID\";\n\n    private DateTimeOffset _applicationStartTime;\n    private Trace2Settings _settings;\n    private string _sid;\n\n    private bool _initialized;\n    // Increment with each new child process that is tracked\n    private int _childProcCounter = 0;\n\n    public Trace2(ICommandContext commandContext)\n    {\n        _commandContext = commandContext;\n    }\n\n    public void Initialize(DateTimeOffset startTime)\n    {\n        if (_initialized)\n        {\n            return;\n        }\n\n        _applicationStartTime = startTime;\n        _settings = _commandContext.Settings.GetTrace2Settings();\n        _sid = ProcessManager.Sid;\n\n        InitializeWriters();\n\n        _initialized = true;\n    }\n\n    public void Start(string appPath,\n        string[] args,\n        string filePath,\n        int lineNumber)\n    {\n        if (!AssemblyUtils.TryGetAssemblyVersion(out string version))\n        {\n            // A version is required for TRACE2, so if this call fails\n            // manually set the version.\n            version = \"0.0.0\";\n        }\n        WriteVersion(version, filePath, lineNumber);\n        WriteStart(appPath, args, filePath, lineNumber);\n    }\n\n    public void Stop(int exitCode, string filePath, int lineNumber)\n    {\n        WriteExit(exitCode, filePath, lineNumber);\n    }\n\n    public void WriteChildStart(DateTimeOffset startTime,\n        Trace2ProcessClass processClass,\n        bool useShell,\n        string appName,\n        string argv,\n        string filePath = \"\",\n        int lineNumber = 0)\n    {\n        // Some child processes are started before TRACE2 can be initialized.\n        // Since certain dependencies are not available until initialization,\n        // we must immediately return if this method is invoked prior to\n        // initialization.\n        if (!_initialized)\n        {\n            return;\n        }\n\n        // Always add name of the application the process is executing\n        var procArgs = new List<string>()\n        {\n            Path.GetFileName(appName)\n        };\n\n        // If the process has arguments, append them.\n        if (!string.IsNullOrEmpty(argv))\n        {\n            procArgs.AddRange(argv.Split(' '));\n        }\n\n        WriteMessage(new ChildStartMessage()\n        {\n            Event = Trace2Event.ChildStart,\n            Sid = _sid,\n            Time = startTime,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            Id = ++_childProcCounter,\n            Classification = processClass,\n            UseShell = useShell,\n            Argv = procArgs,\n            ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,\n            Depth = ProcessManager.Depth,\n        });\n    }\n\n    public void WriteChildExit(\n        double relativeTime,\n        int pid,\n        int code,\n        string filePath = \"\",\n        int lineNumber = 0)\n    {\n        // Some child processes are started before TRACE2 can be initialized.\n        // Since certain dependencies are not available until initialization,\n        // we must immediately return if this method is invoked prior to\n        // initialization.\n        if (!_initialized)\n        {\n            return;\n        }\n\n        WriteMessage(new ChildExitMessage()\n        {\n            Event = Trace2Event.ChildExit,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            Id = _childProcCounter,\n            Pid = pid,\n            Code = code,\n            ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,\n            RelativeTime = relativeTime,\n            Depth = ProcessManager.Depth\n        });\n    }\n\n    public void WriteError(\n        string errorMessage,\n        string parameterizedMessage = null,\n        [System.Runtime.CompilerServices.CallerFilePath] string filePath = \"\",\n        [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0)\n    {\n        // It is possible for an error to be thrown before TRACE2 can be initialized.\n        // Since certain dependencies are not available until initialization,\n        // we must immediately return if this method is invoked prior to\n        // initialization.\n        if (!_initialized)\n        {\n            return;\n        }\n\n        WriteMessage(new ErrorMessage()\n        {\n            Event = Trace2Event.Error,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            Message = errorMessage,\n            ParameterizedMessage = parameterizedMessage ?? errorMessage,\n            Depth = ProcessManager.Depth\n        });\n    }\n\n    public Region CreateRegion(\n        string category,\n        string label,\n        string message,\n        string filePath,\n        int lineNumber)\n    {\n        return new Region(this, category, label, filePath, lineNumber, message);\n    }\n\n    public void WriteRegionEnter(\n        string category,\n        string label,\n        string message = \"\",\n        string filePath = \"\",\n        int lineNumber = 0)\n    {\n        WriteMessage(new RegionEnterMessage()\n        {\n            Event = Trace2Event.RegionEnter,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Category = category,\n            Label = label,\n            Message = message == \"\" ? label : message,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,\n            Depth = ProcessManager.Depth\n        });\n    }\n\n    public void WriteRegionLeave(\n        double relativeTime,\n        string category,\n        string label,\n        string message = \"\",\n        string filePath = \"\",\n        int lineNumber = 0)\n    {\n        WriteMessage(new RegionLeaveMessage()\n        {\n            Event = Trace2Event.RegionLeave,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Category = category,\n            Label = label,\n            Message = message == \"\" ? label : message,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,\n            RelativeTime = relativeTime,\n            Depth = ProcessManager.Depth\n        });\n    }\n\n    protected override void ReleaseManagedResources()\n    {\n        lock (_writersLock)\n        {\n            try\n            {\n                for (int i = 0; i < _writers.Count; i += 1)\n                {\n                    using (var writer = _writers[i])\n                    {\n                        _writers.Remove(writer);\n                    }\n                }\n            }\n            catch\n            {\n                /* squelch */\n            }\n        }\n\n        base.ReleaseManagedResources();\n    }\n\n    internal static bool TryGetPipeName(string eventTarget, out string name)\n    {\n        // Use prefixes to determine whether target is a named pipe/socket\n        if (eventTarget.StartsWith(\"af_unix:\", StringComparison.OrdinalIgnoreCase) ||\n            eventTarget.StartsWith(@\"\\\\.\\pipe\\\", StringComparison.OrdinalIgnoreCase) ||\n            eventTarget.StartsWith(\"//./pipe/\", StringComparison.OrdinalIgnoreCase))\n        {\n            name = PlatformUtils.IsWindows()\n                ? eventTarget.Replace('/', '\\\\')\n                    .TrimUntilIndexOf(@\"\\\\.\\pipe\\\")\n                : eventTarget.Replace(\"af_unix:dgram:\", \"\")\n                    .Replace(\"af_unix:stream:\", \"\")\n                    .Replace(\"af_unix:\", \"\");\n            return true;\n        }\n\n        name = \"\";\n        return false;\n    }\n\n    private void InitializeWriters()\n    {\n        // Set up the correct writer for every enabled format target.\n        foreach (var formatTarget in _settings.FormatTargetsAndValues)\n        {\n            if (TryGetPipeName(formatTarget.Value, out string name)) // Write to named pipe/socket\n            {\n                AddWriter(new Trace2CollectorWriter(formatTarget.Key, (\n                        () => new NamedPipeClientStream(\".\", name,\n                            PipeDirection.Out,\n                            PipeOptions.Asynchronous)\n                    )\n                ));\n            }\n            else if (formatTarget.Value.IsTruthy()) // Write to stderr\n            {\n                AddWriter(new Trace2StreamWriter(formatTarget.Key, _commandContext.Streams.Error));\n            }\n            else if (Path.IsPathRooted(formatTarget.Value)) // Write to file\n            {\n                try\n                {\n                    AddWriter(new Trace2FileWriter(formatTarget.Key, formatTarget.Value));\n                }\n                catch (Exception ex)\n                {\n                    Console.Error.WriteLine($\"warning: unable to trace to file '{formatTarget.Value}': {ex.Message}\");\n                }\n            }\n        }\n    }\n\n    private void WriteVersion(\n        string gcmVersion,\n        string filePath,\n        int lineNumber,\n        string eventFormatVersion = \"3\")\n    {\n        EnsureArgument.NotNull(gcmVersion, nameof(gcmVersion));\n\n        WriteMessage(new VersionMessage()\n        {\n            Event = Trace2Event.Version,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            Evt = eventFormatVersion,\n            Exe = gcmVersion\n        });\n    }\n\n    private void WriteStart(\n        string appPath,\n        string[] args,\n        string filePath,\n        int lineNumber)\n    {\n        // Prepend GCM exe to arguments\n        var argv = new List<string>()\n        {\n            Path.GetFileName(appPath),\n        };\n\n        if (args.Length > 0)\n        {\n            argv.AddRange(args);\n        }\n\n        WriteMessage(new StartMessage()\n        {\n            Event = Trace2Event.Start,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            Argv = argv,\n            ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds\n        });\n    }\n\n    private void WriteExit(int code, string filePath = \"\", int lineNumber = 0)\n    {\n        EnsureArgument.NotNull(code, nameof(code));\n\n        WriteMessage(new ExitMessage()\n        {\n            Event = Trace2Event.Exit,\n            Sid = _sid,\n            Time = DateTimeOffset.UtcNow,\n            Thread = BuildThreadName(),\n            File = Path.GetFileName(filePath),\n            Line = lineNumber,\n            Code = code,\n            ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds\n        });\n    }\n\n    private void AddWriter(ITrace2Writer writer)\n    {\n        ThrowIfDisposed();\n\n        lock (_writersLock)\n        {\n            // Try not to add the same writer more than once\n            if (_writers.Contains(writer))\n                return;\n\n            _writers.Add(writer);\n        }\n    }\n\n    private void WriteMessage(Trace2Message message)\n    {\n        ThrowIfDisposed();\n\n        if (!_initialized)\n        {\n            return;\n        }\n\n        lock (_writersLock)\n        {\n            if (_writers.Count == 0)\n            {\n                return;\n            }\n\n            foreach (var writer in _writers)\n            {\n                if (!writer.Failed)\n                {\n                    writer.Write(message);\n                }\n            }\n        }\n    }\n\n    private static string BuildThreadName()\n    {\n        // If this is the entry thread, call it \"main\", per Trace2 convention\n        if (Thread.CurrentThread.ManagedThreadId == 0)\n        {\n            return \"main\";\n        }\n\n        // If this is a thread pool thread, name it as such\n        if (Thread.CurrentThread.IsThreadPoolThread)\n        {\n            return $\"thread_pool_{Environment.CurrentManagedThreadId}\";\n        }\n\n        // Otherwise, if the thread is named, use it!\n        if (!string.IsNullOrEmpty(Thread.CurrentThread.Name))\n        {\n            return Thread.CurrentThread.Name;\n        }\n\n        // We don't know what this thread is!\n        return string.Empty;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Trace2CollectorWriter.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\nusing System.IO.Pipes;\nusing System.Text;\nusing System.Threading;\nusing KnownGitCfg = GitCredentialManager.Constants.GitConfiguration;\n\nnamespace GitCredentialManager\n{\n    /// <summary>\n    /// Accepts string messages from multiple threads and dispatches them over a named pipe from a\n    /// background thread.\n    /// </summary>\n    public class Trace2CollectorWriter : Trace2Writer\n    {\n        private const int DefaultMaxQueueSize = 256;\n\n        private readonly Func<NamedPipeClientStream> _createPipeFunc;\n        private readonly BlockingCollection<string> _queue;\n\n        private Thread _writerThread;\n        private NamedPipeClientStream _pipeClient;\n\n        public Trace2CollectorWriter(Trace2FormatTarget formatTarget,\n            Func<NamedPipeClientStream> createPipeFunc,\n            int maxQueueSize = DefaultMaxQueueSize) : base(formatTarget)\n        {\n            EnsureArgument.NotNull(createPipeFunc, nameof(createPipeFunc));\n            EnsureArgument.Positive(maxQueueSize, nameof(maxQueueSize));\n\n            _createPipeFunc = createPipeFunc;\n            _queue = new BlockingCollection<string>(new ConcurrentQueue<string>(), boundedCapacity: maxQueueSize);\n\n            Start();\n        }\n\n        public override void Write(Trace2Message message)\n        {\n           _queue.TryAdd(message.ToJson());\n        }\n\n        protected override void ReleaseManagedResources()\n        {\n            Stop();\n\n            _pipeClient?.Dispose();\n            _queue.Dispose();\n            base.ReleaseManagedResources();\n\n            _pipeClient = null;\n            _writerThread = null;\n        }\n\n        private void Start()\n        {\n            try\n            {\n                _writerThread = new Thread(BackgroundWriterThreadProc)\n                {\n                    Name = nameof(Trace2CollectorWriter),\n                    IsBackground = true\n                };\n\n                _writerThread.Start();\n                // Create a new pipe stream instance using the provided factory\n                _pipeClient = _createPipeFunc();\n\n                // Specify an instantaneous timeout because we don't want to hold up the\n                // background thread loop if the pipe is not available.\n                _pipeClient.Connect(timeout: 0);\n            }\n            catch\n            {\n                // Start failed. Disable this writer for this run.\n                Failed = true;\n            }\n        }\n\n        private void Stop()\n        {\n            if (_queue.IsAddingCompleted)\n            {\n                return;\n            }\n\n            // Signal to the queue draining thread that it should drain once more and then terminate.\n            _queue.CompleteAdding();\n            _writerThread.Join();\n            ReleaseManagedResources();\n        }\n\n        private void BackgroundWriterThreadProc()\n        {\n            // Drain the queue of all messages currently in the queue.\n            // TryTake() using an infinite timeout will block until either a message is available (returns true)\n            // or the queue has been marked as completed _and_ is empty (returns false).\n            while (_queue.TryTake(out string message, Timeout.Infinite))\n            {\n                if (message != null)\n                {\n                    WriteMessage(message);\n                }\n            }\n        }\n\n        private void WriteMessage(string message)\n        {\n            try\n            {\n                // We should signal the end of each message with a line-feed (LF) character.\n                if (!message.EndsWith(\"\\n\"))\n                {\n                    message += '\\n';\n                }\n\n                byte[] data = Encoding.UTF8.GetBytes(message);\n                _pipeClient.Write(data, 0, data.Length);\n                _pipeClient.Flush();\n            }\n            catch\n            {\n                // We can't send this message for some reason (e.g., broken pipe); we attempt no recovery or retry\n                // mechanism but rather disable the writer for the rest of this run.\n                Failed = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Trace2Exception.cs",
    "content": "using System;\nusing System.ComponentModel;\nusing System.IO;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Interop;\n\nnamespace GitCredentialManager;\n\npublic class Trace2Exception : Exception\n{\n    public Trace2Exception(ITrace2 trace2, string message) : base(message)\n    {\n        trace2.WriteError(message);\n    }\n\n    public Trace2Exception(ITrace2 trace2, string message, string messageFormat) : base(message)\n    {\n        trace2.WriteError(message, messageFormat);\n    }\n}\n\npublic class Trace2InvalidOperationException : InvalidOperationException\n{\n    public Trace2InvalidOperationException(ITrace2 trace2, string message) : base(message)\n    {\n        trace2.WriteError(message);\n    }\n}\n\npublic class Trace2OAuth2Exception : OAuth2Exception\n{\n    public Trace2OAuth2Exception(ITrace2 trace2, string message) : base(message)\n    {\n        trace2.WriteError(message);\n    }\n\n    public Trace2OAuth2Exception(ITrace2 trace2, string message, string messageFormat) : base(message)\n    {\n        trace2.WriteError(message, messageFormat);\n    }\n}\n\npublic class Trace2InteropException : InteropException\n{\n    public Trace2InteropException(ITrace2 trace2, string message, int errorCode) : base(message, errorCode)\n    {\n        trace2.WriteError($\"message: {message} error code: {errorCode}\");\n    }\n\n    public Trace2InteropException(ITrace2 trace2, string message, Win32Exception ex) : base(message, ex)\n    {\n        trace2.WriteError(message);\n    }\n}\n\npublic class Trace2GitException : GitException\n{\n    public Trace2GitException(ITrace2 trace2, string message, int errorCode, string gitMessage) :\n        base(message, gitMessage, errorCode)\n    {\n        var format = $\"message: '{message}' error code: '{errorCode}' git message: '{{0}}'\";\n        var traceMessage = string.Format(format, gitMessage);\n\n        trace2.WriteError(traceMessage, format);\n    }\n}\n\npublic class Trace2FileNotFoundException : FileNotFoundException\n{\n    public Trace2FileNotFoundException(ITrace2 trace2, string message, string messageFormat, string fileName) :\n        base(message, fileName)\n    {\n        trace2.WriteError(message, messageFormat);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Trace2FileWriter.cs",
    "content": "using System.IO;\nusing System;\n\nnamespace GitCredentialManager;\n\npublic class Trace2FileWriter : Trace2Writer\n{\n    private readonly string _path;\n\n    public Trace2FileWriter(Trace2FormatTarget formatTarget, string path) : base(formatTarget)\n    {\n        _path = path;\n    }\n\n    public override void Write(Trace2Message message)\n    {\n        try\n        {\n            File.AppendAllText(_path, Format(message));\n        }\n        catch (DirectoryNotFoundException)\n        {\n            // Do nothing, as this either means we don't have the\n            // parent directories above the file, or this trace2\n            // target points to a directory.\n        }\n        catch (UnauthorizedAccessException)\n        {\n            // Do nothing, as this either means the file is not\n            // accessible with current permissions, or we are on\n            // Windows and the file is currently open for writing\n            // by another process (likely Git itself.)\n        }\n        catch (IOException)\n        {\n            // Do nothing, as this likely means that the file is currently\n            // open by another process (on Windows).\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/Trace2Message.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace GitCredentialManager;\n\npublic abstract class Trace2Message\n{\n    private const int SourceColumnMaxWidth = 23;\n    private const string NormalPerfTimeFormat = \"HH:mm:ss.ffffff\";\n\n    protected const string EmptyPerformanceSpan =  \"|     |           |           |             \";\n    protected static readonly JsonSerializerOptions JsonSerializerOptions = new()\n    {\n        PropertyNameCaseInsensitive = true,\n        Converters = { new JsonStringEnumConverter(new SnakeCaseNamingPolicy()) }\n    };\n\n    [JsonPropertyName(\"event\")]\n    [JsonPropertyOrder(1)]\n    public Trace2Event Event { get; set; }\n\n    [JsonPropertyName(\"sid\")]\n    [JsonPropertyOrder(2)]\n    public string Sid { get; set; }\n\n    [JsonPropertyName(\"thread\")]\n    [JsonPropertyOrder(3)]\n    public string Thread { get; set; }\n\n    [JsonPropertyName(\"time\")]\n    [JsonPropertyOrder(4)]\n    public DateTimeOffset Time { get; set; }\n\n    [JsonPropertyName(\"file\")]\n    [JsonPropertyOrder(5)]\n    public string File { get; set; }\n\n    [JsonPropertyName(\"line\")]\n    [JsonPropertyOrder(6)]\n    public int Line { get; set; }\n\n    [JsonPropertyName(\"depth\")]\n    [JsonPropertyOrder(7)]\n    public int Depth { get; set; }\n\n    public abstract string ToJson();\n\n    public abstract string ToNormalString();\n\n    public abstract string ToPerformanceString();\n\n    protected abstract string BuildPerformanceSpan();\n\n    protected string BuildNormalString()\n    {\n        string message = GetEventMessage(Trace2FormatTarget.Normal);\n\n        // The normal format uses local time rather than UTC time.\n        string time = Time.ToLocalTime().ToString(NormalPerfTimeFormat);\n        string source = GetSource();\n\n        // Git's TRACE2 normal format is:\n        // [<time> SP <filename>:<line> SP+] <event-name> [[SP] <event-message>] LF\n        return $\"{time} {source,-33} {Event.ToString().ToSnakeCase()} {message}\";\n    }\n\n    protected string BuildPerformanceString()\n    {\n        string message = GetEventMessage(Trace2FormatTarget.Performance);\n\n        // The performance format uses local time rather than UTC time.\n        var time = Time.ToLocalTime().ToString(NormalPerfTimeFormat);\n        var source = GetSource();\n\n        // Git's TRACE2 performance format is:\n        // [<time> SP <filename>:<line> SP+\n        //     BAR SP] d<depth> SP\n        //     BAR SP <thread-name> SP+\n        //     BAR SP <event-name> SP+\n        //     BAR SP [r<repo-id>] SP+\n        //     BAR SP [<t_abs>] SP+\n        //     BAR SP [<t_rel>] SP+\n        //     BAR SP [<category>] SP+\n        //     BAR SP DOTS* <perf-event-message>\n        //     LF\n        return $\"{time} {source,-29}| d{Depth} | {Thread,-24} | {Event.ToString().ToSnakeCase(),-12} {BuildPerformanceSpan()} | {message}\";\n    }\n\n    protected abstract string GetEventMessage(Trace2FormatTarget formatTarget);\n\n    private string GetSource()\n    {\n        // Source column format is file:line\n        string source = $\"{File}:{Line}\";\n        if (source.Length > SourceColumnMaxWidth)\n        {\n            return TraceUtils.FormatSource(source, SourceColumnMaxWidth);\n        }\n\n        return source;\n    }\n\n    internal static string BuildTimeSpan(double time)\n    {\n        var timeString = time.ToString(\"F6\");\n        var component = new PerformanceFormatSpan()\n        {\n            Size = 11,\n            BeginPadding = 2,\n            EndPadding = 1\n        };\n\n        return BuildSpan(component, timeString);\n    }\n\n    internal static string BuildCategorySpan(string category)\n    {\n        var component = new PerformanceFormatSpan()\n        {\n            Size = 13,\n            BeginPadding = 1,\n            EndPadding = 1\n        };\n\n        return BuildSpan(component, category);\n    }\n\n    internal static string BuildRepoSpan(int repo)\n    {\n        var component = new PerformanceFormatSpan()\n        {\n            Size = 5,\n            BeginPadding = 1,\n            EndPadding = 2\n        };\n\n        return BuildSpan(component, $\"r{repo}\");\n    }\n\n    private static string BuildSpan(PerformanceFormatSpan component, string data)\n    {\n        var paddingTotal = component.BeginPadding + component.EndPadding;\n        var dataLimit = component.Size - paddingTotal;\n        var sizeDifference = dataLimit - data.Length;\n\n        if (sizeDifference <= 0)\n        {\n            if (double.TryParse(data, out _))\n            {\n                // Remove all padding for values that take up the entire span\n                if (Math.Abs(sizeDifference) == paddingTotal)\n                {\n                    component.BeginPadding = 0;\n                    component.EndPadding = 0;\n                }\n                else\n                {\n                    // Decrease BeginPadding for large time values that don't occupy entire span\n                    component.BeginPadding += sizeDifference;\n                }\n            }\n            else\n            {\n                // Truncate value\n                data = data.Substring(0, dataLimit);\n            }\n        }\n\n        if (data.Length < dataLimit)\n        {\n            // Increase end padding for short values\n            component.EndPadding += sizeDifference;\n        }\n\n        var beginPadding = new string(' ', component.BeginPadding);\n        var endPadding = new string(' ', component.EndPadding);\n\n        return $\"{beginPadding}{data}{endPadding}\";\n    }\n}\n\npublic class VersionMessage : Trace2Message\n{\n    [JsonPropertyName(\"evt\")]\n    [JsonPropertyOrder(8)]\n    public string Evt { get; set; }\n\n    [JsonPropertyName(\"exe\")]\n    [JsonPropertyOrder(9)]\n    public string Exe { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return EmptyPerformanceSpan;\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        return Exe.ToLower();\n    }\n}\n\npublic class StartMessage : Trace2Message\n{\n    [JsonPropertyName(\"t_abs\")]\n    [JsonPropertyOrder(8)]\n    public double ElapsedTime { get; set; }\n\n    [JsonPropertyName(\"argv\")]\n    [JsonPropertyOrder(9)]\n    public List<string> Argv { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return $\"|     |{BuildTimeSpan(ElapsedTime)}|           |             \";\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        return string.Join(\" \", Argv);\n    }\n}\n\npublic class ExitMessage : Trace2Message\n{\n    [JsonPropertyName(\"t_abs\")]\n    [JsonPropertyOrder(8)]\n    public double ElapsedTime { get; set; }\n\n    [JsonPropertyName(\"code\")]\n    [JsonPropertyOrder(9)]\n    public int Code { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return $\"|     |{BuildTimeSpan(ElapsedTime)}|           |             \";\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        return $\"elapsed:{ElapsedTime} code:{Code}\";\n    }\n}\n\npublic class ChildStartMessage : Trace2Message\n{\n    [JsonPropertyName(\"t_abs\")]\n    [JsonPropertyOrder(8)]\n    public double ElapsedTime { get; set; }\n\n    [JsonPropertyName(\"argv\")]\n    [JsonPropertyOrder(9)]\n    public IList<string> Argv { get; set; }\n\n    [JsonPropertyName(\"child_id\")]\n    [JsonPropertyOrder(10)]\n    public long Id { get; set; }\n\n    [JsonPropertyName(\"child_class\")]\n    [JsonPropertyOrder(11)]\n    public Trace2ProcessClass Classification { get; set; }\n\n    [JsonPropertyName(\"use_shell\")]\n    [JsonPropertyOrder(12)]\n    public bool UseShell { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return $\"|     |{BuildTimeSpan(ElapsedTime)}|           |             \";\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        var sb = new StringBuilder();\n\n        if (formatTarget == Trace2FormatTarget.Performance)\n            sb.Append($\"[ch{Id}]\");\n        else\n            sb.Append($\"[{Id}]\");\n\n        sb.Append($\" {string.Join(\" \", Argv)}\");\n\n        return sb.ToString();\n    }\n}\n\npublic class ChildExitMessage : Trace2Message\n{\n    [JsonPropertyName(\"t_abs\")]\n    [JsonPropertyOrder(8)]\n    public double ElapsedTime { get; set; }\n\n    [JsonPropertyName(\"t_rel\")]\n    [JsonPropertyOrder(9)]\n    public double RelativeTime { get; set; }\n\n    [JsonPropertyName(\"child_id\")]\n    [JsonPropertyOrder(10)]\n    public long Id { get; set; }\n\n    [JsonPropertyName(\"pid\")]\n    [JsonPropertyOrder(11)]\n    public int Pid { get; set; }\n\n    [JsonPropertyName(\"code\")]\n    [JsonPropertyOrder(12)]\n    public int Code { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return $\"|     |{BuildTimeSpan(ElapsedTime)}|{BuildTimeSpan(RelativeTime)}|             \";\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        var sb = new StringBuilder();\n\n        if (formatTarget == Trace2FormatTarget.Performance)\n            sb.Append($\"[ch{Id}]\");\n        else\n            sb.Append($\"[{Id}]\");\n\n        sb.Append($\" pid:{Pid} code:{Code} elapsed:{RelativeTime}\");\n        return sb.ToString();\n    }\n}\n\npublic class ErrorMessage : Trace2Message\n{\n    [JsonPropertyName(\"msg\")]\n    [JsonPropertyOrder(8)]\n    public string Message { get; set; }\n\n    [JsonPropertyName(\"fmt\")]\n    [JsonPropertyOrder(9)]\n    public string ParameterizedMessage { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return EmptyPerformanceSpan;\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        return Message;\n    }\n}\n\npublic abstract class RegionMessage : Trace2Message\n{\n    [JsonPropertyName(\"t_abs\")]\n    [JsonPropertyOrder(8)]\n    public double ElapsedTime { get; set; }\n\n    [JsonPropertyName(\"repo\")]\n    [JsonPropertyOrder(9)]\n    // Defaults to 1, as does Git.\n    // See https://git-scm.com/docs/api-trace2#Documentation/technical/api-trace2.txt-codeltrepo-idgtcode for details.\n    public int Repo { get; set; } = 1;\n\n    // TODO: Remove default value if support for nested regions is implemented.\n    [JsonPropertyName(\"nesting\")]\n    [JsonPropertyOrder(10)]\n    public int Nesting { get; set; } = 1;\n\n    [JsonPropertyName(\"category\")]\n    [JsonPropertyOrder(11)]\n    public string Category { get; set; }\n\n    [JsonPropertyName(\"label\")]\n    [JsonPropertyOrder(12)]\n    public string Label { get; set; }\n\n    [JsonPropertyName(\"msg\")]\n    [JsonPropertyOrder(13)]\n    public string Message { get; set; }\n}\n\npublic class RegionEnterMessage : RegionMessage\n{\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return $\"|{BuildRepoSpan(Repo)}|{BuildTimeSpan(ElapsedTime)}|           |{BuildCategorySpan(Category)}\";\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        return Message;\n    }\n}\n\npublic class RegionLeaveMessage : RegionMessage\n{\n    [JsonPropertyOrder(14)]\n    public double RelativeTime { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this, JsonSerializerOptions);\n    }\n\n    public override string ToNormalString()\n    {\n        return BuildNormalString();\n    }\n\n    public override string ToPerformanceString()\n    {\n        return BuildPerformanceString();\n    }\n\n    protected override string BuildPerformanceSpan()\n    {\n        return $\"|{BuildRepoSpan(Repo)}|{BuildTimeSpan(ElapsedTime)}|{BuildTimeSpan(RelativeTime)}|{BuildCategorySpan(Category)}\";\n    }\n\n    protected override string GetEventMessage(Trace2FormatTarget formatTarget)\n    {\n        return Message;\n    }\n}\n\npublic class SnakeCaseNamingPolicy : JsonNamingPolicy\n{\n    public override string ConvertName(string name) =>\n        name.ToSnakeCase();\n}\n"
  },
  {
    "path": "src/shared/Core/Trace2StreamWriter.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace GitCredentialManager;\n\npublic class Trace2StreamWriter : Trace2Writer\n{\n    private readonly TextWriter _writer;\n\n    public Trace2StreamWriter(Trace2FormatTarget formatTarget, TextWriter writer)\n        : base(formatTarget)\n    {\n        _writer = writer;\n    }\n\n    public override void Write(Trace2Message message)\n    {\n        try\n        {\n            _writer.Write(Format(message));\n            _writer.Flush();\n        }\n        catch\n        {\n            Failed = true;\n        }\n    }\n\n    protected override void ReleaseManagedResources()\n    {\n        _writer.Dispose();\n        base.ReleaseManagedResources();\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/TraceUtils.cs",
    "content": "namespace GitCredentialManager;\n\npublic static class TraceUtils\n{\n    public static string FormatSource(string source, int sourceColumnMaxWidth)\n    {\n        int idx = 0;\n        int maxlen = sourceColumnMaxWidth - 3;\n        int srclen = source.Length;\n\n        while (idx >= 0 && (srclen - idx) > maxlen)\n        {\n            idx = source.IndexOf('\\\\', idx + 1);\n        }\n\n        // If we cannot find a path separator which allows the path to be long enough, just truncate the file name\n        if (idx < 0)\n        {\n            idx = srclen - maxlen;\n        }\n\n        return \"...\" + source.Substring(idx);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Assets/Base.axaml",
    "content": "<ResourceDictionary xmlns=\"https://github.com/avaloniaui\"\n                    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <ResourceDictionary.ThemeDictionaries>\n\n        <ResourceDictionary x:Key=\"Default\">\n            <Color x:Key=\"WindowBackgroundColor\">#F6F6F6</Color>\n            <SolidColorBrush x:Key=\"WindowBackgroundBrush\" Color=\"{StaticResource WindowBackgroundColor}\"/>\n            <SolidColorBrush x:Key=\"DialogWindowCloseButtonBrush\" Color=\"Black\"/>\n            <SolidColorBrush x:Key=\"DialogWindowBorderBrush\" Color=\"#CCCEDB\"/>\n        </ResourceDictionary>\n\n        <ResourceDictionary x:Key=\"Dark\">\n            <Color x:Key=\"WindowBackgroundColor\">#282828</Color>\n            <SolidColorBrush x:Key=\"WindowBackgroundBrush\" Color=\"{StaticResource WindowBackgroundColor}\"/>\n            <SolidColorBrush x:Key=\"DialogWindowCloseButtonBrush\" Color=\"White\"/>\n            <SolidColorBrush x:Key=\"DialogWindowBorderBrush\" Color=\"#474747\"/>\n        </ResourceDictionary>\n\n    </ResourceDictionary.ThemeDictionaries>\n</ResourceDictionary>\n"
  },
  {
    "path": "src/shared/Core/UI/Assets/ButtonHyperlink.axaml",
    "content": "<Styles xmlns=\"https://github.com/avaloniaui\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <Style Selector=\"Button.hyperlink\">\n        <Setter Property=\"Foreground\" Value=\"{StaticResource SystemControlHyperlinkTextBrush}\"/>\n        <Setter Property=\"Background\" Value=\"Transparent\"/>\n        <Setter Property=\"BorderThickness\" Value=\"0\"/>\n        <Setter Property=\"Padding\" Value=\"0\"/>\n        <Setter Property=\"Cursor\" Value=\"Hand\"/>\n    </Style>\n    <Style Selector=\"Button.hyperlink:pointerover TextBlock\">\n        <Setter Property=\"Foreground\" Value=\"{StaticResource SystemControlHyperlinkBaseHighBrush}\"/>\n        <Setter Property=\"Background\" Value=\"Transparent\"/>\n    </Style>\n    <Style Selector=\"Button.hyperlink:disabled TextBlock\">\n        <Setter Property=\"Foreground\" Value=\"{StaticResource TextControlForegroundDisabled}\"/>\n        <Setter Property=\"Background\" Value=\"Transparent\"/>\n    </Style>\n</Styles>\n"
  },
  {
    "path": "src/shared/Core/UI/Assets/Controls.axaml",
    "content": "<Styles xmlns=\"https://github.com/avaloniaui\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <StyleInclude Source=\"avares://gcmcore/UI/Assets/ButtonHyperlink.axaml\"/>\n    <Style Selector=\"TextBox\">\n        <Setter Property=\"VerticalContentAlignment\" Value=\"Center\"/>\n    </Style>\n    <Style Selector=\"TextBox.monospace\">\n        <Setter Property=\"FontFamily\" Value=\"SF Mono, Courier New, Courier, monospace\" />\n    </Style>\n    <Style Selector=\"TextBox.label\">\n        <Setter Property=\"Background\" Value=\"Transparent\" />\n        <Setter Property=\"BorderThickness\" Value=\"0\" />\n        <Setter Property=\"IsReadOnly\" Value=\"True\" />\n    </Style>\n    <Style Selector=\"TextBox.label:focus\">\n        <Setter Property=\"BorderThickness\" Value=\"0\" />\n    </Style>\n</Styles>\n"
  },
  {
    "path": "src/shared/Core/UI/Assets/Images.axaml",
    "content": "<ResourceDictionary xmlns=\"https://github.com/avaloniaui\"\n                    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <ResourceDictionary.ThemeDictionaries>\n        <ResourceDictionary x:Key=\"Default\">\n            <Color x:Key=\"IconColor\">#FF25292F</Color>\n            <Color x:Key=\"WarningColor\">#FFFF7F27</Color>\n            <SolidColorBrush x:Key=\"IconBrush\" Color=\"{DynamicResource IconColor}\"/>\n            <SolidColorBrush x:Key=\"WarningBrush\" Color=\"{DynamicResource WarningColor}\"/>\n        </ResourceDictionary>\n        <ResourceDictionary x:Key=\"Dark\">\n            <Color x:Key=\"IconColor\">White</Color>\n            <Color x:Key=\"WarningColor\">#FFFFD40F</Color>\n            <SolidColorBrush x:Key=\"IconBrush\" Color=\"{DynamicResource IconColor}\"/>\n            <SolidColorBrush x:Key=\"WarningBrush\" Color=\"{DynamicResource WarningColor}\"/>\n        </ResourceDictionary>\n    </ResourceDictionary.ThemeDictionaries>\n    <Pen x:Key=\"WarningPen\" Brush=\"{DynamicResource WarningBrush}\" Thickness=\"2\"/>\n    <!--\n        These icon resource keys must be kept in sync with the IconConverter found in\n        src/shared/Core/UI/Converters/IconConverter.cs.\n      -->\n    <DrawingImage x:Key=\"GcmLogo\">\n        <DrawingImage.Drawing>\n            <DrawingGroup>\n                <DrawingGroup.Children>\n                    <GeometryDrawing Brush=\"#FFF05033\" Geometry=\"m 127.74219 0 c -4.21881 0 -8.43843 1.6093415 -11.65821 4.828125 L 92.875 28.041016 122.31445 57.482422 c 6.84425 -2.310946 14.68947 -0.761046 20.14258 4.693359 5.48141 5.488392 7.0192 13.400136 4.65039 20.267578 l 28.37696 28.376951 c 6.86492 -2.36579 14.78398 -0.83727 20.26562 4.6543 7.66374 7.66179 7.66374 20.07642 0 27.74023 -7.66486 7.66631 -20.07893 7.66631 -27.74805 0 -5.76254 -5.76724 -7.18821 -14.23373 -4.26953 -21.33398 l -26.46289 -26.464844 -0.002 69.640624 c 1.8684 0.92496 3.63248 2.16064 5.18945 3.71094 7.66148 7.6609 7.66148 20.07236 0 27.74609 -7.66374 7.66066 -20.08459 7.66066 -27.74023 0 -7.66262 -7.67305 -7.66262 -20.08452 0 -27.74609 1.89369 -1.89039 4.08382 -3.32218 6.42187 -4.28125 V 94.199219 c -2.33912 -0.955028 -4.52734 -2.375687 -6.42383 -4.28125 -5.80409 -5.799832 -7.20125 -14.319608 -4.22461 -21.447266 L 81.466797 39.443359 4.8300781 116.08203 c -6.4393305 6.44252 -6.4393305 16.88229 0 23.32031 L 42.5 177.07227 V 162.70508 C 35.078345 157.16403 29.678851 149.02425 27.328125 140.07617 19.76286 115.647 40.902921 87.908596 66.359375 88.345703 76.747705 88.003924 87.132367 91.94485 94.875 98.837891 c 18.78493 15.372969 17.87151 47.496839 -0.888672 62.484379 l -2.566406 1.3457 0.222656 12.46503 -8.160156 8.24395 v 34.67578 l 33.119138 33.11915 c 6.43505 6.43757 16.87041 6.43757 23.31446 0 L 251.17188 139.92969 c 6.43732 -6.4407 6.43732 -16.88336 0 -23.32227 l 0.002 -0.01 L 139.39453 4.828125 C 136.1788 1.6093415 131.96099 0 127.74219 0 Z\" />\n                    <GeometryDrawing Brush=\"#FF4D4D4D\" Geometry=\"M 67.333984 94.333984 A 35.333332 35.333332 0 0 0 32 129.66602 A 35.333332 35.333332 0 0 0 48.5 159.54688 L 48.5 234.5 L 54.5 240 L 67 240 L 79 228 L 79 216.5 L 73 210 L 79 203.5 L 73 197 L 79 191 L 73 185.5 L 85.5 173 L 85.5 159.92188 A 35.333332 35.333332 0 0 0 102.66602 129.66602 A 35.333332 35.333332 0 0 0 67.333984 94.333984 z M 66.777344 109 A 9 9 0 0 1 75.777344 118 A 9 9 0 0 1 66.777344 127 A 9 9 0 0 1 57.777344 118 A 9 9 0 0 1 66.777344 109 z M 54.5 168 L 60.5 173 L 60.5 234.5 L 54.5 228 L 54.5 168 z \" />\n                </DrawingGroup.Children>\n            </DrawingGroup>\n        </DrawingImage.Drawing>\n    </DrawingImage>\n    <DrawingImage x:Key=\"PersonIcon\">\n        <DrawingImage.Drawing>\n            <DrawingGroup>\n                <DrawingGroup.Children>\n                    <GeometryDrawing Brush=\"{DynamicResource IconBrush}\" Geometry=\"M12 2.5a5.5 5.5 0 0 1 3.096 10.047 9.005 9.005 0 0 1 5.9 8.181.75.75 0 1 1-1.499.044 7.5 7.5 0 0 0-14.993 0 .75.75 0 0 1-1.5-.045 9.005 9.005 0 0 1 5.9-8.18A5.5 5.5 0 0 1 12 2.5ZM8 8a4 4 0 1 0 8 0 4 4 0 0 0-8 0Z\"/>\n                </DrawingGroup.Children>\n            </DrawingGroup>\n        </DrawingImage.Drawing>\n    </DrawingImage>\n    <DrawingImage x:Key=\"InfoIcon\">\n        <DrawingImage.Drawing>\n            <DrawingGroup>\n                <DrawingGroup.Children>\n                    <GeometryDrawing Brush=\"{DynamicResource IconBrush}\" Geometry=\"M13 7.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm-3 3.75a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v4.25h.75a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.75V12h-.75a.75.75 0 0 1-.75-.75Z\"/>\n                    <GeometryDrawing Brush=\"{DynamicResource IconBrush}\" Geometry=\"M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM2.5 12a9.5 9.5 0 0 0 9.5 9.5 9.5 9.5 0 0 0 9.5-9.5A9.5 9.5 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z\"/>\n                </DrawingGroup.Children>\n            </DrawingGroup>\n        </DrawingImage.Drawing>\n    </DrawingImage>\n    <DrawingImage x:Key=\"HelpIcon\">\n        <DrawingImage.Drawing>\n            <DrawingGroup>\n                <DrawingGroup.Children>\n                    <GeometryDrawing Brush=\"{DynamicResource IconBrush}\" Geometry=\"M10.97 8.265a1.45 1.45 0 0 0-.487.57.75.75 0 0 1-1.341-.67c.2-.402.513-.826.997-1.148C10.627 6.69 11.244 6.5 12 6.5c.658 0 1.369.195 1.934.619a2.45 2.45 0 0 1 1.004 2.006c0 1.033-.513 1.72-1.027 2.215-.19.183-.399.358-.579.508l-.147.123a4.329 4.329 0 0 0-.435.409v1.37a.75.75 0 1 1-1.5 0v-1.473c0-.237.067-.504.247-.736.22-.28.486-.517.718-.714l.183-.153.001-.001c.172-.143.324-.27.47-.412.368-.355.569-.676.569-1.136a.953.953 0 0 0-.404-.806C12.766 8.118 12.384 8 12 8c-.494 0-.814.121-1.03.265ZM13 17a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"/>\n                    <GeometryDrawing Brush=\"{DynamicResource IconBrush}\" Geometry=\"M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM2.5 12a9.5 9.5 0 0 0 9.5 9.5 9.5 9.5 0 0 0 9.5-9.5A9.5 9.5 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z\"/>\n                </DrawingGroup.Children>\n            </DrawingGroup>\n        </DrawingImage.Drawing>\n    </DrawingImage>\n    <DrawingImage x:Key=\"WarningIcon\">\n        <DrawingImage.Drawing>\n            <DrawingGroup>\n                <DrawingGroup.Children>\n                    <GeometryDrawing Pen=\"{DynamicResource WarningPen}\" Geometry=\"M12,2 L22,20 L2,20 Z\"/>\n                    <GeometryDrawing Pen=\"{DynamicResource WarningPen}\" Geometry=\"M12,8 L12,14\"/>\n                    <GeometryDrawing Brush=\"{DynamicResource WarningBrush}\" Geometry=\"M11,16 a1,1 0 1,0 2,0 a1,1 0 1,0 -2,0\"/>\n                </DrawingGroup.Children>\n            </DrawingGroup>\n        </DrawingImage.Drawing>\n    </DrawingImage>\n</ResourceDictionary>\n"
  },
  {
    "path": "src/shared/Core/UI/AvaloniaApp.axaml",
    "content": "<Application xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             x:Class=\"GitCredentialManager.UI.AvaloniaApp\"\n             Name=\"Git Credential Manager\">\n    <Application.Styles>\n        <FluentTheme/>\n        <StyleInclude Source=\"avares://gcmcore/UI/Assets/Controls.axaml\"/>\n    </Application.Styles>\n    <Application.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceInclude Source=\"avares://gcmcore/UI/Assets/Base.axaml\"/>\n                <ResourceInclude Source=\"avares://gcmcore/UI/Assets/Images.axaml\"/>\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </Application.Resources>\n    <NativeMenu.Menu>\n        <NativeMenu>\n            <NativeMenuItem Header=\"About Git Credential Manager\" Click=\"About\" />\n        </NativeMenu>\n    </NativeMenu.Menu>\n</Application>\n"
  },
  {
    "path": "src/shared/Core/UI/AvaloniaApp.axaml.cs",
    "content": "using System;\nusing Avalonia.Controls;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager.UI.Controls;\n\nnamespace GitCredentialManager.UI\n{\n    public class AvaloniaApp : Avalonia.Application\n    {\n        private readonly Func<Window> _mainWindowFunc;\n\n        public AvaloniaApp() { }\n\n        public AvaloniaApp(Func<Window> mainWindowFunc)\n        {\n            _mainWindowFunc = mainWindowFunc;\n        }\n\n        public override void Initialize()\n        {\n            AvaloniaXamlLoader.Load(this);\n        }\n\n        public override void OnFrameworkInitializationCompleted()\n        {\n            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && _mainWindowFunc != null)\n            {\n                desktop.MainWindow = _mainWindowFunc();\n            }\n\n            base.OnFrameworkInitializationCompleted();\n        }\n\n        private void About(object sender, EventArgs e)\n        {\n            var window = new AboutWindow();\n            window.Show();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/AvaloniaUi.cs",
    "content": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Threading;\nusing GitCredentialManager.Interop.Windows.Native;\nusing GitCredentialManager.UI.Controls;\nusing GitCredentialManager.UI.ViewModels;\nusing AvnDispatcher = Avalonia.Threading.Dispatcher;\n\nnamespace GitCredentialManager.UI\n{\n    public static class AvaloniaUi\n    {\n        private static bool _isAppStarted;\n        private static bool _win32SoftwareRendering;\n\n        /// <summary>\n        /// Configure the Avalonia application.\n        /// </summary>\n        /// <param name=\"win32SoftwareRendering\">True to enable software rendering on Windows, false otherwise.</param>\n        /// <remarks>\n        /// This must be invoked before the Avalonia application loop has started.\n        /// </remarks>\n        public static void Initialize(bool win32SoftwareRendering)\n        {\n            if (_isAppStarted)\n            {\n                throw new InvalidOperationException(\"Setup must be called before the Avalonia application is started.\");\n            }\n\n            _win32SoftwareRendering = win32SoftwareRendering;\n        }\n\n        public static Task ShowViewAsync(Func<Control> viewFunc, WindowViewModel viewModel, IntPtr parentHandle, CancellationToken ct) =>\n            ShowWindowAsync(() => new DialogWindow(viewFunc()), viewModel, parentHandle, ct);\n\n        public static Task ShowViewAsync<TView>(WindowViewModel viewModel, IntPtr parentHandle, CancellationToken ct)\n            where TView : Control, new() =>\n            ShowWindowAsync(() => new DialogWindow(new TView()), viewModel, parentHandle, ct);\n\n        public static Task ShowWindowAsync(Func<Window> windowFunc, IntPtr parentHandle, CancellationToken ct) =>\n            ShowWindowAsync(windowFunc, null, parentHandle, ct);\n\n        public static Task ShowWindowAsync<TWindow>(IntPtr parentHandle, CancellationToken ct)\n            where TWindow : Window, new() =>\n            ShowWindowAsync(() => new TWindow(), null, parentHandle, ct);\n\n        public static Task ShowWindowAsync<TWindow>(object dataContext, IntPtr parentHandle, CancellationToken ct)\n            where TWindow : Window, new() =>\n            ShowWindowAsync(() => new TWindow(), dataContext, parentHandle, ct);\n\n        public static Task ShowWindowAsync(Func<Window> windowFunc, object dataContext, IntPtr parentHandle, CancellationToken ct)\n        {\n            if (!_isAppStarted)\n            {\n                _isAppStarted = true;\n\n                var appInitialized = new ManualResetEventSlim();\n\n                // Fire and forget the Avalonia app main loop over to our dispatcher (running on the main/entry thread).\n                // This action only returns on our dispatcher shutdown.\n                Dispatcher.MainThread.Post(appCancelToken =>\n                {\n                    var appBuilder = AppBuilder.Configure<AvaloniaApp>();\n\n#if NETFRAMEWORK\n                    // Set custom rendering options and modes if required\n                    if (PlatformUtils.IsWindows() && _win32SoftwareRendering)\n                    {\n                        appBuilder.With(new Win32PlatformOptions\n                            { RenderingMode = new[] { Win32RenderingMode.Software } });\n                    }\n#endif\n\n                    appBuilder\n#if NETFRAMEWORK\n                        .UseWin32()\n                        .UseSkia()\n#else\n                        .UsePlatformDetect()\n#endif\n                        .LogToTrace()\n                        .SetupWithoutStarting();\n\n                    appInitialized.Set();\n\n                    // Run the application loop (only exit when the dispatcher is shutting down)\n                    AvnDispatcher.UIThread.MainLoop(appCancelToken);\n                });\n\n                // Wait for the action posted above to be dequeued from the dispatcher's job queue\n                // and for the Avalonia framework (and their dispatcher) to be initialized.\n                appInitialized.Wait();\n            }\n\n            // Post the window action to the Avalonia dispatcher (which should be running)\n            return AvnDispatcher.UIThread.InvokeAsync(\n                () => ShowWindowInternal(windowFunc, dataContext, parentHandle, ct),\n                DispatcherPriority.Send\n            );\n        }\n\n        private static Task ShowWindowInternal(Func<Window> windowFunc, object dataContext, IntPtr parentHandle, CancellationToken ct)\n        {\n            var tcs = new TaskCompletionSource<object>();\n            Window window = windowFunc();\n            window.DataContext = dataContext;\n            ct.Register(() =>\n                AvnDispatcher.UIThread.InvokeAsync(() => window.Close())\n            );\n            window.Closed += (s, e) => tcs.SetResult(null);\n            window.Show();\n\n            // Avalonia requires a managed \"Avalonia.Controls.Window\" instance to set the\n            // parent/owner window of our window. Since our parent is external and we only\n            // have a window handle/ID we must manually parent the window.\n            if (parentHandle != IntPtr.Zero)\n            {\n                SetParentExternal(window, parentHandle);\n            }\n\n            // Bring the window in to focus\n            window.Activate();\n            window.Focus();\n\n            // Workaround an issue where \"Activate()\" and \"Focus()\" don't actually\n            // cause the window to become the top-most window. Avalonia is correctly\n            // calling 'makeKeyAndOrderFront' but this isn't working for some reason.\n            if (PlatformUtils.IsMacOS())\n            {\n                window.Topmost = true;\n                window.Topmost = false;\n            }\n\n            return tcs.Task;\n        }\n\n        private static void SetParentExternal(Window window, IntPtr parentHandle)\n        {\n            // We only support parenting on the Windows platform at the moment.\n            if (!PlatformUtils.IsWindows())\n            {\n                return;\n            }\n\n            IntPtr ourHandle = window.TryGetPlatformHandle()!.Handle;\n\n            // Get the desktop scaling factor from our window instance so we\n            // can calculate rects correctly for both our window, and the parent window.\n            double scaling = window.RenderScaling;\n\n            // Get our window rect\n            var ourRect = new PixelRect(\n                PixelPoint.Origin,\n                PixelSize.FromSize(window.ClientSize, scaling));\n\n            // Get the parent rect\n            if (!(GetWindowRectWin32(parentHandle, scaling) is PixelRect parentRect))\n            {\n                return;\n            }\n\n            // Set the position of our window to the center of the parent.\n            PixelRect centerRect = parentRect.CenterRect(ourRect);\n            window.Position = centerRect.Position;\n\n            // Tell the platform native windowing system that we wish to parent\n            // our window to the external window handle.\n            SetWindowParentWin32(ourHandle, parentHandle);\n        }\n\n        private static PixelRect? GetWindowRectWin32(IntPtr hwnd, double scaling)\n        {\n            if (!User32.GetWindowRect(hwnd, out RECT windowRect)) return null;\n            var parentPosition = new PixelPoint(windowRect.left, windowRect.top);\n\n            if (!User32.GetClientRect(hwnd, out RECT clientRect)) return null;\n            var parentClientSize = new Size(clientRect.right, clientRect.bottom) / scaling;\n\n            return new PixelRect(\n                parentPosition,\n                PixelSize.FromSize(parentClientSize, scaling));\n        }\n\n        private static void SetWindowParentWin32(IntPtr hwnd, IntPtr parentHwnd)\n        {\n            // Note that the docs say do NOT call this method to set the parent.. call SetParent instead.\n            // Avalonia UI itself uses this \"incorrect\" method, and when experimenting to try and use SetParent\n            // (and update the WS_POPUP -> WS_CHILD window style), we get an invisible window.. hmm...\n            User32.SetWindowLongPtr(hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Commands/CredentialsCommand.cs",
    "content": "using System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.UI.Commands\n{\n    public abstract class CredentialsCommand : HelperCommand\n    {\n        protected CredentialsCommand(ICommandContext context)\n            : base(context, \"basic\", \"Show basic authentication prompt.\")\n        {\n            var title = new Option<string>(\"--title\", \"Window title (optional).\");\n            AddOption(title);\n\n            var resource = new Option<string>(\"--resource\", \"Resource name or URL (optional).\");\n            AddOption(resource);\n\n            var userName = new Option<string>(\"--username\", \"User name (optional).\");\n            AddOption(userName);\n\n            var noLogo = new Option<bool>(\"--no-logo\", \"Hide the Git Credential Manager logo and logotype.\");\n            AddOption(noLogo);\n\n            this.SetHandler(ExecuteAsync, title, resource, userName, noLogo);\n        }\n\n        private async Task<int> ExecuteAsync(string title, string resource, string userName, bool noLogo)\n        {\n            var viewModel = new CredentialsViewModel();\n\n            if (!string.IsNullOrWhiteSpace(title))\n            {\n                viewModel.Title = title;\n            }\n\n            viewModel.Description = !string.IsNullOrWhiteSpace(resource)\n                ? $\"Enter your credentials for '{resource}'\"\n                : \"Enter your credentials\";\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                viewModel.UserName = userName;\n            }\n\n            viewModel.ShowProductHeader = !noLogo;\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            WriteResult(\n                new Dictionary<string, string>\n                {\n                    [\"username\"] = viewModel.UserName,\n                    [\"password\"] = viewModel.Password\n                }\n            );\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Commands/DefaultAccountCommand.cs",
    "content": "using System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.UI.Commands;\n\npublic abstract class DefaultAccountCommand : HelperCommand\n{\n    protected DefaultAccountCommand(ICommandContext context)\n        : base(context, \"default-account\", \"Show prompt to confirm use of the default OS account.\")\n    {\n        var title = new Option<string>(\"--title\", \"Window title (optional).\");\n        AddOption(title);\n\n        var userName = new Option<string>(\"--username\", \"User name to display.\")\n        {\n            IsRequired = true\n        };\n        AddOption(userName);\n\n        var noLogo = new Option<bool>(\"--no-logo\", \"Hide the Git Credential Manager logo and logotype.\");\n        AddOption(noLogo);\n\n        this.SetHandler(ExecuteAsync, title, userName, noLogo);\n    }\n\n    private async Task<int> ExecuteAsync(string title, string userName, bool noLogo)\n    {\n        var viewModel = new DefaultAccountViewModel(Context.SessionManager)\n        {\n            Title = !string.IsNullOrWhiteSpace(title)\n                ? title\n                : \"Git Credential Manager\",\n            UserName = userName,\n            ShowProductHeader = !noLogo\n        };\n\n        await ShowAsync(viewModel, CancellationToken.None);\n\n        if (!viewModel.WindowResult)\n        {\n            throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n        }\n\n        WriteResult(\n            new Dictionary<string, string>\n            {\n                [\"use_default_account\"] = viewModel.UseDefaultAccount ? \"1\" : \"0\"\n            }\n        );\n\n        return 0;\n    }\n\n    protected abstract Task ShowAsync(DefaultAccountViewModel viewModel, CancellationToken ct);\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Commands/DeviceCodeCommand.cs",
    "content": "using System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.UI.Commands\n{\n    public abstract class DeviceCodeCommand : HelperCommand\n    {\n        protected DeviceCodeCommand(ICommandContext context)\n            : base(context, \"device\", \"Show device code prompt.\")\n        {\n            var code = new Option<string>(\"--code\", \"User code.\");\n            AddOption(code);\n\n            var url =new Option<string>(\"--url\", \"Verification URL.\");\n            AddOption(url);\n\n            var noLogo = new Option<bool>(\"--no-logo\", \"Hide the Git Credential Manager logo and logotype.\");\n            AddOption(noLogo);\n\n            this.SetHandler(ExecuteAsync, code, url, noLogo);\n        }\n\n        private async Task<int> ExecuteAsync(string code, string url, bool noLogo)\n        {\n            var viewModel = new DeviceCodeViewModel(Context.SessionManager)\n            {\n                UserCode = code,\n                VerificationUrl = url,\n            };\n\n            viewModel.ShowProductHeader = !noLogo;\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(DeviceCodeViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Commands/OAuthCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.UI.Commands\n{\n    public abstract class OAuthCommand : HelperCommand\n    {\n        protected OAuthCommand(ICommandContext context)\n            : base(context, \"oauth\", \"Show OAuth authentication prompt.\")\n        {\n            var title = new Option<string>(\"--title\", \"Window title (optional).\");\n            AddOption(title);\n\n            var resource = new Option<string>(\"--resource\", \"Resource name or URL (optional).\");\n            AddOption(resource);\n\n            var browser = new Option<bool>(\"--browser\", \"Show browser authentication option.\");\n            AddOption(browser);\n\n            var deviceCode = new Option<bool>(\"--device-code\", \"Show device code authentication option.\");\n            AddOption(deviceCode);\n\n            var noLogo = new Option<bool>(\"--no-logo\", \"Hide the Git Credential Manager logo and logotype.\");\n            AddOption(noLogo);\n\n            this.SetHandler(ExecuteAsync, title, resource, browser, deviceCode, noLogo);\n        }\n\n        private async Task<int> ExecuteAsync(string title, string resource, bool browser, bool deviceCode, bool noLogo)\n        {\n            var viewModel = new OAuthViewModel();\n\n            if (!string.IsNullOrWhiteSpace(title))\n            {\n                viewModel.Title = title;\n            }\n\n            viewModel.Description = !string.IsNullOrWhiteSpace(resource)\n                ? $\"Sign in to '{resource}'\"\n                : \"Select a sign-in option\";\n\n            viewModel.ShowBrowserLogin = browser;\n            viewModel.ShowDeviceCodeLogin = deviceCode;\n            viewModel.ShowProductHeader = !noLogo;\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            var result = new Dictionary<string, string>();\n            switch (viewModel.SelectedMode)\n            {\n                case OAuthAuthenticationModes.Browser:\n                    result[\"mode\"] = \"browser\";\n                    break;\n\n                case OAuthAuthenticationModes.DeviceCode:\n                    result[\"mode\"] = \"devicecode\";\n                    break;\n\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n\n            WriteResult(result);\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(OAuthViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/AboutWindow.axaml",
    "content": "<Window xmlns=\"https://github.com/avaloniaui\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n        xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n        xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n        xmlns:gcm=\"clr-namespace:GitCredentialManager\"\n        mc:Ignorable=\"d\" d:DesignWidth=\"300\" d:DesignHeight=\"450\"\n        x:Class=\"GitCredentialManager.UI.Controls.AboutWindow\"\n        Title=\"About Git Credential Manager\"\n        CanResize=\"False\" Width=\"300\" SizeToContent=\"Height\"\n        x:Name=\"window\"\n        Background=\"{DynamicResource WindowBackgroundBrush}\">\n    <StackPanel Margin=\"20\">\n        <Image HorizontalAlignment=\"Center\"\n               Margin=\"0,10,0,10\"\n               Source=\"{DynamicResource GcmLogo}\" Width=\"64\" Height=\"64\" />\n        <TextBlock HorizontalAlignment=\"Center\"\n                   Margin=\"0,5\"\n                   FontWeight=\"Bold\" FontSize=\"15\"\n                   Text=\"Git Credential Manager\" />\n        <TextBlock HorizontalAlignment=\"Center\"\n                   FontSize=\"11\"\n                   Text=\"{Binding VersionString, ElementName=window}\"\n                   Margin=\"0,5\"/>\n        <TextBlock HorizontalAlignment=\"Center\"\n                   Margin=\"0,10,0,0\"\n                   FontSize=\"11\" TextWrapping=\"Wrap\" TextAlignment=\"Center\"\n                   Text=\"Secure, cross-platform Git credential storage for popular Git hosting services.\" />\n        <Button HorizontalAlignment=\"Center\"\n                Margin=\"0,5,0,0\"\n                Content=\"{Binding ProjectUrl, ElementName=window}\"\n                FontSize=\"11\" Classes=\"hyperlink\"\n                Click=\"ProjectButton_Click\"/>\n        <TextBlock HorizontalAlignment=\"Center\"\n                   Margin=\"0,10,0,0\"\n                   FontSize=\"11\"\n                   Text=\"Copyright © GitHub\"/>\n    </StackPanel>\n</Window>\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/AboutWindow.axaml.cs",
    "content": "using System.Diagnostics;\nusing Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Interactivity;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitCredentialManager.UI.Controls\n{\n    public partial class AboutWindow : Window\n    {\n        public string VersionString => $\"Version {Constants.GcmVersion}\";\n        public string ProjectUrl => Constants.HelpUrls.GcmProjectUrl;\n\n        public AboutWindow()\n        {\n            InitializeComponent();\n        }\n\n        private void ProjectButton_Click(object sender, RoutedEventArgs e)\n        {\n            var psi = new ProcessStartInfo(ProjectUrl)\n            {\n                UseShellExecute = true\n            };\n            Process.Start(psi);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/DialogWindow.axaml",
    "content": "<Window xmlns=\"https://github.com/avaloniaui\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n        xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n        xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n        xmlns:vm=\"clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore\"\n        xmlns:converters=\"clr-namespace:GitCredentialManager.UI.Converters\"\n        mc:Ignorable=\"d\" d:DesignWidth=\"420\" d:DesignHeight=\"520\"\n        x:Class=\"GitCredentialManager.UI.Controls.DialogWindow\"\n        ExtendClientAreaToDecorationsHint=\"{Binding ExtendClientArea}\"\n        ExtendClientAreaChromeHints=\"{Binding ShowCustomChrome, Converter={x:Static converters:WindowClientAreaConverters.BoolToChromeHints}}\"\n        Title=\"{Binding Title}\"\n        SizeToContent=\"Height\" CanResize=\"False\"\n        Width=\"420\" MaxHeight=\"520\" MinHeight=\"280\"\n        WindowState=\"Normal\" WindowStartupLocation=\"CenterScreen\"\n        ShowInTaskbar=\"True\" ShowActivated=\"True\"\n        PointerPressed=\"Window_PointerPressed\"\n        KeyUp=\"Window_KeyUp\">\n    <Design.DataContext>\n        <vm:WindowViewModel/>\n    </Design.DataContext>\n    <Border BorderBrush=\"{DynamicResource DialogWindowBorderBrush}\"\n            BorderThickness=\"{Binding ShowCustomWindowBorder, Converter={x:Static converters:BoolConvertersEx.ToThickness}}\">\n        <DockPanel>\n            <DockPanel DockPanel.Dock=\"Top\" HorizontalAlignment=\"Stretch\" Margin=\"0\"\n                       IsVisible=\"{Binding ShowCustomChrome}\">\n                <Button DockPanel.Dock=\"Right\" Margin=\"0\"\n                        IsCancel=\"True\" Height=\"35\" Width=\"40\"\n                        Click=\"CloseButton_Click\"\n                        Background=\"Transparent\">\n                    <Button.Styles>\n                        <Style Selector=\"ContentPresenter#PART_ContentPresenter\">\n                            <Setter Property=\"CornerRadius\" Value=\"0\" />\n                        </Style>\n                    </Button.Styles>\n                    <Path Width=\"16\" Height=\"16\"\n                          Fill=\"{DynamicResource DialogWindowCloseButtonBrush}\"\n                          Data=\"F1M8.583,8L13,12.424 12.424,13 8,8.583 3.576,13 3,12.424 7.417,8 3,3.576 3.576,3 8,7.417 12.424,3 13,3.576z\"/>\n                </Button>\n                <TextBlock DockPanel.Dock=\"Left\" Margin=\"10,0,0,0\"\n                           VerticalAlignment=\"Center\"\n                           Text=\"{Binding Title}\"/>\n            </DockPanel>\n\n            <!-- Padding/spacer when extending the client area but not using custom window controls.\n                 Sadly Avalonia doesn't seem to support triggered styles like WPF does, otherwise\n                 we could just apply a different top-margin to the ContentControl. -->\n            <Panel DockPanel.Dock=\"Top\" Height=\"35\">\n                <Panel.IsVisible>\n                    <MultiBinding Converter=\"{x:Static BoolConverters.And}\">\n                        <Binding Path=\"ExtendClientArea\" />\n                        <Binding Path=\"!ShowCustomChrome\" />\n                    </MultiBinding>\n                </Panel.IsVisible>\n            </Panel>\n\n            <!-- DEBUG CONTROLS (toggle with ALT-D from a debug build) -->\n            <StackPanel DockPanel.Dock=\"Bottom\"\n                        IsVisible=\"{Binding ShowDebugControls}\"\n                        Background=\"LightGray\" Orientation=\"Horizontal\">\n                <TextBlock Text=\"DEBUG:\" Margin=\"5,0\" Foreground=\"#888888\" VerticalAlignment=\"Center\"/>\n                <CheckBox IsChecked=\"{Binding ExtendClientArea}\"\n                          Content=\"Extend Client Area\" Margin=\"5,0\"/>\n                <CheckBox IsChecked=\"{Binding ShowCustomChromeOverride}\"\n                          Content=\"Show Custom Chrome\" Margin=\"5,0\"/>\n            </StackPanel>\n\n            <ContentControl x:Name=\"_contentHolder\" Margin=\"30,25,30,30\"/>\n        </DockPanel>\n    </Border>\n\n</Window>\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/DialogWindow.axaml.cs",
    "content": "using System;\nusing Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Input;\nusing Avalonia.Interactivity;\nusing Avalonia.Markup.Xaml;\nusing Avalonia.Threading;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.UI.Controls\n{\n    public partial class DialogWindow : Window\n    {\n        private readonly Control _view;\n\n        public DialogWindow() : this(null)\n        {\n            // Constructor the XAML designer\n        }\n\n        public DialogWindow(Control view)\n        {\n            InitializeComponent();\n            _view = view;\n            _contentHolder.Content = _view;\n        }\n\n        protected override void OnDataContextChanged(EventArgs e)\n        {\n            if (DataContext is WindowViewModel vm)\n            {\n                vm.Accepted += (s, _) => Close(true);\n                vm.Canceled += (s, _) => Close(false);\n\n                // Send a focus request to the child view on idle\n                if (_view is IFocusable focusable)\n                {\n                    Avalonia.Threading.Dispatcher.UIThread.Post(() => focusable.SetFocus(), DispatcherPriority.Normal);\n                }\n            }\n        }\n\n        private void CloseButton_Click(object sender, RoutedEventArgs e)\n        {\n            if (DataContext is WindowViewModel vm)\n            {\n                vm.Cancel();\n            }\n        }\n\n        private void Window_PointerPressed(object sender, PointerPressedEventArgs e)\n        {\n            BeginMoveDrag(e);\n        }\n\n        private void Window_KeyUp(object sender, KeyEventArgs e)\n        {\n#if DEBUG\n            if (e.Key == Key.D && e.KeyModifiers == KeyModifiers.Alt &&\n                DataContext is WindowViewModel vm)\n            {\n                // Toggle debug controls\n                vm.ShowDebugControls = !vm.ShowDebugControls;\n            }\n#endif\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/IFocusable.cs",
    "content": "namespace GitCredentialManager.UI.Controls\n{\n    public interface IFocusable\n    {\n        void SetFocus();\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/ProgressWindow.axaml",
    "content": "<Window xmlns=\"https://github.com/avaloniaui\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n        xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n        xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n        mc:Ignorable=\"d\" d:DesignWidth=\"182\" d:DesignHeight=\"46\"\n        SizeToContent=\"WidthAndHeight\" CanResize=\"False\" Topmost=\"True\"\n        ExtendClientAreaChromeHints=\"NoChrome\" ExtendClientAreaToDecorationsHint=\"True\"\n        ShowInTaskbar=\"False\" Title=\"Git Credential Manager\" WindowStartupLocation=\"CenterScreen\"\n        x:Class=\"GitCredentialManager.UI.Controls.ProgressWindow\">\n    <ProgressBar Orientation=\"Horizontal\"\n                 IsIndeterminate=\"True\"\n                 Margin=\"20\"\n                 Width=\"158\" Height=\"23\" />\n</Window>\n"
  },
  {
    "path": "src/shared/Core/UI/Controls/ProgressWindow.axaml.cs",
    "content": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitCredentialManager.UI.Controls;\n\npublic partial class ProgressWindow : Window\n{\n    public ProgressWindow()\n    {\n        InitializeComponent();\n    }\n\n    public static IntPtr ShowAndGetHandle(CancellationToken ct)\n    {\n        var tsc = new TaskCompletionSource<IntPtr>();\n        \n        Window CreateWindow()\n        {\n            var window = new ProgressWindow();\n            window.Loaded += (s, e) => tsc.SetResult(window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero);\n            return window;\n        }\n\n        Task _ = AvaloniaUi.ShowWindowAsync(CreateWindow, IntPtr.Zero, ct);\n\n        return tsc.Task.GetAwaiter().GetResult();\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Converters/BoolConvertersEx.cs",
    "content": "using System;\nusing System.Globalization;\nusing System.Linq;\nusing Avalonia;\nusing Avalonia.Data.Converters;\n\nnamespace GitCredentialManager.UI.Converters\n{\n    public static class BoolConvertersEx\n    {\n        public static readonly IValueConverter ToThickness = new BoolToThicknessConverter(); \n\n        public static readonly IMultiValueConverter Or =\n            new FuncMultiValueConverter<bool,bool>(x => x.Aggregate(false, (a, b) => a || b));\n\n        public static readonly IMultiValueConverter And =\n            new FuncMultiValueConverter<bool,bool>(x => x.Aggregate(true, (a, b) => a && b));\n\n        private class BoolToThicknessConverter : IValueConverter\n        {\n            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n            {\n                if (value is not bool b)\n                {\n                    return null;\n                }\n\n                if (parameter is int i)\n                {\n                    return b ? new Thickness(i) : new Thickness(0);\n                }\n\n                return b ? new Thickness(1) : new Thickness(0);\n            }\n\n            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n            {\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Converters/WindowClientAreaConverters.cs",
    "content": "﻿using Avalonia.Data.Converters;\nusing Avalonia.Platform;\n\nnamespace GitCredentialManager.UI.Converters\n{\n    public static class WindowClientAreaConverters\n    {\n        public static readonly IValueConverter BoolToChromeHints =\n            new FuncValueConverter<bool, ExtendClientAreaChromeHints>(\n                x => x\n                    ? ExtendClientAreaChromeHints.NoChrome\n                    : ExtendClientAreaChromeHints.PreferSystemChrome);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Dispatcher.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.UI\n{\n    public class Dispatcher\n    {\n        private readonly DispatcherJobQueue _queue = new();\n        private readonly Thread _thread;\n\n        public static Dispatcher MainThread { get; private set; }\n\n        /// <summary>\n        /// Initialize the dispatcher associated to the current thread. See <see cref=\"Thread.CurrentThread\"/>.\n        /// </summary>\n        public static void Initialize()\n        {\n            MainThread = new Dispatcher(Thread.CurrentThread);\n        }\n\n        private Dispatcher(Thread thread)\n        {\n            _thread = thread;\n        }\n\n        public void Run()\n        {\n            // Should only run the dispatcher job queue from the thread that\n            // created the dispatcher.\n            VerifyAccess();\n            _queue.Run();\n        }\n\n        public void Shutdown()\n        {\n            // Can shutdown the dispatcher from any thread.\n            _queue.Shutdown();\n        }\n\n        public bool CheckAccess() => Thread.CurrentThread.ManagedThreadId == _thread.ManagedThreadId;\n\n        /// <summary>\n        /// Ensure the calling thread is the thread associated with this dispatcher.\n        /// </summary>\n        /// <exception cref=\"InvalidOperationException\">\n        /// The calling thread does not have access this dispatcher.\n        /// </exception>\n        public void VerifyAccess()\n        {\n            if (!CheckAccess())\n            {\n                throw new InvalidOperationException(\"Not running on the dispatcher thread.\");\n            }\n        }\n\n        /// <summary>\n        /// Post work to be run on the thread associated with this dispatcher.\n        /// </summary>\n        /// <param name=\"work\">Work to be run.</param>\n        public void Post(Action<CancellationToken> work)\n        {\n            Task _ = InvokeAsync(work);\n        }\n\n        /// <summary>\n        /// Execute work to be run on the thread associated with this dispatcher and wait\n        /// synchronously until the work is complete.\n        /// </summary>\n        /// <param name=\"work\">Work to be run.</param>\n        public Task InvokeAsync(Action<CancellationToken> work)\n        {\n            var tcs = new TaskCompletionSource<object>();\n            _queue.AddJob(new DispatcherJob(work, tcs));\n            return tcs.Task;\n        }\n\n        public Task<TResult> InvokeAsync<TResult>(Func<CancellationToken, TResult> work)\n        {\n            var tcs = new TaskCompletionSource<TResult>();\n            _queue.AddJob(new DispatcherJob<TResult>(work, tcs));\n            return tcs.Task;\n        }\n\n        private interface IDispatcherJob\n        {\n            void Execute(CancellationToken ct);\n        }\n\n        private class DispatcherJob : IDispatcherJob\n        {\n            private readonly Action<CancellationToken> _work;\n            private readonly TaskCompletionSource<object> _tcs;\n\n            public DispatcherJob(Action<CancellationToken> work, TaskCompletionSource<object> tcs)\n            {\n                _work = work;\n                _tcs = tcs;\n            }\n\n            public void Execute(CancellationToken ct)\n            {\n                _work(ct);\n                _tcs?.SetResult(null);\n            }\n        }\n\n        private class DispatcherJob<TResult> : IDispatcherJob\n        {\n            private readonly Func<CancellationToken, TResult> _work;\n            private readonly TaskCompletionSource<TResult> _tcs;\n\n            public DispatcherJob(Func<CancellationToken, TResult> work, TaskCompletionSource<TResult> tcs)\n            {\n                _work = work;\n                _tcs = tcs;\n            }\n\n            public void Execute(CancellationToken ct)\n            {\n                TResult result = _work(ct);\n                _tcs?.SetResult(result);\n            }\n        }\n\n        private class DispatcherJobQueue\n        {\n            private readonly Queue<IDispatcherJob> _queue = new();\n            private readonly CancellationTokenSource _cts = new();\n\n            private enum State\n            {\n                NotStarted,\n                Started,\n                Stopping,\n                Stopped,\n            }\n\n            private State _state = State.NotStarted;\n\n            public void Run()\n            {\n                lock (_queue)\n                {\n                    switch (_state)\n                    {\n                        case State.Started:\n                            throw new InvalidOperationException(\"Dispatcher has already started.\");\n                        case State.Stopping:\n                            throw new InvalidOperationException(\"Dispatcher is shutting down.\");\n                        case State.Stopped:\n                            throw new InvalidOperationException(\"Dispatcher has shut down.\");\n                    }\n\n                    _state = State.Started;\n                }\n\n                while (TryTake(out IDispatcherJob job))\n                {\n                    job.Execute(_cts.Token);\n                }\n            }\n\n            public void Shutdown()\n            {\n                lock (_queue)\n                {\n                    switch (_state)\n                    {\n                        case State.NotStarted:\n                            throw new InvalidOperationException(\"Dispatcher is not running.\");\n                        case State.Stopping:\n                            throw new InvalidOperationException(\"Dispatcher is already shutting down.\");\n                        case State.Stopped:\n                            throw new InvalidOperationException(\"Dispatcher has already shut down.\");\n                    }\n                    _state = State.Stopping;\n                    _cts.Cancel();\n                    Monitor.Pulse(_queue);\n                }\n            }\n\n            public void AddJob(IDispatcherJob job)\n            {\n                lock (_queue)\n                {\n                    switch (_state)\n                    {\n                        case State.Stopping:\n                            throw new InvalidOperationException(\"Dispatcher is shutting down.\");\n                        case State.Stopped:\n                            throw new InvalidOperationException(\"Dispatcher has shut down.\");\n                    }\n\n                    _queue.Enqueue(job);\n                    Monitor.Pulse(_queue);\n                }\n            }\n\n            private bool TryTake(out IDispatcherJob job)\n            {\n                lock (_queue)\n                {\n                    while (_queue.Count == 0)\n                    {\n                        // Only check for stopping state when the queue is empty\n                        // to allow remaining jobs to drain. We check for the stopping\n                        // state in AddJob to ensure no more jobs can be added.\n                        if (_state == State.Stopping)\n                        {\n                            job = null;\n                            return false;\n                        }\n\n                        Monitor.Wait(_queue);\n                    }\n\n                    job = _queue.Dequeue();\n                    return true;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/HelperApplication.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.CommandLine.Builder;\nusing System.CommandLine.Invocation;\nusing System.CommandLine.Parsing;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.UI\n{\n    public class HelperApplication : ApplicationBase\n    {\n        private readonly IList<Command> _commands = new List<Command>();\n\n        public HelperApplication(ICommandContext context) : base(context)\n        {\n        }\n\n        protected override async Task<int> RunInternalAsync(string[] args)\n        {\n            var rootCommand = new RootCommand();\n\n            foreach (Command command in _commands)\n            {\n                rootCommand.AddCommand(command);\n            }\n\n            var parser = new CommandLineBuilder(rootCommand)\n                .UseDefaults()\n                .UseExceptionHandler(OnException)\n                .Build();\n\n            return await parser.InvokeAsync(args);\n        }\n\n        public void RegisterCommand(Command command)\n        {\n            _commands.Add(command);\n        }\n\n        private void OnException(Exception ex, InvocationContext invocationContext)\n        {\n            if (ex is AggregateException aex)\n            {\n                aex.Handle(WriteException);\n            }\n            else\n            {\n                WriteException(ex);\n            }\n\n            invocationContext.ExitCode = -1;\n        }\n\n        private bool WriteException(Exception ex)\n        {\n            Context.Streams.Out.WriteDictionary(new Dictionary<string, string>\n            {\n                [\"error\"] = ex.Message\n            });\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/HelperCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\n\nnamespace GitCredentialManager.UI\n{\n    public abstract class HelperCommand : Command\n    {\n        protected ICommandContext Context { get; }\n\n        public HelperCommand(ICommandContext context, string name, string description)\n            : base(name, description)\n        {\n            Context = context;\n        }\n\n        protected IntPtr GetParentHandle()\n        {\n            if (int.TryParse(Context.Settings.ParentWindowId, out int id))\n            {\n                return new IntPtr(id);\n            }\n\n            return IntPtr.Zero;\n        }\n\n        protected void WriteResult(IDictionary<string, string> result)\n        {\n            Context.Streams.Out.WriteDictionary(result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/RelayCommand.cs",
    "content": "using System;\nusing System.Windows.Input;\n\nnamespace GitCredentialManager.UI\n{\n    public class RelayCommand : ICommand\n    {\n        private readonly Action _execute;\n        private readonly Func<bool> _canExecute;\n\n        public RelayCommand(Action execute, Func<bool> canExecute = null)\n        {\n            _execute = execute;\n            _canExecute = canExecute;\n        }\n\n        public bool CanExecute(object parameter)\n        {\n            if (_canExecute is null)\n            {\n                return true;\n            }\n\n            return _canExecute();\n        }\n\n        public void Execute(object parameter)\n        {\n            _execute();\n        }\n\n        public void RaiseCanExecuteChanged()\n        {\n            CanExecuteChanged?.Invoke(this, EventArgs.Empty);\n        }\n\n        public event EventHandler CanExecuteChanged;\n    }\n\n    public class RelayCommand<T> : ICommand\n    {\n        private readonly Action<T> _execute;\n        private readonly Predicate<T> _canExecute;\n\n        public RelayCommand(Action<T> execute, Predicate<T> canExecute = null)\n        {\n            _execute = execute;\n            _canExecute = canExecute;\n        }\n\n        public bool CanExecute(object parameter)\n        {\n            if (_canExecute is null)\n            {\n                return true;\n            }\n\n            return _canExecute((T)parameter);\n        }\n\n        public void Execute(object parameter)\n        {\n            _execute((T) parameter);\n        }\n\n        public void RaiseCanExecuteChanged()\n        {\n            CanExecuteChanged?.Invoke(this, EventArgs.Empty);\n        }\n\n        public event EventHandler CanExecuteChanged;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/CredentialsViewModel.cs",
    "content": "using System.ComponentModel;\n\nnamespace GitCredentialManager.UI.ViewModels\n{\n    public class CredentialsViewModel : WindowViewModel\n    {\n        private string _userName;\n        private string _password;\n        private string _description;\n        private bool _showProductHeader = true;\n        private RelayCommand _signInCommand;\n\n        public CredentialsViewModel()\n        {\n            SignInCommand = new RelayCommand(Accept, CanSignIn);\n            PropertyChanged += OnPropertyChanged;\n        }\n\n        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)\n        {\n            switch (e.PropertyName)\n            {\n                case nameof(UserName):\n                case nameof(Password):\n                    SignInCommand.RaiseCanExecuteChanged();\n                    break;\n            }\n        }\n\n        private bool CanSignIn()\n        {\n            // Allow empty username or empty password, or both!\n            // This is what the older Windows API CredUIPromptForWindowsCredentials\n            // permitted so we should continue to support any possible scenarios.\n            return true;\n        }\n\n        public string UserName\n        {\n            get => _userName;\n            set => SetAndRaisePropertyChanged(ref _userName, value);\n        }\n\n        public string Password\n        {\n            get => _password;\n            set => SetAndRaisePropertyChanged(ref _password, value);\n        }\n\n        public string Description\n        {\n            get => _description;\n            set => SetAndRaisePropertyChanged(ref _description, value);\n        }\n\n        public bool ShowProductHeader\n        {\n            get => _showProductHeader;\n            set => SetAndRaisePropertyChanged(ref _showProductHeader, value);\n        }\n\n        public RelayCommand SignInCommand\n        {\n            get => _signInCommand;\n            set => SetAndRaisePropertyChanged(ref _signInCommand, value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/DefaultAccountViewModel.cs",
    "content": "using System.Windows.Input;\n\nnamespace GitCredentialManager.UI.ViewModels;\n\npublic class DefaultAccountViewModel : WindowViewModel\n{\n    private readonly ISessionManager _sessionManager;\n\n    private bool _showProductHeader = true;\n    private string _userName;\n    private ICommand _continueCommand;\n    private ICommand _otherAccountCommand;\n    private ICommand _learnMoreCommand;\n\n    public DefaultAccountViewModel()\n    {\n        // For designer only\n    }\n    \n    public DefaultAccountViewModel(ISessionManager sessionManager) : this()\n    {\n        _sessionManager = sessionManager;\n\n        ContinueCommand = new RelayCommand(Continue);\n        OtherAccountCommand = new RelayCommand(OtherAccount);\n        LearnMoreCommand = new RelayCommand(OpenLink);\n    }\n\n    private void OtherAccount()\n    {\n        UseDefaultAccount = false;\n        Accept();\n    }\n\n    private void Continue()\n    {\n        UseDefaultAccount = true;\n        Accept();\n    }\n\n    private void OpenLink()\n    {\n        _sessionManager.OpenBrowser(Link);\n    }\n\n    public bool UseDefaultAccount { get; private set; }\n\n    public string Link => Constants.HelpUrls.GcmDefaultAccount;\n\n    public bool ShowProductHeader\n    {\n        get => _showProductHeader;\n        set => SetAndRaisePropertyChanged(ref _showProductHeader, value);\n    }\n\n    public string UserName\n    {\n        get => _userName;\n        set => SetAndRaisePropertyChanged(ref _userName, value);\n    }\n\n    public ICommand ContinueCommand\n    {\n        get => _continueCommand;\n        set => SetAndRaisePropertyChanged(ref _continueCommand, value);\n    }\n\n    public ICommand OtherAccountCommand\n    {\n        get => _otherAccountCommand;\n        set => SetAndRaisePropertyChanged(ref _otherAccountCommand, value);\n    }\n\n    public ICommand LearnMoreCommand\n    {\n        get => _learnMoreCommand;\n        set => SetAndRaisePropertyChanged(ref _learnMoreCommand, value);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/DeviceCodeViewModel.cs",
    "content": "using System.Windows.Input;\n\nnamespace GitCredentialManager.UI.ViewModels\n{\n    public class DeviceCodeViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n\n        private ICommand _verificationUrlCommand;\n        private string _verificationUrl;\n        private string _userCode;\n        private bool _showProductHeader = true;\n\n        public DeviceCodeViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public DeviceCodeViewModel(ISessionManager sessionManager)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n\n            _sessionManager = sessionManager;\n\n            Title = \"Device code authentication\";\n            VerificationUrlCommand = new RelayCommand(OpenVerificationUrl);\n        }\n\n        private void OpenVerificationUrl()\n        {\n            _sessionManager.OpenBrowser(VerificationUrl);\n        }\n\n        public string UserCode\n        {\n            get => _userCode;\n            set => SetAndRaisePropertyChanged(ref _userCode, value);\n        }\n\n        public string VerificationUrl\n        {\n            get => _verificationUrl;\n            set => SetAndRaisePropertyChanged(ref _verificationUrl, value);\n        }\n\n        public ICommand VerificationUrlCommand\n        {\n            get => _verificationUrlCommand;\n            set => SetAndRaisePropertyChanged(ref _verificationUrlCommand, value);\n        }\n\n        public bool ShowProductHeader\n        {\n            get => _showProductHeader;\n            set => SetAndRaisePropertyChanged(ref _showProductHeader, value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/EnableNtlmViewModel.cs",
    "content": "using System.Windows.Input;\nusing GitCredentialManager.Authentication;\n\nnamespace GitCredentialManager.UI.ViewModels;\n\npublic class EnableNtlmViewModel : WindowViewModel\n{\n    private readonly ISessionManager _sessionManager;\n\n    private string _url;\n    private ICommand _onceCommand;\n    private ICommand _alwaysCommand;\n    private ICommand _noCommand;\n    private ICommand _learnMoreCommand;\n\n    public EnableNtlmViewModel()\n    {\n        // For designer only\n    }\n    \n    public EnableNtlmViewModel(ISessionManager sessionManager) : this()\n    {\n        _sessionManager = sessionManager;\n\n        OnceCommand = new RelayCommand(() => SelectOption(NtlmSupport.Once));\n        AlwaysCommand = new RelayCommand(() => SelectOption(NtlmSupport.Always));\n        NoCommand = new RelayCommand(() => SelectOption(NtlmSupport.Disabled));\n        LearnMoreCommand = new RelayCommand(() => sessionManager.OpenBrowser(Constants.HelpUrls.GcmNtlm));\n    }\n\n    private void SelectOption(NtlmSupport option)\n    {\n        SelectedOption = option;\n        Accept();\n    }\n\n    public NtlmSupport SelectedOption { get; private set; }\n\n    public string Url\n    {\n        get => _url;\n        set => SetAndRaisePropertyChanged(ref _url, value);\n    }\n\n    public ICommand OnceCommand\n    {\n        get => _onceCommand;\n        set => SetAndRaisePropertyChanged(ref _onceCommand, value);\n    }\n\n    public ICommand AlwaysCommand\n    {\n        get => _alwaysCommand;\n        set => SetAndRaisePropertyChanged(ref _alwaysCommand, value);\n    }\n\n    public ICommand NoCommand\n    {\n        get => _noCommand;\n        set => SetAndRaisePropertyChanged(ref _noCommand, value);\n    }\n\n    public ICommand LearnMoreCommand\n    {\n        get => _learnMoreCommand;\n        set => SetAndRaisePropertyChanged(ref _learnMoreCommand, value);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/OAuthViewModel.cs",
    "content": "using GitCredentialManager.Authentication;\n\nnamespace GitCredentialManager.UI.ViewModels\n{\n    public class OAuthViewModel : WindowViewModel\n    {\n        private string _description;\n        private bool _showProductHeader = true;\n        private bool _showBrowserLogin;\n        private bool _showDeviceCodeLogin;\n        private RelayCommand _signInBrowserCommand;\n        private RelayCommand _signInDeviceCodeCommand;\n\n        public OAuthViewModel()\n        {\n            SignInBrowserCommand = new RelayCommand(SignInWithBrowser);\n            SignInDeviceCodeCommand = new RelayCommand(SignInWithDeviceCode);\n        }\n\n        private void SignInWithBrowser()\n        {\n            SelectedMode = OAuthAuthenticationModes.Browser;\n            Accept();\n        }\n\n        private void SignInWithDeviceCode()\n        {\n            SelectedMode = OAuthAuthenticationModes.DeviceCode;\n            Accept();\n        }\n\n        public string Description\n        {\n            get => _description;\n            set => SetAndRaisePropertyChanged(ref _description, value);\n        }\n\n        public bool ShowProductHeader\n        {\n            get => _showProductHeader;\n            set => SetAndRaisePropertyChanged(ref _showProductHeader, value);\n        }\n\n        public bool ShowBrowserLogin\n        {\n            get => _showBrowserLogin;\n            set => SetAndRaisePropertyChanged(ref _showBrowserLogin, value);\n        }\n\n        public bool ShowDeviceCodeLogin\n        {\n            get => _showDeviceCodeLogin;\n            set => SetAndRaisePropertyChanged(ref _showDeviceCodeLogin, value);\n        }\n\n        public RelayCommand SignInBrowserCommand\n        {\n            get => _signInBrowserCommand;\n            set => SetAndRaisePropertyChanged(ref _signInBrowserCommand, value);\n        }\n\n        public RelayCommand SignInDeviceCodeCommand\n        {\n            get => _signInDeviceCodeCommand;\n            set => SetAndRaisePropertyChanged(ref _signInDeviceCodeCommand, value);\n        }\n\n        public OAuthAuthenticationModes SelectedMode { get; private set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/ViewModel.cs",
    "content": "using System.ComponentModel;\nusing System.Runtime.CompilerServices;\n\nnamespace GitCredentialManager.UI.ViewModels\n{\n    public abstract class ViewModel : INotifyPropertyChanged\n    {\n        public event PropertyChangedEventHandler PropertyChanged;\n\n        protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)\n        {\n            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n        }\n\n        protected void SetAndRaisePropertyChanged<T>(\n            ref T field, T value, [CallerMemberName] string propertyName = null)\n        {\n            if (!Equals(field, value))\n            {\n                field = value;\n                RaisePropertyChanged(propertyName);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/ViewModels/WindowViewModel.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.UI.ViewModels\n{\n    public class WindowViewModel : ViewModel\n    {\n        private bool _extendClientArea;\n        private bool _showCustomChromeOverride;\n        private bool _showDebugControls;\n        private string _title;\n\n        public event EventHandler Accepted;\n        public event EventHandler Canceled;\n\n        public WindowViewModel()\n        {\n            Title = Constants.DefaultWindowTitle;\n            \n            // Extend the client area on Windows and macOS only\n            ExtendClientArea = PlatformUtils.IsMacOS() || PlatformUtils.IsWindows();\n        }\n\n        public bool WindowResult { get; private set; }\n\n        public bool ShowDebugControls\n        {\n            get => _showDebugControls;\n            set => SetAndRaisePropertyChanged(ref _showDebugControls, value);\n        }\n\n        public bool ShowCustomChrome\n        {\n            // On macOS we typically do NOT want to show the custom chrome if we've extended the client area\n            // because the native 'traffic light' controls will still be visible and we don't want to show our own.\n            get => ShowCustomChromeOverride || (ExtendClientArea && !PlatformUtils.IsMacOS());\n        }\n\n        public bool ShowCustomWindowBorder\n        {\n            // Draw the window border explicitly on Windows\n            get => ShowCustomChrome && PlatformUtils.IsWindows();\n        }\n\n        public bool ShowCustomChromeOverride\n        {\n            get => _showCustomChromeOverride;\n            set\n            {\n                SetAndRaisePropertyChanged(ref _showCustomChromeOverride, value);\n                RaisePropertyChanged(nameof(ShowCustomChrome));\n                RaisePropertyChanged(nameof(ShowCustomWindowBorder));\n            }\n        }\n\n        public bool ExtendClientArea\n        {\n            get => _extendClientArea;\n            set\n            {\n                SetAndRaisePropertyChanged(ref _extendClientArea, value);\n                RaisePropertyChanged(nameof(ShowCustomChrome));\n                RaisePropertyChanged(nameof(ShowCustomWindowBorder));\n            }\n        }\n\n        public string Title\n        {\n            get => _title;\n            set => SetAndRaisePropertyChanged(ref _title, value);\n        }\n\n        public void Accept()\n        {\n            WindowResult = true;\n            Accepted?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void Cancel()\n        {\n            WindowResult = false;\n            Canceled?.Invoke(this, EventArgs.Empty);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Views/CredentialsView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:vm=\"clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore\"\n             xmlns:converters=\"clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitCredentialManager.UI.Views.CredentialsView\">\n    <Design.DataContext>\n        <vm:CredentialsViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"10,0,10,10\">\n            <StackPanel Margin=\"0\"\n                        Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\"\n                        IsVisible=\"{Binding ShowProductHeader}\">\n                <Image Margin=\"0,0,10,0\"\n                       VerticalAlignment=\"Center\"\n                       Source=\"{DynamicResource GcmLogo}\"\n                       Width=\"32\" Height=\"32\" />\n                <TextBlock Text=\"Git Credential Manager\"\n                           VerticalAlignment=\"Center\"\n                           FontSize=\"18\"\n                           FontWeight=\"Light\"/>\n            </StackPanel>\n\n            <TextBlock Text=\"{Binding Description}\"\n                       FontSize=\"14\"\n                       HorizontalAlignment=\"Center\"\n                       TextWrapping=\"Wrap\"\n                       Margin=\"0,15,0,15\"/>\n        </StackPanel>\n\n        <StackPanel Margin=\"20,0\">\n            <TextBox x:Name=\"_userNameTextBox\"\n                     Watermark=\"Username\" Margin=\"0,0,0,10\"\n                     Text=\"{Binding UserName}\"/>\n            <TextBox x:Name=\"_passwordTextBox\"\n                     Watermark=\"Password\" Margin=\"0,0,0,20\"\n                     PasswordChar=\"●\"\n                     Text=\"{Binding Password}\"/>\n            <Button Content=\"Continue\"\n                    IsDefault=\"True\"\n                    Command=\"{Binding SignInCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"accent\"/>\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/Core/UI/Views/CredentialsView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager.UI.Controls;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitCredentialManager.UI.Views\n{\n    public partial class CredentialsView : UserControl, IFocusable\n    {\n        public CredentialsView()\n        {\n            InitializeComponent();\n        }\n\n        public void SetFocus()\n        {\n            if (!(DataContext is CredentialsViewModel vm))\n            {\n                return;\n            }\n\n            if (string.IsNullOrWhiteSpace(vm.UserName))\n            {\n                // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                if (!PlatformUtils.IsMacOS())\n                    _userNameTextBox.Focus();\n            }\n            else\n            {\n                // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                if (!PlatformUtils.IsMacOS())\n                    _passwordTextBox.Focus();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Views/DefaultAccountView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:vm=\"clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore\"\n             xmlns:converters=\"clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitCredentialManager.UI.Views.DefaultAccountView\">\n    <Design.DataContext>\n        <vm:DefaultAccountViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"10,0,10,10\">\n            <StackPanel Margin=\"0\"\n                        Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\"\n                        IsVisible=\"{Binding ShowProductHeader}\">\n                <Image Margin=\"0,0,10,0\"\n                       VerticalAlignment=\"Center\"\n                       Source=\"{DynamicResource GcmLogo}\"\n                       Width=\"32\" Height=\"32\" />\n                <TextBlock Text=\"Git Credential Manager\"\n                           VerticalAlignment=\"Center\"\n                           FontSize=\"18\"\n                           FontWeight=\"Light\"/>\n            </StackPanel>\n\n            <TextBlock FontSize=\"14\"\n                       HorizontalAlignment=\"Center\"\n                       TextWrapping=\"Wrap\"\n                       Margin=\"0,15,0,15\">\n                Do you want to continue with the current account?\n            </TextBlock>\n        </StackPanel>\n\n        <StackPanel DockPanel.Dock=\"Bottom\"\n                    Margin=\"0,20,0,0\"\n                    Orientation=\"Horizontal\"\n                    HorizontalAlignment=\"Center\">\n            <Image Source=\"{StaticResource HelpIcon}\"\n                   Width=\"16\" Height=\"16\"\n                   Margin=\"0,0,5,0\"/>\n            <Button Content=\"Learn more about operating system accounts\"\n                    Command=\"{Binding LearnMoreCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"hyperlink\"/>\n        </StackPanel>\n\n        <StackPanel Margin=\"20,0\">\n            <StackPanel Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\"\n                        Margin=\"0,0,0,20\">\n                <Image Source=\"{StaticResource PersonIcon}\"\n                       Width=\"32\" Height=\"32\"\n                       VerticalAlignment=\"Center\"\n                       Margin=\"0,0,10,0\"/>\n                <TextBlock Text=\"{Binding UserName}\"\n                           VerticalAlignment=\"Center\"/>\n            </StackPanel>\n            <Button Content=\"Continue\"\n                    IsDefault=\"True\"\n                    Command=\"{Binding ContinueCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Margin=\"0,0,0,10\"\n                    Padding=\"16,8\"\n                    Classes=\"accent\"/>\n            <Button Content=\"Use another account\"\n                    Command=\"{Binding OtherAccountCommand}\"\n                    Padding=\"8,5\"\n                    HorizontalAlignment=\"Center\"/>\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/Core/UI/Views/DefaultAccountView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitCredentialManager.UI.Views\n{\n    public partial class DefaultAccountView : UserControl\n    {\n        public DefaultAccountView()\n        {\n            InitializeComponent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Views/DeviceCodeView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:sharedVms=\"clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitCredentialManager.UI.Views.DeviceCodeView\">\n    <Design.DataContext>\n        <sharedVms:DeviceCodeViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"0,0,0,15\">\n            <StackPanel Margin=\"0\"\n                        Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\"\n                        IsVisible=\"{Binding ShowProductHeader}\">\n                <Image Margin=\"0,0,10,0\"\n                       VerticalAlignment=\"Center\"\n                       Source=\"{DynamicResource GcmLogo}\"\n                       Width=\"32\" Height=\"32\" />\n                <TextBlock Text=\"Git Credential Manager\"\n                           VerticalAlignment=\"Center\"\n                           FontSize=\"18\"\n                           FontWeight=\"Light\"/>\n            </StackPanel>\n        </StackPanel>\n\n        <StackPanel Orientation=\"Vertical\" VerticalAlignment=\"Center\">\n            <TextBlock Text=\"Visit the URL below, sign in, and enter the following device code to continue.\"\n                       Margin=\"0,0,0,20\"\n                       TextWrapping=\"Wrap\" TextAlignment=\"Center\"/>\n            <TextBox Text=\"{Binding UserCode}\"\n                     Margin=\"0,0,0,20\"\n                     HorizontalAlignment=\"Center\"\n                     FontSize=\"24\"\n                     TextAlignment=\"Center\"\n                     Classes=\"label monospace\"/>\n            <Button Content=\"{Binding VerificationUrl}\"\n                    Command=\"{Binding VerificationUrlCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"hyperlink\" />\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/Core/UI/Views/DeviceCodeView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitCredentialManager.UI.Views\n{\n    public partial class DeviceCodeView : UserControl\n    {\n        public DeviceCodeView()\n        {\n            InitializeComponent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Views/EnableNtlmView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:vm=\"clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitCredentialManager.UI.Views.EnableNtlmView\">\n    <Design.DataContext>\n        <vm:EnableNtlmViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"10,0,10,10\">\n            <StackPanel Margin=\"0\"\n                        Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\">\n                <Image Margin=\"0,0,10,0\"\n                       VerticalAlignment=\"Center\"\n                       Source=\"{DynamicResource GcmLogo}\"\n                       Width=\"32\" Height=\"32\" />\n                <TextBlock Text=\"Git Credential Manager\"\n                           VerticalAlignment=\"Center\"\n                           FontSize=\"18\"\n                           FontWeight=\"Light\"/>\n            </StackPanel>\n\n            <TextBlock FontSize=\"14\"\n                       TextAlignment=\"Center\"\n                       HorizontalAlignment=\"Center\"\n                       TextWrapping=\"Wrap\"\n                       Margin=\"0,15,0,15\">\n                NTLM support in Git has been disabled. Do you wish to re-enable NTLM authentication?\n            </TextBlock>\n\n            <TextBlock Text=\"{Binding Url}\"\n                       FontSize=\"14\"\n                       TextAlignment=\"Center\"\n                       HorizontalAlignment=\"Center\"\n                       TextWrapping=\"Wrap\"\n                       Margin=\"0,0,0,15\"/>\n        </StackPanel>\n\n        <StackPanel DockPanel.Dock=\"Bottom\" HorizontalAlignment=\"Center\"\n                    Margin=\"10,20,10,0\">\n            <Grid ColumnDefinitions=\"Auto,*\" Margin=\"0,0,0,0\">\n                <Image Grid.Column=\"0\"\n                       Source=\"{StaticResource WarningIcon}\"\n                       Width=\"16\" Height=\"16\"\n                       Margin=\"0,0,5,0\"/>\n                <TextBlock Grid.Column=\"1\" TextWrapping=\"Wrap\" VerticalAlignment=\"Center\">\n                    <Run>\n                        NTLM authentication is no longer considered secure,\n                        and should only be used with trusted remotes.\n                    </Run>\n                    <InlineUIContainer>\n                        <Button Content=\"Learn more\"\n                                Command=\"{Binding LearnMoreCommand}\"\n                                HorizontalAlignment=\"Center\"\n                                Classes=\"hyperlink\"/>\n                    </InlineUIContainer>\n                </TextBlock>\n            </Grid>\n        </StackPanel>\n\n        <StackPanel Margin=\"20,0\">\n            <Button Content=\"Yes - just this time\"\n                    IsDefault=\"True\"\n                    Command=\"{Binding OnceCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Margin=\"0,0,0,10\"\n                    Padding=\"16,8\"\n                    Classes=\"accent\"/>\n            <Button Content=\"Yes - always for this remote\"\n                    Command=\"{Binding AlwaysCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Margin=\"0,0,0,10\"\n                    Padding=\"16,8\"/>\n            <Button Content=\"No - do not enable NTLM\"\n                    Command=\"{Binding NoCommand}\"\n                    Padding=\"16,8\"\n                    HorizontalAlignment=\"Center\"/>\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/Core/UI/Views/EnableNtlmView.cs",
    "content": "using Avalonia.Controls;\n\nnamespace GitCredentialManager.UI.Views\n{\n    public partial class EnableNtlmView : UserControl\n    {\n        public EnableNtlmView()\n        {\n            InitializeComponent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UI/Views/OAuthView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:vm=\"clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitCredentialManager.UI.Views.OAuthView\">\n    <Design.DataContext>\n        <vm:OAuthViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"10,0,10,10\">\n            <StackPanel Margin=\"0\"\n                        Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\"\n                        IsVisible=\"{Binding ShowProductHeader}\">\n                <Image Margin=\"0,0,10,0\"\n                       VerticalAlignment=\"Center\"\n                       Source=\"{DynamicResource GcmLogo}\"\n                       Width=\"32\" Height=\"32\" />\n                <TextBlock Text=\"Git Credential Manager\"\n                           VerticalAlignment=\"Center\"\n                           FontSize=\"18\"\n                           FontWeight=\"Light\"/>\n            </StackPanel>\n\n            <TextBlock Text=\"{Binding Description}\"\n                       FontSize=\"14\"\n                       HorizontalAlignment=\"Center\"\n                       TextWrapping=\"Wrap\"\n                       Margin=\"0,15,0,15\"/>\n        </StackPanel>\n\n        <StackPanel Margin=\"20,0\">\n            <Button Content=\"Sign in with your browser\"\n                    IsDefault=\"True\"\n                    Command=\"{Binding SignInBrowserCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"accent\"\n                    Margin=\"0,0,0,10\"\n                    IsVisible=\"{Binding ShowBrowserLogin}\"/>\n            <Button Content=\"Sign in with a code\"\n                    IsDefault=\"True\"\n                    Command=\"{Binding SignInDeviceCodeCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    IsVisible=\"{Binding ShowDeviceCodeLogin}\"/>\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/Core/UI/Views/OAuthView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitCredentialManager.UI.Views\n{\n    public partial class OAuthView : UserControl\n    {\n        public OAuthView()\n        {\n            InitializeComponent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/UriExtensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Web;\n\nnamespace GitCredentialManager\n{\n    public static class UriExtensions\n    {\n        public static IDictionary<string, string> ParseQueryString(string queryString)\n        {\n            var dict = new Dictionary<string, string>();\n\n            string[] queryParts = queryString.Split('&');\n            foreach (var queryPart in queryParts)\n            {\n                if (string.IsNullOrWhiteSpace(queryPart)) continue;\n\n                string[] parts = queryPart.Split('=');\n\n                var key = HttpUtility.UrlDecode(parts[0]);\n\n                string value = null;\n                if (parts.Length > 1)\n                {\n                    value = HttpUtility.UrlDecode(parts[1]);\n                }\n\n                dict[key] = value;\n            }\n\n            return dict;\n        }\n\n        public static IDictionary<string, string> GetQueryParameters(this Uri uri)\n        {\n            return ParseQueryString(uri.Query.TrimStart('?'));\n        }\n\n        public static bool TryGetUserInfo(this Uri uri, out string userName, out string password)\n        {\n            EnsureArgument.NotNull(uri, nameof(uri));\n            userName = null;\n            password = null;\n\n            if (string.IsNullOrWhiteSpace(uri.UserInfo))\n            {\n                return false;\n            }\n\n            /* According to RFC 3986 section 3.2.1 (https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1)\n             * the user information component of a URI should look like:\n             *\n             *     url-encode(username):url-encode(password)\n             */\n            string[] split = uri.UserInfo.Split(new[] {':'}, count: 2);\n\n            if (split.Length > 0)\n            {\n                userName = WebUtility.UrlDecode(split[0]);\n            }\n            if (split.Length > 1)\n            {\n                password = WebUtility.UrlDecode(split[1]);\n            }\n\n            return split.Length > 0;\n        }\n\n        public static string GetUserName(this Uri uri)\n        {\n            return TryGetUserInfo(uri, out string userName, out _) ? userName : null;\n        }\n\n        public static Uri WithoutUserInfo(this Uri uri)\n        {\n            if (string.IsNullOrEmpty(uri.UserInfo))\n            {\n                return uri;\n            }\n\n            return new UriBuilder(uri) {UserName = string.Empty, Password = string.Empty}.Uri;\n        }\n\n        public static IEnumerable<string> GetGitConfigurationScopes(this Uri uri)\n        {\n            EnsureArgument.NotNull(uri, nameof(uri));\n\n            string schemeAndDelim = $\"{uri.Scheme}{Uri.SchemeDelimiter}\";\n            string host = uri.Host.TrimEnd('/');\n            // If port is default, don't append\n            string port = uri.IsDefaultPort ? \"\" : $\":{uri.Port}\";\n            string path = uri.AbsolutePath.Trim('/');\n\n            // Unfold the path by component, right-to-left\n            while (!string.IsNullOrWhiteSpace(path))\n            {\n                yield return $\"{schemeAndDelim}{host}{port}/{path}\";\n\n                // Trim off the last path component\n                if (!TryTrimString(path, StringExtensions.TruncateFromLastIndexOf, '/', out path))\n                {\n                    break;\n                }\n            }\n\n            // Check whether the URL only contains hostname.\n            // This usually means the host is on your local network.\n            if (!string.IsNullOrWhiteSpace(host) &&\n                !host.Contains(\".\"))\n            {\n                yield return $\"{schemeAndDelim}{host}{port}\";\n                // If we have reached this point, there are no more subdomains to unfold, so exit early.\n                yield break;\n            }\n\n            // Unfold the host by sub-domain, left-to-right\n            while (!string.IsNullOrWhiteSpace(host))\n            {\n                if (host.Contains(\".\")) // Do not emit just the TLD\n                {\n                    yield return $\"{schemeAndDelim}{host}{port}\";\n                }\n\n                // Trim off the left-most sub-domain\n                if (!TryTrimString(host, StringExtensions.TrimUntilIndexOf, '.', out host))\n                {\n                    break;\n                }\n            }\n        }\n\n        private static bool TryTrimString(string input, Func<string, char, string> func, char c, out string output)\n        {\n            output = func(input, c);\n            return !StringComparer.Ordinal.Equals(input, output);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/WslUtils.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace GitCredentialManager\n{\n    public enum WindowsShell\n    {\n        Cmd,\n        PowerShell,\n    }\n\n    public static class WslUtils\n    {\n        private const string WslUncPrefix = @\"\\\\wsl$\\\";\n        private const string WslLocalHostUncPrefix = @\"\\\\wsl.localhost\\\";\n        private const string WslCommandName = \"wsl.exe\";\n        private const string WslInteropEnvar = \"WSL_INTEROP\";\n        private const string WslConfFilePath = \"/etc/wsl.conf\";\n        private const string DefaultWslMountPrefix = \"/mnt\";\n        private const string DefaultWslSysDriveMountName = \"c\";\n\n        internal const string WslViewShellHandlerName = \"wslview\";\n\n        /// <summary>\n        /// Cached Windows host session ID.\n        /// </summary>\n        /// <remarks>A value less than 0 represents \"unknown\".</remarks>\n        private static int _windowsSessionId = -1;\n\n        /// <summary>\n        /// Cached WSL version.\n        /// </summary>\n        /// <remarks>A value of 0 represents \"not WSL\", and a value less than 0 represents \"unknown\".</remarks>\n        private static int _wslVersion = -1;\n\n        /// <summary>\n        /// Cached Windows system drive mount path.\n        /// </summary>\n        private static string _sysDriveMountPath = null;\n\n        public static bool IsWslDistribution(IEnvironment env, IFileSystem fs, out int wslVersion)\n        {\n            if (_wslVersion < 0)\n            {\n                _wslVersion = GetWslVersion(env, fs);\n            }\n\n            wslVersion = _wslVersion;\n            return _wslVersion > 0;\n        }\n\n        private static int GetWslVersion(IEnvironment env, IFileSystem fs)\n        {\n            // All WSL distributions are Linux.. obviously!\n            if (!PlatformUtils.IsLinux())\n            {\n                return 0;\n            }\n\n            // The WSL_INTEROP variable is set in WSL2 distributions\n            if (env.Variables.TryGetValue(WslInteropEnvar, out _))\n            {\n                return 2;\n            }\n\n            const string procVersionPath = \"/proc/version\";\n            if (fs.FileExists(procVersionPath))\n            {\n                // Both WSL1 and WSL2 distributions include \"[Mm]icrosoft\" in the version string\n                string procVersion = fs.ReadAllText(procVersionPath);\n                if (!Regex.IsMatch(procVersion, \"[Mm]icrosoft\"))\n                {\n                    return 0;\n                }\n\n                // WSL2 distributions return \"WSL2\" in the version string\n                if (Regex.IsMatch(procVersion, \"wsl2\", RegexOptions.IgnoreCase))\n                {\n                    return 2;\n                }\n\n                return 1;\n            }\n\n            return 0;\n        }\n\n        /// <summary>\n        /// Test if a file path points to a location in a Windows Subsystem for Linux distribution.\n        /// </summary>\n        /// <param name=\"path\">Path to test.</param>\n        /// <returns>True if <paramref name=\"path\"/> is a WSL path, false otherwise.</returns>\n        public static bool IsWslPath(string path)\n        {\n            if (string.IsNullOrWhiteSpace(path)) return false;\n\n            return (path.StartsWith(WslUncPrefix, StringComparison.OrdinalIgnoreCase) &&\n                    path.Length > WslUncPrefix.Length) ||\n                   (path.StartsWith(WslLocalHostUncPrefix, StringComparison.OrdinalIgnoreCase) &&\n                    path.Length > WslLocalHostUncPrefix.Length);\n        }\n\n        /// <summary>\n        /// Create a command to be executed in a Windows Subsystem for Linux distribution.\n        /// </summary>\n        /// <param name=\"distribution\">WSL distribution name.</param>\n        /// <param name=\"command\">Command to execute.</param>\n        /// <param name=\"trace2\">The applications TRACE2 tracer.</param>\n        /// <param name=\"workingDirectory\">Optional working directory.</param>\n        /// <returns><see cref=\"Process\"/> object ready to start.</returns>\n        public static ChildProcess CreateWslProcess(string distribution,\n            string command,\n            ITrace2 trace2,\n            string workingDirectory = null)\n        {\n            var args = new StringBuilder();\n            args.AppendFormat(\"--distribution {0} \", distribution);\n            args.AppendFormat(\"--exec {0}\", command);\n\n            string wslExePath = GetWslPath();\n\n            var psi = new ProcessStartInfo(wslExePath, args.ToString())\n            {\n                RedirectStandardInput = true,\n                RedirectStandardOutput = true,\n                RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled\n                UseShellExecute = false,\n                WorkingDirectory = workingDirectory ?? string.Empty\n            };\n\n            return new ChildProcess(trace2, psi);\n        }\n\n        /// <summary>\n        /// Create a command to be executed in a shell in the host Windows operating system.\n        /// </summary>\n        /// <param name=\"fs\">File system.</param>\n        /// <param name=\"shell\">Shell used to execute the command in Windows.</param>\n        /// <param name=\"command\">Command to execute.</param>\n        /// <param name=\"workingDirectory\">Optional working directory.</param>\n        /// <returns><see cref=\"Process\"/> object ready to start.</returns>\n        public static Process CreateWindowsShellProcess(IFileSystem fs,\n            WindowsShell shell, string command, string workingDirectory = null)\n        {\n            string sysDrive = GetSystemDriveMountPath(fs);\n\n            string launcher;\n            var args = new StringBuilder();\n\n            switch (shell)\n            {\n                case WindowsShell.Cmd:\n                    launcher = Path.Combine(sysDrive, \"Windows/cmd.exe\");\n                    args.AppendFormat(\"/C {0}\", command);\n                    break;\n\n                case WindowsShell.PowerShell:\n                    const string psStreamSetup =\n                        \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; \" +\n                        \"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; \";\n\n                    launcher = Path.Combine(sysDrive, \"Windows/System32/WindowsPowerShell/v1.0/powershell.exe\");\n                    args.Append(\" -NoProfile -NonInteractive -ExecutionPolicy Bypass\");\n                    args.AppendFormat(\" -Command \\\"{0} {1}\\\"\", psStreamSetup, command);\n                    break;\n\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(shell));\n            }\n\n            var psi = new ProcessStartInfo(launcher, args.ToString())\n            {\n                RedirectStandardInput = true,\n                RedirectStandardOutput = true,\n                RedirectStandardError = true,\n                UseShellExecute = false,\n                WorkingDirectory = workingDirectory ?? string.Empty\n            };\n\n            return new Process { StartInfo = psi };\n        }\n\n        /// <summary>\n        /// Get the host Windows session ID.\n        /// </summary>\n        /// <returns>Windows session ID, or a negative value if it is not known.</returns>\n        public static int GetWindowsSessionId(IFileSystem fs)\n        {\n            if (_windowsSessionId < 0)\n            {\n                const string script = @\"(Get-Process -ID $PID).SessionId\";\n                using (Process proc = CreateWindowsShellProcess(fs, WindowsShell.PowerShell, script))\n                {\n                    try\n                    {\n                        proc.Start();\n                        proc.WaitForExit();\n                    }\n                    catch\n                    {\n                        // Unable to start the process, return unknown session ID\n                        return -1;\n                    }\n\n                    if (proc.ExitCode == 0)\n                    {\n                        string output = proc.StandardOutput.ReadToEnd().Trim();\n                        if (int.TryParse(output, out int sessionId))\n                        {\n                            _windowsSessionId = sessionId;\n                        }\n                    }\n                }\n            }\n\n            return _windowsSessionId;\n        }\n\n        private static string GetSystemDriveMountPath(IFileSystem fs)\n        {\n            if (_sysDriveMountPath is null)\n            {\n                string mountPrefix = DefaultWslMountPrefix;\n\n                // If the wsl.conf file exists in this distribution the user may\n                // have changed the Windows volume mount point prefix. Use it!\n                if (fs.FileExists(WslConfFilePath))\n                {\n                    // Read wsl.conf for [automount] root = <path>\n                    IniFile wslConf = IniSerializer.Deserialize(fs, WslConfFilePath);\n                    if (wslConf.TryGetSection(\"automount\", out IniSection automountSection) &&\n                        automountSection.TryGetProperty(\"root\", out string value))\n                    {\n                        mountPrefix = value;\n                    }\n                }\n\n                // Try to locate the system volume by looking for the Windows\\System32 directory\n                IEnumerable<string> mountPoints = fs.EnumerateDirectories(mountPrefix);\n                foreach (string mountPoint in mountPoints)\n                {\n                    string sys32Path = Path.Combine(mountPoint, \"Windows\", \"System32\");\n\n                    if (fs.DirectoryExists(sys32Path))\n                    {\n                        _sysDriveMountPath = mountPoint;\n                        return _sysDriveMountPath;\n                    }\n                }\n\n                _sysDriveMountPath = Path.Combine(mountPrefix, DefaultWslSysDriveMountName);\n            }\n\n            return _sysDriveMountPath;\n        }\n\n        public static string ConvertToDistroPath(string path, out string distribution)\n        {\n            if (!IsWslPath(path)) throw new ArgumentException(\"Must provide a WSL path\", nameof(path));\n\n            int distroStart;\n            if (path.StartsWith(WslUncPrefix, StringComparison.OrdinalIgnoreCase))\n            {\n                distroStart = WslUncPrefix.Length;\n            }\n            else if (path.StartsWith(WslLocalHostUncPrefix, StringComparison.OrdinalIgnoreCase))\n            {\n                distroStart = WslLocalHostUncPrefix.Length;\n            }\n            else\n            {\n                throw new Exception(\"Invalid WSL path prefix\");\n            }\n\n            int distroEnd = path.IndexOf('\\\\', distroStart);\n\n            if (distroEnd < 0) distroEnd = path.Length;\n\n            distribution = path.Substring(distroStart, distroEnd - distroStart);\n\n            if (path.Length > distroEnd)\n            {\n                return path.Substring(distroEnd).Replace('\\\\', '/');\n            }\n\n            return \"/\";\n        }\n\n        internal /*for testing purposes*/ static string GetWslPath()\n        {\n            // WSL is only supported on 64-bit operating systems\n            if (!Environment.Is64BitOperatingSystem)\n            {\n                throw new Exception(\"WSL is not supported on 32-bit operating systems\");\n            }\n\n            //\n            // When running as a 32-bit application on a 64-bit operating system, we cannot access the real\n            // C:\\Windows\\System32 directory because the OS will redirect us transparently to the\n            // C:\\Windows\\SysWOW64 directory (containing 32-bit executables).\n            //\n            // In order to access the real 64-bit System32 directory, we must access via the pseudo directory\n            // C:\\Windows\\SysNative that does **not** experience any redirection for 32-bit applications.\n            //\n            // HOWEVER, if we're running as a 64-bit application on a 64-bit operating system, the SysNative\n            // directory does not exist! This means if running as a 64-bit application on a 64-bit OS we must\n            // use the System32 directory name directly.\n            //\n            var sysDir = Environment.ExpandEnvironmentVariables(\n                Environment.Is64BitProcess\n                    ? @\"%WINDIR%\\System32\"\n                    : @\"%WINDIR%\\SysNative\"\n            );\n\n            return Path.Combine(sysDir, WslCommandName);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core/X509Utils.cs",
    "content": "using System.Security.Cryptography.X509Certificates;\n\nnamespace GitCredentialManager;\n\npublic static class X509Utils\n{\n    public static X509Certificate2 GetCertificateByThumbprint(string thumbprint)\n    {\n        foreach (var location in new[]{StoreLocation.CurrentUser, StoreLocation.LocalMachine})\n        {\n            using var store = new X509Store(StoreName.My, location);\n            store.Open(OpenFlags.ReadOnly);\n\n            X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);\n            if (certs.Count > 0)\n            {\n                return certs[0];\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/ApplicationTests.cs",
    "content": "using System.Collections.Generic;\r\nusing System.Threading.Tasks;\r\nusing GitCredentialManager.Tests.Objects;\r\nusing Xunit;\r\n\r\nnamespace GitCredentialManager.Tests\r\n{\r\n    public class ApplicationTests\r\n    {\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_NoHelpers_AddsEmptyAndGcm()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(2, actualValues.Count);\r\n            Assert.Equal(emptyHelper, actualValues[0]);\r\n            Assert.Equal(executablePath, actualValues[1]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_Gcm_AddsEmptyBeforeGcm()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string> {executablePath};\r\n\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(2, actualValues.Count);\r\n            Assert.Equal(emptyHelper, actualValues[0]);\r\n            Assert.Equal(executablePath, actualValues[1]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_EmptyAndGcm_DoesNothing()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                emptyHelper, executablePath\r\n            };\r\n\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(2, actualValues.Count);\r\n            Assert.Equal(emptyHelper, actualValues[0]);\r\n            Assert.Equal(executablePath, actualValues[1]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBefore_DoesNothing()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string beforeHelper = \"foo\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                beforeHelper, emptyHelper, executablePath\r\n            };\r\n\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(3, actualValues.Count);\r\n            Assert.Equal(beforeHelper, actualValues[0]);\r\n            Assert.Equal(emptyHelper, actualValues[1]);\r\n            Assert.Equal(executablePath, actualValues[2]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersAfter_DoesNothing()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string afterHelper = \"foo\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                emptyHelper, executablePath, afterHelper\r\n            };\r\n\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(3, actualValues.Count);\r\n            Assert.Equal(emptyHelper, actualValues[0]);\r\n            Assert.Equal(executablePath, actualValues[1]);\r\n            Assert.Equal(afterHelper, actualValues[2]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBeforeAndAfter_DoesNothing()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string beforeHelper = \"foo\";\r\n            const string afterHelper = \"bar\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                beforeHelper, emptyHelper, executablePath, afterHelper\r\n            };\r\n\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(4, actualValues.Count);\r\n            Assert.Equal(beforeHelper, actualValues[0]);\r\n            Assert.Equal(emptyHelper, actualValues[1]);\r\n            Assert.Equal(executablePath, actualValues[2]);\r\n            Assert.Equal(afterHelper, actualValues[3]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_ConfigureAsync_EmptyAndGcmWithEmptyAfter_RemovesExistingGcmAndAddsEmptyAndGcm()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string afterHelper = \"foo\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                emptyHelper, executablePath, emptyHelper, afterHelper\r\n            };\r\n\r\n            await application.ConfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(5, actualValues.Count);\r\n            Assert.Equal(emptyHelper, actualValues[0]);\r\n            Assert.Equal(emptyHelper, actualValues[1]);\r\n            Assert.Equal(afterHelper, actualValues[2]);\r\n            Assert.Equal(emptyHelper, actualValues[3]);\r\n            Assert.Equal(executablePath, actualValues[4]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_UnconfigureAsync_NoHelpers_DoesNothing()\r\n        {\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n            await application.UnconfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Empty(context.Git.Configuration.Global);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_UnconfigureAsync_Gcm_RemovesGcm()\r\n        {\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string> {executablePath};\r\n\r\n            await application.UnconfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Empty(context.Git.Configuration.Global);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_UnconfigureAsync_EmptyAndGcm_RemovesEmptyAndGcm()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string> {emptyHelper, executablePath};\r\n\r\n            await application.UnconfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Empty(context.Git.Configuration.Global);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersBefore_RemovesEmptyAndGcm()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string beforeHelper = \"foo\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                beforeHelper, emptyHelper, executablePath\r\n            };\r\n\r\n            await application.UnconfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Single(actualValues);\r\n            Assert.Equal(beforeHelper, actualValues[0]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersAfterBefore_RemovesGcmOnly()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string afterHelper = \"bar\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                emptyHelper, executablePath, afterHelper\r\n            };\r\n\r\n            await application.UnconfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(2, actualValues.Count);\r\n            Assert.Equal(emptyHelper, actualValues[0]);\r\n            Assert.Equal(afterHelper, actualValues[1]);\r\n        }\r\n\r\n        [Fact]\r\n        public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersBeforeAndAfter_RemovesGcmOnly()\r\n        {\r\n            const string emptyHelper = \"\";\r\n            const string beforeHelper = \"foo\";\r\n            const string afterHelper = \"bar\";\r\n            const string executablePath = \"/usr/local/share/gcm-core/git-credential-manager\";\r\n            string key = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\r\n\r\n            var context = new TestCommandContext {AppPath = executablePath};\r\n            IConfigurableComponent application = new Application(context);\r\n\r\n            context.Git.Configuration.Global[key] = new List<string>\r\n            {\r\n                beforeHelper, emptyHelper, executablePath, afterHelper\r\n            };\r\n\r\n            await application.UnconfigureAsync(ConfigurationTarget.User);\r\n\r\n            Assert.Single(context.Git.Configuration.Global);\r\n            Assert.True(context.Git.Configuration.Global.TryGetValue(key, out var actualValues));\r\n            Assert.Equal(3, actualValues.Count);\r\n            Assert.Equal(beforeHelper, actualValues[0]);\r\n            Assert.Equal(emptyHelper, actualValues[1]);\r\n            Assert.Equal(afterHelper, actualValues[2]);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/AuthenticationBaseTests.cs",
    "content": "using GitCredentialManager.Authentication;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication\n{\n    public class AuthenticationBaseTests\n    {\n        [Theory]\n        [InlineData(\"foo\", \"foo\")]\n        [InlineData(\"foo bar\", \"\\\"foo bar\\\"\")]\n        [InlineData(\"foo\\nbar\", \"\\\"foo\\nbar\\\"\")]\n        [InlineData(\"foo\\rbar\", \"\\\"foo\\rbar\\\"\")]\n        [InlineData(\"foo\\tbar\", \"\\\"foo\\tbar\\\"\")]\n        [InlineData(\"foo\\\" bar\", \"\\\"foo\\\\\\\" bar\\\"\")]\n        [InlineData(\"foo\\\"\", \"\\\"foo\\\\\\\"\\\"\")]\n        [InlineData(\"\\\"foo\", \"\\\"\\\\\\\"foo\\\"\")]\n        [InlineData(\"foo\\\\\", \"\\\"foo\\\\\\\\\\\"\")]\n        [InlineData(\"foo\\\\\\\"\", \"\\\"foo\\\\\\\\\\\\\\\"\\\"\")]\n        public void AuthenticationBase_QuoteCmdArg(string input, string expected)\n        {\n            string actual = AuthenticationBase.QuoteCmdArg(input);\n            Assert.Equal(expected, actual);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/BasicAuthenticationTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication\n{\n    public class BasicAuthenticationTests\n    {\n        [Fact]\n        public async Task BasicAuthentication_GetCredentials_NullResource_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var basicAuth = new BasicAuthentication(context);\n\n            await Assert.ThrowsAsync<ArgumentNullException>(() => basicAuth.GetCredentialsAsync(null));\n        }\n\n        [Fact]\n        public async Task BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials()\n        {\n            const string testResource = \"https://example.com\";\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            var context = new TestCommandContext {SessionManager = {IsDesktopSession = false}};\n            context.Terminal.SecretPrompts[\"Password\"] = testPassword; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            var basicAuth = new BasicAuthentication(context);\n\n            ICredential credential = await basicAuth.GetCredentialsAsync(testResource, testUserName);\n\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n        }\n\n        [Fact]\n        public async Task BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials()\n        {\n            const string testResource = \"https://example.com\";\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            var context = new TestCommandContext {SessionManager = {IsDesktopSession = false}};\n            context.Terminal.Prompts[\"Username\"] = testUserName;\n            context.Terminal.SecretPrompts[\"Password\"] = testPassword; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            var basicAuth = new BasicAuthentication(context);\n\n            ICredential credential = await basicAuth.GetCredentialsAsync(testResource);\n\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n        }\n\n        [Fact]\n        public async Task BasicAuthentication_GetCredentials_NonDesktopSession_NoTerminalPrompts_ThrowsException()\n        {\n            const string testResource = \"https://example.com\";\n\n            var context = new TestCommandContext\n            {\n                SessionManager = {IsDesktopSession = false},\n                Settings = {IsInteractionAllowed = false},\n            };\n\n            var basicAuth = new BasicAuthentication(context);\n\n            await Assert.ThrowsAsync<GitCredentialManager.Trace2InvalidOperationException>(() => basicAuth.GetCredentialsAsync(testResource));\n        }\n\n        [Fact]\n        public async Task BasicAuthentication_GetCredentials_DesktopSession_UIHelper_CallsHelper()\n        {\n            const string testResource = \"https://example.com\";\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            const string unixHelperPath = \"/usr/local/bin/git-credential-manager-ui\";\n            const string windowsHelperPath = @\"C:\\Program Files\\Git Credential Manager\\git-credential-manager-ui.exe\";\n            string helperPath = PlatformUtils.IsWindows() ? windowsHelperPath : unixHelperPath;\n\n            var context = new TestCommandContext\n            {\n                SessionManager = { IsDesktopSession = true },\n                Environment =\n                {\n                    Variables =\n                    {\n                        [Constants.EnvironmentVariables.GcmUiHelper] = helperPath\n                    }\n                }\n            };\n\n            context.FileSystem.Files[helperPath] = Array.Empty<byte>();\n\n            var auth = new Mock<BasicAuthentication>(MockBehavior.Strict, context);\n            auth.Setup(x => x.InvokeHelperAsync(\n                    It.IsAny<string>(),\n                    $\"basic --resource {testResource}\",\n                    It.IsAny<StreamReader>(),\n                    It.IsAny<System.Threading.CancellationToken>()))\n                .ReturnsAsync(\n                    new Dictionary<string, string>\n                    {\n                        [\"username\"] = testUserName,\n                        [\"password\"] = testPassword\n                    }\n                );\n\n            ICredential credential = await auth.Object.GetCredentialsAsync(testResource);\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n        }\n\n        [Fact]\n        public async Task BasicAuthentication_GetCredentials_DesktopSession_UIHelper_UserName_CallsHelper()\n        {\n            const string testResource = \"https://example.com\";\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            const string unixHelperPath = \"/usr/local/bin/git-credential-manager-ui\";\n            const string windowsHelperPath = @\"C:\\Program Files\\Git Credential Manager\\git-credential-manager-ui.exe\";\n            string helperPath = PlatformUtils.IsWindows() ? windowsHelperPath : unixHelperPath;\n\n            var context = new TestCommandContext\n            {\n                SessionManager = { IsDesktopSession = true },\n                Environment =\n                {\n                    Variables =\n                    {\n                        [Constants.EnvironmentVariables.GcmUiHelper] = helperPath\n                    }\n                }\n            };\n\n            context.FileSystem.Files[helperPath] = Array.Empty<byte>();\n\n            var auth = new Mock<BasicAuthentication>(MockBehavior.Strict, context);\n            auth.Setup(x => x.InvokeHelperAsync(\n                    It.IsAny<string>(),\n                    $\"basic --resource {testResource} --username {testUserName}\",\n                    It.IsAny<StreamReader>(),\n                    It.IsAny<System.Threading.CancellationToken>()))\n                .ReturnsAsync(\n                    new Dictionary<string, string>\n                    {\n                        [\"username\"] = testUserName,\n                        [\"password\"] = testPassword\n                    }\n                );\n\n            ICredential credential = await auth.Object.GetCredentialsAsync(testResource, testUserName);\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs",
    "content": "using System;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Tests.Objects;\nusing Microsoft.Identity.Client.AppConfig;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication\n{\n    public class MicrosoftAuthenticationTests\n    {\n        [Fact]\n        public async Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException()\n        {\n            const string authority = \"https://login.microsoftonline.com/common\";\n            const string clientId = \"C9E8FDA6-1D46-484C-917C-3DBD518F27C3\";\n            Uri redirectUri = new Uri(\"https://localhost\");\n            string[] scopes = {\"user.read\"};\n            const string userName = null; // No user to ensure we do not use an existing token\n\n            var context = new TestCommandContext\n            {\n                Settings = {IsInteractionAllowed = false},\n            };\n\n            var msAuth = new MicrosoftAuthentication(context);\n\n            await Assert.ThrowsAsync<Trace2InvalidOperationException>(\n                () => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false));\n        }\n\n        [Theory]\n        [InlineData(null)]\n        [InlineData(\"\")]\n        [InlineData(\"   \")]\n        [InlineData(\"system\")]\n        [InlineData(\"SYSTEM\")]\n        [InlineData(\"sYsTeM\")]\n        [InlineData(\"00000000-0000-0000-0000-000000000000\")]\n        [InlineData(\"id://00000000-0000-0000-0000-000000000000\")]\n        [InlineData(\"ID://00000000-0000-0000-0000-000000000000\")]\n        [InlineData(\"Id://00000000-0000-0000-0000-000000000000\")]\n        public void MicrosoftAuthentication_GetManagedIdentity_ValidSystemId_ReturnsSystemId(string str)\n        {\n            ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);\n            Assert.Equal(ManagedIdentityId.SystemAssigned, actual);\n        }\n\n        [Theory]\n        [InlineData(\"8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"id://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"ID://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"Id://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"resource://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"RESOURCE://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"rEsOuRcE://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"resource://00000000-0000-0000-0000-000000000000\")]\n        public void MicrosoftAuthentication_GetManagedIdentity_ValidUserIdByClientId_ReturnsUserId(string str)\n        {\n            ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);\n            Assert.NotNull(actual);\n            Assert.NotEqual(ManagedIdentityId.SystemAssigned, actual);\n        }\n\n        [Theory]\n        [InlineData(\"unknown://8B49DCA0-1298-4A0D-AD6D-934E40230839\")]\n        [InlineData(\"this is a string\")]\n        public void MicrosoftAuthentication_GetManagedIdentity_Invalid_ThrowsArgumentException(string str)\n        {\n            Assert.Throws<ArgumentException>(() => MicrosoftAuthentication.GetManagedIdentity(str));\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication\n{\n    public class OAuth2ClientTests\n    {\n        private const string TestClientId = \"9ffe7f11c8\";\n        private const string TestClientSecret = \"62adac63a4614d93833470942a38454f\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n        private static readonly Uri TestRedirectUri = new Uri(\"http://localhost/oauth-callback\");\n\n        [Fact]\n        public async Task OAuth2Client_GetAuthorizationCodeAsync()\n        {\n            const string expectedAuthCode = \"68c39cbd8d\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            OAuth2Application app = CreateTestApplication();\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AuthCodes.Add(expectedAuthCode);\n\n            IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, null, CancellationToken.None);\n\n            Assert.Equal(expectedAuthCode, result.Code);\n        }\n\n        [Theory]\n        [InlineData(\"http://localhost\")]\n        [InlineData(\"http://localhost/\")]\n        [InlineData(\"http://localhost/oauth-callback\")]\n        [InlineData(\"http://localhost/oauth-callback/\")]\n        [InlineData(\"http://127.0.0.1\")]\n        [InlineData(\"http://127.0.0.1/\")]\n        [InlineData(\"http://127.0.0.1/oauth-callback\")]\n        [InlineData(\"http://127.0.0.1/oauth-callback/\")]\n        public async Task OAuth2Client_GetAuthorizationCodeAsync_RedirectUrlOriginalStringPreserved(string expectedRedirectUrl)\n        {\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            OAuth2Application app = new OAuth2Application(TestClientId)\n            {\n                Secret = TestClientSecret,\n                RedirectUris = new[] {new Uri(expectedRedirectUrl)}\n            };\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AuthCodes.Add(\"unused\");\n            server.AuthorizationEndpointInvoked += (_, request) =>\n            {\n                IDictionary<string, string> actualParams = request.RequestUri.GetQueryParameters();\n                Assert.True(actualParams.TryGetValue(OAuth2Constants.RedirectUriParameter, out string actualRedirectUri));\n                Assert.Equal(expectedRedirectUrl, actualRedirectUri);\n            };\n\n            IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);\n\n            var redirectUri = new Uri(expectedRedirectUrl);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client =  new OAuth2Client(\n                new HttpClient(httpHandler),\n                endpoints,\n                TestClientId,\n                trace2,\n                redirectUri,\n                TestClientSecret);\n\n            await client.GetAuthorizationCodeAsync(new[] { \"unused\" }, browser, null, CancellationToken.None);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams()\n        {\n            const string expectedAuthCode = \"68c39cbd8d\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            var extraParams = new Dictionary<string, string>\n            {\n                [\"param1\"] = \"value1\",\n                [\"param2\"] = \"value2\",\n                [\"param3\"] = \"value3\"\n            };\n\n            OAuth2Application app = CreateTestApplication();\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AuthCodes.Add(expectedAuthCode);\n\n            server.AuthorizationEndpointInvoked += (_, request) =>\n            {\n                IDictionary<string, string> actualParams = request.RequestUri.GetQueryParameters();\n                foreach (var expected in extraParams)\n                {\n                    Assert.True(actualParams.TryGetValue(expected.Key, out string actualValue));\n                    Assert.Equal(expected.Value, actualValue);\n                }\n            };\n\n            IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None);\n\n            Assert.Equal(expectedAuthCode, result.Code);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams_OverrideStandardArgs_ThrowsException()\n        {\n            const string expectedAuthCode = \"68c39cbd8d\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            var extraParams = new Dictionary<string, string>\n            {\n                [\"param1\"] = \"value1\",\n                [OAuth2Constants.ClientIdParameter] = \"value2\",\n                [\"param3\"] = \"value3\"\n            };\n\n            OAuth2Application app = CreateTestApplication();\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AuthCodes.Add(expectedAuthCode);\n\n            IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            await Assert.ThrowsAsync<ArgumentException>(() =>\n                client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None));\n        }\n\n        [Fact]\n        public async Task OAuth2Client_GetDeviceCodeAsync()\n        {\n            const string expectedUserCode = \"254583\";\n            const string expectedDeviceCode = \"6d1e34151aff4f41b9f186e177a0b15d\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            OAuth2Application app = CreateTestApplication();\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.UserCodes.Add(expectedUserCode);\n            server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            OAuth2DeviceCodeResult result = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None);\n\n            Assert.Equal(expectedUserCode, result.UserCode);\n            Assert.Equal(expectedDeviceCode, result.DeviceCode);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_GetTokenByAuthorizationCodeAsync()\n        {\n            const string authCode = \"a63ef59691\";\n            const string expectedAccessToken = \"LET_ME_IN\";\n            const string expectedRefreshToken = \"REFRESH_ME\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            OAuth2Application app = CreateTestApplication();\n            app.AuthGrants.Add(new OAuth2Application.AuthCodeGrant(authCode, expectedScopes));\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            var authCodeResult = new OAuth2AuthorizationCodeResult(authCode, TestRedirectUri);\n            OAuth2TokenResult result = await client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);\n\n            Assert.NotNull(result);\n            Assert.Equal(expectedScopes, result.Scopes);\n            Assert.Equal(expectedAccessToken, result.AccessToken);\n            Assert.Equal(expectedRefreshToken, result.RefreshToken);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_GetTokenByRefreshTokenAsync()\n        {\n            const string oldAccessToken = \"OLD_LET_ME_IN\";\n            const string oldRefreshToken = \"OLD_REFRESH_ME\";\n            const string expectedAccessToken = \"NEW_LET_ME_IN\";\n            const string expectedRefreshToken = \"NEW_REFRESH_ME\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            // Setup an existing access and refresh token\n            OAuth2Application app = CreateTestApplication();\n            app.AccessTokens[oldAccessToken] = oldRefreshToken;\n            app.RefreshTokens[oldRefreshToken] = expectedScopes;\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            OAuth2TokenResult result = await client.GetTokenByRefreshTokenAsync(oldRefreshToken, CancellationToken.None);\n\n            Assert.NotNull(result);\n            Assert.Equal(expectedScopes, result.Scopes);\n            Assert.Equal(expectedAccessToken, result.AccessToken);\n            Assert.Equal(expectedRefreshToken, result.RefreshToken);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_GetTokenByDeviceCodeAsync()\n        {\n            const string expectedUserCode = \"342728\";\n            const string expectedDeviceCode = \"ad6498533bf54f4db53e49612a4acfb0\";\n            const string expectedAccessToken = \"LET_ME_IN\";\n            const string expectedRefreshToken = \"REFRESH_ME\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            var grant = new OAuth2Application.DeviceCodeGrant(expectedUserCode, expectedDeviceCode, expectedScopes);\n\n            OAuth2Application app = CreateTestApplication();\n            app.DeviceGrants.Add(grant);\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.UserCodes.Add(expectedUserCode);\n            server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            var deviceCodeResult = new OAuth2DeviceCodeResult(expectedDeviceCode, expectedUserCode, null, null);\n\n            Task<OAuth2TokenResult> resultTask = client.GetTokenByDeviceCodeAsync(deviceCodeResult, CancellationToken.None);\n\n            // Simulate the user taking some time to sign in with the user code\n            Thread.Sleep(1000);\n            server.SignInDeviceWithUserCode(expectedUserCode);\n\n            OAuth2TokenResult result = await resultTask;\n\n            Assert.NotNull(result);\n            Assert.Equal(expectedScopes, result.Scopes);\n            Assert.Equal(expectedAccessToken, result.AccessToken);\n            Assert.Equal(expectedRefreshToken, result.RefreshToken);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_E2E_InteractiveWebFlowAndRefresh()\n        {\n            const string expectedAuthCode = \"e78a711d11\";\n            const string expectedAccessToken1 = \"LET_ME_IN-1\";\n            const string expectedAccessToken2 = \"LET_ME_IN-2\";\n            const string expectedRefreshToken1 = \"REFRESH_ME-1\";\n            const string expectedRefreshToken2 = \"REFRESH_ME-2\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            OAuth2Application app = CreateTestApplication();\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.AuthCodes.Add(expectedAuthCode);\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken1);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1);\n\n            IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            OAuth2AuthorizationCodeResult authCodeResult = await client.GetAuthorizationCodeAsync(\n                expectedScopes, browser, null, CancellationToken.None);\n\n            OAuth2TokenResult result1 = await client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);\n\n            Assert.NotNull(result1);\n            Assert.Equal(expectedScopes, result1.Scopes);\n            Assert.Equal(expectedAccessToken1, result1.AccessToken);\n            Assert.Equal(expectedRefreshToken1, result1.RefreshToken);\n\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken2);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken2);\n\n            OAuth2TokenResult result2 = await client.GetTokenByRefreshTokenAsync(result1.RefreshToken, CancellationToken.None);\n            Assert.NotNull(result2);\n            Assert.Equal(expectedScopes, result2.Scopes);\n            Assert.Equal(expectedAccessToken2, result2.AccessToken);\n            Assert.Equal(expectedRefreshToken2, result2.RefreshToken);\n        }\n\n        [Fact]\n        public async Task OAuth2Client_E2E_DeviceFlowAndRefresh()\n        {\n            const string expectedUserCode = \"736998\";\n            const string expectedDeviceCode = \"db6558b2a1d649758394ac3c2d9e00b1\";\n            const string expectedAccessToken1 = \"LET_ME_IN-1\";\n            const string expectedAccessToken2 = \"LET_ME_IN-2\";\n            const string expectedRefreshToken1 = \"REFRESH_ME-1\";\n            const string expectedRefreshToken2 = \"REFRESH_ME-2\";\n\n            var baseUri = new Uri(\"https://example.com\");\n            OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n\n            string[] expectedScopes = {\"read\", \"write\", \"delete\"};\n\n            OAuth2Application app = CreateTestApplication();\n\n            var server = new TestOAuth2Server(endpoints);\n            server.RegisterApplication(app);\n            server.Bind(httpHandler);\n            server.TokenGenerator.UserCodes.Add(expectedUserCode);\n            server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken1);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1);\n\n            var trace2 = new NullTrace2();\n            OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);\n\n            OAuth2DeviceCodeResult deviceResult = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None);\n\n            // Simulate the user taking some time to sign in with the user code\n            Thread.Sleep(1000);\n            server.SignInDeviceWithUserCode(deviceResult.UserCode);\n\n            OAuth2TokenResult result1 = await client.GetTokenByDeviceCodeAsync(deviceResult, CancellationToken.None);\n\n            Assert.NotNull(result1);\n            Assert.Equal(expectedScopes, result1.Scopes);\n            Assert.Equal(expectedAccessToken1, result1.AccessToken);\n            Assert.Equal(expectedRefreshToken1, result1.RefreshToken);\n\n            server.TokenGenerator.AccessTokens.Add(expectedAccessToken2);\n            server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken2);\n\n            OAuth2TokenResult result2 = await client.GetTokenByRefreshTokenAsync(result1.RefreshToken, CancellationToken.None);\n            Assert.NotNull(result2);\n            Assert.Equal(expectedScopes, result2.Scopes);\n            Assert.Equal(expectedAccessToken2, result2.AccessToken);\n            Assert.Equal(expectedRefreshToken2, result2.RefreshToken);\n        }\n\n        #region Helpers\n\n        private static OAuth2Application CreateTestApplication() => new OAuth2Application(TestClientId)\n        {\n            Secret = TestClientSecret,\n            RedirectUris = new[] {TestRedirectUri}\n        };\n\n        private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, ITrace2 trace2, IOAuth2CodeGenerator generator = null)\n        {\n            return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, trace2, TestRedirectUri, TestClientSecret)\n            {\n                CodeGenerator = generator\n            };\n        }\n\n        private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri)\n        {\n            Uri authEndpoint = new Uri(baseUri, \"/oauth/v2.0/authorize\");\n            Uri tokenEndpoint = new Uri(baseUri, \"/oauth/v2.0/access_token\");\n            Uri deviceEndpoint = new Uri(baseUri, \"/oauth/v2.0/authorize_device\");\n\n            return new OAuth2ServerEndpoints(authEndpoint, tokenEndpoint)\n                {DeviceAuthorizationEndpoint = deviceEndpoint};\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/OAuth2CryptographicCodeGeneratorTests.cs",
    "content": "using System.Linq;\nusing System.Security.Cryptography;\nusing System.Text;\nusing GitCredentialManager.Authentication.OAuth;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication\n{\n    public class OAuth2CryptographicCodeGeneratorTests\n    {\n        private const string ValidBase64UrlCharsNoPad = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n        [Fact]\n        public void OAuth2CryptographicCodeGenerator_CreateNonce_IsUnique()\n        {\n            var generator = new OAuth2CryptographicCodeGenerator();\n\n            // Create a bunch of nonce values\n            var nonces = new string[32];\n            for (int i = 0; i < nonces.Length; i++)\n            {\n                nonces[i] = generator.CreateNonce();\n            }\n\n            // There should be no duplicates\n            string[] uniqueNonces = nonces.Distinct().ToArray();\n            Assert.Equal(uniqueNonces, nonces);\n        }\n\n        [Fact]\n        public void OAuth2CryptographicCodeGenerator_CreatePkceCodeVerifier_IsUniqueBase64UrlStringWithoutPaddingAndLengthBetween43And128()\n        {\n            var generator = new OAuth2CryptographicCodeGenerator();\n\n            // Create a bunch of verifiers\n            var verifiers = new string[32];\n            for (int i = 0; i < verifiers.Length; i++)\n            {\n                string v = generator.CreatePkceCodeVerifier();\n\n                // Assert the verifier is a base64url string without padding\n                char[] vs = v.ToCharArray();\n                Assert.All(vs, x => Assert.Contains(x, ValidBase64UrlCharsNoPad));\n\n                // Assert the verifier is a string of length [43, 128] (inclusive)\n                Assert.InRange(v.Length, 43, 128);\n\n                verifiers[i] = v;\n            }\n\n            // There should be no duplicates\n            string[] uniqueVerifiers = verifiers.Distinct().ToArray();\n            Assert.Equal(uniqueVerifiers, verifiers);\n        }\n\n        [Fact]\n        public void OAuth2CryptographicCodeGenerator_CreatePkceCodeChallenge_Plain_ReturnsVerifierUnchanged()\n        {\n            var generator = new OAuth2CryptographicCodeGenerator();\n\n            var verifier = generator.CreatePkceCodeVerifier();\n            var challenge = generator.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Plain, verifier);\n\n            Assert.Equal(verifier, challenge);\n        }\n\n        [Fact]\n        public void OAuth2CryptographicCodeGenerator_CreatePkceCodeChallenge_Sha256_ReturnsBase64UrlEncodedSha256HashOfAsciiVerifier()\n        {\n            var generator = new OAuth2CryptographicCodeGenerator();\n\n            var verifier = generator.CreatePkceCodeVerifier();\n\n            byte[] verifierAsciiBytes = Encoding.ASCII.GetBytes(verifier);\n            byte[] hashedBytes;\n            using (var sha256 = SHA256.Create())\n            {\n                hashedBytes = sha256.ComputeHash(verifierAsciiBytes);\n            }\n\n            var expectedChallenge = Base64UrlConvert.Encode(hashedBytes, false);\n            var actualChallenge = generator.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, verifier);\n\n            Assert.Equal(expectedChallenge, actualChallenge);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/OAuth2SystemWebBrowserTests.cs",
    "content": "using System;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication;\n\npublic class OAuth2SystemWebBrowserTests\n{\n    [Fact]\n    public void OAuth2SystemWebBrowser_UpdateRedirectUri_NonLoopback_ThrowsError()\n    {\n        var sm = new TestSessionManager();\n        var options = new OAuth2WebBrowserOptions();\n        var browser = new OAuth2SystemWebBrowser(sm, options);\n\n        Assert.Throws<ArgumentException>(() => browser.UpdateRedirectUri(new Uri(\"http://example.com\")));\n    }\n\n    [Theory]\n    [InlineData(\"http://localhost:1234\", \"http://localhost:1234\")]\n    [InlineData(\"http://localhost:1234/\", \"http://localhost:1234/\")]\n    [InlineData(\"http://localhost:1234/oauth-callback\", \"http://localhost:1234/oauth-callback\")]\n    [InlineData(\"http://localhost:1234/oauth-callback/\", \"http://localhost:1234/oauth-callback/\")]\n    [InlineData(\"http://127.0.0.7:1234\", \"http://127.0.0.7:1234\")]\n    [InlineData(\"http://127.0.0.7:1234/\", \"http://127.0.0.7:1234/\")]\n    [InlineData(\"http://127.0.0.7:1234/oauth-callback\", \"http://127.0.0.7:1234/oauth-callback\")]\n    [InlineData(\"http://127.0.0.7:1234/oauth-callback/\", \"http://127.0.0.7:1234/oauth-callback/\")]\n    public void OAuth2SystemWebBrowser_UpdateRedirectUri_SpecificPort(string input, string expected)\n    {\n        var sm = new TestSessionManager();\n        var options = new OAuth2WebBrowserOptions();\n        var browser = new OAuth2SystemWebBrowser(sm, options);\n\n        Uri actualUri = browser.UpdateRedirectUri(new Uri(input));\n\n        Assert.Equal(expected, actualUri.OriginalString);\n    }\n\n    [Theory]\n    [InlineData(\"http://localhost\")]\n    [InlineData(\"http://localhost/\")]\n    [InlineData(\"http://localhost/oauth-callback\")]\n    [InlineData(\"http://localhost/oauth-callback/\")]\n    [InlineData(\"http://127.0.0.7\")]\n    [InlineData(\"http://127.0.0.7/\")]\n    [InlineData(\"http://127.0.0.7/oauth-callback\")]\n    [InlineData(\"http://127.0.0.7/oauth-callback/\")]\n    public void OAuth2SystemWebBrowser_UpdateRedirectUri_AnyPort(string input)\n    {\n        var sm = new TestSessionManager();\n        var options = new OAuth2WebBrowserOptions();\n        var browser = new OAuth2SystemWebBrowser(sm, options);\n\n        var inputUri = new Uri(input);\n        Uri actualUri = browser.UpdateRedirectUri(inputUri);\n\n        Assert.Equal(inputUri.Scheme, actualUri.Scheme);\n        Assert.Equal(inputUri.Host, actualUri.Host);\n        Assert.Equal(\n            inputUri.GetComponents(UriComponents.Path, UriFormat.Unescaped),\n            actualUri.GetComponents(UriComponents.Path, UriFormat.Unescaped)\n        );\n        Assert.False(actualUri.IsDefaultPort);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Authentication/WindowsIntegratedAuthenticationTests.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Authentication\n{\n    public class WindowsIntegratedAuthenticationTests\n    {\n        private const string NegotiateHeader = \"Negotiate [test-challenge-string]\";\n        private const string NtlmHeader      = \"NTLM [test-challenge-string]\";\n\n        [Fact]\n        public async Task WindowsIntegratedAuthentication_GetIsSupportedAsync_NullUri_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var wiaAuth = new WindowsIntegratedAuthentication(context);\n\n            await Assert.ThrowsAsync<ArgumentNullException>(() => wiaAuth.GetAuthenticationTypesAsync(null));\n        }\n\n        [Fact]\n        public async Task WindowsIntegratedAuthentication_GetIsSupportedAsync_NegotiateAndNtlm_ReturnsTrue()\n        {\n            await TestGetIsSupportedAsync(new[] {NegotiateHeader, NtlmHeader}, expected: WindowsAuthenticationTypes.All);\n\n            // Also check with the headers in the other order\n            await TestGetIsSupportedAsync(new[] {NtlmHeader, NegotiateHeader}, expected: WindowsAuthenticationTypes.All);\n        }\n\n        [Fact]\n        public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_Ntlm_ReturnsTrue()\n        {\n            await TestGetIsSupportedAsync(new[]{NtlmHeader}, expected: WindowsAuthenticationTypes.Ntlm);\n        }\n\n        [Fact]\n        public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_Negotiate_ReturnsTrue()\n        {\n            await TestGetIsSupportedAsync(new[]{NegotiateHeader}, expected: WindowsAuthenticationTypes.Negotiate);\n        }\n\n        [Fact]\n        public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_NoHeaders_ReturnsFalse()\n        {\n            await TestGetIsSupportedAsync(new string[0], expected: WindowsAuthenticationTypes.None);\n        }\n\n        [Fact]\n        public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_NoWiaHeaders_ReturnsFalse()\n        {\n            await TestGetIsSupportedAsync(\n                new[]\n                {\n                    \"Bearer\",\n                    \"Bearer foo\",\n                    \"NotNTLM test test NTLM test\",\n                    \"NotNegotiate test test Negotiate test\",\n                    \"NotKerberos test test Negotiate test\"\n                },\n                expected: WindowsAuthenticationTypes.None);\n        }\n\n        #region Helpers\n\n        private static async Task TestGetIsSupportedAsync(string[] wwwAuthHeaders, WindowsAuthenticationTypes expected)\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n\n            var wiaResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            foreach (string headerValue in wwwAuthHeaders)\n            {\n                wiaResponse.Headers.WwwAuthenticate.ParseAdd(headerValue);\n            }\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, wiaResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var wiaAuth = new WindowsIntegratedAuthentication(context);\n\n            WindowsAuthenticationTypes actual = await wiaAuth.GetAuthenticationTypesAsync(uri);\n\n            Assert.Equal(expected, actual);\n            httpHandler.AssertRequest(HttpMethod.Head, uri, expectedNumberOfCalls: 1);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Base64UrlConvertTests.cs",
    "content": "using Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class Base64UrlConvertTests\n    {\n        [Theory]\n        [InlineData(new byte[0], \"\")]\n        [InlineData(new byte[]{4}, \"BA==\")]\n        [InlineData(new byte[]{4,5}, \"BAU=\")]\n        [InlineData(new byte[]{4,5,6}, \"BAUG\")]\n        [InlineData(new byte[]{4,5,6,7}, \"BAUGBw==\")]\n        [InlineData(new byte[]{4,5,6,7,8}, \"BAUGBwg=\")]\n        [InlineData(new byte[]{4,5,6,7,8,9}, \"BAUGBwgJ\")]\n        public void Base64UrlConvert_Encode_WithPadding(byte[] data, string expected)\n        {\n            string actual = Base64UrlConvert.Encode(data, includePadding: true);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(new byte[0], \"\")]\n        [InlineData(new byte[]{4}, \"BA\")]\n        [InlineData(new byte[]{4,5}, \"BAU\")]\n        [InlineData(new byte[]{4,5,6}, \"BAUG\")]\n        [InlineData(new byte[]{4,5,6,7}, \"BAUGBw\")]\n        [InlineData(new byte[]{4,5,6,7,8}, \"BAUGBwg\")]\n        [InlineData(new byte[]{4,5,6,7,8,9}, \"BAUGBwgJ\")]\n        public void Base64UrlConvert_Encode_WithoutPadding(byte[] data, string expected)\n        {\n            string actual = Base64UrlConvert.Encode(data, includePadding: false);\n            Assert.Equal(expected, actual);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/ConfigureCommandTests.cs",
    "content": "using System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Commands\n{\n    public class ConfigureCommandTests\n    {\n        [Fact]\n        public async Task ConfigureCommand_ExecuteAsync_User_InvokesConfigurationServiceConfigureUser()\n        {\n            var configService = new Mock<IConfigurationService>();\n            configService.Setup(x => x.ConfigureAsync(It.IsAny<ConfigurationTarget>()))\n                .Returns(Task.CompletedTask)\n                .Verifiable();\n\n            var context = new TestCommandContext();\n            var command = new ConfigureCommand(context, configService.Object);\n\n            await command.ExecuteAsync(false);\n\n            configService.Verify(x => x.ConfigureAsync(ConfigurationTarget.User), Times.Once);\n        }\n\n        [Fact]\n        public async Task ConfigureCommand_ExecuteAsync_System_InvokesConfigurationServiceConfigureSystem()\n        {\n            var configService = new Mock<IConfigurationService>();\n            configService.Setup(x => x.ConfigureAsync(It.IsAny<ConfigurationTarget>()))\n                .Returns(Task.CompletedTask)\n                .Verifiable();\n\n            var context = new TestCommandContext();\n            var command = new ConfigureCommand(context, configService.Object);\n\n            await command.ExecuteAsync(true);\n\n            configService.Verify(x => x.ConfigureAsync(ConfigurationTarget.System), Times.Once);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/DiagnoseCommandTests.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Security.AccessControl;\nusing System.Text;\nusing GitCredentialManager.Diagnostics;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Core.Tests.Commands;\n\npublic class DiagnoseCommandTests\n{\n    [Fact]\n    public void NetworkingDiagnostic_SendHttpRequest_Primary_OK()\n    {\n        var primaryUriString = \"http://example.com\";\n        var sb = new StringBuilder();\n        var context = new TestCommandContext();\n        var networkingDiagnostic = new NetworkingDiagnostic(context);\n        var primaryUri = new Uri(primaryUriString);\n        var httpHandler = new TestHttpMessageHandler();\n        var httpResponse = new HttpResponseMessage();\n        var expected = $\"Sending HEAD request to {primaryUriString}... OK{Environment.NewLine}\";\n\n        httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);\n\n        networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));\n\n        httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);\n        Assert.Contains(expected, sb.ToString());\n    }\n\n    [Fact]\n    public void NetworkingDiagnostic_SendHttpRequest_Backup_OK()\n    {\n        var primaryUriString = \"http://example.com\";\n        var backupUriString = \"http://httpforever.com\";\n        var sb = new StringBuilder();\n        var context = new TestCommandContext();\n        var networkingDiagnostic = new NetworkingDiagnostic(context);\n        var primaryUri = new Uri(primaryUriString);\n        var backupUri = new Uri(backupUriString);\n        var httpHandler = new TestHttpMessageHandler { SimulatePrimaryUriFailure = true };\n        var httpResponse = new HttpResponseMessage();\n        var expected = $\"Sending HEAD request to {primaryUriString}... warning: HEAD request failed{Environment.NewLine}\" +\n                       $\"Sending HEAD request to {backupUriString}... OK{Environment.NewLine}\";\n\n        httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);\n        httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);\n\n        networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));\n\n        httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);\n        httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);\n        Assert.Contains(expected, sb.ToString());\n    }\n\n    [Fact]\n    public void NetworkingDiagnostic_SendHttpRequest_No_Network()\n    {\n        var primaryUriString = \"http://example.com\";\n        var backupUriString = \"http://httpforever.com\";\n        var sb = new StringBuilder();\n        var context = new TestCommandContext();\n        var networkingDiagnostic = new NetworkingDiagnostic(context);\n        var primaryUri = new Uri(primaryUriString);\n        var backupUri = new Uri(backupUriString);\n        var httpHandler = new TestHttpMessageHandler { SimulateNoNetwork = true };\n        var httpResponse = new HttpResponseMessage();\n        var expected = $\"Sending HEAD request to {primaryUriString}... warning: HEAD request failed{Environment.NewLine}\" +\n                       $\"Sending HEAD request to {backupUriString}... warning: HEAD request failed{Environment.NewLine}\";\n\n        httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);\n        httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);\n\n        networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));\n\n        httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);\n        httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);\n        Assert.Contains(expected, sb.ToString());\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/EraseCommandTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Commands\n{\n    public class EraseCommandTests\n    {\n        [Fact]\n        public async Task EraseCommand_ExecuteAsync_CallsHostProvider()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            var stdin = $\"protocol=http\\nhost=example.com\\nusername={testUserName}\\npassword={testPassword}\\n\\n\";\n            var expectedInput = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"]     = \"example.com\",\n                [\"username\"] = testUserName,\n                [\"password\"] = testPassword // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.EraseCredentialAsync(It.IsAny<InputArguments>()))\n                        .Returns(Task.CompletedTask);\n            var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object};\n            var context = new TestCommandContext\n            {\n                Streams = {In = stdin}\n            };\n\n            var command = new EraseCommand(context, providerRegistry);\n\n            await command.ExecuteAsync();\n\n            providerMock.Verify(\n                x => x.EraseCredentialAsync(It.Is<InputArguments>(y => AreInputArgumentsEquivalent(expectedInput, y))),\n                Times.Once);\n        }\n\n        private static bool AreInputArgumentsEquivalent(InputArguments a, InputArguments b)\n        {\n            return a.Protocol == b.Protocol &&\n                   a.Host     == b.Host &&\n                   a.Path     == b.Path &&\n                   a.UserName == b.UserName &&\n                   a.Password == b.Password;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/GetCommandTests.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Commands\n{\n    public class GetCommandTests\n    {\n        [Fact]\n        public async Task GetCommand_ExecuteAsync_CallsHostProviderAndWritesCredential()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            ICredential testCredential = new GitCredential(testUserName, testPassword);\n            var stdin = $\"protocol=http\\nhost=example.com\\n\\n\";\n            var expectedStdOutDict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"]     = \"example.com\",\n                [\"username\"] = testUserName,\n                [\"password\"] = testPassword\n            };\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.GetCredentialAsync(It.IsAny<InputArguments>()))\n                        .ReturnsAsync(new GetCredentialResult(testCredential));\n            var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object};\n            var context = new TestCommandContext\n            {\n                Streams = {In = stdin}\n            };\n\n            var command = new GetCommand(context, providerRegistry);\n\n            await command.ExecuteAsync();\n\n            IDictionary<string, string> actualStdOutDict = ParseDictionary(context.Streams.Out);\n\n            providerMock.Verify(x => x.GetCredentialAsync(It.IsAny<InputArguments>()), Times.Once);\n            Assert.Equal(expectedStdOutDict, actualStdOutDict);\n        }\n\n        #region Helpers\n\n        private static IDictionary<string, string> ParseDictionary(StringBuilder sb) => ParseDictionary(sb.ToString());\n\n        private static IDictionary<string, string> ParseDictionary(string str) => new StringReader(str).ReadDictionary();\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/GitCommandBaseTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Commands\n{\n    public class GitCommandBaseTests\n    {\n        [Fact]\n        public async Task GitCommandBase_ExecuteAsync_CallsExecuteInternalAsyncWithCorrectArgs()\n        {\n            var mockContext = new Mock<ICommandContext>();\n            var mockStreams = new Mock<IStandardStreams>();\n            var mockProvider = new Mock<IHostProvider>();\n            var mockHostRegistry = new Mock<IHostProviderRegistry>();\n\n            mockHostRegistry.Setup(x => x.GetProviderAsync(It.IsAny<InputArguments>()))\n                .ReturnsAsync(mockProvider.Object)\n                .Verifiable();\n\n            mockProvider.Setup(x => x.IsSupported(It.IsAny<InputArguments>()))\n                .Returns(true);\n\n            string standardIn = \"protocol=test\\nhost=example.com\\npath=a/b/c\\n\\n\";\n            TextReader standardInReader = new StringReader(standardIn);\n\n            mockStreams.Setup(x => x.In).Returns(standardInReader);\n            mockContext.Setup(x => x.Streams).Returns(mockStreams.Object);\n            mockContext.Setup(x => x.Trace).Returns(Mock.Of<ITrace>());\n            mockContext.Setup(x => x.Settings).Returns(Mock.Of<ISettings>());\n\n            GitCommandBase testCommand = new TestCommand(mockContext.Object, mockHostRegistry.Object)\n            {\n                VerifyExecuteInternalAsync = (input, provider) =>\n                {\n                    Assert.Same(mockProvider.Object, provider);\n                    Assert.Equal(\"test\", input.Protocol);\n                    Assert.Equal(\"example.com\", input.Host);\n                    Assert.Equal(\"a/b/c\", input.Path);\n                }\n            };\n\n            await testCommand.ExecuteAsync();\n        }\n\n        [Fact]\n        public async Task GitCommandBase_ExecuteAsync_ConfiguresSettingsRemoteUri()\n        {\n            var mockContext = new Mock<ICommandContext>();\n            var mockStreams = new Mock<IStandardStreams>();\n            var mockProvider = new Mock<IHostProvider>();\n            var mockSettings = new Mock<ISettings>();\n            var mockHostRegistry = new Mock<IHostProviderRegistry>();\n\n            mockHostRegistry.Setup(x => x.GetProviderAsync(It.IsAny<InputArguments>()))\n                .ReturnsAsync(mockProvider.Object);\n\n            string standardIn = \"protocol=test\\nhost=example.com\\npath=a/b/c\\n\\n\";\n            TextReader standardInReader = new StringReader(standardIn);\n\n            var remoteUri = new Uri(\"test://example.com/a/b/c\");\n\n            mockSettings.SetupProperty(x => x.RemoteUri);\n\n            mockStreams.Setup(x => x.In).Returns(standardInReader);\n            mockContext.Setup(x => x.Streams).Returns(mockStreams.Object);\n            mockContext.Setup(x => x.Trace).Returns(Mock.Of<ITrace>());\n            mockContext.Setup(x => x.Settings).Returns(mockSettings.Object);\n\n            GitCommandBase testCommand = new TestCommand(mockContext.Object, mockHostRegistry.Object);\n\n            await testCommand.ExecuteAsync();\n\n            Assert.Equal(remoteUri, mockSettings.Object.RemoteUri);\n        }\n\n        private class TestCommand : GitCommandBase\n        {\n            public TestCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry)\n                : base(context, \"test\", null, hostProviderRegistry)\n            {\n            }\n\n            protected override Task ExecuteInternalAsync(InputArguments input, IHostProvider provider)\n            {\n                VerifyExecuteInternalAsync?.Invoke(input, provider);\n                return Task.CompletedTask;\n            }\n\n            public Action<InputArguments, IHostProvider> VerifyExecuteInternalAsync { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/StoreCommandTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Commands\n{\n    public class StoreCommandTests\n    {[Fact]\n        public async Task StoreCommand_ExecuteAsync_CallsHostProvider()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            var stdin = $\"protocol=http\\nhost=example.com\\nusername={testUserName}\\npassword={testPassword}\\n\\n\";\n            var expectedInput = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"]     = \"example.com\",\n                [\"username\"] = testUserName,\n                [\"password\"] = testPassword\n            });\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.StoreCredentialAsync(It.IsAny<InputArguments>()))\n                        .Returns(Task.CompletedTask);\n            var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object};\n            var context = new TestCommandContext\n            {\n                Streams = {In = stdin}\n            };\n\n            var command = new StoreCommand(context, providerRegistry);\n\n            await command.ExecuteAsync();\n\n            providerMock.Verify(\n                x => x.StoreCredentialAsync(It.Is<InputArguments>(y => AreInputArgumentsEquivalent(expectedInput, y))),\n                Times.Once);\n        }\n\n        bool AreInputArgumentsEquivalent(InputArguments a, InputArguments b)\n        {\n            return a.Protocol == b.Protocol &&\n                   a.Host     == b.Host &&\n                   a.Path     == b.Path &&\n                   a.UserName == b.UserName &&\n                   a.Password == b.Password;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Commands/UnconfigureCommandTests.cs",
    "content": "using System.Threading.Tasks;\nusing GitCredentialManager.Commands;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Commands\n{\n    public class UnconfigureCommandTests\n    {\n        [Fact]\n        public async Task UnconfigureCommand_ExecuteAsync_User_InvokesConfigurationServiceUnconfigureUser()\n        {\n            var configService = new Mock<IConfigurationService>();\n            configService.Setup(x => x.UnconfigureAsync(It.IsAny<ConfigurationTarget>()))\n                .Returns(Task.CompletedTask)\n                .Verifiable();\n\n            var context = new TestCommandContext();\n            var command = new UnconfigureCommand(context, configService.Object);\n\n            await command.ExecuteAsync(false);\n\n            configService.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User), Times.Once);\n        }\n\n        [Fact]\n        public async Task UnconfigureCommand_ExecuteAsync_System_InvokesConfigurationServiceUnconfigureSystem()\n        {\n            var configService = new Mock<IConfigurationService>();\n            configService.Setup(x => x.UnconfigureAsync(It.IsAny<ConfigurationTarget>()))\n                .Returns(Task.CompletedTask)\n                .Verifiable();\n\n            var context = new TestCommandContext();\n            var command = new UnconfigureCommand(context, configService.Object);\n\n            await command.ExecuteAsync(true);\n\n            configService.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System), Times.Once);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/ConfigurationServiceTests.cs",
    "content": "using System.Threading.Tasks;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class ConfigurationServiceTests\n    {\n        [Fact]\n        public async Task ConfigurationService_ConfigureAsync_System_ComponentsAreConfiguredWithSystem()\n        {\n            var context = new TestCommandContext();\n            var service = new ConfigurationService(context);\n\n            var component1 = new Mock<IConfigurableComponent>();\n            var component2 = new Mock<IConfigurableComponent>();\n            var component3 = new Mock<IConfigurableComponent>();\n\n            service.AddComponent(component1.Object);\n            service.AddComponent(component2.Object);\n            service.AddComponent(component3.Object);\n\n            await service.ConfigureAsync(ConfigurationTarget.System);\n\n            component1.Verify(x => x.ConfigureAsync(ConfigurationTarget.System),\n                Times.Once);\n            component2.Verify(x => x.ConfigureAsync(ConfigurationTarget.System),\n                Times.Once);\n            component3.Verify(x => x.ConfigureAsync(ConfigurationTarget.System),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task ConfigurationService_ConfigureAsync_User_ComponentsAreConfiguredWithUser()\n        {\n            var context = new TestCommandContext();\n            var service = new ConfigurationService(context);\n\n            var component1 = new Mock<IConfigurableComponent>();\n            var component2 = new Mock<IConfigurableComponent>();\n            var component3 = new Mock<IConfigurableComponent>();\n\n            service.AddComponent(component1.Object);\n            service.AddComponent(component2.Object);\n            service.AddComponent(component3.Object);\n\n            await service.ConfigureAsync(ConfigurationTarget.User);\n\n            component1.Verify(x => x.ConfigureAsync(ConfigurationTarget.User),\n                Times.Once);\n            component2.Verify(x => x.ConfigureAsync(ConfigurationTarget.User),\n                Times.Once);\n            component3.Verify(x => x.ConfigureAsync(ConfigurationTarget.User),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task ConfigurationService_UnconfigureAsync_System_ComponentsAreUnconfiguredWithSystem()\n        {\n            var context = new TestCommandContext();\n            var service = new ConfigurationService(context);\n\n            var component1 = new Mock<IConfigurableComponent>();\n            var component2 = new Mock<IConfigurableComponent>();\n            var component3 = new Mock<IConfigurableComponent>();\n\n            service.AddComponent(component1.Object);\n            service.AddComponent(component2.Object);\n            service.AddComponent(component3.Object);\n\n            await service.UnconfigureAsync(ConfigurationTarget.System);\n\n            component1.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System),\n                Times.Once);\n            component2.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System),\n                Times.Once);\n            component3.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task ConfigurationService_UnconfigureAsync_User_ComponentsAreUnconfiguredWithUser()\n        {\n            var context = new TestCommandContext();\n            var service = new ConfigurationService(context);\n\n            var component1 = new Mock<IConfigurableComponent>();\n            var component2 = new Mock<IConfigurableComponent>();\n            var component3 = new Mock<IConfigurableComponent>();\n\n            service.AddComponent(component1.Object);\n            service.AddComponent(component2.Object);\n            service.AddComponent(component3.Object);\n\n            await service.UnconfigureAsync(ConfigurationTarget.User);\n\n            component1.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User),\n                Times.Once);\n            component2.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User),\n                Times.Once);\n            component3.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User),\n                Times.Once);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Core.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <LangVersion>latest</LangVersion>\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.2\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.11.1\" />\n    <PackageReference Include=\"ReportGenerator\" Version=\"5.3.10\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <DotNetCliToolReference Include=\"dotnet-xunit\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\TestInfrastructure\\TestInfrastructure.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Core.Tests/CurlCookieTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Core.Tests;\n\npublic class CurlCookieParserTests\n{\n    [Fact]\n    public void CurlCookieParser_EmptyFile_ReturnsNoCookies()\n    {\n        const string content = \"\";\n\n        var trace = new NullTrace();\n        var parser = new CurlCookieParser(trace);\n\n        IList<Cookie> actual = parser.Parse(content);\n\n        Assert.Empty(actual);\n    }\n\n    [Fact]\n    public void CurlCookieParser_Parse_MissingFields_SkipsInvalidLines()\n    {\n        const string content =\n            // Valid cookie\n            \".example.com\\tTRUE\\t/path/here\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n\n            // Missing several fields - not a valid cookie so should be skipped\n            \".example.com\\tTRUE\\tTRUE\\tcookie1\\tvalue1\\n\";\n\n        var trace = new NullTrace();\n        var parser = new CurlCookieParser(trace);\n\n        IList<Cookie> actual = parser.Parse(content);\n\n        Assert.Single(actual);\n        AssertCookie(actual[0], \".example.com\", \"/path/here\", true, 0, \"cookie1\", \"value1\");\n    }\n\n    [Fact]\n    public void CurlCookieParser_Parse_MissingFields_ReturnsValidCookiesWithDefaults()\n    {\n        const string content =\n            // Empty path field (default \"/\")\n            \".example.com\\tTRUE\\t\\tTRUE\\t852852\\tcookie1\\tvalue1\\n\" +\n\n            // Empty expiration field (default 0)\n            \".example.com\\tTRUE\\t/path/here\\tTRUE\\t\\tcookie1\\tvalue1\";\n\n        var trace = new NullTrace();\n        var parser = new CurlCookieParser(trace);\n\n        IList<Cookie> actual = parser.Parse(content);\n\n        Assert.Equal(2, actual.Count);\n        AssertCookie(actual[0], \".example.com\", \"/\", true, 852852, \"cookie1\", \"value1\");\n        AssertCookie(actual[1], \".example.com\", \"/path/here\", true, 0, \"cookie1\", \"value1\");\n    }\n\n    [Fact]\n    public void CurlCookieParser_Parse_ValidFields_ReturnsValidCookies()\n    {\n        const string content =\n            \".example.com\\tTRUE\\t/path\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n            \".example.com\\tfAlSe\\t/path\\ttRuE\\t0\\tcookie1\\tvalue1\\n\" +\n            \".example.com\\tTRUE\\t/path\\tTRUE\\t0\\tcookie1 with spaces\\tvalue1 with spaces\\n\" +\n            \".example.com\\tFALSE\\t/path\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n            \"example.com\\tFALSE\\t/path\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n            \"example.com\\tTRUE\\t/path\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n            \".example.com\\tTRUE\\t/path\\tFALSE\\t0\\tcookie1\\tvalue1\\n\" +\n            \".example.com\\tTRUE\\t/path\\tFALSE\\t401654\\tcookie1\\tvalue1\\n\";\n\n        var trace = new NullTrace();\n        var parser = new CurlCookieParser(trace);\n\n        IList<Cookie> actual = parser.Parse(content);\n\n        Assert.Equal(8, actual.Count);\n        AssertCookie(actual[0], \".example.com\", \"/path\", true, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[1], \"example.com\", \"/path\", true, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[2], \".example.com\", \"/path\", true, 0, \"cookie1 with spaces\", \"value1 with spaces\");\n        AssertCookie(actual[3], \"example.com\", \"/path\", true, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[4], \"example.com\", \"/path\", true, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[5], \"example.com\", \"/path\", true, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[6], \".example.com\", \"/path\", false, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[7], \".example.com\", \"/path\", false, 401654, \"cookie1\", \"value1\");\n    }\n\n    [Fact]\n    public void CurlCookieParser_Parse_Comments_ReturnsCookies()\n    {\n        const string content =\n            // Comment block\n            \"# This is a cookie file with various comments!\\n\" +\n            \"# Lines starting with # are comments, except those that\\n\" +\n            \"# start with exactly '#HttpOnly_'.. two #s is a comment still!\\n\" +\n\n            // This is still a comment!\n            \"##HttpOnly_ <-- this is a comment still!\\n\" +\n\n            // Valid line\n            \".example.com\\tTRUE\\t/\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n\n            // Commented out cookie line\n            \"#.example.com\\tTRUE\\t/\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n\n            // Valid cookie but HTTP only\n            \"#HttpOnly_.example.com\\tTRUE\\t/\\tTRUE\\t0\\tcookie1\\tvalue1\\n\";\n\n        var trace = new NullTrace();\n        var parser = new CurlCookieParser(trace);\n\n        IList<Cookie> actual = parser.Parse(content);\n\n        Assert.Equal(2, actual.Count);\n        AssertCookie(actual[0], \".example.com\", \"/\", true, 0, \"cookie1\", \"value1\");\n        AssertCookie(actual[1], \".example.com\", \"/\", true, 0, \"cookie1\", \"value1\");\n    }\n\n    private static void AssertCookie(Cookie cookie, string domain, string path, bool secureOnly, long expires, string name, string value)\n    {\n        Assert.Equal(name, cookie.Name);\n        Assert.Equal(value, cookie.Value);\n        Assert.Equal(domain, cookie.Domain);\n        Assert.Equal(path, cookie.Path);\n        Assert.Equal(secureOnly, cookie.Secure);\n        Assert.Equal(expires, cookie.Expires.Subtract(DateTime.UnixEpoch).TotalSeconds);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/EnsureArgumentTests.cs",
    "content": "using System;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class EnsureArgumentTests\n    {\n        [Theory]\n        [InlineData(5, 0, 10, true, true)]\n        [InlineData(0, 0, 10, true, true)]\n        [InlineData(10, 0, 10, true, true)]\n        [InlineData(0, -10, 0, true, true)]\n        [InlineData(-10, -10, 0, true, true)]\n        public void EnsureArgument_InRange_DoesNotThrow(int x, int lower, int upper, bool lowerInc, bool upperInc)\n        {\n            EnsureArgument.InRange(x, nameof(x), lower, upper, lowerInc, upperInc);\n        }\n\n        [Theory]\n        [InlineData(-3, 0, 10, true, true)]\n        [InlineData(13, 0, 10, true, true)]\n        [InlineData(10, 0, 10, true, false)]\n        [InlineData(0, 0, 10, false, true)]\n        [InlineData(-10, -10, 0, false, true)]\n        [InlineData(0, -10, 0, true, false)]\n        public void EnsureArgument_InRange_ThrowsException(int x, int lower, int upper, bool lowerInc, bool upperInc)\n        {\n            Assert.Throws<ArgumentOutOfRangeException>(\n                () => EnsureArgument.InRange(x, nameof(x), lower, upper, lowerInc, upperInc)\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/EnumerableExtensionsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class EnumerableExtensionsTests\n    {\n        [Fact]\n        public void EnumerableExtensions_ConcatMany_Null_ThrowsException()\n        {\n            Assert.Throws<ArgumentNullException>(() => EnumerableExtensions.ConcatMany((IEnumerable<object>)null).ToArray());\n        }\n\n        [Fact]\n        public void EnumerableExtensions_ConcatMany_OneSequence_ReturnsSequence()\n        {\n            int[] seq1 = {0, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 8, 9};\n            int[] expectedResult = seq1;\n\n            int[] actualResult = EnumerableExtensions.ConcatMany(seq1).ToArray();\n\n            Assert.Equal(expectedResult, actualResult);\n        }\n\n        [Fact]\n        public void EnumerableExtensions_ConcatMany_TwoSequences_ReturnsConcatenateSequences()\n        {\n            int[] seq1 = {0, 1, 2, 3, 4, 4};\n            int[] seq2 = {4, 4, 5, 6, 7, 8, 9};\n            int[] expectedResult = {0, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 8, 9};\n\n            int[] actualResult = EnumerableExtensions.ConcatMany(seq1, seq2).ToArray();\n\n            Assert.Equal(expectedResult, actualResult);\n        }\n\n        [Fact]\n        public void EnumerableExtensions_ConcatMany_ManySequences_ReturnsConcatenateSequences()\n        {\n            int[] seq1 = {0, 1};\n            int[] seq2 = {2, 3};\n            int[] seq3 = {4, 4};\n            int[] seq4 = {4, 4};\n            int[] seq5 = {5, 6};\n            int[] seq6 = {7, 8};\n            int[] seq7 = {9};\n            int[] expectedResult = {0, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 8, 9};\n\n            int[] actualResult = EnumerableExtensions.ConcatMany(seq1, seq2, seq3, seq4, seq5, seq6, seq7).ToArray();\n\n            Assert.Equal(expectedResult, actualResult);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/EnvironmentTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing GitCredentialManager.Interop.Posix;\nusing GitCredentialManager.Interop.Windows;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class EnvironmentTests\n    {\n        private const string WindowsPathVar = @\"C:\\Users\\john.doe\\bin;C:\\Windows\\system32;C:\\Windows\";\n        private const string WindowsExecName = \"foo.exe\";\n        private const string PosixPathVar = \"/home/john.doe/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\";\n        private const string PosixExecName = \"foo\";\n\n        [WindowsFact]\n        public void WindowsEnvironment_TryLocateExecutable_NotExists_ReturnFalse()\n        {\n            var fs = new TestFileSystem();\n            var envars = new Dictionary<string, string> {[\"PATH\"] = WindowsPathVar};\n            var env = new WindowsEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(WindowsExecName, out string actualPath);\n\n            Assert.False(actualResult);\n            Assert.Null(actualPath);\n        }\n\n        [WindowsFact]\n        public void WindowsEnvironment_TryLocateExecutable_Exists_ReturnTrueAndPath()\n        {\n            string expectedPath = @\"C:\\Windows\\system32\\foo.exe\";\n            var fs = new TestFileSystem\n            {\n                Files = new Dictionary<string, byte[]>\n                {\n                    [expectedPath] = Array.Empty<byte>()\n                }\n            };\n            var envars = new Dictionary<string, string> {[\"PATH\"] = WindowsPathVar};\n            var env = new WindowsEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(WindowsExecName, out string actualPath);\n\n            Assert.True(actualResult);\n            Assert.Equal(expectedPath, actualPath);\n        }\n\n        [WindowsFact]\n        public void WindowsEnvironment_TryLocateExecutable_ExistsMultiple_ReturnTrueAndFirstPath()\n        {\n            string expectedPath = @\"C:\\Users\\john.doe\\bin\\foo.exe\";\n            var fs = new TestFileSystem\n            {\n                Files = new Dictionary<string, byte[]>\n                {\n                    [expectedPath] = Array.Empty<byte>(),\n                    [@\"C:\\Windows\\system32\\foo.exe\"] = Array.Empty<byte>(),\n                    [@\"C:\\Windows\\foo.exe\"] = Array.Empty<byte>(),\n                }\n            };\n            var envars = new Dictionary<string, string> {[\"PATH\"] = WindowsPathVar};\n            var env = new WindowsEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(WindowsExecName, out string actualPath);\n\n            Assert.True(actualResult);\n            Assert.Equal(expectedPath, actualPath);\n        }\n\n        [PosixFact]\n        public void PosixEnvironment_TryLocateExecutable_NotExists_ReturnFalse()\n        {\n            var fs = new TestFileSystem();\n            var envars = new Dictionary<string, string> {[\"PATH\"] = PosixPathVar};\n            var env = new PosixEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);\n\n            Assert.False(actualResult);\n            Assert.Null(actualPath);\n        }\n\n        [PosixFact]\n        public void PosixEnvironment_TryLocateExecutable_Exists_ReturnTrueAndPath()\n        {\n            string expectedPath = \"/usr/local/bin/foo\";\n            var fs = new TestFileSystem\n            {\n                Files = new Dictionary<string, byte[]>\n                {\n                    [expectedPath] = Array.Empty<byte>(),\n                }\n            };\n            var envars = new Dictionary<string, string> {[\"PATH\"] = PosixPathVar};\n            var env = new PosixEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);\n\n            Assert.True(actualResult);\n            Assert.Equal(expectedPath, actualPath);\n        }\n\n        [PosixFact]\n        public void PosixEnvironment_TryLocateExecutable_ExistsMultiple_ReturnTrueAndFirstPath()\n        {\n            string expectedPath = \"/home/john.doe/bin/foo\";\n            var fs = new TestFileSystem\n            {\n                Files = new Dictionary<string, byte[]>\n                {\n                    [expectedPath] = Array.Empty<byte>(),\n                    [\"/usr/local/bin/foo\"] = Array.Empty<byte>(),\n                    [\"/bin/foo\"] = Array.Empty<byte>(),\n                }\n            };\n            var envars = new Dictionary<string, string> {[\"PATH\"] = PosixPathVar};\n            var env = new PosixEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);\n\n            Assert.True(actualResult);\n            Assert.Equal(expectedPath, actualPath);\n        }\n\n        [MacOSFact]\n        public void MacOSEnvironment_TryLocateExecutable_Paths_Are_Ignored()\n        {\n            List<string> pathsToIgnore = new List<string>()\n            {\n                \"/home/john.doe/bin/foo\"\n            };\n            string expectedPath = \"/usr/local/bin/foo\";\n\n            var fs = new TestFileSystem\n            {\n                Files = new Dictionary<string, byte[]>\n                {\n                    [pathsToIgnore.FirstOrDefault()] = Array.Empty<byte>(),\n                    [expectedPath] = Array.Empty<byte>(),\n                }\n            };\n            var envars = new Dictionary<string, string> {[\"PATH\"] = PosixPathVar};\n            var env = new PosixEnvironment(fs, envars);\n\n            bool actualResult = env.TryLocateExecutable(PosixExecName, pathsToIgnore, out string actualPath);\n\n            Assert.True(actualResult);\n            Assert.Equal(expectedPath, actualPath);\n        }\n\n        [PosixFact]\n        public void PosixEnvironment_SetEnvironmentVariable_Sets_Expected_Value()\n        {\n            var variable = \"FOO_BAR\";\n            var value = \"baz\";\n                \n            var fs = new TestFileSystem();\n            var envars = new Dictionary<string, string>();\n            var env = new PosixEnvironment(fs, envars);\n\n            env.SetEnvironmentVariable(variable, value);\n\n            Assert.Contains(env.Variables, item \n                => item.Key.Equals(variable) && item.Value.Equals(value));\n        }\n\n        [WindowsFact]\n        public void WindowsEnvironment_SetEnvironmentVariable_Sets_Expected_Value()\n        {\n            var variable = \"FOO_BAR\";\n            var value = \"baz\";\n                \n            var fs = new TestFileSystem();\n            var envars = new Dictionary<string, string>();\n            var env = new WindowsEnvironment(fs, envars);\n\n            env.SetEnvironmentVariable(variable, value);\n\n            Assert.Contains(env.Variables, item \n                => item.Key.Equals(variable) && item.Value.Equals(value));\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GenericHostProviderTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class GenericHostProviderTests\n    {\n        [Theory]\n        [InlineData(\"http\", true)]\n        [InlineData(\"HTTP\", true)]\n        [InlineData(\"hTtP\", true)]\n        [InlineData(\"https\", true)]\n        [InlineData(\"HTTPS\", true)]\n        [InlineData(\"hTtPs\", true)]\n        [InlineData(\"ssh\", true)]\n        [InlineData(\"SSH\", true)]\n        [InlineData(\"sSh\", true)]\n        [InlineData(\"smpt\", true)]\n        [InlineData(\"SmtP\", true)]\n        [InlineData(\"SMTP\", true)]\n        [InlineData(\"imap\", true)]\n        [InlineData(\"iMAp\", true)]\n        [InlineData(\"IMAP\", true)]\n        [InlineData(\"file\", true)]\n        [InlineData(\"fIlE\", true)]\n        [InlineData(\"FILE\", true)]\n        [InlineData(\"\", false)]\n        [InlineData(null, false)]\n        public void GenericHostProvider_IsSupported(string protocol, bool expected)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"]     = \"example.com\",\n                [\"path\"]     = \"foo/bar\",\n            });\n\n            var provider = new GenericHostProvider(new TestCommandContext());\n\n            Assert.Equal(expected, provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void GenericHostProvider_GetCredentialServiceUrl_ReturnsCorrectKey()\n        {\n            const string expectedService = \"https://example.com/foo/bar\";\n\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [\"path\"]     = \"foo/bar\",\n                [\"username\"] = \"john.doe\",\n            });\n\n            var provider = new GenericHostProvider(new TestCommandContext());\n\n            string actualService = provider.GetServiceName(input);\n\n            Assert.Equal(expectedService, actualService);\n        }\n\n        [Fact]\n        public async Task GenericHostProvider_CreateCredentialAsync_WiaNotAllowed_ReturnsBasicCredentialNoWiaCheck()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n            });\n\n            const string testUserName = \"basicUser\";\n            const string testPassword = \"basicPass\";\n            var basicCredential = new GitCredential(testUserName, testPassword);\n\n            var context = new TestCommandContext\n            {\n                Settings = {IsWindowsIntegratedAuthenticationEnabled = false}\n            };\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .ReturnsAsync(basicCredential)\n                .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n            wiaAuthMock.Verify(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()), Times.Never);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n        }\n\n        [Fact]\n        public async Task GenericHostProvider_CreateCredentialAsync_LegacyAuthorityBasic_ReturnsBasicCredentialNoWiaCheck()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n            });\n\n            const string testUserName = \"basicUser\";\n            const string testPassword = \"basicPass\";\n            var basicCredential = new GitCredential(testUserName, testPassword);\n\n            var context = new TestCommandContext\n            {\n                Settings = {LegacyAuthorityOverride = \"basic\"}\n            };\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .ReturnsAsync(basicCredential)\n                .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n            wiaAuthMock.Verify(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()), Times.Never);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n        }\n\n        [Fact]\n        public async Task GenericHostProvider_CreateCredentialAsync_NonHttpProtocol_ReturnsBasicCredentialNoWiaCheck()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"smtp\",\n                [\"host\"]     = \"example.com\",\n            });\n\n            const string testUserName = \"basicUser\";\n            const string testPassword = \"basicPass\";\n            var basicCredential = new GitCredential(testUserName, testPassword);\n\n            var context = new TestCommandContext();\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .ReturnsAsync(basicCredential)\n                .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n            wiaAuthMock.Verify(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()), Times.Never);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n        }\n\n        [PosixFact]\n        public async Task GenericHostProvider_CreateCredentialAsync_NonWindows_WiaSupported_ReturnsBasicCredential()\n        {\n            await TestCreateCredentialAsync_ReturnsBasicCredential(WindowsAuthenticationTypes.All);\n        }\n\n        [WindowsFact]\n        public async Task GenericHostProvider_CreateCredentialAsync_Windows_WiaSupported_ReturnsEmptyCredential()\n        {\n            await TestCreateCredentialAsync_ReturnsEmptyCredential(WindowsAuthenticationTypes.All);\n        }\n\n        [Fact]\n        public async Task GenericHostProvider_CreateCredentialAsync_WiaNotSupported_ReturnsBasicCredential()\n        {\n            await TestCreateCredentialAsync_ReturnsBasicCredential(WindowsAuthenticationTypes.None);\n        }\n\n        [WindowsFact]\n        private static async Task GenericHostProvider_NtlmSuppressed_AllowOnce()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [Constants.CredentialProtocol.NtlmKey] = Constants.CredentialProtocol.NtlmSuppressed,\n            });\n\n            var configKey =\n                $\"{Constants.GitConfiguration.Http.SectionName}.https://example.com.{Constants.GitConfiguration.Http.AllowNtlmAuth}\";\n\n            var context = new TestCommandContext();\n            context.Git.Configuration.Global.Clear();\n\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            wiaAuthMock.Setup(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()))\n                .ReturnsAsync(WindowsAuthenticationTypes.Ntlm);\n            wiaAuthMock.Setup(x => x.AskEnableNtlmAsync(It.IsAny<Uri>()))\n                .ReturnsAsync(NtlmSupport.Once);\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(string.Empty, credential.Account);\n            Assert.Equal(string.Empty, credential.Password);\n            Assert.True(result.AdditionalProperties.TryGetValue(Constants.CredentialProtocol.NtlmKey, out string ntlmValue));\n            Assert.Equal(Constants.CredentialProtocol.NtlmAllow, ntlmValue);\n\n            wiaAuthMock.Verify(x => x.AskEnableNtlmAsync(It.IsAny<Uri>()), Times.Once);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);\n\n            Assert.False(context.Git.Configuration.Global.TryGetValue(configKey, out _));\n        }\n\n        [WindowsFact]\n        private static async Task GenericHostProvider_NtlmSuppressed_AllowAlways()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [Constants.CredentialProtocol.NtlmKey] = Constants.CredentialProtocol.NtlmSuppressed,\n            });\n\n            var configKey =\n                $\"{Constants.GitConfiguration.Http.SectionName}.https://example.com.{Constants.GitConfiguration.Http.AllowNtlmAuth}\";\n\n            var context = new TestCommandContext();\n            context.Git.Configuration.Global.Clear();\n\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            wiaAuthMock.Setup(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()))\n                .ReturnsAsync(WindowsAuthenticationTypes.Ntlm);\n            wiaAuthMock.Setup(x => x.AskEnableNtlmAsync(It.IsAny<Uri>()))\n                .ReturnsAsync(NtlmSupport.Always);\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(string.Empty, credential.Account);\n            Assert.Equal(string.Empty, credential.Password);\n            Assert.True(result.AdditionalProperties.TryGetValue(Constants.CredentialProtocol.NtlmKey, out string ntlmValue));\n            Assert.Equal(Constants.CredentialProtocol.NtlmAllow, ntlmValue);\n\n            wiaAuthMock.Verify(x => x.AskEnableNtlmAsync(It.IsAny<Uri>()), Times.Once);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);\n\n            Assert.True(context.Git.Configuration.Global.TryGetValue(configKey, out IList<string> configValues));\n            string configValue = Assert.Single(configValues);\n            Assert.True(configValue.IsTruthy());\n        }\n\n        [WindowsFact]\n        private static async Task GenericHostProvider_NtlmSuppressed_Disabled()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [Constants.CredentialProtocol.NtlmKey] = Constants.CredentialProtocol.NtlmSuppressed,\n            });\n\n            var configKey =\n                $\"{Constants.GitConfiguration.Http.SectionName}.https://example.com.{Constants.GitConfiguration.Http.AllowNtlmAuth}\";\n\n            var context = new TestCommandContext();\n            context.Git.Configuration.Global.Clear();\n\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .ReturnsAsync(new GitCredential(\"testUser\", \"testPassword\"));\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            wiaAuthMock.Setup(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()))\n                .ReturnsAsync(WindowsAuthenticationTypes.Ntlm);\n            wiaAuthMock.Setup(x => x.AskEnableNtlmAsync(It.IsAny<Uri>()))\n                .ReturnsAsync(NtlmSupport.Disabled);\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(\"testUser\", credential.Account);\n            Assert.Equal(\"testPassword\", credential.Password);\n            Assert.False(result.AdditionalProperties.TryGetValue(Constants.CredentialProtocol.NtlmKey, out _));\n\n            wiaAuthMock.Verify(x => x.AskEnableNtlmAsync(It.IsAny<Uri>()), Times.Once);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n\n            Assert.False(context.Git.Configuration.Global.TryGetValue(configKey, out _));\n        }\n\n        [Fact]\n        public async Task GenericHostProvider_GenerateCredentialAsync_OAuth_CompleteOAuthConfig_UsesOAuth()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"git.example.com\",\n                [\"path\"]     = \"foo\"\n            });\n\n            const string testUserName = \"TEST_OAUTH_USER\";\n            const string testAcessToken = \"OAUTH_TOKEN\";\n            const string testRefreshToken = \"OAUTH_REFRESH_TOKEN\";\n            const string testResource = \"https://git.example.com/foo\";\n            const string expectedRefreshTokenService = \"https://refresh_token.git.example.com/foo\";\n\n            var authMode = OAuthAuthenticationModes.Browser;\n            string[] scopes = { \"code:write\", \"code:read\" };\n            string clientId = \"3eadfc62-9e91-45d3-8c60-20ccd6d0c7cf\";\n            string clientSecret = \"C1DA8B93CCB5F5B93DA\";\n            string redirectUri = \"http://localhost\";\n            string authzEndpoint = \"/oauth/authorize\";\n            string tokenEndpoint = \"/oauth/token\";\n            string deviceEndpoint = \"/oauth/device\";\n\n            string GetKey(string name) => $\"{Constants.GitConfiguration.Credential.SectionName}.https://example.com.{name}\";\n\n            var context = new TestCommandContext\n            {\n                Git =\n                {\n                    Configuration =\n                    {\n                        Global =\n                        {\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthClientId)] = new[] { clientId },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthClientSecret)] = new[] { clientSecret },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthRedirectUri)] = new[] { redirectUri },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthScopes)] = new[] { string.Join(' ', scopes) },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthAuthzEndpoint)] = new[] { authzEndpoint },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthTokenEndpoint)] = new[] { tokenEndpoint },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthDeviceEndpoint)] = new[] { deviceEndpoint },\n                            [GetKey(Constants.GitConfiguration.Credential.OAuthDefaultUserName)] = new[] { testUserName },\n                        }\n                    }\n                },\n                Settings =\n                {\n                    RemoteUri = new Uri(testResource)\n                }\n            };\n\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            var oauthMock = new Mock<IOAuthAuthentication>();\n            oauthMock.Setup(x =>\n                x.GetAuthenticationModeAsync(It.IsAny<string>(), It.IsAny<OAuthAuthenticationModes>()))\n                .ReturnsAsync(authMode);\n            oauthMock.Setup(x => x.GetTokenByBrowserAsync(It.IsAny<OAuth2Client>(), It.IsAny<string[]>()))\n                .ReturnsAsync(new OAuth2TokenResult(testAcessToken, \"access_token\")\n                {\n                    Scopes = scopes,\n                    RefreshToken = testRefreshToken\n                });\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testAcessToken, credential.Password);\n\n            Assert.True(context.CredentialStore.TryGet(expectedRefreshTokenService, null, out TestCredential refreshToken));\n            Assert.Equal(testUserName, refreshToken.Account);\n            Assert.Equal(testRefreshToken, refreshToken.Password);\n\n            oauthMock.Verify(x => x.GetAuthenticationModeAsync(testResource, OAuthAuthenticationModes.All), Times.Once);\n            oauthMock.Verify(x => x.GetTokenByBrowserAsync(It.IsAny<OAuth2Client>(), scopes), Times.Once);\n            oauthMock.Verify(x => x.GetTokenByDeviceCodeAsync(It.IsAny<OAuth2Client>(), scopes), Times.Never);\n            wiaAuthMock.Verify(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()), Times.Never);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);\n        }\n\n        #region Helpers\n\n        private static async Task TestCreateCredentialAsync_ReturnsEmptyCredential(WindowsAuthenticationTypes supportedWiaTypes)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n            });\n\n            var context = new TestCommandContext();\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                         .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            wiaAuthMock.Setup(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()))\n                       .ReturnsAsync(supportedWiaTypes);\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(string.Empty, credential.Account);\n            Assert.Equal(string.Empty, credential.Password);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);\n        }\n\n        private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(WindowsAuthenticationTypes supportedWiaTypes)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n            });\n\n            const string testUserName = \"basicUser\";\n            const string testPassword = \"basicPass\";\n            var basicCredential = new GitCredential(testUserName, testPassword);\n\n            var context = new TestCommandContext();\n            var basicAuthMock = new Mock<IBasicAuthentication>();\n            basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))\n                         .ReturnsAsync(basicCredential)\n                         .Verifiable();\n            var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();\n            wiaAuthMock.Setup(x => x.GetAuthenticationTypesAsync(It.IsAny<Uri>()))\n                       .ReturnsAsync(supportedWiaTypes);\n            var oauthMock = new Mock<IOAuthAuthentication>();\n\n            var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object);\n\n            var result = await provider.GenerateCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(testUserName, credential.Account);\n            Assert.Equal(testPassword, credential.Password);\n            basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GenericOAuthConfigTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class GenericOAuthConfigTests\n    {\n        [Fact]\n        public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue()\n        {\n            var protocol = \"https\";\n            var host = \"example.com\";\n            var remoteUri = new Uri($\"{protocol}://{host}\");\n            const string expectedClientId = \"115845b0-77f8-4c06-a3dc-7d277381fad1\";\n            const string expectedClientSecret = \"4D35385D9F24\";\n            const string expectedUserName = \"TEST_USER\";\n            const string authzEndpoint = \"/oauth/authorize\";\n            const string tokenEndpoint = \"/oauth/token\";\n            const string deviceEndpoint = \"/oauth/device\";\n            string[] expectedScopes = { \"scope1\", \"scope2\" };\n            var expectedRedirectUri = new Uri(\"http://localhost:12345\");\n            var expectedAuthzEndpoint = new Uri(remoteUri, authzEndpoint);\n            var expectedTokenEndpoint = new Uri(remoteUri, tokenEndpoint);\n            var expectedDeviceEndpoint = new Uri(remoteUri, deviceEndpoint);\n\n            string GetKey(string name) => $\"{Constants.GitConfiguration.Credential.SectionName}.https://example.com.{name}\";\n\n            var trace = new NullTrace();\n            var settings = new TestSettings\n            {\n                GitConfiguration = new TestGitConfiguration\n                {\n                    Global =\n                    {\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthClientId)] = new[] { expectedClientId },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthClientSecret)] = new[] { expectedClientSecret },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthRedirectUri)] = new[] { expectedRedirectUri.ToString() },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthScopes)] = new[] { string.Join(' ', expectedScopes) },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthAuthzEndpoint)] = new[] { authzEndpoint },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthTokenEndpoint)] = new[] { tokenEndpoint },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthDeviceEndpoint)] = new[] { deviceEndpoint },\n                        [GetKey(Constants.GitConfiguration.Credential.OAuthDefaultUserName)] = new[] { expectedUserName },\n                    }\n                },\n                RemoteUri = remoteUri\n            };\n\n            var input = new InputArguments(new Dictionary<string, string> {\n                {\"protocol\", protocol},\n                {\"host\", host}, \n            });\n\n            bool result = GenericOAuthConfig.TryGet(trace, settings, input, out GenericOAuthConfig config);\n\n            Assert.True(result);\n            Assert.Equal(expectedClientId, config.ClientId);\n            Assert.Equal(expectedClientSecret, config.ClientSecret);\n            Assert.Equal(expectedRedirectUri, config.RedirectUri);\n            Assert.Equal(expectedScopes, config.Scopes);\n            Assert.Equal(expectedAuthzEndpoint, config.Endpoints.AuthorizationEndpoint);\n            Assert.Equal(expectedTokenEndpoint, config.Endpoints.TokenEndpoint);\n            Assert.Equal(expectedDeviceEndpoint, config.Endpoints.DeviceAuthorizationEndpoint);\n            Assert.Equal(expectedUserName, config.DefaultUserName);\n            Assert.True(config.UseAuthHeader);\n        }\n\n        [Fact]\n        public void GenericOAuthConfig_TryGet_Gitea()\n        {\n            var protocol = \"https\";\n            var host = \"example.com\";\n            var remoteUri = new Uri($\"{protocol}://{host}\");\n            const string expectedClientId = GenericOAuthConfig.WellKnown.GiteaClientId;\n            string[] expectedScopes = Array.Empty<string>();\n            var expectedRedirectUri = GenericOAuthConfig.WellKnown.LocalIPv4RedirectUri;\n            var expectedAuthzEndpoint = new Uri(remoteUri, GenericOAuthConfig.WellKnown.GiteaAuthzEndpoint);\n            var expectedTokenEndpoint = new Uri(remoteUri, GenericOAuthConfig.WellKnown.GiteaTokenEndpoint);\n\n            var trace = new NullTrace();\n            var settings = new TestSettings\n            {\n                RemoteUri = remoteUri\n            };\n\n            var input = new InputArguments(new Dictionary<string, string> {\n                {\"protocol\", protocol},\n                {\"host\", host},\n                {\"wwwauth\", \"Basic realm=\\\"Gitea\\\"\"}\n            });\n\n            bool result = GenericOAuthConfig.TryGet(trace, settings, input, out GenericOAuthConfig config);\n\n            Assert.True(result);\n            Assert.Equal(expectedClientId, config.ClientId);\n            Assert.Equal(expectedRedirectUri, config.RedirectUri);\n            Assert.Equal(expectedScopes, config.Scopes);\n            Assert.Equal(expectedAuthzEndpoint, config.Endpoints.AuthorizationEndpoint);\n            Assert.Equal(expectedTokenEndpoint, config.Endpoints.TokenEndpoint);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GitConfigurationKeyComparerTests.cs",
    "content": "using Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class GitConfigurationKeyComparerTests\n    {\n        [Theory]\n        [InlineData(\"\", \"\", true)]\n        [InlineData(null, null, true)]\n        [InlineData(\"   \", \" \", true)]\n        [InlineData(\"foo\", \"foo\", true)]\n        [InlineData(\"foo\", \"FOO\", true)]\n        [InlineData(\"foo\", \"bar\", false)]\n        [InlineData(\"foo.bar\", \"foo.bar\", true)]\n        [InlineData(\"foo.bar\", \"foo.fish\", false)]\n        [InlineData(\"fish.bar\", \"foo.bar\", false)]\n        [InlineData(\"foo.bar\", \"FOO.BAR\", true)]\n        [InlineData(\"foo.bar\", \"foo.BAR\", true)]\n        [InlineData(\"foo.bar\", \"FOO.bar\", true)]\n        [InlineData(\"foo.example.com.bar\", \"foo.example.com.bar\", true)]\n        [InlineData(\"foo.example.com.bar\", \"foo.example.com.BAR\", true)]\n        [InlineData(\"foo.example.com.bar\", \"FOO.example.com.BAR\", true)]\n        [InlineData(\"foo.example.com.bar\", \"FOO.example.com.bar\", true)]\n        [InlineData(\"foo.example.com.bar\", \"foo.EXAMPLE.COM.bar\", false)]\n        public void GitConfigurationKeyComparer_Equals(string x, string y, bool expected)\n        {\n            bool actual = GitConfigurationKeyComparer.Instance.Equals(x, y);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"\", \"\", 0)]\n        [InlineData(null, null, 0)]\n        [InlineData(\"   \", \" \", 0)]\n        [InlineData(\"foo\", \"foo\", 0)]\n        [InlineData(\"foo\", \"FOO\", 0)]\n        [InlineData(\"foo\", \"bar\", 4)]\n        [InlineData(\"foo.bar\", \"foo.bar\", 0)]\n        [InlineData(\"foo.bar\", \"foo.fish\", -4)]\n        [InlineData(\"fish.bar\", \"foo.bar\", -6)]\n        [InlineData(\"foo.bar\", \"FOO.BAR\", 0)]\n        [InlineData(\"foo.bar\", \"foo.BAR\", 0)]\n        [InlineData(\"foo.bar\", \"FOO.bar\", 0)]\n        [InlineData(\"foo.example.com.bar\", \"foo.example.com.bar\", 0)]\n        [InlineData(\"foo.example.com.bar\", \"foo.example.com.BAR\", 0)]\n        [InlineData(\"foo.example.com.bar\", \"FOO.example.com.BAR\", 0)]\n        [InlineData(\"foo.example.com.bar\", \"FOO.example.com.bar\", 0)]\n        [InlineData(\"foo.example.com.bar\", \"foo.EXAMPLE.COM.bar\", 32)]\n        public void GitConfigurationKeyComparer_Compare(string x, string y, int expected)\n        {\n            int actual = GitConfigurationKeyComparer.Instance.Compare(x, y);\n            Assert.Equal(expected, actual);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GitConfigurationTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\nusing static GitCredentialManager.Tests.GitTestUtilities;\n\nnamespace GitCredentialManager.Tests\n{\n    public class GitConfigurationTests\n    {\n        [Theory]\n        [InlineData(null, \"\\\"\\\"\")]\n        [InlineData(\"\", \"\\\"\\\"\")]\n        [InlineData(\"hello\", \"hello\")]\n        [InlineData(\"hello world\", \"\\\"hello world\\\"\")]\n        [InlineData(\"C:\\\\app.exe\", \"C:\\\\app.exe\")]\n        [InlineData(\"C:\\\\path with space\\\\app.exe\", \"\\\"C:\\\\path with space\\\\app.exe\\\"\")]\n        [InlineData(\"''\", \"\\\"''\\\"\")]\n        [InlineData(\"'hello'\", \"\\\"'hello'\\\"\")]\n        [InlineData(\"'hello world'\", \"\\\"'hello world'\\\"\")]\n        [InlineData(\"'C:\\\\app.exe'\", \"\\\"'C:\\\\app.exe'\\\"\")]\n        [InlineData(\"'C:\\\\path with space\\\\app.exe'\", \"\\\"'C:\\\\path with space\\\\app.exe'\\\"\")]\n        [InlineData(\"\\\"\\\"\", \"\\\"\\\\\\\"\\\\\\\"\\\"\")]\n        [InlineData(\"\\\"hello\\\"\", \"\\\"\\\\\\\"hello\\\\\\\"\\\"\")]\n        [InlineData(\"\\\"hello world\\\"\", \"\\\"\\\\\\\"hello world\\\\\\\"\\\"\")]\n        [InlineData(\"\\\"C:\\\\app.exe\\\"\", \"\\\"\\\\\\\"C:\\\\app.exe\\\\\\\"\\\"\")]\n        [InlineData(\"\\\"C:\\\\path with space\\\\app.exe\\\"\", \"\\\"\\\\\\\"C:\\\\path with space\\\\app.exe\\\\\\\"\\\"\")]\n        [InlineData(\"\\\\\", \"\\\\\")]\n        [InlineData(\"\\\\\\\\\", \"\\\\\\\\\")]\n        [InlineData(\"\\\\\\\\\\\\\", \"\\\\\\\\\\\\\")]\n        [InlineData(\"\\\"\", \"\\\"\\\\\\\"\\\"\")]\n        [InlineData(\"\\\\\\\"\", \"\\\"\\\\\\\\\\\\\\\"\\\"\")]\n        [InlineData(\"\\\\\\\\\\\"\", \"\\\"\\\\\\\\\\\\\\\\\\\\\\\"\\\"\")]\n        [InlineData(\"\\\"\\\\\", \"\\\"\\\\\\\"\\\\\\\\\\\"\")]\n        [InlineData(\"\\\"\\\\\\\\\", \"\\\"\\\\\\\"\\\\\\\\\\\\\\\\\\\"\")]\n        [InlineData(\"ab\\\\\", \"ab\\\\\")]\n        [InlineData(\"a b\\\\\", \"\\\"a b\\\\\\\\\\\"\")]\n        public void GitConfiguration_QuoteCmdArg(string input, string expected)\n        {\n            string actual = GitProcessConfiguration.QuoteCmdArg(input);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void GitProcess_GetConfiguration_ReturnsConfiguration()\n        {\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath);\n            var config = git.GetConfiguration();\n            Assert.NotNull(config);\n        }\n\n        [Fact]\n        public void GitConfiguration_Enumerate_CallbackReturnsTrue_InvokesCallbackForEachEntry()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local foo.name lancelot\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local foo.quest seek-holy-grail\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local foo.favcolor blue\").AssertSuccess();\n\n            var expectedVisitedEntries = new List<(string name, string value)>\n            {\n                (\"foo.name\", \"lancelot\"),\n                (\"foo.quest\", \"seek-holy-grail\"),\n                (\"foo.favcolor\", \"blue\")\n            };\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            var actualVisitedEntries = new List<(string name, string value)>();\n\n            bool cb(GitConfigurationEntry entry)\n            {\n                if (entry.Key.StartsWith(\"foo.\"))\n                {\n                    actualVisitedEntries.Add((entry.Key, entry.Value));\n                }\n\n                // Continue enumeration\n                return true;\n            }\n\n            config.Enumerate(cb);\n\n            Assert.Equal(expectedVisitedEntries, actualVisitedEntries);\n        }\n\n        [Fact]\n        public void GitConfiguration_Enumerate_CallbackReturnsFalse_InvokesCallbackForEachEntryUntilReturnsFalse()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local foo.name lancelot\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local foo.quest seek-holy-grail\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local foo.favcolor blue\").AssertSuccess();\n\n            var expectedVisitedEntries = new List<(string name, string value)>\n            {\n                (\"foo.name\", \"lancelot\"),\n                (\"foo.quest\", \"seek-holy-grail\")\n            };\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            var actualVisitedEntries = new List<(string name, string value)>();\n\n            bool cb(GitConfigurationEntry entry)\n            {\n                if (entry.Key.StartsWith(\"foo.\"))\n                {\n                    actualVisitedEntries.Add((entry.Key, entry.Value));\n                }\n\n                // Stop enumeration after 2 'foo' entries\n                return actualVisitedEntries.Count < 2;\n            }\n\n            config.Enumerate(cb);\n\n            Assert.Equal(expectedVisitedEntries, actualVisitedEntries);\n        }\n\n        [Fact]\n        public void GitConfiguration_TryGet_Name_Exists_ReturnsTrueOutString()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local user.name john.doe\").AssertSuccess();\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            bool result = config.TryGet(\"user.name\", false, out string value);\n            Assert.True(result);\n            Assert.NotNull(value);\n            Assert.Equal(\"john.doe\", value);\n        }\n\n        [Fact]\n        public void GitConfiguration_TryGet_Name_DoesNotExists_ReturnsFalse()\n        {\n            string repoPath = CreateRepository();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            string randomName = $\"{Guid.NewGuid():N}.{Guid.NewGuid():N}\";\n            bool result = config.TryGet(randomName, false, out string value);\n            Assert.False(result);\n            Assert.Null(value);\n        }\n\n        [Fact]\n        public void GitConfiguration_TryGet_IsPath_True_ReturnsCanonicalPath()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local example.path ~/test\").AssertSuccess();\n\n            string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            bool result = config.TryGet(\"example.path\", true, out string value);\n            Assert.True(result);\n            Assert.NotNull(value);\n            Assert.Equal(Path.GetFullPath($\"{homeDirectory}/test\").Replace(\"\\\\\", \"/\"), value);\n        }\n\n        [Fact]\n        public void GitConfiguration_TryGet_IsPath_False_ReturnsRawConfig()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local example.path ~/test\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            bool result = config.TryGet(\"example.path\", false, out string value);\n            Assert.True(result);\n            Assert.NotNull(value);\n            Assert.Equal($\"~/test\", value);\n        }\n\n        [Fact]\n        public void GitConfiguration_TryGet_BoolType_ReturnsCanonicalBool()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local example.bool fAlSe\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Bool,\n                \"example.bool\", out string value);\n            Assert.True(result);\n            Assert.NotNull(value);\n            Assert.Equal(\"false\", value);\n        }\n\n        [Fact]\n        public void GitConfiguration_TryGet_BoolWithoutType_ReturnsRawConfig()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local example.bool fAlSe\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Raw,\n                \"example.bool\", out string value);\n            Assert.True(result);\n            Assert.NotNull(value);\n            Assert.Equal(\"fAlSe\", value);\n        }\n\n        [Fact]\n        public void GitConfiguration_Get_Name_Exists_ReturnsString()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local user.name john.doe\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            string value = config.Get(\"user.name\");\n            Assert.NotNull(value);\n            Assert.Equal(\"john.doe\", value);\n        }\n\n        [Fact]\n        public void GitConfiguration_Get_Name_DoesNotExists_ThrowsException()\n        {\n            string repoPath = CreateRepository();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            string randomName = $\"{Guid.NewGuid():N}.{Guid.NewGuid():N}\";\n            Assert.Throws<KeyNotFoundException>(() => config.Get(randomName));\n        }\n\n        [Fact]\n        public void GitConfiguration_Set_Local_SetsLocalConfig()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);;\n            IGitConfiguration config = git.GetConfiguration();\n\n            config.Set(GitConfigurationLevel.Local, \"core.foobar\", \"foo123\");\n\n            GitResult localResult = ExecGit(repoPath, workDirPath, \"config --local core.foobar\");\n\n            Assert.Equal(\"foo123\", localResult.StandardOutput.Trim());\n        }\n\n        [Fact]\n        public void GitConfiguration_Set_All_ThrowsException()\n        {\n            string repoPath = CreateRepository(out _);\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            Assert.Throws<InvalidOperationException>(() =>\n                config.Set(GitConfigurationLevel.All, \"core.foobar\", \"test123\"));\n        }\n\n        [Fact]\n        public void GitConfiguration_Unset_Global_UnsetsGlobalConfig()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            try\n            {\n                ExecGit(repoPath, workDirPath, \"config --global core.foobar alice\").AssertSuccess();\n                ExecGit(repoPath, workDirPath, \"config --local core.foobar bob\").AssertSuccess();\n\n                string gitPath = GetGitPath();\n                var trace = new NullTrace();\n                var trace2 = new NullTrace2();\n                var processManager = new TestProcessManager();\n                var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n                IGitConfiguration config = git.GetConfiguration();\n\n                config.Unset(GitConfigurationLevel.Global, \"core.foobar\");\n\n                GitResult globalResult = ExecGit(repoPath, workDirPath, \"config --global core.foobar\");\n                GitResult localResult = ExecGit(repoPath, workDirPath, \"config --local core.foobar\");\n\n                Assert.Equal(string.Empty, globalResult.StandardOutput.Trim());\n                Assert.Equal(\"bob\", localResult.StandardOutput.Trim());\n            }\n            finally\n            {\n                // Cleanup global config changes\n                ExecGit(repoPath, workDirPath, \"config --global --unset core.foobar\");\n            }\n        }\n\n        [Fact]\n        public void GitConfiguration_Unset_Local_UnsetsLocalConfig()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n\n            try\n            {\n                ExecGit(repoPath, workDirPath, \"config --global core.foobar alice\").AssertSuccess();\n                ExecGit(repoPath, workDirPath, \"config --local core.foobar bob\").AssertSuccess();\n\n                string gitPath = GetGitPath();\n                var trace = new NullTrace();\n                var trace2 = new NullTrace2();\n                var processManager = new TestProcessManager();\n                var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n                IGitConfiguration config = git.GetConfiguration();\n\n                config.Unset(GitConfigurationLevel.Local, \"core.foobar\");\n\n                GitResult globalResult = ExecGit(repoPath, workDirPath, \"config --global core.foobar\");\n                GitResult localResult = ExecGit(repoPath, workDirPath, \"config --local core.foobar\");\n\n                Assert.Equal(\"alice\", globalResult.StandardOutput.Trim());\n                Assert.Equal(string.Empty, localResult.StandardOutput.Trim());\n            }\n            finally\n            {\n                // Cleanup global config changes\n                ExecGit(repoPath, workDirPath, \"config --global --unset core.foobar\");\n            }\n        }\n\n        [Fact]\n        public void GitConfiguration_Unset_All_ThrowsException()\n        {\n            string repoPath = CreateRepository(out _);\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            Assert.Throws<InvalidOperationException>(() => config.Unset(GitConfigurationLevel.All, \"core.foobar\"));\n        }\n\n        [Fact]\n        public void GitConfiguration_UnsetAll_UnsetsAllConfig()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local --add core.foobar foo1\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local --add core.foobar foo2\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local --add core.foobar bar1\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            config.UnsetAll(GitConfigurationLevel.Local, \"core.foobar\", \"foo*\");\n\n            GitResult result = ExecGit(repoPath, workDirPath, \"config --local --get-all core.foobar\");\n\n            Assert.Equal(\"bar1\", result.StandardOutput.Trim());\n        }\n\n        [Fact]\n        public void GitConfiguration_UnsetAll_All_ThrowsException()\n        {\n            string repoPath = CreateRepository(out _);\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            Assert.Throws<InvalidOperationException>(() =>\n                config.UnsetAll(GitConfigurationLevel.All, \"core.foobar\", Constants.RegexPatterns.Any));\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheTryGet_ReturnsValueFromCache()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local user.name john.doe\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local user.email john@example.com\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            // First access loads cache\n            bool result1 = config.TryGet(\"user.name\", false, out string value1);\n            Assert.True(result1);\n            Assert.Equal(\"john.doe\", value1);\n\n            // Second access should use cache\n            bool result2 = config.TryGet(\"user.email\", false, out string value2);\n            Assert.True(result2);\n            Assert.Equal(\"john@example.com\", value2);\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheGetAll_ReturnsAllValuesFromCache()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local --add test.multi value1\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local --add test.multi value2\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local --add test.multi value3\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            var values = new List<string>(config.GetAll(\"test.multi\"));\n\n            Assert.Equal(3, values.Count);\n            Assert.Equal(\"value1\", values[0]);\n            Assert.Equal(\"value2\", values[1]);\n            Assert.Equal(\"value3\", values[2]);\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheEnumerate_EnumeratesFromCache()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local cache.name test-value\").AssertSuccess();\n            ExecGit(repoPath, workDirPath, \"config --local cache.enabled true\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            var cacheEntries = new List<(string key, string value)>();\n            config.Enumerate(entry =>\n            {\n                if (entry.Key.StartsWith(\"cache.\"))\n                {\n                    cacheEntries.Add((entry.Key, entry.Value));\n                }\n                return true;\n            });\n\n            Assert.Equal(2, cacheEntries.Count);\n            Assert.Contains((\"cache.name\", \"test-value\"), cacheEntries);\n            Assert.Contains((\"cache.enabled\", \"true\"), cacheEntries);\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheInvalidation_SetInvalidatesCache()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local test.value initial\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            // Load cache with initial value\n            bool result1 = config.TryGet(\"test.value\", false, out string value1);\n            Assert.True(result1);\n            Assert.Equal(\"initial\", value1);\n\n            // Set new value (should invalidate cache)\n            config.Set(GitConfigurationLevel.Local, \"test.value\", \"updated\");\n\n            // Next read should get updated value\n            bool result2 = config.TryGet(\"test.value\", false, out string value2);\n            Assert.True(result2);\n            Assert.Equal(\"updated\", value2);\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheInvalidation_AddInvalidatesCache()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local test.multi first\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            // Load cache\n            var values1 = new List<string>(config.GetAll(\"test.multi\"));\n            Assert.Single(values1);\n            Assert.Equal(\"first\", values1[0]);\n\n            // Add new value (should invalidate cache)\n            config.Add(GitConfigurationLevel.Local, \"test.multi\", \"second\");\n\n            // Next read should include new value\n            var values2 = new List<string>(config.GetAll(\"test.multi\"));\n            Assert.Equal(2, values2.Count);\n            Assert.Equal(\"first\", values2[0]);\n            Assert.Equal(\"second\", values2[1]);\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheInvalidation_UnsetInvalidatesCache()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local test.value exists\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            // Load cache\n            bool result1 = config.TryGet(\"test.value\", false, out string value1);\n            Assert.True(result1);\n            Assert.Equal(\"exists\", value1);\n\n            // Unset value (should invalidate cache)\n            config.Unset(GitConfigurationLevel.Local, \"test.value\");\n\n            // Next read should not find value\n            bool result2 = config.TryGet(\"test.value\", false, out string value2);\n            Assert.False(result2);\n            Assert.Null(value2);\n        }\n\n        [Fact]\n        public void GitConfiguration_CacheLevelFilter_ReturnsOnlyLocalValues()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n\n            try\n            {\n                ExecGit(repoPath, workDirPath, \"config --global test.level global-value\").AssertSuccess();\n                ExecGit(repoPath, workDirPath, \"config --local test.level local-value\").AssertSuccess();\n\n                string gitPath = GetGitPath();\n                var trace = new NullTrace();\n                var trace2 = new NullTrace2();\n                var processManager = new TestProcessManager();\n\n                var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n                IGitConfiguration config = git.GetConfiguration();\n\n                // Get local value only\n                bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Raw,\n                    \"test.level\", out string value);\n                Assert.True(result);\n                Assert.Equal(\"local-value\", value);\n            }\n            finally\n            {\n                // Cleanup global config\n                ExecGit(repoPath, workDirPath, \"config --global --unset test.level\");\n            }\n        }\n\n        [Fact]\n        public void GitConfiguration_TypedQuery_CanonicalizesValues()\n        {\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, \"config --local test.path ~/example\").AssertSuccess();\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);\n            IGitConfiguration config = git.GetConfiguration();\n\n            // Path type queries use a separate cache loaded with --type=path,\n            // so Git canonicalizes the values during cache load.\n            bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Path,\n                \"test.path\", out string value);\n            Assert.True(result);\n            Assert.NotNull(value);\n            // Value should be canonicalized path, not raw \"~/example\"\n            Assert.NotEqual(\"~/example\", value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GitStreamReaderTests.cs",
    "content": "using System.IO;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests;\n\npublic class GitStreamReaderTests\n{\n    #region ReadLineAsync\n\n    [Fact]\n    public async Task GitStreamReader_ReadLineAsync_LF()\n    {\n        // hello\\n\n        // world\\n\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\nworld\\n\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = await reader.ReadLineAsync();\n        string actual2 = await reader.ReadLineAsync();\n        string actual3 = await reader.ReadLineAsync();\n\n        Assert.Equal(\"hello\", actual1);\n        Assert.Equal(\"world\", actual2);\n        Assert.Null(actual3);\n    }\n\n    [Fact]\n    public async Task GitStreamReader_ReadLineAsync_CR()\n    {\n        // hello\\rworld\\r\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\rworld\\r\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = await reader.ReadLineAsync();\n        string actual2 = await reader.ReadLineAsync();\n\n        Assert.Equal(\"hello\\rworld\\r\", actual1);\n        Assert.Null(actual2);\n    }\n\n    [Fact]\n    public async Task GitStreamReader_ReadLineAsync_CRLF()\n    {\n        // hello\\r\\n\n        // world\\r\\n\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\r\\nworld\\r\\n\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = await reader.ReadLineAsync();\n        string actual2 = await reader.ReadLineAsync();\n        string actual3 = await reader.ReadLineAsync();\n\n        Assert.Equal(\"hello\", actual1);\n        Assert.Equal(\"world\", actual2);\n        Assert.Null(actual3);\n    }\n\n    [Fact]\n    public async Task GitStreamReader_ReadLineAsync_Mixed()\n    {\n        // hello\\r\\n\n        // world\\rthis\\n\n        // is\\n\n        // a\\n\n        // \\rmixed\\rnewline\\r\\n\n        // \\n\n        // string\\n\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\r\\nworld\\rthis\\nis\\na\\n\\rmixed\\rnewline\\r\\n\\nstring\\n\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = await reader.ReadLineAsync();\n        string actual2 = await reader.ReadLineAsync();\n        string actual3 = await reader.ReadLineAsync();\n        string actual4 = await reader.ReadLineAsync();\n        string actual5 = await reader.ReadLineAsync();\n        string actual6 = await reader.ReadLineAsync();\n        string actual7 = await reader.ReadLineAsync();\n        string actual8 = await reader.ReadLineAsync();\n\n        Assert.Equal(\"hello\", actual1);\n        Assert.Equal(\"world\\rthis\", actual2);\n        Assert.Equal(\"is\", actual3);\n        Assert.Equal(\"a\", actual4);\n        Assert.Equal(\"\\rmixed\\rnewline\", actual5);\n        Assert.Equal(\"\", actual6);\n        Assert.Equal(\"string\", actual7);\n        Assert.Null(actual8);\n    }\n\n    #endregion\n\n    #region ReadLine\n\n    [Fact]\n    public void GitStreamReader_ReadLine_LF()\n    {\n        // hello\\n\n        // world\\n\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\nworld\\n\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = reader.ReadLine();\n        string actual2 = reader.ReadLine();\n        string actual3 = reader.ReadLine();\n\n        Assert.Equal(\"hello\", actual1);\n        Assert.Equal(\"world\", actual2);\n        Assert.Null(actual3);\n    }\n\n    [Fact]\n    public void GitStreamReader_ReadLine_CR()\n    {\n        // hello\\rworld\\r\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\rworld\\r\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = reader.ReadLine();\n        string actual2 = reader.ReadLine();\n\n        Assert.Equal(\"hello\\rworld\\r\", actual1);\n        Assert.Null(actual2);\n    }\n\n    [Fact]\n    public void GitStreamReader_ReadLine_CRLF()\n    {\n        // hello\\r\\n\n        // world\\r\\n\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\r\\nworld\\r\\n\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = reader.ReadLine();\n        string actual2 = reader.ReadLine();\n        string actual3 = reader.ReadLine();\n\n        Assert.Equal(\"hello\", actual1);\n        Assert.Equal(\"world\", actual2);\n        Assert.Null(actual3);\n    }\n\n    [Fact]\n    public void GitStreamReader_ReadLine_Mixed()\n    {\n        // hello\\r\\n\n        // world\\rthis\\n\n        // is\\n\n        // a\\n\n        // \\rmixed\\rnewline\\r\\n\n        // \\n\n        // string\\n\n\n        byte[] buffer = Encoding.UTF8.GetBytes(\"hello\\r\\nworld\\rthis\\nis\\na\\n\\rmixed\\rnewline\\r\\n\\nstring\\n\");\n        using var stream = new MemoryStream(buffer);\n        var reader = new GitStreamReader(stream, Encoding.UTF8);\n\n        string actual1 = reader.ReadLine();\n        string actual2 = reader.ReadLine();\n        string actual3 = reader.ReadLine();\n        string actual4 = reader.ReadLine();\n        string actual5 = reader.ReadLine();\n        string actual6 = reader.ReadLine();\n        string actual7 = reader.ReadLine();\n        string actual8 = reader.ReadLine();\n\n        Assert.Equal(\"hello\", actual1);\n        Assert.Equal(\"world\\rthis\", actual2);\n        Assert.Equal(\"is\", actual3);\n        Assert.Equal(\"a\", actual4);\n        Assert.Equal(\"\\rmixed\\rnewline\", actual5);\n        Assert.Equal(\"\", actual6);\n        Assert.Equal(\"string\", actual7);\n        Assert.Null(actual8);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GitTests.cs",
    "content": "using System.IO;\nusing System.Linq;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\nusing static GitCredentialManager.Tests.GitTestUtilities;\n\nnamespace GitCredentialManager.Tests\n{\n    public class GitTests\n    {\n        [Fact]\n        public void Git_GetCurrentRepository_NoLocalRepo_ReturnsNull()\n        {\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath());\n\n            string actual = git.GetCurrentRepository();\n\n            Assert.Null(actual);\n        }\n\n        [Fact]\n        public void Git_GetCurrentRepository_LocalRepo_ReturnsNotNull()\n        {\n            CreateRepository(out string workDirPath);\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n\n            string actual = git.GetCurrentRepository();\n\n            Assert.NotNull(actual);\n        }\n\n        [Fact]\n        public void Git_GetRemotes_NoLocalRepo_ReturnsEmpty()\n        {\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath());\n\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Empty(remotes);\n        }\n\n        [Fact]\n        public void Git_GetRemotes_NoRemotes_ReturnsEmpty()\n        {\n            CreateRepository(out string workDirPath);\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Empty(remotes);\n        }\n\n        [Fact]\n        public void Git_GetRemotes_OneRemote_ReturnsRemote()\n        {\n            string name = \"origin\";\n            string url = \"https://example.com\";\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, $\"remote add {name} {url}\");\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Single(remotes);\n            AssertRemote(name, url, remotes[0]);\n        }\n\n        [Fact]\n        public void Git_GetRemotes_OneRemoteFetchAndPull_ReturnsRemote()\n        {\n            string name = \"origin\";\n            string fetchUrl = \"https://fetch.example.com\";\n            string pushUrl = \"https://push.example.com\";\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, $\"remote add {name} {fetchUrl}\");\n            ExecGit(repoPath, workDirPath, $\"remote set-url --push {name} {pushUrl}\");\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Single(remotes);\n            AssertRemote(name, fetchUrl, pushUrl, remotes[0]);\n        }\n\n        [Theory]\n        [InlineData(\"ssh://user@example.com/account/repo.git\")]\n        [InlineData(\"user@example.com:account/repo.git\")]\n        [InlineData(\"git://example.com/path/to/repo.git\")]\n        [InlineData(\"file:///path/to/repo.git\")]\n        [InlineData(\"/path/to/repo.git\")]\n        public void Git_GetRemotes_NonHttpRemote_ReturnsRemote(string url)\n        {\n            string name = \"origin\";\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, $\"remote add {name} {url}\");\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Single(remotes);\n            AssertRemote(name, url, remotes[0]);\n        }\n\n        [Fact]\n        public void Git_GetRemotes_MultipleRemotes_ReturnsAllRemotes()\n        {\n            string name1 = \"origin\";\n            string name2 = \"test\";\n            string name3 = \"upstream\";\n            string url1 = \"https://example.com/origin\";\n            string url2 = \"https://example.com/test\";\n            string url3 = \"https://example.com/upstream\";\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, $\"remote add {name1} {url1}\");\n            ExecGit(repoPath, workDirPath, $\"remote add {name2} {url2}\");\n            ExecGit(repoPath, workDirPath, $\"remote add {name3} {url3}\");\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Equal(3, remotes.Length);\n\n            AssertRemote(name1, url1, remotes[0]);\n            AssertRemote(name2, url2, remotes[1]);\n            AssertRemote(name3, url3, remotes[2]);\n        }\n\n        [Fact]\n        public void Git_GetRemotes_RemoteNoFetchOnlyPull_ReturnsRemote()\n        {\n            string name = \"origin\";\n            string pushUrl = \"https://example.com\";\n            string repoPath = CreateRepository(out string workDirPath);\n            ExecGit(repoPath, workDirPath, $\"remote add {name} {pushUrl}\");\n            ExecGit(repoPath, workDirPath, $\"remote set-url --push {name} {pushUrl}\");\n            ExecGit(repoPath, workDirPath, $\"config --unset --local remote.{name}.url\");\n\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);\n            GitRemote[] remotes = git.GetRemotes().ToArray();\n\n            Assert.Single(remotes);\n\n            AssertRemote(name, null, pushUrl, remotes[0]);\n        }\n\n        [Fact]\n        public void Git_Version_ReturnsVersion()\n        {\n            string gitPath = GetGitPath();\n            var trace = new NullTrace();\n            var trace2 = new NullTrace2();\n            var processManager = new TestProcessManager();\n\n            var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath());\n            GitVersion version = git.Version;\n\n            Assert.NotEqual(new GitVersion(), version);\n\n        }\n\n        #region Test Helpers\n\n        private static void AssertRemote(string expectedName, string expectedUrl, GitRemote remote)\n        {\n            Assert.Equal(expectedName, remote.Name);\n            Assert.Equal(expectedUrl, remote.FetchUrl);\n            Assert.Equal(expectedUrl, remote.PushUrl);\n        }\n\n        private static void AssertRemote(string expectedName, string expectedFetchUrl, string expectedPushUrl, GitRemote remote)\n        {\n            Assert.Equal(expectedName, remote.Name);\n            Assert.Equal(expectedFetchUrl, remote.FetchUrl);\n            Assert.Equal(expectedPushUrl, remote.PushUrl);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/GitVersionTests.cs",
    "content": "using System;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class GitVersionTests\n    {\n        [Theory]\n        [InlineData(null, 1)]\n        [InlineData(\"2\", 1)]\n        [InlineData(\"3\", -1)]\n        [InlineData(\"2.33\", 0)]\n        [InlineData(\"2.32.0\", 1)]\n        [InlineData(\"2.33.0.windows.0.1\", 0)]\n        [InlineData(\"2.33.0.2\", -1)]\n        public void GitVersion_CompareTo_2_33_0(string input, int expectedCompare)\n        {\n            GitVersion baseline = new GitVersion(2, 33, 0);\n            GitVersion actual = new GitVersion(input);\n            Assert.Equal(expectedCompare, baseline.CompareTo(actual));\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/HostProviderRegistryTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class HostProviderRegistryTests\n    {\n        [Fact]\n        public void HostProviderRegistry_Register_AutoProviderId_ThrowException()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var provider = new Mock<IHostProvider>();\n            provider.Setup(x => x.Id).Returns(Constants.ProviderIdAuto);\n\n            Assert.Throws<ArgumentException>(() => registry.Register(provider.Object, HostProviderPriority.Normal));\n        }\n\n        [Fact]\n        public void HostProviderRegistry_Register_AutoAuthorityId_ThrowException()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var provider = new Mock<IHostProvider>();\n            provider.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"foo\", Constants.AuthorityIdAuto, \"bar\"});\n\n            Assert.Throws<ArgumentException>(() => registry.Register(provider.Object, HostProviderPriority.Normal));\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_NoProviders_ThrowException()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var input = new InputArguments(new Dictionary<string, string>());\n\n            await Assert.ThrowsAsync<GitCredentialManager.Trace2Exception>(() => registry.GetProviderAsync(input));\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_ReturnsSupportedProvider()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider2Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_StaticMatch_DoesNotSetProviderGlobalConfig()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            string providerId = \"myProvider\";\n            string configKey = string.Format(CultureInfo.InvariantCulture,\n                \"{0}.https://example.com.{1}\",\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.Provider);\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.Id).Returns(providerId);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n\n            registry.Register(providerMock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(providerMock.Object, result);\n            Assert.False(context.Git.Configuration.Global.TryGetValue(configKey, out _));\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_DynamicMatch_SetsProviderGlobalConfig()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            string providerId = \"myProvider\";\n            string configKey = string.Format(CultureInfo.InvariantCulture,\n                \"{0}.https://example.com.{1}\",\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.Provider);\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.Id).Returns(providerId);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(true);\n\n            registry.Register(providerMock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(providerMock.Object, result);\n            Assert.True(context.Git.Configuration.Global.TryGetValue(configKey, out IList<string> config));\n            Assert.Single(config);\n            Assert.Equal(providerId, config[0]);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_DynamicMatch_SetsProviderGlobalConfig_HostWithPath()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com/alice/repo.git/\");\n            InputArguments input = CreateInputArguments(remote);\n\n            string providerId = \"myProvider\";\n            string configKey = string.Format(CultureInfo.InvariantCulture,\n                \"{0}.https://example.com/alice/repo.git.{1}\", // expect any trailing slash to be removed\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.Provider);\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.Id).Returns(providerId);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(true);\n\n            registry.Register(providerMock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(providerMock.Object, result);\n            Assert.True(context.Git.Configuration.Global.TryGetValue(configKey, out IList<string> config));\n            Assert.Single(config);\n            Assert.Equal(providerId, config[0]);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_MultipleValidProviders_ReturnsFirstRegistered()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider1Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_MultipleValidProvidersMultipleLevels_ReturnsFirstHighestRegistered()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            var provider4Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider4Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Low);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.High);\n            registry.Register(provider4Mock.Object, HostProviderPriority.Low);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider3Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_ProviderSpecified_ReturnsProvider()\n        {\n            var context = new TestCommandContext\n            {\n                Settings = {ProviderOverride = \"provider3\"}\n            };\n            var registry = new HostProviderRegistry(context);\n            var input = new InputArguments(new Dictionary<string, string>());\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.Id).Returns(\"provider1\");\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider2Mock.Setup(x => x.Id).Returns(\"provider2\");\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.Id).Returns(\"provider3\");\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider3Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_AutoProviderSpecified_ReturnsFirstSupportedProvider()\n        {\n            var context = new TestCommandContext\n            {\n                Settings = {ProviderOverride = Constants.ProviderIdAuto}\n            };\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.Id).Returns(\"provider1\");\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider2Mock.Setup(x => x.Id).Returns(\"provider2\");\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.Id).Returns(\"provider3\");\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider2Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_UnknownProviderSpecified_ReturnsFirstSupportedProvider()\n        {\n            var context = new TestCommandContext\n            {\n                Settings = {ProviderOverride = \"provider42\"}\n            };\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.Id).Returns(\"provider1\");\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider2Mock.Setup(x => x.Id).Returns(\"provider2\");\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.Id).Returns(\"provider3\");\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider2Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_LegacyAuthoritySpecified_ReturnsProvider()\n        {\n            var context = new TestCommandContext\n            {\n                Settings = {LegacyAuthorityOverride = \"authorityB\"}\n            };\n            var registry = new HostProviderRegistry(context);\n            var input = new InputArguments(new Dictionary<string, string>());\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"authorityA\"});\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider2Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"authorityB\", \"authorityC\"});\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider3Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"authorityD\"});\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider2Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_AutoLegacyAuthoritySpecified_ReturnsFirstSupportedProvider()\n        {\n            var context = new TestCommandContext\n            {\n                Settings = {LegacyAuthorityOverride = Constants.AuthorityIdAuto}\n            };\n            var registry = new HostProviderRegistry(context);\n            var remote = new Uri(\"https://example.com\");\n            InputArguments input = CreateInputArguments(remote);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            var provider2Mock = new Mock<IHostProvider>();\n            var provider3Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"authorityA\"});\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider2Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"authorityB\", \"authorityC\"});\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            provider3Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{\"authorityD\"});\n            provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider3Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            Assert.Same(provider2Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_NetworkProbe_ReturnsSupportedProvider()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remoteUri = new Uri(\"https://provider2.onprem.example.com\");\n            InputArguments input = CreateInputArguments(remoteUri);\n\n            var provider1Mock = new Mock<IHostProvider>();\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider1Mock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(false);\n\n            var provider2Mock = new Mock<IHostProvider>();\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            provider2Mock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(true);\n\n            var responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized)\n            {\n                Headers = { { \"X-Provider2\", \"true\" } }\n            };\n\n            var httpHandler = new TestHttpMessageHandler();\n\n            httpHandler.Setup(HttpMethod.Head, remoteUri, responseMessage);\n            context.HttpClientFactory.MessageHandler = httpHandler;\n\n            registry.Register(provider1Mock.Object, HostProviderPriority.Normal);\n            registry.Register(provider2Mock.Object, HostProviderPriority.Normal);\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            httpHandler.AssertRequest(HttpMethod.Head, remoteUri, 1);\n            Assert.Same(provider2Mock.Object, result);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_NetworkProbe_TimeoutZero_NoNetworkCall()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remoteUri = new Uri(\"https://onprem.example.com\");\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = remoteUri.Scheme,\n                    [\"host\"] = remoteUri.Host\n                }\n            );\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(true);\n\n            var responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            var httpHandler = new TestHttpMessageHandler();\n\n            httpHandler.Setup(HttpMethod.Head, remoteUri, responseMessage);\n            context.HttpClientFactory.MessageHandler = httpHandler;\n\n            registry.Register(providerMock.Object, HostProviderPriority.Normal);\n\n            context.Settings.AutoDetectProviderTimeout = 0;\n\n            await Assert.ThrowsAnyAsync<Exception>(() => registry.GetProviderAsync(input));\n\n            httpHandler.AssertRequest(HttpMethod.Head, remoteUri, 0);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_NetworkProbe_TimeoutNegative_NoNetworkCall()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remoteUri = new Uri(\"https://onprem.example.com\");\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = remoteUri.Scheme,\n                    [\"host\"] = remoteUri.Host\n                }\n            );\n\n            var providerMock = new Mock<IHostProvider>();\n            providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            providerMock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(true);\n\n            var responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            var httpHandler = new TestHttpMessageHandler();\n\n            httpHandler.Setup(HttpMethod.Head, remoteUri, responseMessage);\n            context.HttpClientFactory.MessageHandler = httpHandler;\n\n            registry.Register(providerMock.Object, HostProviderPriority.Normal);\n\n            context.Settings.AutoDetectProviderTimeout = -1;\n\n            await Assert.ThrowsAnyAsync<Exception>(() => registry.GetProviderAsync(input));\n\n            httpHandler.AssertRequest(HttpMethod.Head, remoteUri, 0);\n        }\n\n        [Fact]\n        public async Task HostProviderRegistry_GetProvider_Auto_NetworkProbe_NoNetwork_ReturnsLastProvider()\n        {\n            var context = new TestCommandContext();\n            var registry = new HostProviderRegistry(context);\n            var remoteUri = new Uri(\"https://provider2.onprem.example.com\");\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = remoteUri.Scheme,\n                    [\"host\"] = remoteUri.Host\n                }\n            );\n\n            var highProviderMock = new Mock<IHostProvider>();\n            highProviderMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);\n            highProviderMock.Setup(x => x.IsSupported(It.IsAny<HttpResponseMessage>())).Returns(false);\n            registry.Register(highProviderMock.Object, HostProviderPriority.Normal);\n\n            var lowProviderMock = new Mock<IHostProvider>();\n            lowProviderMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);\n            registry.Register(lowProviderMock.Object, HostProviderPriority.Low);\n\n            var responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized)\n            {\n                Headers = { { \"X-Provider2\", \"true\" } }\n            };\n\n            var httpHandler = new TestHttpMessageHandler\n            {\n                SimulateNoNetwork = true,\n            };\n\n            httpHandler.Setup(HttpMethod.Head, remoteUri, responseMessage);\n            context.HttpClientFactory.MessageHandler = httpHandler;\n\n            IHostProvider result = await registry.GetProviderAsync(input);\n\n            httpHandler.AssertRequest(HttpMethod.Head, remoteUri, 1);\n            Assert.Same(lowProviderMock.Object, result);\n        }\n\n        public static InputArguments CreateInputArguments(Uri uri)\n        {\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = uri.Scheme,\n                [\"host\"] = uri.IsDefaultPort ? uri.Host : $\"{uri.Host}:{uri.Port}\"\n            };\n\n            if (!string.IsNullOrWhiteSpace(uri.AbsolutePath) && uri.AbsolutePath != \"/\")\n            {\n                dict[\"path\"] = uri.AbsolutePath.TrimEnd('/');\n            }\n\n            return new InputArguments(dict);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/HostProviderTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class HostProviderTests\n    {\n        #region GetCredentialAsync\n\n        [Fact]\n        public async Task HostProvider_GetCredentialAsync_CredentialExists_ReturnsExistingCredential()\n        {\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string service = \"https://example.com\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = userName,\n                [\"password\"] = password, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(service, userName, password);\n            var provider = new TestHostProvider(context)\n            {\n                IsSupportedFunc = _ => true,\n                GenerateCredentialFunc = _ =>\n                {\n                    Assert.Fail(\"Should never be called\");\n                    return null;\n                },\n            };\n\n            var result = await ((IHostProvider) provider).GetCredentialAsync(input);\n            ICredential actualCredential = result.Credential; \n\n            Assert.Equal(userName, actualCredential.Account);\n            Assert.Equal(password, actualCredential.Password);\n        }\n\n        [Fact]\n        public async Task HostProvider_GetCredentialAsync_CredentialDoesNotExist_ReturnsNewGeneratedCredential()\n        {\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = userName,\n                [\"password\"] = password, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            bool generateWasCalled = false;\n            var context = new TestCommandContext();\n            var provider = new TestHostProvider(context)\n            {\n                IsSupportedFunc = _ => true,\n                GenerateCredentialFunc = _ =>\n                {\n                    generateWasCalled = true;\n                    return new GitCredential(userName, password);\n                },\n            };\n\n            var result = await ((IHostProvider) provider).GetCredentialAsync(input);\n            ICredential actualCredential = result.Credential;\n\n            Assert.True(generateWasCalled);\n            Assert.Equal(userName, actualCredential.Account);\n            Assert.Equal(password, actualCredential.Password);\n        }\n\n\n            #endregion\n\n        #region StoreCredentialAsync\n\n        [Fact]\n        public async Task HostProvider_StoreCredentialAsync_EmptyCredential_DoesNotStoreCredential()\n        {\n            const string emptyUserName = \"\";\n            const string emptyPassword = \"\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = emptyUserName,\n                [\"password\"] = emptyPassword, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var context = new TestCommandContext();\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).StoreCredentialAsync(input);\n\n            Assert.Equal(0, context.CredentialStore.Count);\n        }\n\n        [Fact]\n        public async Task HostProvider_StoreCredentialAsync_NonEmptyCredential_StoresCredential()\n        {\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string service = \"https://example.com\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = userName,\n                [\"password\"] = password, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var context = new TestCommandContext();\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).StoreCredentialAsync(input);\n\n            Assert.Equal(1, context.CredentialStore.Count);\n            Assert.True(context.CredentialStore.TryGet(service, userName, out var storedCredential));\n            Assert.Equal(userName, storedCredential.Account);\n            Assert.Equal(password, storedCredential.Password);\n        }\n\n        [Fact]\n        public async Task HostProvider_StoreCredentialAsync_NonEmptyCredential_ExistingCredential_UpdatesCredential()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPasswordOld = \"letmein123-old\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testPasswordNew = \"letmein123-new\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testService = \"https://example.com\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = testUserName,\n                [\"password\"] = testPasswordNew, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(testService, testUserName, testPasswordOld);\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).StoreCredentialAsync(input);\n\n            Assert.Equal(1, context.CredentialStore.Count);\n            Assert.True(context.CredentialStore.TryGet(testService, testUserName, out var storedCredential));\n            Assert.Equal(testUserName, storedCredential.Account);\n            Assert.Equal(testPasswordNew, storedCredential.Password);\n        }\n\n        #endregion\n\n        #region EraseCredentialAsync\n\n        [Fact]\n        public async Task HostProvider_EraseCredentialAsync_NoInputUser_CredentialExists_ErasesOneCredential()\n        {\n            const string service = \"https://example.com\";\n            const string userName1 = \"john.doe\";\n            const string userName2 = \"alice\";\n            const string userName3 = \"bob\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n            });\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(service, userName1, \"letmein123\");\n            context.CredentialStore.Add(service, userName2, \"do-not-erase-me\");\n            context.CredentialStore.Add(service, userName3, \"here-forever\");\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).EraseCredentialAsync(input);\n\n            Assert.Equal(2, context.CredentialStore.Count);\n        }\n\n        [Fact]\n        public async Task HostProvider_EraseCredentialAsync_InputUser_CredentialExists_UserNotMatch_DoesNothing()\n        {\n            const string userName1 = \"john.doe\";\n            const string userName2 = \"alice\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string service = \"https://example.com\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = userName1,\n                [\"password\"] = password, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(service, userName2, password);\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).EraseCredentialAsync(input);\n\n            Assert.Equal(1, context.CredentialStore.Count);\n            Assert.True(context.CredentialStore.Contains(service, userName2));\n        }\n\n        [Fact]\n        public async Task HostProvider_EraseCredentialAsync_InputUser_CredentialExists_UserMatch_ErasesCredential()\n        {\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string service = \"https://example.com\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"username\"] = userName,\n                [\"password\"] = password, // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            });\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(service, userName, password);\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).EraseCredentialAsync(input);\n\n            Assert.Equal(0, context.CredentialStore.Count);\n            Assert.False(context.CredentialStore.Contains(service, userName));\n        }\n\n        [Fact]\n        public async Task HostProvider_EraseCredentialAsync_DifferentHost_DoesNothing()\n        {\n            const string service2 = \"https://example2.com\";\n            const string service3 = \"https://example3.com\";\n            const string userName = \"john.doe\";\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example1.com\",\n            });\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(service2, userName, \"keep-me\");\n            context.CredentialStore.Add(service3, userName, \"also-keep-me\");\n            var provider = new TestHostProvider(context);\n\n            await ((IHostProvider) provider).EraseCredentialAsync(input);\n\n            Assert.Equal(2, context.CredentialStore.Count);\n            Assert.True(context.CredentialStore.Contains(service2, userName));\n            Assert.True(context.CredentialStore.Contains(service3, userName));\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/HttpClientExtensionsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class HttpClientExtensionsTests\n    {\n        [Fact]\n        public async Task HttpClientExtensions_SendAsync_SendsRequestMessage()\n        {\n            var method = HttpMethod.Get;\n            var uri = new Uri(\"http://example.com\");\n\n            var httpHandler = new TestHttpMessageHandler{ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(method, uri, request =>\n            {\n                Assert.Equal(method, request.Method);\n                Assert.Equal(uri, request.RequestUri);\n\n                return new HttpResponseMessage();\n            });\n\n            var httpClient = new HttpClient(httpHandler);\n\n            await HttpClientExtensions.SendAsync(httpClient, method, uri);\n        }\n\n        [Fact]\n        public async Task HttpClientExtensions_SendAsync_Content_SetsContent()\n        {\n            var method = HttpMethod.Get;\n            var uri = new Uri(\"http://example.com\");\n\n            var expectedContent = new StringContent(\"foobar\");\n\n            var httpHandler = new TestHttpMessageHandler{ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(method, uri, request =>\n            {\n                Assert.Same(expectedContent,request.Content);\n\n                return new HttpResponseMessage();\n            });\n\n            var httpClient = new HttpClient(httpHandler);\n\n            await HttpClientExtensions.SendAsync(httpClient, method, uri, null, expectedContent);\n        }\n\n        [Fact]\n        public async Task HttpClientExtensions_SendAsync_Headers_SetsHeaders()\n        {\n            var method = HttpMethod.Get;\n            var uri = new Uri(\"http://example.com\");\n\n            var customHeaders = new Dictionary<string, IEnumerable<string>>\n            {\n                [\"header0\"] = new string[0],\n                [\"header1\"] = new []{ \"first-value\" },\n                [\"header2\"] = new []{ \"first-value\", \"second-value\"},\n                [\"header3\"] = new []{ \"first-value\", \"second-value\", \"third-value\"},\n            };\n\n            var httpHandler = new TestHttpMessageHandler{ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(method, uri, request =>\n            {\n                Assert.False(request.Headers.Contains(\"header0\"));\n                Assert.True(request.Headers.Contains(\"header1\"));\n                Assert.True(request.Headers.Contains(\"header2\"));\n                Assert.True(request.Headers.Contains(\"header3\"));\n                Assert.Equal(customHeaders[\"header1\"], request.Headers.GetValues(\"header1\"));\n                Assert.Equal(customHeaders[\"header2\"], request.Headers.GetValues(\"header2\"));\n                Assert.Equal(customHeaders[\"header3\"], request.Headers.GetValues(\"header3\"));\n\n                return new HttpResponseMessage();\n            });\n\n            var httpClient = new HttpClient(httpHandler);\n\n            await HttpClientExtensions.SendAsync(httpClient, method, uri, customHeaders);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/HttpClientFactoryTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing Moq;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class HttpClientFactoryTests\n    {\n        [Fact]\n        public void HttpClientFactory_GetClient_SetsDefaultHeaders()\n        {\n            var factory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), Mock.Of<ISettings>(), new TestStandardStreams());\n\n            HttpClient client = factory.CreateClient();\n\n            Assert.NotNull(client);\n            Assert.Equal(Constants.GetHttpUserAgent(Mock.Of<ITrace2>()), client.DefaultRequestHeaders.UserAgent.ToString());\n            Assert.True(client.DefaultRequestHeaders.CacheControl.NoCache);\n        }\n\n        [Fact]\n        public void HttpClientFactory_GetClient_MultipleCalls_ReturnsNewInstance()\n        {\n            var factory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), Mock.Of<ISettings>(), new TestStandardStreams());\n\n            HttpClient client1 = factory.CreateClient();\n            HttpClient client2 = factory.CreateClient();\n\n            Assert.NotSame(client1, client2);\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_NoProxy_ReturnsFalseOutNull()\n        {\n            const string repoPath = \"/tmp/repos/foo\";\n            const string repoRemote  = \"https://remote.example.com/foo.git\";\n\n            var repoRemoteUri = new Uri(repoRemote);\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.False(result);\n            Assert.Null(proxy);\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyNoCredentials_ReturnsTrueOutProxyWithUrlDefaultCredentials()\n        {\n            const string proxyUrl = \"https://proxy.example.com/git\";\n            const string repoPath = \"/tmp/repos/foo\";\n            const string repoRemote = \"https://remote.example.com/foo.git\";\n\n            var repoRemoteUri = new Uri(repoRemote);\n            var proxyConfig = new ProxyConfiguration(new Uri(proxyUrl));\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.True(result);\n            Assert.NotNull(proxy);\n            Uri configuredProxyUrl = proxy.GetProxy(repoRemoteUri);\n            Assert.Equal(proxyUrl, configuredProxyUrl?.ToString());\n\n            AssertDefaultCredentials(proxy.Credentials);\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyWithBypass_ReturnsTrueOutProxyWithBypassedHosts()\n        {\n            const string proxyUrl = \"https://proxy.example.com/git\";\n            const string repoPath = \"/tmp/repos/foo\";\n            const string repoRemote = \"https://remote.example.com/foo.git\";\n\n            var noProxyRaw = \"contoso.com,fabrikam.com\";\n            var repoRemoteUri = new Uri(repoRemote);\n            var proxyConfig = new ProxyConfiguration(\n                new Uri(proxyUrl),\n                userName: null,\n                password: null,\n                noProxyRaw: noProxyRaw);\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.True(result);\n            Assert.NotNull(proxy);\n            Uri configuredProxyUrl = proxy.GetProxy(repoRemoteUri);\n            Assert.Equal(proxyUrl, configuredProxyUrl?.ToString());\n\n            Assert.True(proxy.IsBypassed(new Uri(\"https://contoso.com\")));\n            Assert.True(proxy.IsBypassed(new Uri(\"http://fabrikam.com\")));\n            Assert.True(proxy.IsBypassed(new Uri(\"https://subdomain.fabrikam.com\")));\n            Assert.False(proxy.IsBypassed(repoRemoteUri));\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyWithWildcardBypass_ReturnsFalse()\n        {\n            const string proxyUrl = \"https://proxy.example.com/git\";\n            const string repoPath = \"/tmp/repos/foo\";\n            const string repoRemote = \"https://remote.example.com/foo.git\";\n\n            var noProxyRaw = \"*\";\n            var repoRemoteUri = new Uri(repoRemote);\n            var proxyConfig = new ProxyConfiguration(\n                new Uri(proxyUrl),\n                userName: null,\n                password: null,\n                noProxyRaw: noProxyRaw);\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.False(result);\n            Assert.Null(proxy);\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyWithCredentials_ReturnsTrueOutProxyWithUrlConfiguredCredentials()\n        {\n            const string proxyUser   = \"john.doe\";\n            const string proxyPass   = \"letmein\";\n            const string proxyUrl    = \"https://proxy.example.com/git\";\n            const string repoPath    = \"/tmp/repos/foo\";\n            const string repoRemote  = \"https://remote.example.com/foo.git\";\n\n            var repoRemoteUri = new Uri(repoRemote);\n            var proxyConfig = new ProxyConfiguration(\n                new Uri(proxyUrl),\n                proxyUser,\n                proxyPass);\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.True(result);\n            Assert.NotNull(proxy);\n            Uri configuredProxyUrl = proxy.GetProxy(repoRemoteUri);\n            Assert.Equal(proxyUrl, configuredProxyUrl?.ToString());\n\n            Assert.NotNull(proxy.Credentials);\n            Assert.IsType<NetworkCredential>(proxy.Credentials);\n            var configuredCredentials = (NetworkCredential) proxy.Credentials;\n            Assert.Equal(proxyUser, configuredCredentials.UserName);\n            Assert.Equal(proxyPass, configuredCredentials.Password);\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyWithNonEmptyUserAndEmptyPass_ReturnsTrueOutProxyWithUrlConfiguredCredentials()\n        {\n            const string proxyUrl    = \"https://proxy.example.com/git\";\n            const string proxyUser   = \"john.doe\";\n            const string repoPath    = \"/tmp/repos/foo\";\n            const string repoRemote  = \"https://remote.example.com/foo.git\";\n\n            var repoRemoteUri = new Uri(repoRemote);\n            var proxyConfig = new ProxyConfiguration(\n                new Uri(proxyUrl),\n                proxyUser,\n                password: null);\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.True(result);\n            Assert.NotNull(proxy);\n            Uri configuredProxyUrl = proxy.GetProxy(repoRemoteUri);\n            Assert.Equal(proxyUrl, configuredProxyUrl?.ToString());\n\n            Assert.NotNull(proxy.Credentials);\n            Assert.IsType<NetworkCredential>(proxy.Credentials);\n            var configuredCredentials = (NetworkCredential) proxy.Credentials;\n            Assert.Equal(proxyUser, configuredCredentials.UserName);\n            Assert.True(string.IsNullOrWhiteSpace(configuredCredentials.Password));\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyWithEmptyUserAndNonEmptyPass_ReturnsTrueOutProxyWithUrlConfiguredCredentials()\n        {\n            const string proxyUrl    = \"https://proxy.example.com/git\";\n            const string proxyPass   = \"letmein\";\n            const string repoPath    = \"/tmp/repos/foo\";\n            const string repoRemote  = \"https://remote.example.com/foo.git\";\n\n            var repoRemoteUri = new Uri(repoRemote);\n            var proxyConfig = new ProxyConfiguration(\n                new Uri(proxyUrl),\n                userName: null,\n                password: proxyPass);\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.True(result);\n            Assert.NotNull(proxy);\n            Uri configuredProxyUrl = proxy.GetProxy(repoRemoteUri);\n            Assert.Equal(proxyUrl, configuredProxyUrl?.ToString());\n\n            Assert.NotNull(proxy.Credentials);\n            Assert.IsType<NetworkCredential>(proxy.Credentials);\n            var configuredCredentials = (NetworkCredential) proxy.Credentials;\n            Assert.True(string.IsNullOrWhiteSpace(configuredCredentials.UserName));\n            Assert.Equal(proxyPass, configuredCredentials.Password);\n        }\n\n        [Fact]\n        public void HttpClientFactory_TryCreateProxy_ProxyEmptyUserAndEmptyPass_ReturnsTrueOutProxyWithUrlDefaultCredentials()\n        {\n            const string proxyUrl    = \"https://proxy.example.com/git\";\n            const string repoPath = \"/tmp/repos/foo\";\n            const string repoRemote = \"https://remote.example.com/foo.git\";\n            var repoRemoteUri = new Uri(repoRemote);\n\n            var proxyConfig = new ProxyConfiguration(\n                new Uri(proxyUrl),\n                userName: string.Empty,\n                password: string.Empty);\n\n            var settings = new TestSettings\n            {\n                RemoteUri = repoRemoteUri,\n                RepositoryPath = repoPath,\n                ProxyConfiguration = proxyConfig\n            };\n            var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, Mock.Of<IStandardStreams>());\n\n            bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);\n\n            Assert.True(result);\n            Assert.NotNull(proxy);\n            Uri configuredProxyUrl = proxy.GetProxy(repoRemoteUri);\n            Assert.Equal(proxyUrl, configuredProxyUrl?.ToString());\n\n            AssertDefaultCredentials(proxy.Credentials);\n        }\n\n        [Theory]\n        [InlineData(null, TlsBackend.OpenSsl, false, false)]\n        [InlineData(\"ca-bundle.crt\", TlsBackend.Other, false, true)]\n        [InlineData(\"ca-bundle.crt\", TlsBackend.Schannel, false, false)]\n        [InlineData(\"ca-bundle.crt\", TlsBackend.Schannel, true, true)]\n        public void HttpClientFactory_GetClient_ChecksCertBundleOnlyIfEnabled(string customCertBundle,\n            TlsBackend tlsBackend, bool useCustomCertBundleWithSchannel, bool expectBundleChecked)\n        {\n            var fileSystemMock = new Mock<IFileSystem>();\n            fileSystemMock.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(true);\n\n            var settings = new TestSettings()\n            {\n                CustomCertificateBundlePath = customCertBundle,\n                TlsBackend = tlsBackend,\n                UseCustomCertificateBundleWithSchannel = useCustomCertBundleWithSchannel\n            };\n\n            var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, new TestStandardStreams());\n\n            HttpClient client = factory.CreateClient();\n\n            fileSystemMock.Verify(fs => fs.FileExists(It.IsAny<string>()), expectBundleChecked ? Times.Once : Times.Never);\n        }\n\n        [Theory]\n        [InlineData(null, false, null)]\n        [InlineData(\"~/.git-cookie\", true, \"# Netscape HTTP Cookie File\\n\" +\n                          \"# https://curl.haxx.se/rfc/cookie_spec.html\\n\" +\n                          \"# This is a generated file! Do not edit.\\n\" +\n                          \"\\n\" +\n                          \".example.com\\tTRUE\\t/\\tTRUE\\t0\\tcookie1\\tvalue1\\n\" +\n                          \".example.com\\tTRUE\\t/\\tTRUE\\t0\\tcookie2\\tvalue2\\n\" +\n                          \"#HttpOnly_.example.com\\tTRUE\\t/\\tTRUE\\t0\\tcookie3\\tvalue3\\n\")]\n        public void HttpClientFactory_GetClient_SetCookieOnlyIfEnabled(string cookieFilePath, bool expectCookieChecked, string cookieFileContent)\n        {\n            var fileSystemMock = new Mock<IFileSystem>();\n            fileSystemMock.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(true);\n            if (!string.IsNullOrWhiteSpace(cookieFileContent))\n            {\n                fileSystemMock.Setup(fs => fs.ReadAllText(cookieFilePath)).Returns(cookieFileContent);\n            }\n\n            var settings = new TestSettings()\n            {\n                CustomCookieFilePath = cookieFilePath\n            };\n\n            var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, new TestStandardStreams());\n\n            HttpClient client = factory.CreateClient();\n\n            fileSystemMock.Verify(fs => fs.FileExists(It.IsAny<string>()), expectCookieChecked ? Times.AtLeastOnce : Times.Never);\n        }\n\n        private static void AssertDefaultCredentials(ICredentials credentials)\n        {\n            var netCred = (NetworkCredential) credentials;\n\n            Assert.Equal(string.Empty, netCred.Domain);\n            Assert.Equal(string.Empty, netCred.UserName);\n            Assert.Equal(string.Empty, netCred.Password);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/HttpRequestExtensionsTests.cs",
    "content": "using System.Net.Http;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class HttpRequestExtensionsTests\n    {\n        [Fact]\n        public void HttpRequestExtensions_AddBasicAuthenticationHeader_ComplexUserPass_ReturnsCorrectString()\n        {\n            const string expected = \"aGVsbG8tbXlfbmFtZSBpczpqb2huLmRvZTp0aGlzIWlzQVA0U1NXMFJEOiB3aXRoPyBfbG90cyBvZi8gY2hhcnM=\";\n            const string testUserName = \"hello-my_name is:john.doe\";\n            const string testPassword = \"this!isAP4SSW0RD: with? _lots of/ chars\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            TestAddBasicAuthenticationHeader(testUserName, testPassword, expected);\n        }\n\n        [Fact]\n        public void HttpRequestExtensions_AddBasicAuthenticationHeader_EmptyUserName_ReturnsCorrectString()\n        {\n            const string expected = \"OmxldG1laW4xMjM=\";\n            const string testUserName = \"\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            TestAddBasicAuthenticationHeader(testUserName, testPassword, expected);\n        }\n\n        [Fact]\n        public void HttpRequestExtensions_AddBasicAuthenticationHeader_EmptyPassword_ReturnsCorrectString()\n        {\n            const string expected = \"am9obi5kb2U6\";\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            TestAddBasicAuthenticationHeader(testUserName, testPassword, expected);\n        }\n\n        [Fact]\n        public void HttpRequestExtensions_AddBasicAuthenticationHeader_EmptyCredential_ReturnsCorrectString()\n        {\n            const string expected = \"Og==\";\n            const string testUserName = \"\";\n            const string testPassword = \"\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            TestAddBasicAuthenticationHeader(testUserName, testPassword, expected);\n        }\n\n        private static void TestAddBasicAuthenticationHeader(string userName, string password, string expectedParameterValue)\n        {\n            var message = new HttpRequestMessage();\n            message.AddBasicAuthenticationHeader(userName, password);\n\n            var authHeader = message.Headers.Authorization;\n            Assert.NotNull(authHeader);\n            Assert.Equal(Constants.Http.WwwAuthenticateBasicScheme, authHeader.Scheme);\n            Assert.Equal(expectedParameterValue, authHeader.Parameter);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/IniFileTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Text;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class IniFileTests\n    {\n        [Fact]\n        public void IniSectionName_Equality()\n        {\n            var a1 = new IniSectionName(\"foo\");\n            var b1 = new IniSectionName(\"foo\");\n            Assert.Equal(a1,b1);\n            Assert.Equal(a1.GetHashCode(),b1.GetHashCode());\n\n            var a2 = new IniSectionName(\"foo\");\n            var b2 = new IniSectionName(\"FOO\");\n            Assert.Equal(a2,b2);\n            Assert.Equal(a2.GetHashCode(),b2.GetHashCode());\n\n            var a3 = new IniSectionName(\"foo\", \"bar\");\n            var b3 = new IniSectionName(\"foo\", \"BAR\");\n            Assert.NotEqual(a3,b3);\n            Assert.NotEqual(a3.GetHashCode(),b3.GetHashCode());\n\n            var a4 = new IniSectionName(\"foo\", \"bar\");\n            var b4 = new IniSectionName(\"FOO\", \"bar\");\n            Assert.Equal(a4,b4);\n            Assert.Equal(a4.GetHashCode(),b4.GetHashCode());\n        }\n\n        [Fact]\n        public void IniSerializer_Deserialize()\n        {\n            const string path = \"/tmp/test.ini\";\n            string iniText = @\"\n[one]\n    foo = 123\n  [two]\n    foo   =   abc\n# comment\n[two \"\"subsection name\"\"] # comment [section]\n    foo = this is different # comment prop = val\n\n#[notasection]\n\n    [\n[bad #section]\nrecovery tests]\n[]\n    ]\n\n    [three]\n    bar = a\n    bar = b\n    # comment\n    bar = c\n    empty =\n[TWO]\n    foo = hello\n    widget = \"\"Hello, World!\"\"\n[four]\n[five]\n    prop1 = \"\"this hash # is inside quotes\"\"\n    prop2 = \"\"this hash # is inside quotes\"\" # this line has two hashes\n    prop3 = \"\"   this dquoted string has three spaces around   \"\"\n    #prop4 = this property has been commented-out\n\";\n\n            var fs = new TestFileSystem\n            {\n                Files = { [path] = Encoding.UTF8.GetBytes(iniText) }\n            };\n\n            IniFile ini = IniSerializer.Deserialize(fs, path);\n\n            Assert.Equal(6, ini.Sections.Count);\n\n            AssertSection(ini, \"one\", out IniSection one);\n            Assert.Single(one.Properties);\n            AssertProperty(one, \"foo\", \"123\");\n\n            AssertSection(ini, \"two\", out IniSection twoA);\n            Assert.Equal(3, twoA.Properties.Count);\n            AssertProperty(twoA, \"foo\", \"hello\");\n            AssertProperty(twoA, \"widget\", \"Hello, World!\");\n\n            AssertSection(ini, \"two\", \"subsection name\", out IniSection twoB);\n            Assert.Single(twoB.Properties);\n            AssertProperty(twoB, \"foo\", \"this is different\");\n\n            AssertSection(ini, \"three\", out IniSection three);\n            Assert.Equal(4, three.Properties.Count);\n            AssertMultiProperty(three, \"bar\", \"a\", \"b\", \"c\");\n            AssertProperty(three, \"empty\", \"\");\n\n            AssertSection(ini, \"four\", out IniSection four);\n            Assert.Empty(four.Properties);\n\n            AssertSection(ini, \"five\", out IniSection five);\n            Assert.Equal(3, five.Properties.Count);\n            AssertProperty(five, \"prop1\", \"this hash # is inside quotes\");\n            AssertProperty(five, \"prop2\", \"this hash # is inside quotes\");\n            AssertProperty(five, \"prop3\", \"   this dquoted string has three spaces around   \");\n        }\n\n        private static void AssertSection(IniFile file, string name, out IniSection section)\n        {\n            Assert.True(file.TryGetSection(name, out section));\n            Assert.Equal(name, section.Name.Name);\n            Assert.Null(section.Name.SubName);\n        }\n\n        private static void AssertSection(IniFile file, string name, string subName, out IniSection section)\n        {\n            Assert.True(file.TryGetSection(name, subName, out section));\n            Assert.Equal(name, section.Name.Name);\n            Assert.Equal(subName, section.Name.SubName);\n        }\n\n        private static void AssertProperty(IniSection section, string name, string value)\n        {\n            Assert.True(section.TryGetProperty(name, out var actualValue));\n            Assert.Equal(value, actualValue);\n        }\n\n        private static void AssertMultiProperty(IniSection section, string name, params string[] values)\n        {\n            Assert.True(section.TryGetMultiProperty(name, out IEnumerable<string> actualValues));\n            Assert.Equal(values, actualValues);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/InputArgumentsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class InputArgumentsTests\n    {\n        [Fact]\n        public void InputArguments_Ctor_Null_ThrowsArgNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => new InputArguments((IDictionary<string, string>)null));\n            Assert.Throws<ArgumentNullException>(() => new InputArguments((IDictionary<string, IList<string>>)null));\n        }\n\n        [Fact]\n        public void InputArguments_CommonArguments_ValuePresent_ReturnsValues()\n        {\n            var dict = new Dictionary<string, IList<string>>\n            {\n                [\"protocol\"] = new[] { \"https\" },\n                [\"host\"]     = new[] { \"example.com\" },\n                [\"path\"]     = new[] { \"an/example/path\" },\n                [\"username\"] = new[] { \"john.doe\" },\n                [\"password\"] = new[] { \"password123\" },\n                [\"wwwauth\"]  = new[]\n                {\n                    \"basic realm=\\\"example.com\\\"\",\n                    \"bearer authorize_uri=https://id.example.com p=1 q=0\"\n                }\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Assert.Equal(\"https\",           inputArgs.Protocol);\n            Assert.Equal(\"example.com\",     inputArgs.Host);\n            Assert.Equal(\"an/example/path\", inputArgs.Path);\n            Assert.Equal(\"john.doe\",        inputArgs.UserName);\n            Assert.Equal(\"password123\",     inputArgs.Password);\n            Assert.Equal(new[]\n                {\n                    \"basic realm=\\\"example.com\\\"\",\n                    \"bearer authorize_uri=https://id.example.com p=1 q=0\"\n                },\n                inputArgs.WwwAuth);\n        }\n\n        [Fact]\n        public void InputArguments_CommonArguments_ValueMissing_ReturnsNullOrEmptyCollection()\n        {\n            var dict = new Dictionary<string, string>();\n\n            var inputArgs = new InputArguments(dict);\n\n            Assert.Null(inputArgs.Protocol);\n            Assert.Null(inputArgs.Host);\n            Assert.Null(inputArgs.Path);\n            Assert.Null(inputArgs.UserName);\n            Assert.Null(inputArgs.Password);\n            Assert.Empty(inputArgs.WwwAuth);\n        }\n\n        [Fact]\n        public void InputArguments_OtherArguments()\n        {\n            var dict = new Dictionary<string, IList<string>>\n            {\n                [\"foo\"] = new[] { \"bar\" },\n                [\"multi\"] = new[] { \"val1\", \"val2\", \"val3\" },\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Assert.Equal(\"bar\", inputArgs[\"foo\"]);\n            Assert.Equal(\"bar\", inputArgs.GetArgumentOrDefault(\"foo\"));\n            Assert.Equal(new[] { \"val1\", \"val2\", \"val3\" }, inputArgs.GetMultiArgumentOrDefault(\"multi\"));\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_NoAuthority_ReturnsNull()\n        {\n            var dict = new Dictionary<string, string>();\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri();\n\n            Assert.Null(actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_Authority_ReturnsUriWithAuthority()\n        {\n            var expectedUri = new Uri(\"https://example.com/\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri();\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_IncludeUser_Authority_ReturnsUriWithAuthorityAndUser()\n        {\n            var expectedUri = new Uri(\"https://john.doe@example.com/\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n\n                // Username should appear in the returned URI; the password should not\n                [\"username\"] = \"john.doe\",\n                [\"password\"] = \"password123\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri(includeUser: true);\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_IncludeUserSpecialCharacters_Authority_ReturnsUriWithAuthorityAndUser()\n        {\n            var expectedUri = new Uri(\"https://john.doe%40domain.com@example.com/\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n\n                // Username should appear in the returned URI; the password should not\n                [\"username\"] = \"john.doe@domain.com\",\n                [\"password\"] = \"password123\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri(includeUser: true);\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_IncludeUserNoUser_Authority_ReturnsUriWithAuthority()\n        {\n            var expectedUri = new Uri(\"https://example.com/\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri(includeUser: true);\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_AuthorityAndPort_ReturnsUriWithAuthorityAndPort()\n        {\n            var expectedUri = new Uri(\"https://example.com:456/\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com:456\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri();\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_AuthorityPath_ReturnsUriWithAuthorityAndPath()\n        {\n            var expectedUri = new Uri(\"https://example.com/an/example/path\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [\"path\"]     = \"an/example/path\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri();\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_AuthorityPathUserInfo_ReturnsUriWithAuthorityAndPath()\n        {\n            var expectedUri = new Uri(\"https://example.com/an/example/path\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [\"path\"]     = \"an/example/path\",\n\n                // Username and password are not expected to appear in the returned URI\n                [\"username\"] = \"john.doe\",\n                [\"password\"] = \"password123\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri();\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Theory]\n        [InlineData(\"foo?query=true\")]\n        [InlineData(\"foo#fragment\")]\n        [InlineData(\"foo?query=true#fragment\")]\n        public void InputArguments_GetRemoteUri_PathQueryFragment_ReturnsCorrectUri(string path)\n        {\n            var expectedUri = new Uri($\"https://example.com/{path}\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [\"path\"]     = path\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri();\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_GetRemoteUri_IncludeUser_AuthorityPathUserInfo_ReturnsUriWithAll()\n        {\n            var expectedUri = new Uri(\"https://john.doe@example.com/an/example/path\");\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\",\n                [\"path\"]     = \"an/example/path\",\n\n                // Username should appear in the returned URI; the password should not\n                [\"username\"] = \"john.doe\",\n                [\"password\"] = \"password123\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            Uri actualUri = inputArgs.GetRemoteUri(includeUser: true);\n\n            Assert.NotNull(actualUri);\n            Assert.Equal(expectedUri, actualUri);\n        }\n\n        [Fact]\n        public void InputArguments_TryGetHostAndPort_NoPort_ReturnsHostName()\n        {\n            const string expectedHostName = \"example.com\";\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            bool result = inputArgs.TryGetHostAndPort(out string actualHostName, out int? actualPort);\n\n            Assert.True(result);\n            Assert.NotNull(actualHostName);\n            Assert.Equal(expectedHostName, actualHostName);\n            Assert.Null(actualPort);\n        }\n\n        [Fact]\n        public void InputArguments_TryGetHostAndPort_Port_ReturnsHostNameAndPort()\n        {\n            const string expectedHostName = \"example.com\";\n            const int expectedPort = 456;\n\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com:456\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            bool result = inputArgs.TryGetHostAndPort(out string actualHostName, out int? actualPort);\n\n            Assert.True(result);\n            Assert.NotNull(actualHostName);\n            Assert.Equal(expectedHostName, actualHostName);\n            Assert.NotNull(actualPort);\n            Assert.Equal(expectedPort, actualPort);\n        }\n\n        [Fact]\n        public void InputArguments_TryGetHostAndPort_BadPort_ReturnsFalse()\n        {\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"]     = \"example.com:not-a-port\"\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            bool result = inputArgs.TryGetHostAndPort(out _, out int? actualPort);\n\n            Assert.False(result);\n            Assert.Null(actualPort);\n        }\n\n        [Fact]\n        public void InputArguments_TryGetHostAndPort_NoHostNoPort_ReturnsFalse()\n        {\n            var dict = new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n            };\n\n            var inputArgs = new InputArguments(dict);\n\n            bool result = inputArgs.TryGetHostAndPort(out _, out _);\n\n            Assert.False(result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs",
    "content": "using System.Collections.Generic;\nusing GitCredentialManager.Interop.Linux;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Interop.Linux;\n\npublic class LinuxConfigParserTests\n{\n    [Fact]\n    public void LinuxConfigParser_Parse()\n    {\n        const string contents =\n            \"\"\"\n            #\n            # This is a config file complete with comments\n            # and empty..\n\n            # lines, as well as lines with..\n            #                              \n            # only whitespace (like above ^), and..\n            invalid lines like this one, not a comment\n            # Here's the first real properties:\n            core.overrideMe=This is the first config value\n            baz.specialChars=I contain special chars like = in my value # this is a comment\n            # and let's have with a comment that also contains a = in side\n            #\n            core.overrideMe=This is the second config value\n            bar.scope.foo=123456\n            core.overrideMe=This is the correct value\n                  ###### comments that start ## with whitespace and extra ## inside\n            strings.one=\"here we have a dq string\"\n            strings.two='here we have a sq string'\n            strings.three=    'here we have another sq string'   # have another sq string\n            strings.four=\"this has 'nested quotes' inside\"\n            strings.five='mixed \"quotes\" the other way around'\n            strings.six='this has an \\'escaped\\' set of quotes'\n            \"\"\";\n\n        var expected = new Dictionary<string, string>\n        {\n            [\"core.overrideMe\"] = \"This is the correct value\",\n            [\"bar.scope.foo\"] = \"123456\",\n            [\"baz.specialChars\"] = \"I contain special chars like = in my value\",\n            [\"strings.one\"] = \"here we have a dq string\",\n            [\"strings.two\"] = \"here we have a sq string\",\n            [\"strings.three\"] = \"here we have another sq string\",\n            [\"strings.four\"] = \"this has 'nested quotes' inside\",\n            [\"strings.five\"] = \"mixed \\\"quotes\\\" the other way around\",\n            [\"strings.six\"] = \"this has an \\\\'escaped\\\\' set of quotes\",\n        };\n\n        var parser = new LinuxConfigParser(new NullTrace());\n\n        Assert.Equal(expected, parser.Parse(contents));\n    }\n}"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Linux/LinuxFileSystemTests.cs",
    "content": "using System.IO;\nusing GitCredentialManager.Interop.Linux;\nusing Xunit;\nusing static GitCredentialManager.Tests.TestUtils;\n\nnamespace GitCredentialManager.Tests.Interop.Linux\n{\n    public class LinuxFileSystemTests\n    {\n        [LinuxFact]\n        public static void LinuxFileSystem_IsSamePath_SamePath_ReturnsTrue()\n        {\n            var fs = new LinuxFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA = CreateFile(baseDir, \"a.file\");\n\n            Assert.True(fs.IsSamePath(fileA, fileA));\n        }\n\n        [LinuxFact]\n        public static void LinuxFileSystem_IsSamePath_DifferentFile_ReturnsFalse()\n        {\n            var fs = new LinuxFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA = CreateFile(baseDir, \"a.file\");\n            string fileB = CreateFile(baseDir, \"b.file\");\n\n            Assert.False(fs.IsSamePath(fileA, fileB));\n            Assert.False(fs.IsSamePath(fileB, fileA));\n        }\n\n        [LinuxFact]\n        public static void LinuxFileSystem_IsSamePath_SameFileDifferentCase_ReturnsFalse()\n        {\n            var fs = new LinuxFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = Path.Combine(baseDir, \"A.file\");\n\n            Assert.False(fs.IsSamePath(fileA1, fileA2));\n            Assert.False(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [LinuxFact]\n        public static void LinuxFileSystem_IsSamePath_SameFileDifferentPathNormalization_ReturnsTrue()\n        {\n            var fs = new LinuxFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string subDir = CreateDirectory(baseDir, \"subDir1\", \"subDir2\");\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = Path.Combine(subDir, \"..\", \"..\", \"a.file\");\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [LinuxFact]\n        public static void LinuxFileSystem_IsSamePath_SameFileViaSymlink_ReturnsTrue()\n        {\n            var fs = new LinuxFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = CreateFileSymlink(baseDir, \"a.link\", fileA1);\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [LinuxFact]\n        public static void LinuxFileSystem_IsSamePath_SameFileRelativePath_ReturnsTrue()\n        {\n            var fs = new LinuxFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = \"./a.file\";\n\n            using (ChangeDirectory(baseDir))\n            {\n                Assert.True(fs.IsSamePath(fileA1, fileA2));\n                Assert.True(fs.IsSamePath(fileA2, fileA1));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs",
    "content": "using System.Collections.Generic;\nusing GitCredentialManager.Interop.Linux;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Interop.Linux;\n\npublic class LinuxSettingsTests\n{\n    [LinuxFact]\n    public void LinuxSettings_TryGetExternalDefault_CombinesFiles()\n    {\n        var env = new TestEnvironment();\n        var git = new TestGit();\n        var trace = new NullTrace();\n        var fs = new TestFileSystem();\n\n        var utf8 = EncodingEx.UTF8NoBom;\n\n        fs.Directories = new HashSet<string>\n        {\n            \"/\",\n            \"/etc\",\n            \"/etc/git-credential-manager\",\n            \"/etc/git-credential-manager/config.d\"\n        };\n\n        const string config1 = \"core.overrideMe=value1\";\n        const string config2 = \"core.overrideMe=value2\";\n        const string config3 = \"core.overrideMe=value3\";\n\n        fs.Files = new Dictionary<string, byte[]>\n        {\n            [\"/etc/git-credential-manager/config.d/01-first\"] = utf8.GetBytes(config1),\n            [\"/etc/git-credential-manager/config.d/02-second\"] = utf8.GetBytes(config2),\n            [\"/etc/git-credential-manager/config.d/03-third\"] = utf8.GetBytes(config3),\n        };\n\n        var settings = new LinuxSettings(env, git, trace, fs);\n\n        bool result = settings.TryGetExternalDefault(\n            \"core\", null, \"overrideMe\", out string value);\n\n        Assert.True(result);\n        Assert.Equal(\"value3\", value);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Linux/SecretServiceCollectionTests.cs",
    "content": "using System;\nusing Xunit;\nusing GitCredentialManager.Interop.Linux;\n\nnamespace GitCredentialManager.Tests.Interop.Linux\n{\n    public class SecretServiceCollectionTests\n    {\n        private const string TestNamespace = \"git-test\";\n\n        [LinuxFact(Skip = \"Cannot run headless\")]\n        public void SecretServiceCollection_ReadWriteDelete()\n        {\n            var collection = new SecretServiceCollection(TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            try\n            {\n                // Write\n                collection.AddOrUpdate(service, userName, password);\n\n                // Read\n                ICredential outCredential = collection.Get(service, userName);\n\n                Assert.NotNull(outCredential);\n                Assert.Equal(userName, userName);\n                Assert.Equal(password, outCredential.Password);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                collection.Remove(service, userName);\n            }\n        }\n\n        [LinuxFact(Skip = \"Cannot run headless\")]\n        public void SecretServiceCollection_Get_NotFound_ReturnsNull()\n        {\n            var collection = new SecretServiceCollection(TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            ICredential credential = collection.Get(service, null);\n            Assert.Null(credential);\n        }\n\n        [LinuxFact(Skip = \"Cannot run headless\")]\n        public void SecretServiceCollection_Remove_NotFound_ReturnsFalse()\n        {\n            var collection = new SecretServiceCollection(TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            bool result = collection.Remove(service, account: null);\n            Assert.False(result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/MacOS/MacOSFileSystemTests.cs",
    "content": "using System.IO;\nusing GitCredentialManager.Interop.MacOS;\nusing Xunit;\nusing static GitCredentialManager.Tests.TestUtils;\n\nnamespace GitCredentialManager.Tests.Interop.MacOS\n{\n    public class MacOSFileSystemTests\n    {\n        [MacOSFact]\n        public static void MacOSFileSystem_IsSamePath_SamePath_ReturnsTrue()\n        {\n            var fs = new MacOSFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA = CreateFile(baseDir, \"a.file\");\n\n            Assert.True(fs.IsSamePath(fileA, fileA));\n        }\n\n        [MacOSFact]\n        public static void MacOSFileSystem_IsSamePath_DifferentFile_ReturnsFalse()\n        {\n            var fs = new MacOSFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA = CreateFile(baseDir, \"a.file\");\n            string fileB = CreateFile(baseDir, \"b.file\");\n\n            Assert.False(fs.IsSamePath(fileA, fileB));\n            Assert.False(fs.IsSamePath(fileB, fileA));\n        }\n\n        [MacOSFact]\n        public static void MacOSFileSystem_IsSamePath_SameFileDifferentCase_ReturnsTrue()\n        {\n            var fs = new MacOSFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = Path.Combine(baseDir, \"A.file\");\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [MacOSFact]\n        public static void MacOSFileSystem_IsSamePath_SameFileDifferentPathNormalization_ReturnsTrue()\n        {\n            var fs = new MacOSFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string subDir = CreateDirectory(baseDir, \"subDir1\", \"subDir2\");\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = Path.Combine(subDir, \"..\", \"..\", \"a.file\");\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [MacOSFact]\n        public static void MacOSFileSystem_IsSamePath_SameFileViaSymlink_ReturnsTrue()\n        {\n            var fs = new MacOSFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = CreateFileSymlink(baseDir, \"a.link\", fileA1);\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [MacOSFact]\n        public static void MacOSFileSystem_IsSamePath_SameFileRelativePath_ReturnsTrue()\n        {\n            var fs = new MacOSFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = \"./a.file\";\n\n            using (ChangeDirectory(baseDir))\n            {\n                Assert.True(fs.IsSamePath(fileA1, fileA2));\n                Assert.True(fs.IsSamePath(fileA2, fileA1));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/MacOS/MacOSKeychainTests.cs",
    "content": "using System;\nusing Xunit;\nusing GitCredentialManager.Interop;\nusing GitCredentialManager.Interop.MacOS;\nusing GitCredentialManager.Interop.MacOS.Native;\n\nnamespace GitCredentialManager.Tests.Interop.MacOS\n{\n    public class MacOSKeychainTests\n    {\n        private const string TestNamespace = \"git-test\";\n\n        [MacOSFact]\n        public void MacOSKeychain_ReadWriteDelete()\n        {\n            var keychain = new MacOSKeychain(TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n            const string account = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            try\n            {\n                // Write\n                keychain.AddOrUpdate(service, account, password);\n\n                // Read\n                ICredential outCredential = keychain.Get(service, account);\n\n                Assert.NotNull(outCredential);\n                Assert.Equal(account, outCredential.Account);\n                Assert.Equal(password, outCredential.Password);\n            }\n            // There is an unknown issue that the keychain can sometimes get itself in where all API calls\n            // result in an errSecAuthFailed error. The only solution seems to be a machine restart, which\n            // isn't really possible in CI!\n            // The problem has plagued others who are calling the same Keychain APIs from C# such as the\n            // MSAL.NET team - they don't know either. It might have something to do with the code signing\n            // signature of the binary (our collective best theory).\n            // It's probably only diagnosable at this point by Apple, but we don't have a reliable way to\n            // reproduce the problem.\n            // For now we will just mark the test as \"skipped\" when we hit this problem.\n            catch (InteropException iex) when (iex.ErrorCode == SecurityFramework.ErrorSecAuthFailed)\n            {\n                AssertEx.Skip(\"macOS Keychain is in an invalid state (errSecAuthFailed)\");\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                keychain.Remove(service, account);\n            }\n        }\n\n        [MacOSFact]\n        public void MacOSKeychain_Get_NotFound_ReturnsNull()\n        {\n            var keychain = new MacOSKeychain(TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            ICredential credential = keychain.Get(service, account: null);\n            Assert.Null(credential);\n        }\n\n        [MacOSFact]\n        public void MacOSKeychain_Remove_NotFound_ReturnsFalse()\n        {\n            var keychain = new MacOSKeychain(TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            bool result = keychain.Remove(service, account: null);\n            Assert.False(result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Xunit;\nusing GitCredentialManager.Interop.MacOS;\nusing static GitCredentialManager.Tests.TestUtils;\n\nnamespace GitCredentialManager.Tests.Interop.MacOS;\n\npublic class MacOSPreferencesTests\n{\n    private const string TestAppId = \"com.example.gcm-test\";\n    private const string DefaultsPath = \"/usr/bin/defaults\";\n\n    [MacOSFact]\n    public async Task MacOSPreferences_ReadPreferences()\n    {\n        try\n        {\n            await SetupTestPreferencesAsync();\n\n            var pref = new MacOSPreferences(TestAppId);\n\n            // Exists\n            string stringValue = pref.GetString(\"myString\");\n            int? intValue = pref.GetInteger(\"myInt\");\n            IDictionary<string, string> dictValue = pref.GetDictionary(\"myDict\");\n\n            Assert.NotNull(stringValue);\n            Assert.Equal(\"this is a string\", stringValue);\n            Assert.NotNull(intValue);\n            Assert.Equal(42, intValue);\n            Assert.NotNull(dictValue);\n            Assert.Equal(2, dictValue.Count);\n            Assert.Equal(\"value1\", dictValue[\"dict-k1\"]);\n            Assert.Equal(\"value2\", dictValue[\"dict-k2\"]);\n\n            // Does not exist\n            string missingString = pref.GetString(\"missingString\");\n            int? missingInt = pref.GetInteger(\"missingInt\");\n            IDictionary<string, string> missingDict = pref.GetDictionary(\"missingDict\");\n\n            Assert.Null(missingString);\n            Assert.Null(missingInt);\n            Assert.Null(missingDict);\n        }\n        finally\n        {\n            await CleanupTestPreferencesAsync();\n        }\n    }\n\n    private static async Task SetupTestPreferencesAsync()\n    {\n        // Using the defaults command set up preferences for the test app\n        await RunCommandAsync(DefaultsPath, $\"write {TestAppId} myString \\\"this is a string\\\"\");\n        await RunCommandAsync(DefaultsPath, $\"write {TestAppId} myInt -int 42\");\n        await RunCommandAsync(DefaultsPath, $\"write {TestAppId} myDict -dict dict-k1 value1 dict-k2 value2\");\n    }\n\n    private static async Task CleanupTestPreferencesAsync()\n    {\n        // Delete the test app preferences\n        // defaults delete com.example.gcm-test\n        await RunCommandAsync(DefaultsPath, $\"delete {TestAppId}\");\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Posix/GnuPassCredentialStoreTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Text;\nusing Xunit;\nusing GitCredentialManager.Interop.Posix;\nusing GitCredentialManager.Tests.Objects;\n\nnamespace GitCredentialManager.Tests.Interop.Posix\n{\n    public class GnuPassCredentialStoreTests\n    {\n        private const string TestNamespace = \"git-test\";\n\n        [PosixFact]\n        public void GnuPassCredentialStore_ReadWriteDelete()\n        {\n            var fs = new TestFileSystem();\n            var gpg = new TestGpg(fs);\n            string storeRoot = InitializePasswordStore(fs, gpg);\n\n            var collection = new GpgPassCredentialStore(fs, gpg, storeRoot, TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string uniqueGuid = Guid.NewGuid().ToString(\"N\");\n            string service = $\"https://example.com/{uniqueGuid}\";\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedSlug = $\"{TestNamespace}/https/example.com/{uniqueGuid}/{userName}.gpg\";\n            string expectedFilePath = Path.Combine(storeRoot, expectedSlug);\n            string expectedFileContents = password + Environment.NewLine +\n                                          $\"service={service}\" + Environment.NewLine +\n                                          $\"account={userName}\" + Environment.NewLine;\n            byte[] expectedFileBytes = Encoding.UTF8.GetBytes(expectedFileContents);\n\n            try\n            {\n                // Write\n                collection.AddOrUpdate(service, userName, password);\n\n                // Read\n                ICredential outCredential = collection.Get(service, userName);\n\n                Assert.NotNull(outCredential);\n                Assert.Equal(userName, userName);\n                Assert.Equal(password, outCredential.Password);\n                Assert.True(fs.Files.ContainsKey(expectedFilePath));\n                Assert.Equal(expectedFileBytes, fs.Files[expectedFilePath]);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                collection.Remove(service, userName);\n            }\n        }\n\n        [PosixFact]\n        public void GnuPassCredentialStore_Get_NotFound_ReturnsNull()\n        {\n            var fs = new TestFileSystem();\n            var gpg = new TestGpg(fs);\n            string storeRoot = InitializePasswordStore(fs, gpg);\n\n            var collection = new GpgPassCredentialStore(fs, gpg, storeRoot, TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            ICredential credential = collection.Get(service, null);\n            Assert.Null(credential);\n        }\n\n        [PosixFact]\n        public void GnuPassCredentialStore_Remove_NotFound_ReturnsFalse()\n        {\n            var fs = new TestFileSystem();\n            var gpg = new TestGpg(fs);\n            string storeRoot = InitializePasswordStore(fs, gpg);\n\n            var collection = new GpgPassCredentialStore(fs, gpg, storeRoot, TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            bool result = collection.Remove(service, account: null);\n            Assert.False(result);\n        }\n\n        [PosixFact]\n        public void GnuPassCredentialStore_ReadWriteDelete_GpgIdInSubdirectory()\n        {\n            var fs = new TestFileSystem();\n            var gpg = new TestGpg(fs);\n\n            string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);\n            string storePath = Path.Combine(homePath, \".password-store\");\n            const string userId = \"gcm-test@example.com\";\n\n            // Place .gpg-id only in the namespace subdirectory (not the store root),\n            // simulating a pass store where the root has no .gpg-id but submodules do.\n            string subDirPath = Path.Combine(storePath, TestNamespace);\n            string gpgIdPath = Path.Combine(subDirPath, \".gpg-id\");\n\n            gpg.GenerateKeys(userId);\n\n            fs.Directories.Add(storePath);\n            fs.Directories.Add(subDirPath);\n            fs.Files[gpgIdPath] = Encoding.UTF8.GetBytes(userId);\n\n            var collection = new GpgPassCredentialStore(fs, gpg, storePath, TestNamespace);\n\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n            const string userName = \"john.doe\";\n            string password = Guid.NewGuid().ToString(\"N\");\n\n            try\n            {\n                // Write\n                collection.AddOrUpdate(service, userName, password);\n\n                // Read\n                ICredential outCredential = collection.Get(service, userName);\n\n                Assert.NotNull(outCredential);\n                Assert.Equal(userName, outCredential.Account);\n                Assert.Equal(password, outCredential.Password);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                collection.Remove(service, userName);\n            }\n        }\n\n        [PosixFact]\n        public void GnuPassCredentialStore_WriteCredential_MultipleGpgIds_UsesNearestGpgId()\n        {\n            // Verify that when two subdirectories each have their own .gpg-id, encrypting a credential\n            // under one subdirectory uses that subdirectory's GPG identity, not the other one.\n            var fs = new TestFileSystem();\n            var gpg = new TestGpg(fs);\n\n            string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);\n            string storePath = Path.Combine(homePath, \".password-store\");\n\n            const string personalUserId = \"personal@example.com\";\n            const string workUserId = \"work@example.com\";\n\n            // Only register the personal key; if the wrong (work) key is picked, EncryptFile will throw.\n            gpg.GenerateKeys(personalUserId);\n\n            string personalSubDir = Path.Combine(storePath, \"personal\");\n            string workSubDir = Path.Combine(storePath, \"work\");\n\n            fs.Directories.Add(storePath);\n            fs.Directories.Add(personalSubDir);\n            fs.Directories.Add(workSubDir);\n            fs.Files[Path.Combine(personalSubDir, \".gpg-id\")] = Encoding.UTF8.GetBytes(personalUserId);\n            fs.Files[Path.Combine(workSubDir, \".gpg-id\")] = Encoding.UTF8.GetBytes(workUserId);\n\n            // Use \"personal\" namespace so credentials are stored under storePath/personal/...\n            var collection = new GpgPassCredentialStore(fs, gpg, storePath, \"personal\");\n\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n            const string userName = \"john.doe\";\n            string password = Guid.NewGuid().ToString(\"N\");\n\n            try\n            {\n                // Write - should pick personal/.gpg-id (personalUserId), not work/.gpg-id (workUserId)\n                collection.AddOrUpdate(service, userName, password);\n\n                ICredential outCredential = collection.Get(service, userName);\n\n                Assert.NotNull(outCredential);\n                Assert.Equal(userName, outCredential.Account);\n                Assert.Equal(password, outCredential.Password);\n            }\n            finally\n            {\n                collection.Remove(service, userName);\n            }\n        }\n\n        private static string InitializePasswordStore(TestFileSystem fs, TestGpg gpg)\n        {\n            string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);\n            string storePath = Path.Combine(homePath, \".password-store\");\n            string userId = \"gcm-test@example.com\";\n            string gpgIdPath = Path.Combine(storePath, \".gpg-id\");\n\n            // Ensure we have a GPG key for use with testing\n            gpg.GenerateKeys(userId);\n\n            // Init the password store\n            fs.Directories.Add(storePath);\n            fs.Files[gpgIdPath] = Encoding.UTF8.GetBytes(userId);\n\n            return storePath;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Posix/PosixFileSystemTests.cs",
    "content": "using System.IO;\nusing GitCredentialManager.Interop.Posix;\nusing Xunit;\nusing static GitCredentialManager.Tests.TestUtils;\n\nnamespace GitCredentialManager.Tests.Interop.Posix\n{\n    public class PosixFileSystemTests\n    {\n        [PosixFact]\n        public void PosixFileSystem_ResolveSymlinks_FileLinks()\n        {\n            string baseDir = GetTempDirectory();\n            string realPath = CreateFile(baseDir, \"realFile.txt\");\n            string linkPath = CreateFileSymlink(baseDir, \"linkFile.txt\", realPath);\n\n            string actual = PosixFileSystem.ResolveSymbolicLinks(linkPath);\n\n            Assert.Equal(realPath, actual);\n        }\n\n        [PosixFact]\n        public void PosixFileSystem_ResolveSymlinks_DirectoryLinks()\n        {\n            //\n            // Create a real file inside of a directory that is a symlink\n            // to another directory.\n            //\n            //     /tmp/{uuid}/linkDir/ -> /tmp/{uuid}/realDir/\n            //\n            string baseDir = GetTempDirectory();\n            string realDir = CreateDirectory(baseDir, \"realDir\");\n            string linkDir = CreateDirectorySymlink(baseDir, \"linkDir\", realDir);\n            string filePath = CreateFile(linkDir, \"file.txt\");\n\n            string actual = PosixFileSystem.ResolveSymbolicLinks(filePath);\n\n            string expected = Path.Combine(realDir, \"file.txt\");\n\n            Assert.Equal(expected, actual);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/U8StringConverterTests.cs",
    "content": "using System;\nusing GitCredentialManager.Interop;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Interop\n{\n    public class U8StringConverterTests\n    {\n        /*\n         * Grinning face with squinting eyes and sweat drop\n         *   😅\n         *   Codepoint : U+1F605\n         *   UTF-16    : 0xD83D 0xDE05\n         *   UTF-8     : 0xF0 0x9F 0x98 0x85\n         *\n         * Latin small letter dotless I\n         *   ı\n         *   Codepoint : U+0131\n         *   UTF-16    : 0x0131\n         *   UTF-8     : 0xC4 0xB1\n         *\n         * Greek capital letter omega\n         *   Ω\n         *   Codepoint : U+03A9\n         *   UTF-16    : 0x03A9\n         *   UTF-8     : 0xCE 0xA9\n         *\n         * Snowman without snow\n         *   ⛄\n         *   Codepoint : U+26C4\n         *   UTF-16    : 0x26C4\n         *   UTF-8     : 0xE2 0x9B 0x84\n         */\n\n        // \"Unicode 😅 ıs awesome! Ω ⛄\";\n        private const string ComplexString = \"Unicode \\uD83D\\uDE05 \\u0131s awesome! \\u03A9 \\u26C4\";\n        private static readonly byte[] ComplexUtf8 =\n        {\n            (byte)'U', (byte)'n', (byte)'i', (byte)'c', (byte)'o', (byte)'d', (byte)'e', (byte)' ',\n            (byte)'\\u00F0', (byte)'\\u009F', (byte)'\\u0098', (byte)'\\u0085', (byte)' ',\n            (byte)'\\u00C4', (byte)'\\u00B1', (byte)'s', (byte)' ',\n            (byte)'a', (byte)'w', (byte)'e', (byte)'s', (byte)'o', (byte)'m', (byte)'e', (byte)'!', (byte)' ',\n            (byte)'\\u00CE', (byte)'\\u00A9', (byte)' ',\n            (byte)'\\u00E2', (byte)'\\u009B', (byte)'\\u0084', (byte)'\\0'\n        };\n\n        private const string SimpleString = \"Hello, World!\";\n        private static readonly byte[] SimpleUtf8 =\n        {\n            (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)',', (byte)' ',\n            (byte)'W', (byte)'o', (byte)'r', (byte)'l', (byte)'d', (byte)'!', (byte)'\\0'\n        };\n\n        private static readonly byte[] NullString = {(byte) '\\0'};\n\n        [Fact]\n        public void U8StringConverter_ToNative_Null_ReturnsNullPointer()\n        {\n            IntPtr actual = U8StringConverter.ToNative(null);\n\n            Assert.Equal(IntPtr.Zero, actual);\n        }\n\n\n        [Fact]\n        public void U8StringConverter_ToNative_EmptyString_ReturnsNullByte()\n        {\n            unsafe\n            {\n                byte* actual = (byte*) U8StringConverter.ToNative(string.Empty);\n\n                AssertCStringEqual(NullString,  actual);\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToNative_SimpleString_ReturnsExpectedBytes()\n        {\n            unsafe\n            {\n                byte* actual = (byte*) U8StringConverter.ToNative(SimpleString);\n\n                AssertCStringEqual(SimpleUtf8,  actual);\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToNative_ComplexString_ReturnsExpectedBytes()\n        {\n            unsafe\n            {\n                byte* actual = (byte*) U8StringConverter.ToNative(ComplexString);\n\n                AssertCStringEqual(ComplexUtf8,  actual);\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToManaged_Null_ReturnsNull()\n        {\n            unsafe\n            {\n                string actual = U8StringConverter.ToManaged(null);\n\n                Assert.Null(actual);\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToManaged_ZeroPtr_ReturnsNull()\n        {\n            unsafe\n            {\n                string actual = U8StringConverter.ToManaged((byte*) IntPtr.Zero);\n\n                Assert.Null(actual);\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToManaged_NullByte_ReturnsEmptyString()\n        {\n            unsafe\n            {\n                fixed (byte* ptr = NullString)\n                {\n                    string actual = U8StringConverter.ToManaged(ptr);\n\n                    Assert.Equal(string.Empty, actual);\n                }\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToManaged_SimpleString_ReturnsExpectedString()\n        {\n            unsafe\n            {\n                fixed (byte* ptr = SimpleUtf8)\n                {\n                    string actual = U8StringConverter.ToManaged(ptr);\n\n                    Assert.Equal(SimpleString, actual);\n                }\n            }\n        }\n\n        [Fact]\n        public void U8StringConverter_ToManaged_ComplexString_ReturnsExpectedString()\n        {\n            unsafe\n            {\n                fixed (byte* ptr = ComplexUtf8)\n                {\n                    string actual = U8StringConverter.ToManaged(ptr);\n\n                    Assert.Equal(ComplexString, actual);\n                }\n            }\n        }\n\n        private static unsafe void AssertCStringEqual(byte[] expected, byte* actual)\n        {\n            for (int i = 0; i < expected.Length; i++)\n            {\n                byte actualByte = *(actual + i);\n\n                // Check we don't hit a null terminating byte too soon\n                if (i < expected.Length - 1)\n                {\n                    Assert.NotEqual((byte) '\\0', actualByte);\n                }\n\n                Assert.Equal(expected[i], actualByte);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Windows/DpapiCredentialStoreTests.cs",
    "content": "using System;\nusing Xunit;\nusing GitCredentialManager.Interop.Windows;\nusing GitCredentialManager.Tests.Objects;\nusing System.IO;\nusing System.Text;\nusing System.Security.Cryptography;\n\nnamespace GitCredentialManager.Tests.Interop.Windows\n{\n    public class DpapiCredentialStoreTests\n    {\n        private const string TestStoreRoot = @\"C:\\dpapi_store\";\n        private const string TestNamespace = \"git-test\";\n\n        [WindowsFact]\n        public void DpapiCredentialStore_AddOrUpdate_CreatesUTF8ProtectedFile()\n        {\n            var fs = new TestFileSystem();\n            var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);\n\n            string service = \"https://example.com\";\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedServiceSlug = Path.Combine(TestNamespace, \"https\", \"example.com\");\n            string expectedFileName = $\"{userName}.credential\";\n            string expectedFilePath = Path.Combine(TestStoreRoot, expectedServiceSlug, expectedFileName);\n\n            store.AddOrUpdate(service, userName, password);\n\n            Assert.True(fs.Directories.Contains(Path.Combine(TestStoreRoot, expectedServiceSlug)));\n            Assert.True(fs.Files.TryGetValue(expectedFilePath, out byte[] data));\n\n            string contents = Encoding.UTF8.GetString(data);\n            Assert.False(string.IsNullOrWhiteSpace(contents));\n\n            string[] lines = contents.Split(Environment.NewLine);\n\n            Assert.Equal(4, lines.Length);\n\n            byte[] cryptoData = Convert.FromBase64String(lines[0]);\n            byte[] plainData = ProtectedData.Unprotect(cryptoData, null, DataProtectionScope.CurrentUser);\n            string plainLine0 = Encoding.UTF8.GetString(plainData);\n\n            Assert.Equal(password, plainLine0);\n            Assert.Equal($\"service={service}\", lines[1]);\n            Assert.Equal($\"account={userName}\", lines[2]);\n            Assert.True(string.IsNullOrWhiteSpace(lines[3]));\n        }\n\n        [WindowsFact]\n        public void DpapiCredentialStore_Get_KeyNotFound_ReturnsNull()\n        {\n            var fs = new TestFileSystem();\n            var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = Guid.NewGuid().ToString(\"N\");\n\n            ICredential credential = store.Get(service, account: null);\n            Assert.Null(credential);\n        }\n\n        [WindowsFact]\n        public void DpapiCredentialStore_Get_ReadProtectedFile()\n        {\n            var fs = new TestFileSystem();\n            var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);\n\n            string service = \"https://example.com\";\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string serviceSlug = Path.Combine(TestNamespace, \"https\", \"example.com\");\n            string fileName = $\"{userName}.credential\";\n            string filePath = Path.Combine(TestStoreRoot, serviceSlug, fileName);\n\n            byte[] plainData = Encoding.UTF8.GetBytes(password);\n            byte[] cryptoData = ProtectedData.Protect(plainData, null, DataProtectionScope.CurrentUser);\n            string cryptoLine0 = Convert.ToBase64String(cryptoData);\n\n            var contents = new StringBuilder();\n            contents.AppendLine(cryptoLine0);\n            contents.AppendLine($\"service={service}\");\n            contents.AppendLine($\"account={userName}\");\n            contents.AppendLine();\n\n            byte[] data = Encoding.UTF8.GetBytes(contents.ToString());\n\n            fs.Directories.Add(Path.Combine(TestStoreRoot, serviceSlug));\n            fs.Files[filePath] = data;\n\n            ICredential credential = store.Get(service, userName);\n\n            Assert.Equal(password, credential.Password);\n            Assert.Equal(userName, credential.Account);\n        }\n\n        [WindowsFact]\n        public void DpapiCredentialStore_Remove_KeyNotFound_ReturnsFalse()\n        {\n            var fs = new TestFileSystem();\n            var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = Guid.NewGuid().ToString(\"N\");\n\n            bool result = store.Remove(service, account: null);\n            Assert.False(result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Windows/WindowsCredentialManagerTests.cs",
    "content": "using System;\nusing Xunit;\nusing GitCredentialManager.Interop.Windows;\nusing GitCredentialManager.Interop.Windows.Native;\n\nnamespace GitCredentialManager.Tests.Interop.Windows\n{\n    public class WindowsCredentialManagerTests\n    {\n        private const string TestNamespace = \"git-test\";\n\n        [WindowsFact]\n        public void WindowsCredentialManager_ReadWriteDelete()\n        {\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string uniqueGuid = Guid.NewGuid().ToString(\"N\");\n            string service = $\"https://example.com/{uniqueGuid}\";\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedTargetName = $\"{TestNamespace}:https://example.com/{uniqueGuid}\";\n\n            try\n            {\n                // Write\n                credManager.AddOrUpdate(service, userName, password);\n\n                // Read\n                ICredential cred = credManager.Get(service, userName);\n\n                // Valdiate\n                var winCred = cred as WindowsCredential;\n                Assert.NotNull(winCred);\n                Assert.Equal(userName, winCred.UserName);\n                Assert.Equal(password, winCred.Password);\n                Assert.Equal(service, winCred.Service);\n                Assert.Equal(expectedTargetName, winCred.TargetName);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                credManager.Remove(service, userName);\n            }\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_AddOrUpdate_UsernameWithAtCharacter()\n        {\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string uniqueGuid = Guid.NewGuid().ToString(\"N\");\n            string service = $\"https://example.com/{uniqueGuid}\";\n            const string userName = \"john.doe@auth.com\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedTargetName = $\"{TestNamespace}:https://example.com/{uniqueGuid}\";\n\n            try\n            {\n                // Write\n                credManager.AddOrUpdate(service, userName, password);\n\n                // Read\n                ICredential cred = credManager.Get(service, userName);\n\n                // Validate\n                var winCred = cred as WindowsCredential;\n                Assert.NotNull(winCred);\n                Assert.Equal(userName, winCred.UserName);\n                Assert.Equal(password, winCred.Password);\n                Assert.Equal(service, winCred.Service);\n                Assert.Equal(expectedTargetName, winCred.TargetName);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                credManager.Remove(service, userName);\n            }\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_Get_KeyNotFound_ReturnsNull()\n        {\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = Guid.NewGuid().ToString(\"N\");\n\n            ICredential credential = credManager.Get(service, account: null);\n            Assert.Null(credential);\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_Remove_KeyNotFound_ReturnsFalse()\n        {\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = Guid.NewGuid().ToString(\"N\");\n\n            bool result = credManager.Remove(service, account: null);\n            Assert.False(result);\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_AddOrUpdate_TargetNameAlreadyExists_CreatesWithUserInTargetName()\n        {\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string uniqueGuid = Guid.NewGuid().ToString(\"N\");\n            string service = $\"https://example.com/{uniqueGuid}\";\n            const string userName1 = \"john.doe\";\n            const string userName2 = \"jane.doe\";\n            const string password1 = \"letmein123\";  // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string password2 = \"password123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedTargetName1 = $\"{TestNamespace}:https://example.com/{uniqueGuid}\";\n            string expectedTargetName2 = $\"{TestNamespace}:https://{userName2}@example.com/{uniqueGuid}\";\n\n            try\n            {\n                // Add first credential\n                credManager.AddOrUpdate(service, userName1, password1);\n\n                // Add second credential\n                credManager.AddOrUpdate(service, userName2, password2);\n\n                // Validate first credential properties\n                ICredential cred1 = credManager.Get(service, userName1);\n                var winCred1 = cred1 as WindowsCredential;\n                Assert.NotNull(winCred1);\n                Assert.Equal(userName1, winCred1.UserName);\n                Assert.Equal(password1, winCred1.Password);\n                Assert.Equal(service,   winCred1.Service);\n                Assert.Equal(expectedTargetName1, winCred1.TargetName);\n\n                // Validate second credential properties\n                ICredential cred2 = credManager.Get(service, userName2);\n                var winCred2 = cred2 as WindowsCredential;\n                Assert.NotNull(winCred2);\n                Assert.Equal(userName2, winCred2.UserName);\n                Assert.Equal(password2, winCred2.Password);\n                Assert.Equal(service,   winCred2.Service);\n                Assert.Equal(expectedTargetName2, winCred2.TargetName);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves in case of failures\n                credManager.Remove(service, userName1);\n                credManager.Remove(service, userName2);\n            }\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_AddOrUpdate_TargetNameAlreadyExistsAndUserWithAtCharacter_CreatesWithEscapedUserInTargetName()\n        {\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string uniqueGuid = Guid.NewGuid().ToString(\"N\");\n            string service = $\"https://example.com/{uniqueGuid}\";\n            const string userName1 = \"john.doe@auth.com\";\n            const string userName2 = \"jane.doe@auth.com\";\n            const string escapedUserName2 = \"jane.doe_auth.com\";\n            const string password1 = \"letmein123\";  // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string password2 = \"password123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedTargetName1 = $\"{TestNamespace}:https://example.com/{uniqueGuid}\";\n            string expectedTargetName2 = $\"{TestNamespace}:https://{escapedUserName2}@example.com/{uniqueGuid}\";\n\n            try\n            {\n                // Add first credential\n                credManager.AddOrUpdate(service, userName1, password1);\n\n                // Add second credential\n                credManager.AddOrUpdate(service, userName2, password2);\n\n                // Validate first credential properties\n                ICredential cred1 = credManager.Get(service, userName1);\n                var winCred1 = cred1 as WindowsCredential;\n                Assert.NotNull(winCred1);\n                Assert.Equal(userName1, winCred1.UserName);\n                Assert.Equal(password1, winCred1.Password);\n                Assert.Equal(service,   winCred1.Service);\n                Assert.Equal(expectedTargetName1, winCred1.TargetName);\n\n                // Validate second credential properties\n                ICredential cred2 = credManager.Get(service, userName2);\n                var winCred2 = cred2 as WindowsCredential;\n                Assert.NotNull(winCred2);\n                Assert.Equal(userName2, winCred2.UserName);\n                Assert.Equal(password2, winCred2.Password);\n                Assert.Equal(service,   winCred2.Service);\n                Assert.Equal(expectedTargetName2, winCred2.TargetName);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves in case of failures\n                credManager.Remove(service, userName1);\n                credManager.Remove(service, userName2);\n            }\n        }\n\n        [Theory]\n        [InlineData(\"https://example.com\", \"https://example.com\")]\n        [InlineData(\"https://example.com/\", \"https://example.com/\")]\n        [InlineData(\"https://example.com/@\", \"https://example.com/@\")]\n        [InlineData(\"https://example.com/path\", \"https://example.com/path\")]\n        [InlineData(\"https://example.com/path@\", \"https://example.com/path@\")]\n        [InlineData(\"https://example.com:123/path@\", \"https://example.com:123/path@\")]\n        [InlineData(\"https://example.com/path/\", \"https://example.com/path/\")]\n        [InlineData(\"https://example.com/path@/\", \"https://example.com/path@/\")]\n        [InlineData(\"https://example.com:123/path@/\", \"https://example.com:123/path@/\")]\n        [InlineData(\"https://example.com/path/foo\", \"https://example.com/path/foo\")]\n        [InlineData(\"https://example.com/path@/foo\", \"https://example.com/path@/foo\")]\n        [InlineData(\"https://userinfo@example.com\", \"https://example.com\")]\n        [InlineData(\"https://userinfo@example.com/\", \"https://example.com/\")]\n        [InlineData(\"https://userinfo@example.com/@\", \"https://example.com/@\")]\n        [InlineData(\"https://userinfo@example.com/path\", \"https://example.com/path\")]\n        [InlineData(\"https://userinfo@example.com/path@\", \"https://example.com/path@\")]\n        [InlineData(\"https://userinfo@example.com/path/\", \"https://example.com/path/\")]\n        [InlineData(\"https://userinfo@example.com:123/path/\", \"https://example.com:123/path/\")]\n        [InlineData(\"https://userinfo@example.com/path@/\", \"https://example.com/path@/\")]\n        [InlineData(\"https://userinfo@example.com:123/path@/\", \"https://example.com:123/path@/\")]\n        [InlineData(\"https://userinfo@example.com/path/foo\", \"https://example.com/path/foo\")]\n        [InlineData(\"https://userinfo@example.com/path@/foo\", \"https://example.com/path@/foo\")]\n        public void WindowsCredentialManager_RemoveUriUserInfo(string input, string expected)\n        {\n            string actual = WindowsCredentialManager.RemoveUriUserInfo(input);\n            Assert.Equal(expected, actual);\n        }\n\n        [WindowsTheory]\n        [InlineData(\"https://example.com\", null, \"https://example.com\", \"alice\", true)]\n        [InlineData(\"https://example.com\", \"alice\", \"https://example.com\", \"alice\", true)]\n        [InlineData(\"https://example.com\", null, \"https://example.com:443\", \"alice\", true)]\n        [InlineData(\"https://example.com\", \"alice\", \"https://example.com:443\", \"alice\", true)]\n        [InlineData(\"https://example.com:1234\", null, \"https://example.com:1234\", \"alice\", true)]\n        [InlineData(\"https://example.com:1234\", \"alice\", \"https://example.com:1234\", \"alice\", true)]\n        [InlineData(\"https://example.com\", null, \"http://example.com\", \"alice\", false)]\n        [InlineData(\"https://example.com\", \"alice\", \"http://example.com\", \"alice\", false)]\n        [InlineData(\"https://example.com\", \"alice\", \"http://example.com:443\", \"alice\", false)]\n        [InlineData(\"http://example.com:443\", \"alice\", \"https://example.com\", \"alice\", false)]\n        [InlineData(\"https://example.com\", \"bob\", \"https://example.com\", \"alice\", false)]\n        [InlineData(\"https://example.com\", \"bob\", \"https://bob@example.com\", \"bob\", true)]\n        [InlineData(\"https://example.com\", \"bob\", \"https://example.com\", \"bob\", true)]\n        [InlineData(\"https://example.com\", \"alice\", \"https://example.com\", \"ALICE\", false)] // username case sensitive\n        [InlineData(\"https://example.com\", \"alice\", \"https://EXAMPLE.com\", \"alice\", true)] // host NOT case sensitive\n        [InlineData(\"https://example.com/path\", \"alice\", \"https://example.com/path\", \"alice\", true)]\n        [InlineData(\"https://example.com/path\", \"alice\", \"https://example.com/PATH\", \"alice\", true)] // path NOT case sensitive\n        public void WindowsCredentialManager_IsMatch(\n            string service, string account, string targetName, string userName, bool expected)\n        {\n            string fullTargetName = $\"{WindowsCredentialManager.TargetNameLegacyGenericPrefix}{TestNamespace}:{targetName}\";\n            var win32Cred = new Win32Credential\n            {\n                UserName =  userName,\n                TargetName = fullTargetName\n            };\n\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            bool actual = credManager.IsMatch(service, account, win32Cred);\n\n            Assert.Equal(expected, actual);\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_IsMatch_NoNamespace_NotMatched()\n        {\n            var win32Cred = new Win32Credential\n            {\n                UserName = \"test\",\n                TargetName = $\"{WindowsCredentialManager.TargetNameLegacyGenericPrefix}https://example.com\"\n            };\n\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            bool result = credManager.IsMatch(\"https://example.com\", null, win32Cred);\n\n            Assert.False(result);\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_IsMatch_DifferentNamespace_NotMatched()\n        {\n            var win32Cred = new Win32Credential\n            {\n                UserName = \"test\",\n                TargetName = $\"{WindowsCredentialManager.TargetNameLegacyGenericPrefix}:random-namespace:https://example.com\"\n            };\n\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            bool result = credManager.IsMatch(\"https://example.com\", null, win32Cred);\n\n            Assert.False(result);\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_IsMatch_CaseSensitiveNamespace_NotMatched()\n        {\n            var win32Cred = new Win32Credential\n            {\n                UserName = \"test\",\n                TargetName = $\"{WindowsCredentialManager.TargetNameLegacyGenericPrefix}:nAmEsPaCe:https://example.com\"\n            };\n\n            var credManager = new WindowsCredentialManager(\"namespace\");\n\n            bool result = credManager.IsMatch(\"https://example.com\", null, win32Cred);\n\n            Assert.False(result);\n        }\n\n        [WindowsFact]\n        public void WindowsCredentialManager_IsMatch_NoNamespaceInQuery_IsMatched()\n        {\n            var win32Cred = new Win32Credential\n            {\n                UserName = \"test\",\n                TargetName = $\"{WindowsCredentialManager.TargetNameLegacyGenericPrefix}https://example.com\"\n            };\n\n            var credManager = new WindowsCredentialManager();\n\n            bool result = credManager.IsMatch(\"https://example.com\", null, win32Cred);\n\n            Assert.True(result);\n        }\n\n        [WindowsTheory]\n        [InlineData(\"https://example.com\", null, \"https://example.com\")]\n        [InlineData(\"https://example.com\", \"bob\", \"https://bob@example.com\")]\n        [InlineData(\"https://example.com\", \"bob@id.example.com\", \"https://bob_id.example.com@example.com\")] // @ in user\n        [InlineData(\"https://example.com:443\", null, \"https://example.com\")] // default port\n        [InlineData(\"https://example.com:1234\", null, \"https://example.com:1234\")]\n        [InlineData(\"https://example.com/path\", null, \"https://example.com/path\")]\n        [InlineData(\"https://example.com/path/with/more/parts\", null, \"https://example.com/path/with/more/parts\")]\n        [InlineData(\"https://example.com/path/trim/\", null, \"https://example.com/path/trim\")] // path trailing slash\n        [InlineData(\"https://example.com/\", null, \"https://example.com\")] // no path trailing slash\n        public void WindowsCredentialManager_CreateTargetName(string service, string account, string expected)\n        {\n            string fullExpected = $\"{TestNamespace}:{expected}\";\n\n            var credManager = new WindowsCredentialManager(TestNamespace);\n\n            string actual = credManager.CreateTargetName(service, account);\n\n            Assert.Equal(fullExpected, actual);\n        }\n\n        [WindowsTheory]\n        [InlineData(TestNamespace, \"https://example.com\", null, $\"{TestNamespace}:https://example.com\")]\n        [InlineData(null, \"https://example.com\", null, \"https://example.com\")]\n        [InlineData(\"\", \"https://example.com\", null, \"https://example.com\")]\n        [InlineData(\"    \", \"https://example.com\", null, \"https://example.com\")]\n        public void WindowsCredentialManager_CreateTargetName_Namespace(string @namespace, string service, string account, string expected)\n        {\n            var credManager = new WindowsCredentialManager(@namespace);\n\n            string actual = credManager.CreateTargetName(service, account);\n\n            Assert.Equal(expected, actual);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Windows/WindowsFileSystemTests.cs",
    "content": "using System.IO;\nusing GitCredentialManager.Interop.Windows;\nusing Xunit;\nusing static GitCredentialManager.Tests.TestUtils;\n\nnamespace GitCredentialManager.Tests.Interop.Windows\n{\n    public class WindowsFileSystemTests\n    {\n        [WindowsFact]\n        public static void WindowsFileSystem_IsSamePath_SamePath_ReturnsTrue()\n        {\n            var fs = new WindowsFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA = CreateFile(baseDir, \"a.file\");\n\n            Assert.True(fs.IsSamePath(fileA, fileA));\n        }\n\n        [WindowsFact]\n        public static void WindowsFileSystem_IsSamePath_DifferentFile_ReturnsFalse()\n        {\n            var fs = new WindowsFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA = CreateFile(baseDir, \"a.file\");\n            string fileB = CreateFile(baseDir, \"b.file\");\n\n            Assert.False(fs.IsSamePath(fileA, fileB));\n            Assert.False(fs.IsSamePath(fileB, fileA));\n        }\n\n        [WindowsFact]\n        public static void WindowsFileSystem_IsSamePath_SameFileDifferentCase_ReturnsTrue()\n        {\n            var fs = new WindowsFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = Path.Combine(baseDir, \"A.file\");\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [WindowsFact]\n        public static void WindowsFileSystem_IsSamePath_SameFileDifferentPathNormalization_ReturnsTrue()\n        {\n            var fs = new WindowsFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string subDir = CreateDirectory(baseDir, \"subDir1\", \"subDir2\");\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = Path.Combine(subDir, \"..\", \"..\", \"a.file\");\n\n            Assert.True(fs.IsSamePath(fileA1, fileA2));\n            Assert.True(fs.IsSamePath(fileA2, fileA1));\n        }\n\n        [WindowsFact]\n        public static void WindowsFileSystem_IsSamePath_SameFileRelativePath_ReturnsTrue()\n        {\n            var fs = new WindowsFileSystem();\n\n            string baseDir = GetTempDirectory();\n            string fileA1 = CreateFile(baseDir, \"a.file\");\n            string fileA2 = @\".\\a.file\";\n\n            using (ChangeDirectory(baseDir))\n            {\n                Assert.True(fs.IsSamePath(fileA1, fileA2));\n                Assert.True(fs.IsSamePath(fileA2, fileA1));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Interop/Windows/WindowsSystemPromptsTests.cs",
    "content": "using System;\nusing GitCredentialManager.Interop.Windows;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Interop.Windows\n{\n    public class WindowsSystemPromptsTests\n    {\n        [Fact]\n        public void WindowsSystemPrompts_ShowCredentialPrompt_NullResource_ThrowsException()\n        {\n            var sysPrompts = new WindowsSystemPrompts();\n            Assert.Throws<ArgumentNullException>(() => sysPrompts.ShowCredentialPrompt(null, null, out _));\n        }\n\n        [Fact]\n        public void WindowsSystemPrompts_ShowCredentialPrompt_EmptyResource_ThrowsException()\n        {\n            var sysPrompts = new WindowsSystemPrompts();\n            Assert.Throws<ArgumentException>(() => sysPrompts.ShowCredentialPrompt(string.Empty, null, out _));\n        }\n\n        [Fact]\n        public void WindowsSystemPrompts_ShowCredentialPrompt_WhiteSpaceResource_ThrowsException()\n        {\n            var sysPrompts = new WindowsSystemPrompts();\n            Assert.Throws<ArgumentException>(() => sysPrompts.ShowCredentialPrompt(\"   \", null, out _));\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/PlaintextCredentialStoreTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Text;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class PlaintextCredentialStoreTests\n    {\n        private const string TestNamespace = \"git-test\";\n        private const string StoreRoot = \"/tmp/test-store\";\n\n        [Fact]\n        public void PlaintextCredentialStore_ReadWriteDelete()\n        {\n            var fs = new TestFileSystem();\n\n            var collection = new PlaintextCredentialStore(fs, StoreRoot, TestNamespace);\n\n            // Create a service that is guaranteed to be unique\n            string uniqueGuid = Guid.NewGuid().ToString(\"N\");\n            string service = $\"https://example.com/{uniqueGuid}\";\n            const string userName = \"john.doe\";\n            const string password = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n\n            string expectedSlug = Path.Combine(\n                TestNamespace,\n                \"https\",\n                \"example.com\",\n                uniqueGuid,\n                $\"{userName}.credential\");\n            string expectedFilePath = Path.Combine(StoreRoot, expectedSlug);\n            string expectedFileContents = password + Environment.NewLine +\n                                          $\"service={service}\" + Environment.NewLine +\n                                          $\"account={userName}\" + Environment.NewLine;\n            byte[] expectedFileBytes = Encoding.UTF8.GetBytes(expectedFileContents);\n\n            try\n            {\n                // Write\n                collection.AddOrUpdate(service, userName, password);\n\n                // Read\n                ICredential outCredential = collection.Get(service, userName);\n\n                Assert.NotNull(outCredential);\n                Assert.Equal(userName, userName);\n                Assert.Equal(password, outCredential.Password);\n                Assert.True(fs.Files.ContainsKey(expectedFilePath));\n                Assert.Equal(expectedFileBytes, fs.Files[expectedFilePath]);\n            }\n            finally\n            {\n                // Ensure we clean up after ourselves even in case of 'get' failures\n                collection.Remove(service, userName);\n            }\n        }\n\n        [Fact]\n        public void PlaintextCredentialStore_Get_NotFound_ReturnsNull()\n        {\n            var fs = new TestFileSystem();\n\n            var collection = new PlaintextCredentialStore(fs, StoreRoot, TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            ICredential credential = collection.Get(service, null);\n            Assert.Null(credential);\n        }\n\n        [Fact]\n        public void PlaintextCredentialStore_Remove_NotFound_ReturnsFalse()\n        {\n            var fs = new TestFileSystem();\n\n            var collection = new PlaintextCredentialStore(fs, StoreRoot, TestNamespace);\n\n            // Unique service; guaranteed not to exist!\n            string service = $\"https://example.com/{Guid.NewGuid():N}\";\n\n            bool result = collection.Remove(service, account: null);\n            Assert.False(result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/ProcessManagerTests.cs",
    "content": "using GitCredentialManager;\nusing Xunit;\n\nnamespace Core.Tests;\n\npublic class ProcessManagerTests\n{\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"foo\", 0)]\n    [InlineData(\"foo/bar\", 1)]\n    [InlineData(\"foo/bar/baz\", 2)]\n    public void CreateSid_Envar_Returns_Expected_Sid(string input, int expected)\n    {\n        ProcessManager.Sid = input;\n        var actual = ProcessManager.GetProcessDepth();\n\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"foo\", 0)]\n    [InlineData(\"foo/bar\", 1)]\n    [InlineData(\"foo/bar/baz\", 2)]\n    public void TryGetProcessDepth_Returns_Expected_Depth(string input, int expected)\n    {\n        ProcessManager.Sid = input;\n        var actual = ProcessManager.GetProcessDepth();\n\n        Assert.Equal(expected, actual);\n    }\n}"
  },
  {
    "path": "src/shared/Core.Tests/SettingsTests.cs",
    "content": "using System;\nusing System.Linq;\nusing System.Net;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class SettingsTests\n    {\n        [Fact]\n        public void Settings_IsDebuggingEnabled_EnvarUnset_ReturnsFalse()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsDebuggingEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsDebuggingEnabled_EnvarTruthy_ReturnsTrue()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmDebug] = \"1\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsDebuggingEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsDebuggingEnabled_EnvarFalsey_ReturnsFalse()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmDebug] = \"0\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsDebuggingEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsTerminalPromptsEnabled_EnvarUnset_ReturnsTrue()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsTerminalPromptsEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsTerminalPromptsEnabled_EnvarTruthy_ReturnsTrue()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GitTerminalPrompts] = \"1\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsTerminalPromptsEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsTerminalPromptsEnabled_EnvarFalsey_ReturnsFalse()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GitTerminalPrompts] = \"0\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsTerminalPromptsEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_EnvarUnset_ReturnsTrue()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_EnvarTruthy_ReturnsTrue()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmInteractive] = \"1\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_EnvarFalsey_ReturnsFalse()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmInteractive] = \"0\"},\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_ConfigAuto_ReturnsTrue()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Interactive;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"auto\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_ConfigAlways_ReturnsTrue()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Interactive;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"always\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_ConfigNever_ReturnsFalse()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Interactive;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"never\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_ConfigTruthy_ReturnsTrue()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Interactive;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"1\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_ConfigFalsey_ReturnsFalse()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Interactive;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"0\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsInteractionAllowed_ConfigNonBooleanyValue_ReturnsTrue()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Interactive;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {Guid.NewGuid().ToString()};\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsInteractionAllowed);\n        }\n\n        [Fact]\n        public void Settings_IsTracingEnabled_EnvarUnset_ReturnsFalse()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n            var result = settings.GetTracingEnabled(out string actualValue);\n\n            Assert.False(result);\n        }\n\n        [Fact]\n        public void Settings_IsTracingEnabled_EnvarTruthy_ReturnsTrueOutValue()\n        {\n            const string expectedValue = \"1\";\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmTrace] = expectedValue}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n            var result = settings.GetTracingEnabled(out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_IsTracingEnabled_EnvarFalsey_ReturnsFalseOutValue()\n        {\n            const string expectedValue = \"0\";\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmTrace] = expectedValue}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n            var result = settings.GetTracingEnabled(out string actualValue);\n\n            Assert.False(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n\n        [Fact]\n        public void Settings_IsTracingEnabled_EnvarPathy_ReturnsTrueOutValue()\n        {\n            const string expectedValue = \"/tmp/gcm.log\";\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmTrace] = expectedValue}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n            var result = settings.GetTracingEnabled(out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_IsSecretTracingEnabled_EnvarUnset_ReturnsFalse()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsSecretTracingEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsSecretTracingEnabled_EnvarTruthy_ReturnsTrue()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmTraceSecrets] = \"1\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsSecretTracingEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsSecretTracingEnabled_EnvarFalsey_ReturnsFalse()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmTraceSecrets] = \"0\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsSecretTracingEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarUnset_ReturnsTrue()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarTruthy_ReturnsTrue()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmAllowWia] = \"1\"}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarFalsey_ReturnsFalse()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmAllowWia] = \"0\"},\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarNonBooleanyValue_ReturnsTrue()\n        {\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmAllowWia] = Guid.NewGuid().ToString(\"N\")},\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigUnset_ReturnsTrue()\n        {\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigTruthy_ReturnsTrue()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.AllowWia;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"1\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigFalsey_ReturnsFalse()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.AllowWia;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {\"0\"};\n\n            var settings = new Settings(envars, git);\n\n            Assert.False(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Fact]\n        public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigNonBooleanyValue_ReturnsTrue()\n        {\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.AllowWia;\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {Guid.NewGuid().ToString()};\n\n            var settings = new Settings(envars, git);\n\n            Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);\n        }\n\n        [Theory]\n        [InlineData(\"\", new string[0])]\n        [InlineData(\"    \", new string[0])]\n        [InlineData(\",\", new string[0])]\n        [InlineData(\"example.com\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\"example.com:8080\", new[] { @\"(\\.|\\:\\/\\/)example\\.com:8080$\" })]\n        [InlineData(\"example.com,\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\",example.com\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\",example.com,\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\".example.com\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\"..example.com\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\"*.example.com\", new[] { @\"(\\.|\\:\\/\\/)example\\.com$\" })]\n        [InlineData(\"my.example.com\", new[] { @\"(\\.|\\:\\/\\/)my\\.example\\.com$\" })]\n        [InlineData(\"example.com,contoso.com,fabrikam.com\", new[]\n        {\n            @\"(\\.|\\:\\/\\/)example\\.com$\",\n            @\"(\\.|\\:\\/\\/)contoso\\.com$\",\n            @\"(\\.|\\:\\/\\/)fabrikam\\.com$\"\n        })]\n        public void Settings_ProxyConfiguration_ConvertToBypassRegexArray(string input, string[] expected)\n        {\n            string[] actual = ProxyConfiguration.ConvertToBypassRegexArray(input).ToArray();\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"example.com\", \"http://example.com\", true)]\n        [InlineData(\"example.com\", \"https://example.com\", true)]\n        [InlineData(\"example.com\", \"https://www.example.com\", true)]\n        [InlineData(\"example.com\", \"http://www.example.com:80\", true)]\n        [InlineData(\"example.com\", \"https://www.example.com:443\", true)]\n        [InlineData(\"example.com\", \"https://www.example.com:8080\", false)]\n        [InlineData(\"example.com\", \"http://notanexample.com\", false)]\n        [InlineData(\"example.com\", \"https://notanexample.com\", false)]\n        [InlineData(\"example.com\", \"https://www.notanexample.com\", false)]\n        [InlineData(\"example.com\", \"https://example.com.otherltd\", false)]\n        [InlineData(\"example.com:8080\", \"http://example.com\", false)]\n        [InlineData(\"my.example.com\", \"http://example.com\", false)]\n        public void Settings_ProxyConfiguration_ConvertToBypassRegexArray_WebProxyBypass(string noProxy, string address, bool expected)\n        {\n            var bypassList = ProxyConfiguration.ConvertToBypassRegexArray(noProxy).ToArray();\n            var webProxy = new WebProxy(\"https://localhost:8080/proxy\")\n            {\n                BypassList = bypassList\n            };\n\n            bool actual = webProxy.IsBypassed(new Uri(address));\n\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_Unset_ReturnsNull()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration config = settings.GetProxyConfiguration();\n\n            Assert.Null(config);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_GcmHttpConfig_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.HttpProxy;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy}\n            };\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {settingValue.ToString()};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.True(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_GcmHttpsConfig_ReturnsValue()\n        {\n            const string remoteUrl = \"https://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.HttpsProxy;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy}\n            };\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {settingValue.ToString()};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.True(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_GitHttpConfig_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Http.SectionName;\n            const string property = Constants.GitConfiguration.Http.Proxy;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy}\n            };\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {settingValue.ToString()};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.False(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_GitHttpConfig_EmptyScopedUriUnscoped_ReturnsNull()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Http.SectionName;\n            const string property = Constants.GitConfiguration.Http.Proxy;\n            var remoteUri = new Uri(remoteUrl);\n\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {settingValue.ToString()};\n            git.Configuration.Global[$\"{section}.{remoteUrl}.{property}\"] = new[] {string.Empty};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.Null(actualConfig);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_CurlHttpEnvar_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables =\n                {\n                    [Constants.EnvironmentVariables.CurlHttpProxy] = settingValue.ToString(),\n                    [Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy\n                }\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.False(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_CurlHttpsEnvar_ReturnsValue()\n        {\n            const string remoteUrl = \"https://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables =\n                {\n                    [Constants.EnvironmentVariables.CurlHttpsProxy] = settingValue.ToString(),\n                    [Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy\n                }\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.False(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_TryGetProxy_CurlAllEnvar_ReturnsValue()\n        {\n            const string remoteUrl = \"https://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables =\n                {\n                    [Constants.EnvironmentVariables.CurlAllProxy] = settingValue.ToString(),\n                    [Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy\n                }\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.False(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_LegacyGcmEnvar_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedUserName = \"john.doe\";\n            const string expectedPassword = \"letmein123\";\n            var expectedAddress = new Uri(\"http://proxy.example.com\");\n            var settingValue = new Uri(\"http://john.doe:letmein123@proxy.example.com\");\n            var expectedNoProxy = \"contoso.com,fabrikam.com\";\n\n            var envars = new TestEnvironment\n            {\n                Variables =\n                {\n                    [Constants.EnvironmentVariables.GcmHttpProxy] = settingValue.ToString(),\n                    [Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy\n                }\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n\n            Assert.NotNull(actualConfig);\n            Assert.Equal(expectedAddress, actualConfig.Address);\n            Assert.Equal(expectedUserName, actualConfig.UserName);\n            Assert.Equal(expectedPassword, actualConfig.Password);\n            Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);\n            Assert.True(actualConfig.IsDeprecatedSource);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_Precedence_ReturnsValue()\n        {\n            // 1. GCM proxy Git configuration (deprecated)\n            //      credential.httpsProxy\n            //      credential.httpProxy\n            // 2. Standard Git configuration\n            //      http.proxy\n            // 3. cURL environment variables\n            //      http_proxy\n            //      HTTPS_PROXY\n            //      http_proxy (note that uppercase HTTP_PROXY is not supported by libcurl)\n            //      all_proxy\n            //      ALL_PROXY\n            // 4. GCM proxy environment variable (deprecated)\n            //      GCM_HTTP_PROXY\n\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            var value1 = new Uri(\"http://proxy1.example.com\");\n            var value2 = new Uri(\"http://proxy2.example.com\");\n            var value3 = new Uri(\"http://proxy3.example.com\");\n            var value4 = new Uri(\"http://proxy4.example.com\");\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            void RunTest(Uri expectedValue)\n            {\n                var settings = new Settings(envars, git)\n                {\n                    RemoteUri = remoteUri\n                };\n                ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n                Assert.Equal(expectedValue, actualConfig.Address);\n            }\n\n             // Test case 1: cURL environment variables > GCM_HTTP_PROXY\n            envars.Variables[Constants.EnvironmentVariables.GcmHttpProxy] = value1.ToString();\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpProxy] = value2.ToString();\n            RunTest(value2);\n\n            // Test case 2: http.proxy > cURL environment variables\n            string httpProxyKey = $\"{Constants.GitConfiguration.Http.SectionName}.{Constants.GitConfiguration.Http.Proxy}\";\n            git.Configuration.Global[httpProxyKey] = new[] {value3.ToString()};\n            RunTest(value3);\n\n            // Test case 3: credential.httpProxy > http.proxy\n            string credentialProxyKey = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.HttpProxy}\";\n            git.Configuration.Global[credentialProxyKey] = new[] {value4.ToString()};\n            RunTest(value4);\n        }\n\n        [Fact]\n        public void Settings_ProxyConfiguration_CurlEnvarPrecedence_PrefersLowercase()\n        {\n            // Expected precedence:\n            //  https_proxy\n            //  HTTPS_PROXY\n            //  http_proxy (note that uppercase HTTP_PROXY is not supported by libcurl)\n            //  all_proxy\n            //  ALL_PROXY\n\n            const string remoteUrl = \"https://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            var value1 = new Uri(\"http://proxy1.example.com\");\n            var value2 = new Uri(\"http://proxy2.example.com\");\n\n            // The differentiation between upper- and lowercase environment variables is not possible\n            // on some platforms (Windows) so force the comparer to case-sensitive in the test so we\n            // can run this on all platforms.\n            var envars = new TestEnvironment(envarComparer: StringComparer.Ordinal);\n            var git = new TestGit();\n\n            void RunTest(Uri expectedValue)\n            {\n                var settings = new Settings(envars, git)\n                {\n                    RemoteUri = remoteUri\n                };\n                ProxyConfiguration actualConfig = settings.GetProxyConfiguration();\n                Assert.Equal(expectedValue, actualConfig.Address);\n            }\n\n            // Test case 1: https_proxy > HTTPS_PROXY\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpsProxy] = value1.ToString();\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpsProxyUpper] = value2.ToString();\n            RunTest(value1);\n\n            // Test case 2a: https_proxy > http_proxy\n            envars.Variables.Clear();\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpsProxy] = value1.ToString();\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpProxy] = value2.ToString();\n            RunTest(value1);\n\n            // Test case 2b: HTTPS_PROXY > http_proxy\n            envars.Variables.Clear();\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpsProxyUpper] = value1.ToString();\n            envars.Variables[Constants.EnvironmentVariables.CurlHttpProxy] = value2.ToString();\n            RunTest(value1);\n\n            // Test case 3: all_proxy > ALL_PROXY\n            envars.Variables.Clear();\n            envars.Variables[Constants.EnvironmentVariables.CurlAllProxy] = value1.ToString();\n            envars.Variables[Constants.EnvironmentVariables.CurlAllProxyUpper] = value2.ToString();\n            RunTest(value1);\n        }\n\n        [Fact]\n        public void Settings_ProviderOverride_Unset_ReturnsNull()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string value = settings.ProviderOverride;\n\n            Assert.Null(value);\n        }\n\n        [Fact]\n        public void Settings_ProviderOverride_EnvarSet_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"provider1\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmProvider] = expectedValue}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string actualValue = settings.ProviderOverride;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_ProviderOverride_ConfigSet_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Provider;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"provider1\";\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {expectedValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string actualValue = settings.ProviderOverride;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_ProviderOverride_EnvarAndConfigSet_ReturnsEnvarValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Provider;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"provider1\";\n            const string otherValue = \"provider2\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmProvider] = expectedValue}\n            };\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {otherValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string actualValue = settings.ProviderOverride;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_LegacyAuthorityOverride_Unset_ReturnsNull()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string value = settings.LegacyAuthorityOverride;\n\n            Assert.Null(value);\n        }\n\n        [Fact]\n        public void Settings_LegacyAuthorityOverride_EnvarSet_ReturnsValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"provider1\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmAuthority] = expectedValue}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string actualValue = settings.LegacyAuthorityOverride;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_LegacyAuthorityOverride_ConfigSet_ReturnsTrueOutValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Authority;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"provider1\";\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {expectedValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var actualValue = settings.LegacyAuthorityOverride;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_LegacyAuthorityOverride_EnvarAndConfigSet_ReturnsEnvarValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string section = Constants.GitConfiguration.Credential.SectionName;\n            const string property = Constants.GitConfiguration.Credential.Authority;\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"provider1\";\n            const string otherValue = \"provider2\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[Constants.EnvironmentVariables.GcmAuthority] = expectedValue}\n            };\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {otherValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var actualValue = settings.LegacyAuthorityOverride;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_TryGetSetting_EnvarSet_ReturnsTrueOutValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"Hello, World!\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[envarName] = expectedValue}\n            };\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var result = settings.TryGetSetting(envarName, section, property, out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_TryGetSetting_EnvarUnset_ReturnsFalse()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var result = settings.TryGetSetting(envarName, section, property, out string actualValue);\n\n            Assert.False(result);\n            Assert.Null(actualValue);\n        }\n\n        [Fact]\n        public void Settings_TryGetSetting_GlobalConfig_ReturnsTrueAndValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"Hello, World!\";\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Global[$\"{section}.{property}\"] = new[] {expectedValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var result = settings.TryGetSetting( envarName, section, property, out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_TryGetSetting_RepoConfig_ReturnsTrueAndValue()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"Hello, World!\";\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Local[$\"{section}.{property}\"] = new[] {expectedValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var result = settings.TryGetSetting(envarName, section, property, out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_TryGetSetting_ScopedConfig()\n        {\n            const string remoteUrl = \"http://example.com/foo/bar/bazz.git\";\n            const string scope1 = \"example.com\";\n            const string scope2 = \"example.com/foo/bar\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"Hello, World!\";\n            const string otherValue = \"Goodbye, World!\";\n\n            var envars = new TestEnvironment();\n            var git = new TestGit();\n            git.Configuration.Local[$\"{section}.{scope1}.{property}\"] = new []{otherValue};\n            git.Configuration.Local[$\"{section}.{scope2}.{property}\"] = new []{expectedValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var result = settings.TryGetSetting(envarName, section, property, out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_TryGetSetting_EnvarAndConfig_EnvarTakesPrecedence()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string expectedValue = \"Hello, World!\";\n            const string otherValue = \"Goodbye, World!\";\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[envarName] = expectedValue}\n            };\n            var git = new TestGit();\n            git.Configuration.Local[$\"{section}.{property}\"] = new[] {otherValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            var result = settings.TryGetSetting(envarName, section, property, out string actualValue);\n\n            Assert.True(result);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        [Fact]\n        public void Settings_GetSettingValues_EnvarAndMultipleConfig_ReturnsAllWithCorrectPrecedence()\n        {\n            const string remoteUrl = \"http://example.com/foo.git\";\n            const string scope1 = \"http://example.com\";\n            const string scope2 = \"example.com\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string value1 = \"First value\";\n            const string value2 = \"Second value\";\n            const string value3 = \"Third value\";\n            const string value4 = \"Last value\";\n\n            string[] expectedValues = {value1, value2, value3, value4};\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[envarName] = value1}\n            };\n            var git = new TestGit();\n            git.Configuration.Local[$\"{section}.{scope1}.{property}\"] = new[]{value2};\n            git.Configuration.Local[$\"{section}.{scope2}.{property}\"] = new[]{value3};\n            git.Configuration.Local[$\"{section}.{property}\"]          = new[]{value4};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n            string[] actualValues = settings.GetSettingValues(envarName, section, property, false).ToArray();\n\n            Assert.Equal(expectedValues, actualValues);\n        }\n\n        [Fact]\n        public void Settings_GetSettingValues_ReturnsAllMatchingValues()\n        {\n            const string remoteUrl = \"http://example.com/foo/bar/bazz.git\";\n            const string broadScope = \"example.com\";\n            const string tightScope = \"example.com/foo/bar\";\n            const string otherScope1 = \"test.com\";\n            const string otherScope2 = \"sub.test.com\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string envarValue = \"envar-value\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string tightScopeValue = \"value-scope1\";\n            const string broadScopeValue = \"value-scope2\";\n            const string noScopeValue = \"value-no-scope\";\n            const string otherValue1 = \"other-scope1\";\n            const string otherValue2 = \"other-scope2\";\n\n            string[] expectedValues = {envarValue, tightScopeValue, broadScopeValue, noScopeValue};\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[envarName] = envarValue}\n            };\n\n            var git = new TestGit();\n            git.Configuration.Local[$\"{section}.{property}\"] = new[] {noScopeValue};\n            git.Configuration.Local[$\"{section}.{broadScope}.{property}\"] = new[] {broadScopeValue};\n            git.Configuration.Local[$\"{section}.{tightScope}.{property}\"] = new[] {tightScopeValue};\n            git.Configuration.Local[$\"{section}.{otherScope1}.{property}\"] = new[] {otherValue1};\n            git.Configuration.Local[$\"{section}.{otherScope2}.{property}\"] = new[] {otherValue2};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            string[] actualValues = settings.GetSettingValues(envarName, section, property, false).ToArray();\n\n            Assert.NotNull(actualValues);\n            Assert.Equal(expectedValues, actualValues);\n        }\n\n        [Fact]\n        public void Settings_GetSettingValues_IgnoresSectionAndPropertyCase_ScopeIsCaseSensitive()\n        {\n            const string remoteUrl = \"http://example.com/foo/bar/bazz.git\";\n            const string scopeLo = \"example.com\";\n            const string scopeHi = \"EXAMPLE.COM\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string envarValue = \"envar-value\";\n            const string sectionLo = \"gcmtest\";\n            const string sectionHi = \"GCMTEST\";\n            const string sectionMix = \"GcMtEsT\";\n            const string propertyLo = \"bar\";\n            const string propertyHi = \"BAR\";\n            const string propertyMix = \"bAr\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string noScopeValue = \"the-value\";\n            const string lowScopeValue = \"value-scope-lo\";\n            const string highScopeValue = \"value-scope-hi\";\n\n            string[] expectedValues = {envarValue, lowScopeValue, noScopeValue};\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[envarName] = envarValue}\n            };\n\n            var git = new TestGit();\n            git.Configuration.Local[$\"{sectionLo}.{propertyHi}\"] = new[] {noScopeValue};\n            git.Configuration.Local[$\"{sectionHi}.{scopeLo}.{propertyHi}\"] = new[] {lowScopeValue};\n            git.Configuration.Local[$\"{sectionLo}.{scopeHi}.{propertyLo}\"] = new[] {highScopeValue};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            string[] actualValues = settings.GetSettingValues(envarName, sectionMix, propertyMix, false).ToArray();\n\n            Assert.NotNull(actualValues);\n            Assert.Equal(expectedValues, actualValues);\n        }\n\n        [Theory]\n        [InlineData(false, \"~\")]\n        [InlineData(true, TestGitConfiguration.CanonicalPathPrefix)]\n        public void Settings_GetSettingValues_IsPath_ReturnsAllParsedValues(bool isPath, string expectedPrefix)\n        {\n            const string remoteUrl = \"http://example.com/foo/bar/bazz.git\";\n            const string broadScope = \"example.com\";\n            const string tightScope = \"example.com/foo/bar\";\n            const string envarName = \"GCM_TESTVAR\";\n            const string envarValue = \"envar-value\";\n            const string section = \"gcmtest\";\n            const string property = \"bar\";\n            var remoteUri = new Uri(remoteUrl);\n\n            const string tightScopeValue = \"path-tight\";\n            const string broadScopeValue = \"path-broad\";\n            const string noScopeValue = \"path-no-scope\";\n\n            string[] expectedValues = {\n                envarValue,\n                $\"{expectedPrefix}/{tightScopeValue}\",\n                broadScopeValue,\n                $\"{expectedPrefix}/{noScopeValue}\"\n            };\n\n            var envars = new TestEnvironment\n            {\n                Variables = {[envarName] = envarValue}\n            };\n\n            var git = new TestGit();\n            git.Configuration.Local[$\"{section}.{property}\"] = new[] {$\"~/{noScopeValue}\"};\n            git.Configuration.Local[$\"{section}.{broadScope}.{property}\"] = new[] {broadScopeValue};\n            git.Configuration.Local[$\"{section}.{tightScope}.{property}\"] = new[] {$\"~/{tightScopeValue}\"};\n\n            var settings = new Settings(envars, git)\n            {\n                RemoteUri = remoteUri\n            };\n\n            string[] actualValues = settings.GetSettingValues(envarName, section, property, isPath).ToArray();\n\n            Assert.NotNull(actualValues);\n            Assert.Equal(expectedValues, actualValues);\n        }\n\n        [Theory]\n        [InlineData(null, null, null)]\n        [InlineData(null, \"ca-config.crt\", \"ca-config.crt\")]\n        [InlineData(\"ca-envar.crt\", \"ca-config.crt\", \"ca-envar.crt\")]\n        public void Settings_CustomCertificateBundlePath_ReturnsExpectedValue(string sslCaInfoEnvar, string sslCaInfoConfig, string expectedValue)\n        {\n            const string envarName = Constants.EnvironmentVariables.GitSslCaInfo;\n            const string section = Constants.GitConfiguration.Http.SectionName;\n            const string sslCaInfo = Constants.GitConfiguration.Http.SslCaInfo;\n\n            var envars = new TestEnvironment();\n            if (sslCaInfoEnvar != null)\n            {\n                envars.Variables[envarName] = sslCaInfoEnvar;\n            }\n\n            var git = new TestGit();\n            if (sslCaInfoConfig != null)\n            {\n                git.Configuration.Local[$\"{section}.{sslCaInfo}\"] = new[] {sslCaInfoConfig};\n            }\n\n            var settings = new Settings(envars, git);\n\n            string actualValue = settings.CustomCertificateBundlePath;\n\n            if (expectedValue is null)\n            {\n                Assert.Null(actualValue);\n            }\n            else\n            {\n                Assert.NotNull(actualValue);\n                Assert.Equal(expectedValue, actualValue);\n            }\n        }\n\n        [Theory]\n        [InlineData(null, TlsBackend.OpenSsl)]\n        [InlineData(\"schannel\", TlsBackend.Schannel)]\n        [InlineData(\"gnutls\", TlsBackend.Other)]\n        public void Settings_TlsBackend_ReturnsExpectedValue(string sslBackendConfig, TlsBackend expectedValue)\n        {\n            const string section = Constants.GitConfiguration.Http.SectionName;\n            const string sslBackend = Constants.GitConfiguration.Http.SslBackend;\n\n            var envars = new TestEnvironment();\n\n            var git = new TestGit();\n            if (sslBackendConfig != null)\n            {\n                git.Configuration.Local[$\"{section}.{sslBackend}\"] = new[] {sslBackendConfig};\n            }\n\n            var settings = new Settings(envars, git);\n\n            TlsBackend actualValue = settings.TlsBackend;\n\n            Assert.Equal(expectedValue, actualValue);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/StreamExtensionsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class StreamExtensionsTests\n    {\n        private const string LF   = \"\\n\";\n        private const string CRLF = \"\\r\\n\";\n\n        #region Dictionary\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_EmptyString_ReturnsEmptyDictionary()\n        {\n            string input = string.Empty;\n\n            var output = ReadStringStream(input, StreamExtensions.ReadDictionary);\n\n            Assert.NotNull(output);\n            Assert.Empty(output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_TerminatedLF_ReturnsDictionary()\n        {\n            string input = \"a=1\\nb=2\\nc=3\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n            AssertDictionary(\"1\", \"a\", output);\n            AssertDictionary(\"2\", \"b\", output);\n            AssertDictionary(\"3\", \"c\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_TerminatedCRLF_ReturnsDictionary()\n        {\n            string input = \"a=1\\r\\nb=2\\r\\nc=3\\r\\n\\r\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n            AssertDictionary(\"1\", \"a\", output);\n            AssertDictionary(\"2\", \"b\", output);\n            AssertDictionary(\"3\", \"c\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_CaseSensitive_ReturnsDictionaryWithMultipleEntries()\n        {\n            string input = \"a=1\\nA=2\\n\\n\";\n\n            var output = ReadStringStream(input, x => StreamExtensions.ReadDictionary(x, StringComparer.Ordinal));\n\n            Assert.NotNull(output);\n            Assert.Equal(2, output.Count);\n            AssertDictionary(\"1\", \"a\", output);\n            AssertDictionary(\"2\", \"A\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_CaseInsensitive_ReturnsDictionaryWithLastValue()\n        {\n            string input = \"a=1\\nA=2\\n\\n\";\n\n            var output = ReadStringStream(input, x => StreamExtensions.ReadDictionary(x, StringComparer.OrdinalIgnoreCase));\n\n            Assert.NotNull(output);\n            Assert.Single(output);\n            AssertDictionary(\"2\", \"a\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_Spaces_ReturnsCorrectKeysAndValues()\n        {\n            string input = \"key a=value 1\\n  key b  = 2 \\nkey\\tc\\t=\\t3\\t\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n            AssertDictionary(\"value 1\", \"key a\", output);\n            AssertDictionary(\" 2 \", \"  key b  \", output);\n            AssertDictionary(\"\\t3\\t\", \"key\\tc\\t\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadDictionary_EqualsInValues_ReturnsCorrectKeysAndValues()\n        {\n            string input = \"a=value=1\\nb=value=2\\nc=value=3\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n            AssertDictionary(\"value=1\", \"a\", output);\n            AssertDictionary(\"value=2\", \"b\", output);\n            AssertDictionary(\"value=3\", \"c\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterLF_EmptyDictionary_WritesLineLF()\n        {\n            var input = new Dictionary<string, string>();\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(LF, output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterCRLF_EmptyDictionary_WritesLineCRLF()\n        {\n            var input = new Dictionary<string, string>();\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: CRLF);\n\n            Assert.Equal(CRLF, output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterLF_Entries_WritesKVPsAndLF()\n        {\n            var input = new Dictionary<string, string>\n            {\n                [\"a\"] = \"1\",\n                [\"b\"] = \"2\",\n                [\"c\"] = \"3\"\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(\"a=1\\nb=2\\nc=3\\n\\n\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterCRLF_Entries_WritesKVPsAndCRLF()\n        {\n            var input = new Dictionary<string, string>\n            {\n                [\"a\"] = \"1\",\n                [\"b\"] = \"2\",\n                [\"c\"] = \"3\"\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: CRLF);\n\n            Assert.Equal(\"a=1\\r\\nb=2\\r\\nc=3\\r\\n\\r\\n\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterLF_EntriesWithSpaces_WritesKVPsAndLF()\n        {\n            var input = new Dictionary<string, string>\n            {\n                [\"key a\"] = \"value 1\",\n                [\"  key b  \"] = \" value 2 \",\n                [\"\\tvalue\\tc\\t\"] = \"\\t3\\t\"\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(\"key a=value 1\\n  key b  = value 2 \\n\\tvalue\\tc\\t=\\t3\\t\\n\\n\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterCRLF_EntriesWithSpaces_WritesKVPsAndCRLF()\n        {\n            var input = new Dictionary<string, string>\n            {\n                [\"key a\"] = \"value 1\",\n                [\"  key b  \"] = \" value 2 \",\n                [\"\\tvalue\\tc\\t\"] = \"\\t3\\t\"\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: CRLF);\n\n            Assert.Equal(\"key a=value 1\\r\\n  key b  = value 2 \\r\\n\\tvalue\\tc\\t=\\t3\\t\\r\\n\\r\\n\", output);\n        }\n\n        #endregion\n\n        #region MultiDictionary\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_EmptyString_ReturnsEmptyDictionary()\n        {\n            string input = string.Empty;\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Empty(output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_TerminatedLF_ReturnsDictionary()\n        {\n            string input = \"a=1\\nb=2\\nc=3\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n\n            AssertMultiDictionary(new[] { \"1\" }, \"a\", output);\n            AssertMultiDictionary(new[] { \"2\" }, \"b\", output);\n            AssertMultiDictionary(new[] { \"3\" }, \"c\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_TerminatedCRLF_ReturnsDictionary()\n        {\n            string input = \"a=1\\r\\nb=2\\r\\nc=3\\r\\n\\r\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n            AssertMultiDictionary(new[] { \"1\" }, \"a\", output);\n            AssertMultiDictionary(new[] { \"2\" }, \"b\", output);\n            AssertMultiDictionary(new[] { \"3\" }, \"c\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_CaseSensitive_ReturnsDictionaryWithMultipleEntries()\n        {\n            string input = \"a=1\\nA=2\\n\\n\";\n\n            var output = ReadStringStream(input, x => StreamExtensions.ReadMultiDictionary(x, StringComparer.Ordinal));\n\n            Assert.NotNull(output);\n            Assert.Equal(2, output.Count);\n            AssertMultiDictionary(new[] { \"1\" }, \"a\", output);\n            AssertMultiDictionary(new[] { \"2\" }, \"A\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_CaseInsensitive_ReturnsDictionaryWithLastValue()\n        {\n            string input = \"a=1\\nA=2\\n\\n\";\n\n            var output = ReadStringStream(input, x => StreamExtensions.ReadMultiDictionary(x, StringComparer.OrdinalIgnoreCase));\n\n            Assert.NotNull(output);\n            Assert.Single(output);\n            AssertMultiDictionary(new[] { \"2\" }, \"a\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_EmptyString_ReturnsKeyWithEmptyStringValue()\n        {\n            string input = \"a=\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Single(output);\n\n            AssertMultiDictionary(new[] { String.Empty,  }, \"a\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_Spaces_ReturnsCorrectKeysAndValues()\n        {\n            string input = \"key a=value 1\\n  key b  = 2 \\nkey\\tc\\t=\\t3\\t\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n\n            AssertMultiDictionary(new[] { \"value 1\" }, \"key a\", output);\n            AssertMultiDictionary(new[] { \" 2 \" }, \"  key b  \", output);\n            AssertMultiDictionary(new[] { \"\\t3\\t\" }, \"key\\tc\\t\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_EqualsInValues_ReturnsCorrectKeysAndValues()\n        {\n            string input = \"a=value=1\\nb=value=2\\nc=value=3\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(3, output.Count);\n            AssertMultiDictionary(new[] { \"value=1\" }, \"a\", output);\n            AssertMultiDictionary(new[] { \"value=2\" }, \"b\", output);\n            AssertMultiDictionary(new[] { \"value=3\" }, \"c\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_ReadMultiDictionary_MultiValue_ReturnsDictionary()\n        {\n            string input = \"odd[]=1\\neven[]=2\\neven[]=4\\nodd[]=3\\n\\n\";\n\n            var output = ReadStringStream(input, StreamExtensions.ReadMultiDictionary);\n\n            Assert.NotNull(output);\n            Assert.Equal(2, output.Count);\n            AssertMultiDictionary(new[] { \"1\", \"3\" }, \"odd\", output);\n            AssertMultiDictionary(new[] { \"2\", \"4\" }, \"even\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterLF_EmptyMultiDictionary_WritesLineLF()\n        {\n            var input = new Dictionary<string, IList<string>>();\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(LF, output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterCRLF_EmptyMultiDictionary_WritesLineCRLF()\n        {\n            var input = new Dictionary<string, IList<string>>();\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: CRLF);\n\n            Assert.Equal(CRLF, output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterLF_MultiEntries_WritesKVPListsAndLF()\n        {\n            var input = new Dictionary<string, IList<string>>\n            {\n                [\"a\"] = new[] { \"1\", \"2\", \"3\" },\n                [\"b\"] = new[] { \"4\", \"5\", },\n                [\"c\"] = new[] { \"6\" }\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(\"a[]=1\\na[]=2\\na[]=3\\nb[]=4\\nb[]=5\\nc=6\\n\\n\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_TextWriterCRLF_MultiEntries_WritesKVPListsAndCRLF()\n        {\n            var input = new Dictionary<string, IList<string>>\n            {\n                [\"a\"] = new[] { \"1\", \"2\", \"3\" },\n                [\"b\"] = new[] { \"4\", \"5\", },\n                [\"c\"] = new[] { \"6\" }\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: CRLF);\n\n            Assert.Equal(\"a[]=1\\r\\na[]=2\\r\\na[]=3\\r\\nb[]=4\\r\\nb[]=5\\r\\nc=6\\r\\n\\r\\n\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_NoMultiEntries_WritesKVPsAndLF()\n        {\n            var input = new Dictionary<string, IList<string>>\n            {\n                [\"a\"] = new[] {\"1\"},\n                [\"b\"] = new[] {\"2\"},\n                [\"c\"] = new[] {\"3\"}\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(\"a=1\\nb=2\\nc=3\\n\\n\", output);\n        }\n\n        [Fact]\n        public void StreamExtensions_WriteDictionary_MultiEntriesWithEmpty_WritesKVPListsAndLF()\n        {\n            var input = new Dictionary<string, IList<string>>\n            {\n                [\"a\"] = new[] {\"1\", \"2\", \"\", \"3\", \"4\"},\n                [\"b\"] = new[] {\"5\"},\n                [\"c\"] = new[] {\"6\", \"7\", \"\"}\n            };\n\n            string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);\n\n            Assert.Equal(\"a[]=3\\na[]=4\\nb=5\\n\\n\", output);\n        }\n\n        #endregion\n\n        #region Helpers\n\n        private static T ReadStringStream<T>(string input, Func<TextReader, T> func)\n        {\n            T output;\n            using (var reader = new StringReader(input))\n            {\n                output = func(reader);\n            }\n\n            return output;\n        }\n\n        private static string WriteStringStream<T>(T input, Action<TextWriter, T> action, string newLine)\n        {\n            var output = new StringBuilder();\n            using (var writer = new StringWriter(output){NewLine = newLine})\n            {\n                action(writer, input);\n            }\n\n            return output.ToString();\n        }\n\n        private static void AssertDictionary(string expectedValue, string key, IDictionary<string, string> dict)\n        {\n            Assert.True(dict.TryGetValue(key, out string actualValue));\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        private static void AssertMultiDictionary(IList<string> expectedValues,\n            string key,\n            IDictionary<string, IList<string>> dict)\n        {\n            Assert.True(dict.TryGetValue(key, out IList<string> actualValues));\n            Assert.Equal(expectedValues, actualValues);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/StringExtensionsTests.cs",
    "content": "using System;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class StringExtensionsTests\n    {\n        [Theory]\n        [InlineData(\"true\", true)]\n        [InlineData(\"TRUE\", true)]\n        [InlineData(\"tRuE\", true)]\n        [InlineData(\"yes\", true)]\n        [InlineData(\"YES\", true)]\n        [InlineData(\"yEs\", true)]\n        [InlineData(\"on\", true)]\n        [InlineData(\"ON\", true)]\n        [InlineData(\"oN\", true)]\n        [InlineData(\"1\", true)]\n        [InlineData(\"false\", false)]\n        [InlineData(\"i am a random string\", false)]\n        [InlineData(\"\", false)]\n        [InlineData(\"     \", false)]\n        [InlineData(\"\\t\", false)]\n        [InlineData(null, false)]\n        public void StringExtensions_IsTruthy(string input, bool expected)\n        {\n            if (expected)\n            {\n                Assert.True(StringExtensions.IsTruthy(input));\n            }\n            else\n            {\n                Assert.False(StringExtensions.IsTruthy(input));\n            }\n        }\n\n        [Theory]\n        [InlineData(\"false\", true)]\n        [InlineData(\"FALSE\", true)]\n        [InlineData(\"fAlSe\", true)]\n        [InlineData(\"no\", true)]\n        [InlineData(\"NO\", true)]\n        [InlineData(\"nO\", true)]\n        [InlineData(\"off\", true)]\n        [InlineData(\"OFF\", true)]\n        [InlineData(\"oFf\", true)]\n        [InlineData(\"0\", true)]\n        [InlineData(\"true\", false)]\n        [InlineData(\"i am a random string\", false)]\n        [InlineData(\"\", false)]\n        [InlineData(\"     \", false)]\n        [InlineData(\"\\t\", false)]\n        [InlineData(null, false)]\n        public void StringExtensions_IsFalsey(string input, bool expected)\n        {\n            if (expected)\n            {\n                Assert.True(StringExtensions.IsFalsey(input));\n            }\n            else\n            {\n                Assert.False(StringExtensions.IsFalsey(input));\n            }\n        }\n\n        [Theory]\n        [InlineData(\"true\", true)]\n        [InlineData(\"TRUE\", true)]\n        [InlineData(\"tRuE\", true)]\n        [InlineData(\"yes\", true)]\n        [InlineData(\"YES\", true)]\n        [InlineData(\"yEs\", true)]\n        [InlineData(\"on\", true)]\n        [InlineData(\"ON\", true)]\n        [InlineData(\"oN\", true)]\n        [InlineData(\"1\", true)]\n        [InlineData(\"false\", false)]\n        [InlineData(\"FALSE\", false)]\n        [InlineData(\"fAlSe\", false)]\n        [InlineData(\"no\", false)]\n        [InlineData(\"NO\", false)]\n        [InlineData(\"nO\", false)]\n        [InlineData(\"off\", false)]\n        [InlineData(\"OFF\", false)]\n        [InlineData(\"oFf\", false)]\n        [InlineData(\"0\", false)]\n        [InlineData(\"i am a random string\", null)]\n        [InlineData(\"\", null)]\n        [InlineData(\"     \", null)]\n        [InlineData(\"\\t\", null)]\n        [InlineData(null, null)]\n        public void StringExtensions_ToBooleany(string input, bool? expected)\n        {\n            Assert.Equal(expected, StringExtensions.ToBooleany(input));\n        }\n\n        [Theory]\n        [InlineData(\"true\", false, true)]\n        [InlineData(\"TRUE\", false, true)]\n        [InlineData(\"tRuE\", false, true)]\n        [InlineData(\"yes\", false, true)]\n        [InlineData(\"YES\", false, true)]\n        [InlineData(\"yEs\", false, true)]\n        [InlineData(\"on\", false, true)]\n        [InlineData(\"ON\", false, true)]\n        [InlineData(\"oN\", false, true)]\n        [InlineData(\"1\", false, true)]\n        [InlineData(\"false\", true, false)]\n        [InlineData(\"FALSE\", true, false)]\n        [InlineData(\"fAlSe\", true, false)]\n        [InlineData(\"no\", true, false)]\n        [InlineData(\"NO\", true, false)]\n        [InlineData(\"nO\", true, false)]\n        [InlineData(\"off\", true, false)]\n        [InlineData(\"OFF\", true, false)]\n        [InlineData(\"oFf\", true, false)]\n        [InlineData(\"0\", true, false)]\n        [InlineData(\"i am a random string\", true, true)]\n        [InlineData(\"\", true, true)]\n        [InlineData(\"     \", true, true)]\n        [InlineData(\"\\t\", true, true)]\n        [InlineData(null, true, true)]\n        [InlineData(\"i am a random string\", false, false)]\n        [InlineData(\"\", false, false)]\n        [InlineData(\"     \", false, false)]\n        [InlineData(\"\\t\", false, false)]\n        [InlineData(null, false, false)]\n        public void StringExtensions_ToBooleanyOrDefault(string input, bool defaultValue, bool expected)\n        {\n            Assert.Equal(expected, StringExtensions.ToBooleanyOrDefault(input, defaultValue));\n        }\n\n        [Theory]\n        [InlineData(\"\", '/', \"\")]\n        [InlineData(\"/\", '/', \"\")]\n        [InlineData(\"foo\", '/', \"foo\")]\n        [InlineData(\"foo/\", '/', \"foo\")]\n        [InlineData(\"foo/bar\", '/', \"foo\")]\n        [InlineData(\"foo/bar/\", '/', \"foo\")]\n        public void StringExtensions_TruncateLastIndexOf(string input, char c, string expected)\n        {\n            string actual = StringExtensions.TruncateFromIndexOf(input, c);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void StringExtensions_TruncateLastIndexOf_Null_ThrowsArgumentNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => StringExtensions.TruncateFromIndexOf(null, '/'));\n        }\n\n        [Theory]\n        [InlineData(\"\", '/', \"\")]\n        [InlineData(\"/\", '/', \"\")]\n        [InlineData(\"foo\", '/', \"foo\")]\n        [InlineData(\"foo/\", '/', \"foo\")]\n        [InlineData(\"foo/bar\", '/', \"foo\")]\n        [InlineData(\"foo/bar/\", '/', \"foo/bar\")]\n        public void StringExtensions_TruncateFromLastIndexOf(string input, char c, string expected)\n        {\n            string actual = StringExtensions.TruncateFromLastIndexOf(input, c);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void StringExtensions_TruncateFromLastIndexOf_Null_ThrowsArgumentNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => StringExtensions.TruncateFromLastIndexOf(null, '/'));\n        }\n\n        [Theory]\n        [InlineData(\"\", '/', \"\")]\n        [InlineData(\"/\", '/', \"\")]\n        [InlineData(\"foo\", '/', \"foo\")]\n        [InlineData(\"foo/\", '/', \"\")]\n        [InlineData(\"foo/bar\", '/', \"bar\")]\n        [InlineData(\"foo/bar/\", '/', \"bar/\")]\n        public void StringExtensions_TrimUntilIndexOf_Character(string input, char c, string expected)\n        {\n            string actual = StringExtensions.TrimUntilIndexOf(input, c);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void StringExtensions_TrimUntilIndexOf_Character_Null_ThrowsArgumentNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => StringExtensions.TrimUntilIndexOf(null, '/'));\n        }\n\n        [Theory]\n        [InlineData(\"\", \"://\", \"\")]\n        [InlineData(\"://\", \"://\", \"\")]\n        [InlineData(\"foo\", \"://\", \"foo\")]\n        [InlineData(\"foo://\", \"://\", \"\")]\n        [InlineData(\"foo://bar\", \"://\", \"bar\")]\n        [InlineData(\"foo://bar/\", \"://\", \"bar/\")]\n        public void StringExtensions_TrimUntilIndexOf_String(string input, string trim, string expected)\n        {\n            string actual = StringExtensions.TrimUntilIndexOf(input, trim);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"TRIM\", StringComparison.Ordinal, \"barTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"trim\", StringComparison.Ordinal, \"fooTRIMbarTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"tRiM\", StringComparison.Ordinal, \"fooTRIMbarTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"TRIM\", StringComparison.OrdinalIgnoreCase, \"barTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"trim\", StringComparison.OrdinalIgnoreCase, \"barTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"tRiM\", StringComparison.OrdinalIgnoreCase, \"barTRIMsoup\")]\n        public void StringExtensions_TrimUntilIndexOf_String_ComparisonType(string input, string trim, StringComparison comparisonType, string expected)\n        {\n            string actual = StringExtensions.TrimUntilIndexOf(input, trim, comparisonType);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void StringExtensions_TrimUntilIndexOf_String_Null_ThrowsArgumentNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => StringExtensions.TrimUntilIndexOf(null, \"://\"));\n        }\n\n        [Theory]\n        [InlineData(\"\", '/', \"\")]\n        [InlineData(\"/\", '/', \"\")]\n        [InlineData(\"foo\", '/', \"foo\")]\n        [InlineData(\"foo/\", '/', \"\")]\n        [InlineData(\"foo/bar\", '/', \"bar\")]\n        [InlineData(\"foo/bar/\", '/', \"\")]\n        public void StringExtensions_TrimUntilLastIndexOf_Character(string input, char c, string expected)\n        {\n            string actual = StringExtensions.TrimUntilLastIndexOf(input, c);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void StringExtensions_TrimUntilLastIndexOf_Character_Null_ThrowsArgumentNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => StringExtensions.TrimUntilLastIndexOf(null, '/'));\n        }\n\n        [Theory]\n        [InlineData(\"\", \"://\", \"\")]\n        [InlineData(\"://\", \"://\", \"\")]\n        [InlineData(\"foo\", \"://\", \"foo\")]\n        [InlineData(\"foo://\", \"://\", \"\")]\n        [InlineData(\"foo://bar\", \"://\", \"bar\")]\n        [InlineData(\"foo://bar/\", \"://\", \"bar/\")]\n        [InlineData(\"foo:/bar/baz\", \":\", \"/bar/baz\")]\n        public void StringExtensions_TrimUntilLastIndexOf_String(string input, string trim, string expected)\n        {\n            string actual = StringExtensions.TrimUntilLastIndexOf(input, trim);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"TRIM\", StringComparison.Ordinal, \"soup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"trim\", StringComparison.Ordinal, \"fooTRIMbarTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"tRiM\", StringComparison.Ordinal, \"fooTRIMbarTRIMsoup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"TRIM\", StringComparison.OrdinalIgnoreCase, \"soup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"trim\", StringComparison.OrdinalIgnoreCase, \"soup\")]\n        [InlineData(\"fooTRIMbarTRIMsoup\", \"tRiM\", StringComparison.OrdinalIgnoreCase, \"soup\")]\n        public void StringExtensions_TrimUntilLastIndexOf_String_ComparisonType(string input, string trim, StringComparison comparisonType, string expected)\n        {\n            string actual = StringExtensions.TrimUntilLastIndexOf(input, trim, comparisonType);\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void StringExtensions_TrimUntilLastIndexOf_String_Null_ThrowsArgumentNullException()\n        {\n            Assert.Throws<ArgumentNullException>(() => StringExtensions.TrimUntilLastIndexOf(null, \"://\"));\n        }\n\n        [Theory]\n        [InlineData(\"fooTRIMbar\", \"TRIM\", \"foobar\")]\n        [InlineData(\"fooTRIMbar\", \"trim\", \"fooTRIMbar\")]\n        [InlineData(\"fooTRIMbar\", \"tRiM\", \"fooTRIMbar\")]\n        public void StringExtensions_TrimMiddle_String(string input, string trim, string expected)\n        {\n            string actual = StringExtensions.TrimMiddle(input, trim);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"fooTRIMbar\", \"TRIM\", StringComparison.Ordinal, \"foobar\")]\n        [InlineData(\"fooTRIMbar\", \"trim\", StringComparison.Ordinal, \"fooTRIMbar\")]\n        [InlineData(\"fooTRIMbar\", \"tRiM\", StringComparison.Ordinal, \"fooTRIMbar\")]\n        [InlineData(\"fooTRIMbar\", \"TRIM\", StringComparison.OrdinalIgnoreCase, \"foobar\")]\n        [InlineData(\"fooTRIMbar\", \"trim\", StringComparison.OrdinalIgnoreCase, \"foobar\")]\n        [InlineData(\"fooTRIMbar\", \"tRiM\", StringComparison.OrdinalIgnoreCase, \"foobar\")]\n        public void StringExtensions_TrimMiddle_String_ComparisonType(string input, string trim, StringComparison comparisonType, string expected)\n        {\n            string actual = StringExtensions.TrimMiddle(input, trim, comparisonType);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"FooBar\", \"foo_bar\")]\n        [InlineData(\"fooBar\", \"foo_bar\")]\n        [InlineData(\"FBBaz\", \"fb_baz\")]\n        [InlineData(\"Foo\", \"foo\")]\n        [InlineData(\"Fo\", \"fo\")]\n        [InlineData(\"fO\", \"f_o\")]\n        [InlineData(\"OO\", \"oo\")]\n        [InlineData(\"F\", \"f\")]\n        [InlineData(\"\", \"\")]\n        public void StringExtensions_ToSnakeCase_Converts_Correctly(string input, string expected)\n        {\n            Assert.Equal(expected, input.ToSnakeCase());\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/TestProcessManager.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\n\nnamespace GitCredentialManager.Tests;\n\npublic class TestProcessManager : IProcessManager\n{\n    public ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)\n    {\n        var psi = new ProcessStartInfo(path, args)\n        {\n            RedirectStandardInput = true,\n            RedirectStandardOutput = true,\n            RedirectStandardError = true, // Ok to redirect stderr for testing\n            UseShellExecute = useShellExecute,\n            WorkingDirectory = workingDirectory ?? string.Empty\n        };\n\n        return CreateProcess(psi);\n    }\n\n    public ChildProcess CreateProcess(ProcessStartInfo psi)\n    {\n        return new ChildProcess(new NullTrace2(), psi);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/TokenEndpointResponseJsonTest.cs",
    "content": "using System;\nusing System.Text.Json;\nusing GitCredentialManager.Authentication.OAuth.Json;\nusing Xunit;\n\nnamespace Core.Tests;\n\npublic class TokenEndpointResponseJsonTest\n{\n    [Fact]\n    public void TokenEndpointResponseJson_Deserialize_Uses_Scope()\n    {\n        var accessToken = \"123\";\n        var tokenType = \"Bearer\";\n        var expiresIn = 1000;\n        var scopesString = \"x,y,z\";\n        var scopeString = \"a,b,c\";\n        var json = $\"{{\\\"access_token\\\": \\\"{accessToken}\\\", \\\"token_type\\\": \\\"{tokenType}\\\", \\\"expires_in\\\": {expiresIn}, \\\"scopes\\\": \\\"{scopesString}\\\", \\\"scope\\\": \\\"{scopeString}\\\"}}\";\n\n        var result = JsonSerializer.Deserialize<TokenEndpointResponseJson>(json,\n            new JsonSerializerOptions\n            {\n                PropertyNameCaseInsensitive = true\n            });\n\n        Assert.Equal(accessToken, result.AccessToken);\n        Assert.Equal(tokenType, result.TokenType);\n        Assert.Equal(expiresIn, result.ExpiresIn);\n        Assert.Equal(scopeString, result.Scope);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Trace2MessageTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing GitCredentialManager;\nusing Xunit;\n\nnamespace Core.Tests;\n\npublic class Trace2MessageTests\n{\n    [Theory]\n    [InlineData(0.013772,     \"  0.013772 \")]\n    [InlineData(26.316083,    \" 26.316083 \")]\n    [InlineData(100.316083,   \"100.316083 \")]\n    [InlineData(1000.316083,  \"1000.316083\")]\n    public void BuildTimeSpan_Match_Returns_Expected_String(double input, string expected)\n    {\n        var actual = Trace2Message.BuildTimeSpan(input);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void BuildRepoSpan_Match_Returns_Expected_String()\n    {\n        var input = 1;\n        var expected = \" r1  \";\n        var actual = Trace2Message.BuildRepoSpan(input);\n        Assert.Equal(expected, actual);\n    }\n\n    [Theory]\n    [InlineData(\"foo\",               \" foo         \")]\n    [InlineData(\"foobar\",            \" foobar      \")]\n    [InlineData(\"foo_bar_baz\",       \" foo_bar_baz \")]\n    [InlineData(\"foobarbazfoo\",      \" foobarbazfo \")]\n    public void BuildCategorySpan_Match_Returns_Expected_String(string input, string expected)\n    {\n        var actual = Trace2Message.BuildCategorySpan(input);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void Event_Message_Without_Snake_Case_ToJson_Creates_Expected_Json()\n    {\n        var errorMessage = new ErrorMessage()\n        {\n            Event = Trace2Event.Error,\n            Sid = \"123\",\n            Thread = \"main\",\n            Time = new DateTimeOffset(),\n            File = \"foo.cs\",\n            Line = 1,\n            Depth = 1,\n            Message = \"bar\",\n            ParameterizedMessage = \"baz\"\n        };\n\n        var expected = \"{\\\"event\\\":\\\"error\\\",\\\"sid\\\":\\\"123\\\",\\\"thread\\\":\\\"main\\\",\\\"time\\\":\\\"0001-01-01T00:00:00+00:00\\\",\\\"file\\\":\\\"foo.cs\\\",\\\"line\\\":1,\\\"depth\\\":1,\\\"msg\\\":\\\"bar\\\",\\\"fmt\\\":\\\"baz\\\"}\";\n        var actual = errorMessage.ToJson();\n\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void Event_Message_With_Snake_Case_ToJson_Creates_Expected_Json()\n    {\n        var childStartMessage = new ChildStartMessage()\n        {\n            Event = Trace2Event.ChildStart,\n            Sid = \"123\",\n            Thread = \"main\",\n            Time = new DateTimeOffset(),\n            File = \"foo.cs\",\n            Line = 1,\n            Depth = 1,\n            Id = 1,\n            Classification = Trace2ProcessClass.UIHelper,\n            UseShell = false,\n            Argv = new List<string>() { \"bar\", \"baz\" },\n            ElapsedTime = 0.05\n        };\n\n        var expected = \"{\\\"event\\\":\\\"child_start\\\",\\\"sid\\\":\\\"123\\\",\\\"thread\\\":\\\"main\\\",\\\"time\\\":\\\"0001-01-01T00:00:00+00:00\\\",\\\"file\\\":\\\"foo.cs\\\",\\\"line\\\":1,\\\"depth\\\":1,\\\"t_abs\\\":0.05,\\\"argv\\\":[\\\"bar\\\",\\\"baz\\\"],\\\"child_id\\\":1,\\\"child_class\\\":\\\"ui_helper\\\",\\\"use_shell\\\":false}\";\n        var actual = childStartMessage.ToJson();\n\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/Trace2Tests.cs",
    "content": "using Xunit;\n\nnamespace GitCredentialManager.Tests;\n\npublic class Trace2Tests\n{\n    [PosixTheory]\n    [InlineData(\"af_unix:foo\", \"foo\")]\n    [InlineData(\"af_unix:foo/bar\", \"foo/bar\")]\n    [InlineData(\"af_unix:stream:foo/bar\", \"foo/bar\")]\n    [InlineData(\"af_unix:dgram:foo/bar/baz\", \"foo/bar/baz\")]\n    public void TryGetPipeName_Posix_Returns_Expected_Value(string input, string expected)\n    {\n        var isSuccessful = Trace2.TryGetPipeName(input, out var actual);\n\n        Assert.True(isSuccessful);\n        Assert.Equal(actual, expected);\n    }\n\n    [WindowsTheory]\n    [InlineData(\"\\\\\\\\.\\\\pipe\\\\git-foo\", \"git-foo\")]\n    [InlineData(\"\\\\\\\\.\\\\pipe\\\\git-foo-bar\", \"git-foo-bar\")]\n    [InlineData(\"\\\\\\\\.\\\\pipe\\\\foo\\\\git-bar\", \"foo\\\\git-bar\")]\n    public void TryGetPipeName_Windows_Returns_Expected_Value(string input, string expected)\n    {\n        var isSuccessful = Trace2.TryGetPipeName(input, out var actual);\n\n        Assert.True(isSuccessful);\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/TraceTests.cs",
    "content": "\nusing System;\nusing System.IO;\nusing System.Text;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class TraceTests\n    {\n        [Fact]\n        public void Trace_WriteLineSecrets_SecretTracingEnabled_WritesSecretValues()\n        {\n            const string secret1 = \"foo\";\n            const string secret2 = \"bar\";\n            const string secret3 = \"test\";\n\n            var sb = new StringBuilder();\n            var listener = new StringWriter(sb);\n\n            var trace = new Trace();\n            trace.AddListener(listener);\n            trace.IsSecretTracingEnabled = true;\n\n            trace.WriteLineSecrets(\"Secrets: {0} {1} {2}\", new object[]{ secret1, secret2, secret3 });\n\n            string expectedTraceEnd = $\"Secrets: {secret1} {secret2} {secret3}\\n\";\n            string actualTrace = sb.ToString();\n\n            Assert.EndsWith(expectedTraceEnd, actualTrace, StringComparison.Ordinal);\n        }\n\n        [Fact]\n        public void Trace_WriteLineSecrets_SecretTracingDisabled_WritesMaskedValues()\n        {\n            const string mask = \"********\";\n            const string secret1 = \"foo\";\n            const string secret2 = \"bar\";\n            const string secret3 = \"test\";\n\n            var sb = new StringBuilder();\n            var listener = new StringWriter(sb);\n\n            var trace = new Trace();\n            trace.AddListener(listener);\n            trace.IsSecretTracingEnabled = false;\n\n            trace.WriteLineSecrets(\"Secrets: {0} {1} {2}\", new object[]{ secret1, secret2, secret3 });\n\n            string expectedTraceEnd = $\"Secrets: {mask} {mask} {mask}\\n\";\n            string actualTrace = sb.ToString();\n\n            Assert.EndsWith(expectedTraceEnd, actualTrace, StringComparison.Ordinal);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/TraceUtilsTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Text;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests;\n\npublic class TraceUtilsTests\n{\n    [Theory]\n    [InlineData(\"/foo/bar/baz/boo\", 10, \"...baz/boo\")]\n    [InlineData(\"thisfileshouldbetruncated\", 12, \"...truncated\")]\n    public void FormatSource_ReturnsExpectedSourceValues(string path, int sourceColumnMaxWidth, string expectedSource)\n    {\n        string actualSource = TraceUtils.FormatSource(path, sourceColumnMaxWidth);\n        Assert.Equal(actualSource, expectedSource);\n    }\n}"
  },
  {
    "path": "src/shared/Core.Tests/UriExtensionsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class UriExtensionsTests\n    {\n        [Fact]\n        public void UriExtensions_GetQueryParameters()\n        {\n            var uri = new Uri(\"https://example.com/foo/bar?q1=value1&q2=value%20with%20spaces&key%20with%20spaces=value3\");\n\n            IDictionary<string, string> result = uri.GetQueryParameters();\n\n            Assert.Equal(3, result.Count);\n\n            Assert.True(result.TryGetValue(\"q1\", out string value1));\n            Assert.Equal(\"value1\", value1);\n\n            Assert.True(result.TryGetValue(\"q2\", out string value2));\n            Assert.Equal(\"value with spaces\", value2);\n\n            Assert.True(result.TryGetValue(\"key with spaces\", out string value3));\n            Assert.Equal(\"value3\", value3);\n        }\n\n        [Theory]\n        [InlineData(\"http://hostname\", \"http://hostname\")]\n        [InlineData(\"http://example.com\",\n            \"http://example.com\")]\n        [InlineData(\"http://hostname:7990\", \"http://hostname:7990\")]\n        [InlineData(\"http://foo.example.com\",\n            \"http://foo.example.com\", \"http://example.com\")]\n        [InlineData(\"http://example.com/foo\",\n            \"http://example.com/foo\", \"http://example.com\")]\n        [InlineData(\"http://example.com/foo?query=true#fragment\",\n            \"http://example.com/foo\", \"http://example.com\")]\n        [InlineData(\"http://buzz.foo.example.com/bar/baz\",\n            \"http://buzz.foo.example.com/bar/baz\", \"http://buzz.foo.example.com/bar\", \"http://buzz.foo.example.com\", \"http://foo.example.com\", \"http://example.com\")]\n        public void UriExtensions_GetGitConfigurationScopes(string start, params string[] expected)\n        {\n            var startUri = new Uri(start);\n            string[] actual = UriExtensions.GetGitConfigurationScopes(startUri).ToArray();\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(\"http://example.com\", false, null, null)]\n\n        [InlineData(\"http://john.doe:password123@example.com\",\n            true, \"john.doe\", \"password123\")]\n\n        [InlineData(\"http://john.doe@example.com\",\n            true, \"john.doe\", null)]\n\n        [InlineData(\"http://john.doe:@example.com\",\n            true, \"john.doe\", \"\")]\n\n        [InlineData(\"http://:password123@example.com\",\n            true, \"\", \"password123\")]\n\n        [InlineData(\"http://john.doe:::password123@example.com\",\n            true, \"john.doe\", \"::password123\")]\n\n        [InlineData(\"http://john%20doe:password%20123@example.com\",\n            true, \"john doe\", \"password 123\")]\n        public void UriExtensions_GetUserInfo(string input, bool expectedResult, string expectedUser, string expectedPass)\n        {\n            var uri = new Uri(input);\n\n            bool actualResult = UriExtensions.TryGetUserInfo(uri, out string actualUser, out string actualPass);\n\n            Assert.Equal(expectedResult, actualResult);\n            Assert.Equal(expectedUser, actualUser);\n            Assert.Equal(expectedPass, actualPass);\n        }\n\n        [Theory]\n        [InlineData(\"http://example.com\", \"http://example.com\")]\n        [InlineData(\"http://john.doe:password123@example.com\", \"http://example.com\")]\n        [InlineData(\"http://john.doe@example.com\", \"http://example.com\")]\n        [InlineData(\"http://john.doe:@example.com\", \"http://example.com\")]\n        [InlineData(\"http://:password123@example.com\", \"http://example.com\")]\n        [InlineData(\"http://john.doe:::password123@example.com\", \"http://example.com\")]\n        [InlineData(\"http://john%20doe:password%20123@example.com\", \"http://example.com\")]\n        public void UriExtensions_WithoutUserInfo(string input, string expected)\n        {\n            var uri = new Uri(input);\n\n            Uri result = UriExtensions.WithoutUserInfo(uri);\n\n            Assert.Equal(expected, result.ToString().TrimEnd('/'));\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Core.Tests/WslUtilsTests.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing Moq;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class WslUtilsTests\n    {\n        [Theory]\n        [InlineData(null, false)]\n        [InlineData(@\"\", false)]\n        [InlineData(@\" \", false)]\n        [InlineData(@\"\\\", false)]\n        [InlineData(@\"wsl\", false)]\n        [InlineData(@\"\\wsl\\ubuntu\\home\", false)]\n        [InlineData(@\"\\\\wsl\\ubuntu\\home\", false)]\n        [InlineData(@\"\\wsl$\\ubuntu\\home\", false)]\n        [InlineData(@\"wsl$\\ubuntu\\home\", false)]\n        [InlineData(@\"//wsl$/ubuntu/home\", false)]\n        [InlineData(@\"\\\\wsl\", false)]\n        [InlineData(@\"\\\\wsl$\", false)]\n        [InlineData(@\"\\\\wsl$\\\", false)]\n        [InlineData(@\"\\\\wsl$\\ubuntu\", true)]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\\", true)]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\home\", true)]\n        [InlineData(@\"\\\\WSL$\\UBUNTU\\home\", true)]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\home\\\", true)]\n        [InlineData(@\"\\\\wsl$\\openSUSE-42\\home\", true)]\n        [InlineData(@\"wsl.localhost\", false)]\n        [InlineData(@\"\\wsl.localhost\\ubuntu\\home\", false)]\n        [InlineData(@\"wsl.localhost\\ubuntu\\home\", false)]\n        [InlineData(@\"//wsl.localhost/ubuntu/home\", false)]\n        [InlineData(@\"\\\\wsl.localhost\", false)]\n        [InlineData(@\"\\\\wsl.localhost\\\", false)]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\", true)]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\\", true)]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\home\", true)]\n        [InlineData(@\"\\\\WSL.LOCALHOST\\UBUNTU\\home\", true)]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\home\\\", true)]\n        [InlineData(@\"\\\\wsl.localhost\\openSUSE-42\\home\", true)]\n        public void WslUtils_IsWslPath(string path, bool expected)\n        {\n            bool actual = WslUtils.IsWslPath(path);\n            Assert.Equal(expected, actual);\n        }\n\n        [Theory]\n        [InlineData(@\"\\\\wsl$\\ubuntu\", \"ubuntu\", \"/\")]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\\", \"ubuntu\", \"/\")]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\home\", \"ubuntu\", \"/home\")]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\HOME\", \"ubuntu\", \"/HOME\")]\n        [InlineData(@\"\\\\wsl$\\UBUNTU\\home\", \"UBUNTU\", \"/home\")]\n        [InlineData(@\"\\\\wsl$\\ubuntu\\home\\\", \"ubuntu\", \"/home/\")]\n        [InlineData(@\"\\\\wsl$\\openSUSE-42\\home\", \"openSUSE-42\", \"/home\")]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\", \"ubuntu\", \"/\")]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\\", \"ubuntu\", \"/\")]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\home\", \"ubuntu\", \"/home\")]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\HOME\", \"ubuntu\", \"/HOME\")]\n        [InlineData(@\"\\\\wsl.localhost\\UBUNTU\\home\", \"UBUNTU\", \"/home\")]\n        [InlineData(@\"\\\\wsl.localhost\\ubuntu\\home\\\", \"ubuntu\", \"/home/\")]\n        [InlineData(@\"\\\\wsl.localhost\\openSUSE-42\\home\", \"openSUSE-42\", \"/home\")]\n        public void WslUtils_ConvertToDistroPath(string path, string expectedDistro, string expectedPath)\n        {\n            string actualPath = WslUtils.ConvertToDistroPath(path, out string actualDistro);\n            Assert.Equal(expectedPath, actualPath);\n            Assert.Equal(expectedDistro, actualDistro);\n        }\n\n        [Theory]\n        [InlineData(null)]\n        [InlineData(@\"\")]\n        [InlineData(@\" \")]\n        [InlineData(@\"\\\")]\n        [InlineData(@\"wsl\")]\n        [InlineData(@\"\\wsl\\ubuntu\\home\")]\n        [InlineData(@\"\\\\wsl\\ubuntu\\home\")]\n        [InlineData(@\"\\wsl$\\ubuntu\\home\")]\n        [InlineData(@\"wsl$\\ubuntu\\home\")]\n        [InlineData(@\"//wsl$/ubuntu/home\")]\n        [InlineData(@\"\\\\wsl\")]\n        [InlineData(@\"\\\\wsl$\")]\n        [InlineData(@\"\\\\wsl$\\\")]\n        [InlineData(@\"wsl.localhost\")]\n        [InlineData(@\"\\wsl.localhost\\ubuntu\\home\")]\n        [InlineData(@\"wsl.localhost\\ubuntu\\home\")]\n        [InlineData(@\"//wsl.localhost/ubuntu/home\")]\n        [InlineData(@\"\\\\wsl.localhost\")]\n        [InlineData(@\"\\\\wsl.localhost\\\")]\n        public void WslUtils_ConvertToDistroPath_Invalid_ThrowsException(string path)\n        {\n            Assert.Throws<ArgumentException>(() => WslUtils.ConvertToDistroPath(path, out _));\n        }\n\n        [WindowsFact]\n        public void WslUtils_CreateWslProcess()\n        {\n            const string distribution = \"ubuntu\";\n            const string command = \"/usr/lib/git-core/git version\";\n\n            string expectedFileName = WslUtils.GetWslPath();\n            string expectedArgs = $\"--distribution {distribution} --exec {command}\";\n\n            ChildProcess process = WslUtils.CreateWslProcess(distribution, command, Mock.Of<ITrace2>());\n\n            Assert.NotNull(process);\n            Assert.Equal(expectedArgs, process.StartInfo.Arguments);\n            Assert.Equal(expectedFileName, process.StartInfo.FileName);\n            Assert.True(process.StartInfo.RedirectStandardInput);\n            Assert.True(process.StartInfo.RedirectStandardOutput);\n            Assert.False(process.StartInfo.RedirectStandardError);\n            Assert.False(process.StartInfo.UseShellExecute);\n        }\n\n        [WindowsFact]\n        public void WslUtils_CreateWslProcess_WorkingDirectory()\n        {\n            const string distribution = \"ubuntu\";\n            const string command = \"/usr/lib/git-core/git version\";\n            const string expectedWorkingDirectory = @\"C:\\Projects\\\";\n\n            string expectedFileName = WslUtils.GetWslPath();\n            string expectedArgs = $\"--distribution {distribution} --exec {command}\";\n\n            ChildProcess process = WslUtils.CreateWslProcess(distribution, command, Mock.Of<ITrace2>(), expectedWorkingDirectory);\n\n            Assert.NotNull(process);\n            Assert.Equal(expectedArgs, process.StartInfo.Arguments);\n            Assert.Equal(expectedFileName, process.StartInfo.FileName);\n            Assert.True(process.StartInfo.RedirectStandardInput);\n            Assert.True(process.StartInfo.RedirectStandardOutput);\n            Assert.False(process.StartInfo.RedirectStandardError);\n            Assert.False(process.StartInfo.UseShellExecute);\n            Assert.Equal(expectedWorkingDirectory, process.StartInfo.WorkingDirectory);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Directory.Build.props",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <!-- Import parent Directory.Build.props file -->\n  <Import Project=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))\" />\n\n  <!-- Set binary and intermediate output directories -->\n  <PropertyGroup>\n    <PlatformOutPath>$(RepoOutPath)shared\\</PlatformOutPath>\n    <ProjectOutPath>$(PlatformOutPath)$(MSBuildProjectName)\\</ProjectOutPath>\n    <BaseOutputPath>$(ProjectOutPath)bin\\</BaseOutputPath>\n    <BaseIntermediateOutputPath>$(ProjectOutPath)obj\\</BaseIntermediateOutputPath>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/shared/DotnetTool/DotnetTool.csproj",
    "content": "<Project Sdk=\"Microsoft.Build.NoTargets/3.5.6\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <PackAsTool>true</PackAsTool>\n    <NuSpecFile>dotnet-tool.nuspec</NuSpecFile>\n    <!-- Inject correct properties into NuSpec -->\n    <NuspecProperties>\n      version=$(PackageVersion);\n      publishDir=$(PublishDir);\n    </NuspecProperties>\n    <NoBuild>true</NoBuild>\n    <OutputPath>$(ProjectOutPath)nupkg\\$(Configuration)\\</OutputPath>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/shared/DotnetTool/DotnetToolSettings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<DotNetCliTool Version=\"1\">\n  <Commands>\n    <Command Name=\"git-credential-manager\" EntryPoint=\"git-credential-manager.dll\" Runner=\"dotnet\" Version=\"6.0\" />\n  </Commands>\n</DotNetCliTool>\n"
  },
  {
    "path": "src/shared/DotnetTool/dotnet-tool.nuspec",
    "content": "\n<package xmlns=\"http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd\">\n    <metadata>\n        <id>git-credential-manager</id>\n        <version>$version$</version>\n        <description>Secure, cross-platform Git credential storage with authentication to Azure Repos, GitHub, and other popular Git hosting services.</description>\n        <authors>git-credential-manager</authors>\n        <icon>images\\icon.png</icon>\n        <packageTypes>\n            <packageType name=\"DotnetTool\" />\n        </packageTypes>\n  </metadata>\n  <files>\n    <file src=\"tools/net8.0/any/**\" />\n    <file src=\"images/**\" />\n  </files>\n</package>\n"
  },
  {
    "path": "src/shared/DotnetTool/layout.ps1",
    "content": "<#\n.SYNOPSIS\n    Lays out the .NET tool package directory.\n\n.PARAMETER Configuration\n    Build configuration (Debug/Release). Defaults to Debug.\n\n.PARAMETER Output\n    Root output directory for the nupkg layout. If omitted:\n    out/shared/DotnetTool/nupkg/<Configuration>\n\n.EXAMPLE\n    pwsh ./layout.ps1 -Configuration Release\n\n.EXAMPLE\n    pwsh ./layout.ps1 -Output C:\\temp\\tool-layout\n\n#>\n\n[CmdletBinding()]\nparam(\n\t[string]$Configuration = \"Debug\",\n\t[string]$Output\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = 'Stop'\n\nfunction Make-Absolute {\n\tparam([string]$Path)\n\tif ([string]::IsNullOrWhiteSpace($Path)) { return $null }\n\tif ([System.IO.Path]::IsPathRooted($Path)) { return $Path }\n\treturn (Join-Path -Path (Get-Location) -ChildPath $Path)\n}\n\nWrite-Host \"Starting layout...\" -ForegroundColor Cyan\n\n# Directories\n$ScriptDir = $PSScriptRoot\n$Root = (Resolve-Path (Join-Path $ScriptDir \"..\\..\\..\")).Path\n$Src = Join-Path $Root \"src\"\n$Out = Join-Path $Root \"out\"\n$DotnetToolRel = \"shared/DotnetTool\"\n$GcmSrc = Join-Path $Src \"shared\\Git-Credential-Manager\"\n$ProjOut = Join-Path $Out $DotnetToolRel\n\n$Framework = \"net8.0\"\n\nif (-not $Output -or $Output.Trim() -eq \"\") {\n\t$Output = Join-Path $ProjOut \"nupkg\\$Configuration\"\n}\n\n$ImgOut = Join-Path $Output \"images\"\n$BinOut = Join-Path $Output \"tools\\$Framework\\any\"\n\n# Cleanup previous layout\nif (Test-Path $Output) {\n\tWrite-Host \"Cleaning existing output directory '$Output'...\"\n\tRemove-Item -Force -Recurse $Output\n}\n\n# Recreate directories\n$null = New-Item -ItemType Directory -Path $BinOut -Force\n$null = New-Item -ItemType Directory -Path $ImgOut -Force\n\n# Determine DOTNET_ROOT if not set\nif (-not $env:DOTNET_ROOT -or $env:DOTNET_ROOT.Trim() -eq \"\") {\n\t$dotnetCmd = Get-Command dotnet -ErrorAction Stop\n\t$env:DOTNET_ROOT = Split-Path -Parent $dotnetCmd.Source\n}\n\nWrite-Host \"Publishing core application...\"\n& \"$env:DOTNET_ROOT/dotnet\" publish $GcmSrc `\n\t--configuration $Configuration `\n\t--framework $Framework `\n\t--output (Make-Absolute $BinOut) `\n\t-p:UseAppHost=false\n\nif ($LASTEXITCODE -ne 0) {\n\tWrite-Error \"dotnet publish failed with exit code $LASTEXITCODE\"\n\texit $LASTEXITCODE\n}\n\nWrite-Host \"Copying package configuration file...\"\nCopy-Item -Path (Join-Path $Src \"$DotnetToolRel\\DotnetToolSettings.xml\") -Destination $BinOut -Force\n\nWrite-Host \"Copying images...\"\nCopy-Item -Path (Join-Path $Src \"$DotnetToolRel\\icon.png\") -Destination $ImgOut -Force\n\nWrite-Host \"Layout complete.\" -ForegroundColor Green\n"
  },
  {
    "path": "src/shared/DotnetTool/pack.ps1",
    "content": "<#\n.SYNOPSIS\n    Creates the NuGet package for the .NET tool.\n\n.PARAMETER Configuration\n    Build configuration (Debug/Release). Defaults to Debug.\n\n.PARAMETER Version\n    Package version (required).\n\n.PARAMETER PackageRoot\n    Root of the pre-laid-out package structure (from layout). Defaults to:\n    out/shared/DotnetTool/nupkg/<Configuration>\n\n.PARAMETER Output\n    Optional directory for the produced .nupkg/.snupkg. If omitted NuGet chooses.\n\n.EXAMPLE\n    pwsh ./pack.ps1 -Version 2.0.123-beta\n\n.EXAMPLE\n    pwsh ./pack.ps1 -Configuration Release -Version 2.1.0 -Output C:\\pkgs\n\n#>\n\n[CmdletBinding()]\nparam(\n\t[string]$Configuration = \"Debug\",\n\t[Parameter(Mandatory = $true)]\n\t[string]$Version,\n\t[string]$PackageRoot,\n\t[string]$Output\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = 'Stop'\n\nWrite-Host \"Starting pack...\" -ForegroundColor Cyan\n\n# Directories\n$ScriptDir = $PSScriptRoot\n$Root = (Resolve-Path (Join-Path $ScriptDir \"..\\..\\..\")).Path\n$Src = Join-Path $Root \"src\"\n$Out = Join-Path $Root \"out\"\n$DotnetToolRel = \"shared\\DotnetTool\"\n$NuspecFile = Join-Path $Src \"$DotnetToolRel\\dotnet-tool.nuspec\"\n\nif (-not (Test-Path $NuspecFile)) {\n\tWrite-Error \"Could not locate nuspec file at '$NuspecFile'\"\n\texit 1\n}\n\nif (-not $PackageRoot -or $PackageRoot.Trim() -eq \"\") {\n\t$PackageRoot = Join-Path $Out \"$DotnetToolRel\\nupkg\\$Configuration\"\n}\n\nif (-not (Test-Path $PackageRoot)) {\n\tWrite-Error \"Package root '$PackageRoot' does not exist. Run layout.ps1 first.\"\n\texit 1\n}\n\n# Locate nuget\n$nugetCmd = Get-Command nuget -ErrorAction SilentlyContinue\nif (-not $nugetCmd) {\n\tWrite-Error \"nuget CLI not found in PATH (install: https://www.nuget.org/downloads)\"\n\texit 1\n}\n$nugetExe = $nugetCmd.Source\n\nWrite-Host \"Creating .NET tool package...\"\n\n$packArgs = @(\n\t\"pack\", \"$NuspecFile\",\n\t\"-Properties\", \"Configuration=$Configuration\",\n\t\"-Version\", $Version,\n\t\"-Symbols\", \"-SymbolPackageFormat\", \"snupkg\",\n\t\"-BasePath\", \"$PackageRoot\"\n)\n\nif ($Output -and $Output.Trim() -ne \"\") {\n\tif (-not (Test-Path $Output)) {\n\t\tWrite-Host \"Creating output directory '$Output'...\"\n\t\tNew-Item -ItemType Directory -Force -Path $Output | Out-Null\n\t}\n\t$packArgs += @(\"-OutputDirectory\", \"$Output\")\n}\n\n& $nugetExe @packArgs\n\nif ($LASTEXITCODE -ne 0) {\n\tWrite-Error \"nuget pack failed with exit code $LASTEXITCODE\"\n\texit $LASTEXITCODE\n}\n\nWrite-Host \".NET tool pack complete.\" -ForegroundColor Green\n"
  },
  {
    "path": "src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFrameworks>net8.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"'$(OSPlatform)'=='windows'\">net472;net8.0</TargetFrameworks>\n    <RuntimeIdentifiers>win-x86;win-x64;win-arm64;osx-x64;linux-x64;osx-arm64;linux-arm64;linux-arm</RuntimeIdentifiers>\n    <AssemblyName>git-credential-manager</AssemblyName>\n    <RootNamespace>GitCredentialManager</RootNamespace>\n    <ApplicationIcon>$(RepoAssetsPath)gcmicon.ico</ApplicationIcon>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n    <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Atlassian.Bitbucket\\Atlassian.Bitbucket.csproj\" />\n    <ProjectReference Include=\"..\\GitHub\\GitHub.csproj\" />\n    <ProjectReference Include=\"..\\GitLab\\GitLab.csproj\" />\n    <ProjectReference Include=\"..\\Microsoft.AzureRepos\\Microsoft.AzureRepos.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Content Include=\"NOTICE\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Git-Credential-Manager/NOTICE",
    "content": "NOTICES AND INFORMATION\nDo Not Translate or Localize\n\nThis repository for Git Credential Manager includes material from the\nprojects listed below.\n\n--------------------------------------------------------------------------------\n1. GitHub/VisualStudio (https://github.com/github/VisualStudio)\n\nCopyright (c) GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n--------------------------------------------------------------------------------\n2. dotnet/runtime (https://github.com/dotnet/runtime)\n\nThe MIT License (MIT)\n\nCopyright (c) .NET Foundation and Contributors\n\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "src/shared/Git-Credential-Manager/Program.cs",
    "content": "using System;\nusing System.Threading;\nusing Atlassian.Bitbucket;\nusing Avalonia;\nusing GitHub;\nusing GitLab;\nusing Microsoft.AzureRepos;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.UI;\n\nnamespace GitCredentialManager\n{\n    public static class Program\n    {\n        private static int _exitCode;\n\n        public static void Main(string[] args)\n        {\n            // Create the dispatcher on the main thread. This is required\n            // for some platform UI services such as macOS that mandates\n            // all controls are created/accessed on the initial thread\n            // created by the process (the process entry thread).\n            Dispatcher.Initialize();\n\n            // Run AppMain in a new thread and keep the main thread free\n            // to process the dispatcher's job queue.\n            var appMain = new Thread(AppMain) {Name = nameof(AppMain)};\n            appMain.Start(args);\n\n            // Process the dispatcher job queue (aka: message pump, run-loop, etc...)\n            // We must ensure to run this on the same thread that it was created on\n            // (the main thread) so we cannot use any async/await calls between\n            // Dispatcher.Initialize and Run.\n            Dispatcher.MainThread.Run();\n\n            // Dispatcher was shutdown\n            Environment.Exit(_exitCode);\n        }\n\n        private static void AppMain(object o)\n        {\n            string[] args = (string[])o;\n\n            var startTime = DateTimeOffset.UtcNow;\n            // Set the session id (sid) and start time for the GCM process, to be\n            // used when TRACE2 tracing is enabled.\n            ProcessManager.CreateSid();\n\n            using (var context = new CommandContext())\n            using (var app = new Application(context))\n            {\n                // Initialize TRACE2 system\n                context.Trace2.Initialize(startTime);\n\n                // Write the start and version events\n                context.Trace2.Start(context.ApplicationPath, args);\n\n                // Register all supported host providers at the normal priority.\n                // The generic provider should never win against a more specific one, so register it with low priority.\n                app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal);\n                app.RegisterProvider(new BitbucketHostProvider(context), HostProviderPriority.Normal);\n                app.RegisterProvider(new GitHubHostProvider(context), HostProviderPriority.Normal);\n                app.RegisterProvider(new GitLabHostProvider(context), HostProviderPriority.Normal);\n                app.RegisterProvider(new GenericHostProvider(context), HostProviderPriority.Low);\n\n                _exitCode = app.RunAsync(args)\n                    .ConfigureAwait(false)\n                    .GetAwaiter()\n                    .GetResult();\n\n                context.Trace2.Stop(_exitCode);\n                Dispatcher.MainThread.Shutdown();\n            }\n        }\n\n        // Required for Avalonia designer\n        static AppBuilder BuildAvaloniaApp() =>\n            AppBuilder.Configure<AvaloniaApp>()\n#if NETFRAMEWORK\n                .UseWin32()\n                .UseSkia()\n#else\n                .UsePlatformDetect()\n#endif\n                .LogToTrace();\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/AuthenticationResult.cs",
    "content": "using GitCredentialManager;\n\nnamespace GitHub\n{\n    public struct AuthenticationResult\n    {\n        public AuthenticationResult(GitHubAuthenticationResultType type)\n        {\n            Type = type;\n            Token = null;\n        }\n\n        public AuthenticationResult(GitHubAuthenticationResultType type, string token)\n        {\n            Type = type;\n            Token = token;\n        }\n\n        public GitHubAuthenticationResultType Type { get; }\n\n        public string Token { get; }\n    }\n\n    public enum GitHubAuthenticationResultType\n    {\n        Success,\n        Failure,\n        TwoFactorApp,\n        TwoFactorSms,\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/Diagnostics/GitHubApiDiagnostic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Text;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Diagnostics;\n\nnamespace GitHub.Diagnostics\n{\n    public class GitHubApiDiagnostic : Diagnostic\n    {\n        private readonly IGitHubRestApi _api;\n\n        public GitHubApiDiagnostic(IGitHubRestApi api, ICommandContext commandContext)\n            : base(\"GitHub API\", commandContext)\n        {\n            _api = api;\n        }\n\n        protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)\n        {\n            var targetUri = new Uri(\"https://github.com\");\n            log.AppendLine($\"Using '{targetUri}' as API target.\");\n\n            log.Append(\"Querying '/meta' endpoint...\");\n            GitHubMetaInfo metaInfo = await _api.GetMetaInfoAsync(targetUri);\n            log.AppendLine(\" OK\");\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHub.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"'$(OSPlatform)'=='windows'\">net8.0;net472</TargetFrameworks>\n    <AssemblyName>GitHub</AssemblyName>\n    <RootNamespace>GitHub</RootNamespace>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' == 'net472'\">\n    <Reference Include=\"System.Net.Http\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/GitHub/GitHubAuthChallenge.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\n\nnamespace GitHub;\n\npublic class GitHubAuthChallenge : IEquatable<GitHubAuthChallenge>\n{\n    private static readonly Regex BasicRegex = new(@\"Basic\\s+(?'props1'.*)realm=\"\"GitHub\"\"(?'props2'.*)\",\n        RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n    public static IList<GitHubAuthChallenge> FromHeaders(IEnumerable<string> headers)\n    {\n        var challenges = new List<GitHubAuthChallenge>();\n        foreach (string header in headers)\n        {\n            var match = BasicRegex.Match(header);\n            if (match.Success)\n            {\n                IDictionary<string, string> props = ParseProperties(match.Groups[\"props1\"].Value + match.Groups[\"props2\"]);\n\n                // The enterprise shortcode is provided in the `domain_hint` property, whereas the\n                // enterprise name/slug is provided in the `enterprise_hint` property.\n                props.TryGetValue(\"domain_hint\", out string domain);\n                props.TryGetValue(\"enterprise_hint\", out string enterprise);\n\n                var challenge = new GitHubAuthChallenge(domain, enterprise);\n\n                challenges.Add(challenge);\n            }\n        }\n\n        return challenges;\n    }\n\n    private static IDictionary<string, string> ParseProperties(string str)\n    {\n        var props = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n        foreach (string prop in str.Split(' '))\n        {\n            int delim = prop.IndexOf('=');\n            if (delim < 0)\n            {\n                continue;\n            }\n\n            string key = prop.Substring(0, delim).Trim();\n            string value = prop.Substring(delim + 1).Trim('\"');\n\n            props[key] = value;\n        }\n\n        return props;\n    }\n\n    public GitHubAuthChallenge() { }\n\n    public GitHubAuthChallenge(string domain, string enterprise)\n    {\n        Domain = domain;\n        Enterprise = enterprise;\n    }\n\n    public string Domain { get; }\n\n    public string Enterprise { get; }\n\n    public bool IsDomainMember(string userName)\n    {\n        if (string.IsNullOrWhiteSpace(userName))\n        {\n            return false;\n        }\n\n        int delim = userName.LastIndexOf('_');\n        if (delim < 0)\n        {\n            return string.IsNullOrWhiteSpace(Domain);\n        }\n\n        // Check for users that contain underscores but are not EMU logins\n        if (GitHubConstants.InvalidUnderscoreLogins.Contains(userName, StringComparer.OrdinalIgnoreCase))\n        {\n            return string.IsNullOrWhiteSpace(Domain);\n        }\n\n        string shortCode = userName.Substring(delim + 1);\n        return StringComparer.OrdinalIgnoreCase.Equals(Domain, shortCode);\n    }\n\n    public bool Equals(GitHubAuthChallenge other)\n    {\n        if (ReferenceEquals(null, other)) return false;\n        if (ReferenceEquals(this, other)) return true;\n        return string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) &&\n               string.Equals(Enterprise, other.Enterprise, StringComparison.OrdinalIgnoreCase);\n    }\n\n    public override bool Equals(object obj)\n    {\n        if (ReferenceEquals(null, obj)) return false;\n        if (ReferenceEquals(this, obj)) return true;\n        if (obj.GetType() != GetType()) return false;\n        return Equals((GitHubAuthChallenge)obj);\n    }\n\n    public override int GetHashCode()\n    {\n        return Domain.GetHashCode() * 1019 ^\n               Enterprise.GetHashCode() * 337;\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubAuthentication.cs",
    "content": "using System;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.UI;\nusing GitHub.UI.ViewModels;\nusing GitHub.UI.Views;\n\nnamespace GitHub\n{\n    public interface IGitHubAuthentication : IDisposable\n    {\n        Task<string> SelectAccountAsync(Uri targetUri, IEnumerable<string> accounts);\n\n        Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes);\n\n        Task<string> GetTwoFactorCodeAsync(Uri targetUri, bool isSms);\n\n        Task<OAuth2TokenResult> GetOAuthTokenViaBrowserAsync(Uri targetUri, IEnumerable<string> scopes, string loginHint);\n\n        Task<OAuth2TokenResult> GetOAuthTokenViaDeviceCodeAsync(Uri targetUri, IEnumerable<string> scopes);\n    }\n\n    public class AuthenticationPromptResult\n    {\n        public AuthenticationPromptResult(AuthenticationModes mode)\n        {\n            AuthenticationMode = mode;\n        }\n\n        public AuthenticationPromptResult(AuthenticationModes mode, ICredential credential)\n            : this(mode)\n        {\n            Credential = credential;\n        }\n\n        public AuthenticationModes AuthenticationMode { get; }\n\n        public ICredential Credential { get; set; }\n    }\n\n    [Flags]\n    public enum AuthenticationModes\n    {\n        None  = 0,\n        Basic = 1,\n        Browser = 1 << 1,\n        Pat     = 1 << 2,\n        Device  = 1 << 3,\n\n        OAuth = Browser | Device,\n        All   = Basic | OAuth | Pat\n    }\n\n    public class GitHubAuthentication : AuthenticationBase, IGitHubAuthentication\n    {\n        public static readonly string[] AuthorityIds =\n        {\n            \"github\",\n        };\n\n        public GitHubAuthentication(ICommandContext context)\n            : base(context) {}\n\n        public async Task<string> SelectAccountAsync(Uri targetUri, IEnumerable<string> accounts)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string command, out string args))\n                {\n                    var promptArgs = new StringBuilder(args);\n                    promptArgs.Append(\"select-account\");\n\n                    if (!GitHubHostProvider.IsGitHubDotCom(targetUri))\n                    {\n                        promptArgs.AppendFormat(\" --enterprise-url {0}\", QuoteCmdArg(targetUri.ToString()));\n                    }\n\n                    // Write the accounts to the standard input of the helper process to avoid any issues\n                    // with escaping special characters, and to avoid max argument length problems.\n                    byte[] bytes = Encoding.UTF8.GetBytes(string.Join(\"\\n\", accounts));\n                    using var ms = new MemoryStream(bytes);\n                    using var stdin = new StreamReader(ms);\n\n                    IDictionary<string, string> resultDict = await InvokeHelperAsync(command, promptArgs.ToString(), stdin);\n\n                    if (!resultDict.TryGetValue(\"account\", out string selectedAccount))\n                    {\n                        throw new Exception(\"Missing 'account' in response\");\n                    }\n\n                    return string.IsNullOrWhiteSpace(selectedAccount) ? null : selectedAccount;\n                }\n\n                var viewModel = new SelectAccountViewModel(Context.SessionManager, accounts);\n\n                if (!GitHubHostProvider.IsGitHubDotCom(targetUri))\n                {\n                    viewModel.EnterpriseUrl = targetUri.ToString();\n                }\n\n                await AvaloniaUi.ShowViewAsync<SelectAccountView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n                ThrowIfWindowCancelled(viewModel);\n\n                return viewModel.SelectedAccount?.UserName;\n            }\n\n            ThrowIfTerminalPromptsDisabled();\n            var menuTitle = $\"Select an account for '{targetUri}'\";\n            var menu = new TerminalMenu(Context.Terminal, menuTitle);\n            var addNewItem = menu.Add(\"Add a new account\");\n\n            foreach (string account in accounts)\n            {\n                menu.Add(account);\n            }\n\n            TerminalMenuItem choice = menu.Show();\n            return choice == addNewItem ? null : choice.Name;\n        }\n\n        public async Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            // If we cannot start a browser then don't offer the option\n            if (!Context.SessionManager.IsWebBrowserAvailable)\n            {\n                modes = modes & ~AuthenticationModes.Browser;\n            }\n\n            // We need at least one mode!\n            if (modes == AuthenticationModes.None)\n            {\n                throw new ArgumentException(@$\"Must specify at least one {nameof(AuthenticationModes)}\", nameof(modes));\n            }\n\n            // If there is no mode choice to be made and no interaction required,\n            // just return that result.\n            if (modes == AuthenticationModes.Browser ||\n                modes == AuthenticationModes.Device)\n            {\n                return new AuthenticationPromptResult(modes);\n            }\n\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string command, out string args))\n                {\n                    return await GetAuthenticationViaHelperAsync(targetUri, userName, modes, command, args);\n                }\n\n                return await GetAuthenticationViaUiAsync(targetUri, userName, modes);\n            }\n\n            return GetAuthenticationViaTty(targetUri, userName, modes);\n        }\n\n        private async Task<AuthenticationPromptResult> GetAuthenticationViaUiAsync(\n            Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            var viewModel = new CredentialsViewModel(Context.SessionManager, Context.ProcessManager)\n            {\n                ShowBrowserLogin = (modes & AuthenticationModes.Browser) != 0,\n                ShowDeviceLogin  = (modes & AuthenticationModes.Device) != 0,\n                ShowTokenLogin   = (modes & AuthenticationModes.Pat) != 0,\n                ShowBasicLogin   = (modes & AuthenticationModes.Basic) != 0,\n            };\n\n            if (!GitHubHostProvider.IsGitHubDotCom(targetUri))\n            {\n                viewModel.EnterpriseUrl = targetUri.ToString();\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                viewModel.UserName = userName;\n            }\n\n            await AvaloniaUi.ShowViewAsync<CredentialsView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n            ThrowIfWindowCancelled(viewModel);\n\n            switch (viewModel.SelectedMode)\n            {\n                case AuthenticationModes.Basic:\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Basic,\n                        new GitCredential(viewModel.UserName, viewModel.Password)\n                    );\n\n                case AuthenticationModes.Browser:\n                    return new AuthenticationPromptResult(AuthenticationModes.Browser);\n\n                case AuthenticationModes.Device:\n                    return new AuthenticationPromptResult(AuthenticationModes.Device);\n\n                case AuthenticationModes.Pat:\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Pat,\n                        new GitCredential(userName, viewModel.Token)\n                    );\n\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n        }\n\n        private AuthenticationPromptResult GetAuthenticationViaTty(Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            switch (modes)\n            {\n                case AuthenticationModes.Basic:\n                    Context.Terminal.WriteLine(\"Enter GitHub credentials for '{0}'...\", targetUri);\n\n                    if (string.IsNullOrWhiteSpace(userName))\n                    {\n                        userName = Context.Terminal.Prompt(\"Username\");\n                    }\n                    else\n                    {\n                        Context.Terminal.WriteLine(\"Username: {0}\", userName);\n                    }\n\n                    string password = Context.Terminal.PromptSecret(\"Password\");\n\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Basic, new GitCredential(userName, password));\n\n                case AuthenticationModes.Browser:\n                    return new AuthenticationPromptResult(AuthenticationModes.Browser);\n\n                case AuthenticationModes.Device:\n                    return new AuthenticationPromptResult(AuthenticationModes.Device);\n\n                case AuthenticationModes.Pat:\n                    Context.Terminal.WriteLine(\"Enter GitHub personal access token for '{0}'...\", targetUri);\n                    string pat = Context.Terminal.PromptSecret(\"Token\");\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Pat, new GitCredential(userName, pat));\n\n                case AuthenticationModes.None:\n                    throw new ArgumentOutOfRangeException(nameof(modes),\n                        @$\"At least one {nameof(AuthenticationModes)} must be supplied\");\n\n                default:\n                    var menuTitle = $\"Select an authentication method for '{targetUri}'\";\n                    var menu = new TerminalMenu(Context.Terminal, menuTitle);\n\n                    TerminalMenuItem browserItem = null;\n                    TerminalMenuItem deviceItem = null;\n                    TerminalMenuItem basicItem = null;\n                    TerminalMenuItem patItem = null;\n\n                    if ((modes & AuthenticationModes.Browser) != 0) browserItem = menu.Add(\"Web browser\");\n                    if ((modes & AuthenticationModes.Device) != 0) deviceItem = menu.Add(\"Device code\");\n                    if ((modes & AuthenticationModes.Pat) != 0) patItem = menu.Add(\"Personal access token\");\n                    if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add(\"Username/password\");\n\n                    // Default to the 'first' choice in the menu\n                    TerminalMenuItem choice = menu.Show(0);\n\n                    if (choice == browserItem) goto case AuthenticationModes.Browser;\n                    if (choice == deviceItem) goto case AuthenticationModes.Device;\n                    if (choice == basicItem) goto case AuthenticationModes.Basic;\n                    if (choice == patItem) goto case AuthenticationModes.Pat;\n\n                    throw new Exception();\n            }\n        }\n\n        private async Task<AuthenticationPromptResult> GetAuthenticationViaHelperAsync(\n            Uri targetUri, string userName, AuthenticationModes modes, string command, string args)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"prompt\");\n            if (modes == AuthenticationModes.All)\n            {\n                promptArgs.Append(\" --all\");\n            }\n            else\n            {\n                if ((modes & AuthenticationModes.Basic) != 0) promptArgs.Append(\" --basic\");\n                if ((modes & AuthenticationModes.Browser) != 0) promptArgs.Append(\" --browser\");\n                if ((modes & AuthenticationModes.Device) != 0) promptArgs.Append(\" --device\");\n                if ((modes & AuthenticationModes.Pat) != 0) promptArgs.Append(\" --pat\");\n            }\n\n            if (!GitHubHostProvider.IsGitHubDotCom(targetUri))\n                promptArgs.AppendFormat(\" --enterprise-url {0}\", QuoteCmdArg(targetUri.ToString()));\n            if (!string.IsNullOrWhiteSpace(userName)) promptArgs.AppendFormat(\" --username {0}\", QuoteCmdArg(userName));\n\n            IDictionary<string, string> resultDict = await InvokeHelperAsync(command, promptArgs.ToString(), null);\n\n            if (!resultDict.TryGetValue(\"mode\", out string responseMode))\n            {\n                throw new Trace2Exception(Context.Trace2, \"Missing 'mode' in response\");\n            }\n\n            switch (responseMode.ToLowerInvariant())\n            {\n                case \"pat\":\n                    if (!resultDict.TryGetValue(\"pat\", out string pat))\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing 'pat' in response\");\n                    }\n\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Pat, new GitCredential(userName, pat));\n\n                case \"browser\":\n                    return new AuthenticationPromptResult(AuthenticationModes.Browser);\n\n                case \"device\":\n                    return new AuthenticationPromptResult(AuthenticationModes.Device);\n\n                case \"basic\":\n                    if (!resultDict.TryGetValue(\"username\", out userName))\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing 'username' in response\");\n                    }\n\n                    if (!resultDict.TryGetValue(\"password\", out string password))\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing 'password' in response\");\n                    }\n\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Basic, new GitCredential(userName, password));\n\n                default:\n                    throw new Trace2Exception(Context.Trace2,\n                        $\"Unknown mode value in response '{responseMode}'\");\n            }\n        }\n\n        public async Task<string> GetTwoFactorCodeAsync(Uri targetUri, bool isSms)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string command, out string args))\n                {\n                    return await GetTwoFactorCodeViaHelperAsync(isSms, args, command);\n                }\n\n                return await GetTwoFactorCodeViaUiAsync(targetUri, isSms);\n            }\n\n            return GetTwoFactorCodeViaTty(isSms);\n        }\n\n        private async Task<string> GetTwoFactorCodeViaUiAsync(Uri targetUri, bool isSms)\n        {\n            var viewModel = new TwoFactorViewModel(Context.SessionManager, Context.ProcessManager)\n            {\n                IsSms = isSms\n            };\n\n            await AvaloniaUi.ShowViewAsync<TwoFactorView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n            \n            ThrowIfWindowCancelled(viewModel);\n\n            return viewModel.Code;\n        }\n\n        private string GetTwoFactorCodeViaTty(bool isSms)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            Context.Terminal.WriteLine(\"Two-factor authentication is enabled and an authentication code is required.\");\n\n            Context.Terminal.WriteLine(isSms\n                ? \"An SMS containing the authentication code has been sent to your registered device.\"\n                : \"Use your registered authentication app to generate an authentication code.\");\n\n            return Context.Terminal.Prompt(\"Authentication code\");\n        }\n\n        private async Task<string> GetTwoFactorCodeViaHelperAsync(bool isSms, string args, string command)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"2fa\");\n            if (isSms) promptArgs.Append(\" --sms\");\n\n            IDictionary<string, string> resultDict = await InvokeHelperAsync(command, promptArgs.ToString(), null);\n\n            if (!resultDict.TryGetValue(\"code\", out string authCode))\n            {\n                throw new Trace2Exception(Context.Trace2, \"Missing 'code' in response\");\n            }\n\n            return authCode;\n        }\n\n        public async Task<OAuth2TokenResult> GetOAuthTokenViaBrowserAsync(Uri targetUri, IEnumerable<string> scopes, string loginHint)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2);\n\n            // Can we launch the user's default web browser?\n            if (!Context.SessionManager.IsWebBrowserAvailable)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"Browser authentication requires a desktop session\");\n            }\n\n            var browserOptions = new OAuth2WebBrowserOptions\n            {\n                SuccessResponseHtml = GitHubResources.AuthenticationResponseSuccessHtml,\n                FailureResponseHtmlFormat = GitHubResources.AuthenticationResponseFailureHtmlFormat\n            };\n            var browser = new OAuth2SystemWebBrowser(Context.SessionManager, browserOptions);\n\n            // If we have a login hint we should pass this to GitHub as an extra query parameter\n            IDictionary<string, string> queryParams = null;\n            if (loginHint != null)\n            {\n                queryParams = new Dictionary<string, string>\n                {\n                    [\"login\"] = loginHint\n                };\n            }\n\n            // Write message to the terminal (if any is attached) for some feedback that we're waiting for a web response\n            Context.Terminal.WriteLine(\"info: please complete authentication in your browser...\");\n\n            OAuth2AuthorizationCodeResult authCodeResult =\n                await oauthClient.GetAuthorizationCodeAsync(scopes, browser, queryParams, CancellationToken.None);\n\n            return await oauthClient.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);\n        }\n\n        public async Task<OAuth2TokenResult> GetOAuthTokenViaDeviceCodeAsync(Uri targetUri, IEnumerable<string> scopes)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2);\n            OAuth2DeviceCodeResult dcr = await oauthClient.GetDeviceCodeAsync(scopes, CancellationToken.None);\n\n            // If we have a desktop session show the device code in a dialog\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                var promptCts = new CancellationTokenSource();\n                var tokenCts = new CancellationTokenSource();\n\n                // Show the dialog with the device code but don't await its closure\n                Task promptTask = TryFindHelperCommand(out string command, out string args)\n                    ? ShowDeviceCodeViaHelperAsync(dcr, command, args, promptCts.Token)\n                    : ShowDeviceCodeViaUiAsync(dcr, promptCts.Token);\n\n                // Start the request for an OAuth token but don't wait\n                Task<OAuth2TokenResult> tokenTask = oauthClient.GetTokenByDeviceCodeAsync(dcr, tokenCts.Token);\n\n                Task t = await Task.WhenAny(promptTask, tokenTask);\n\n                // If the dialog was closed the user wishes to cancel the request\n                if (t == promptTask)\n                {\n                    tokenCts.Cancel();\n                }\n\n                OAuth2TokenResult tokenResult;\n                try\n                {\n                    tokenResult = await tokenTask;\n                }\n                catch (OperationCanceledException)\n                {\n                    throw new Trace2InvalidOperationException(Context.Trace2,\n                        \"User canceled device code authentication\");\n                }\n\n                // Close the dialog\n                promptCts.Cancel();\n\n                return tokenResult;\n            }\n\n            return await GetOAuthTokenViaDeviceCodeViaTtyAsync(oauthClient, dcr);\n        }\n\n        private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationToken ct)\n        {\n            var viewModel = new DeviceCodeViewModel(Context.SessionManager)\n            {\n                UserCode = dcr.UserCode,\n                VerificationUrl = dcr.VerificationUri.ToString(),\n            };\n\n            return AvaloniaUi.ShowViewAsync<DeviceCodeView>(viewModel, GetParentWindowHandle(), ct);\n        }\n\n        private async Task<OAuth2TokenResult> GetOAuthTokenViaDeviceCodeViaTtyAsync(GitHubOAuth2Client oauthClient, OAuth2DeviceCodeResult dcr)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            string deviceMessage =\n                $\"To complete authentication please visit {dcr.VerificationUri} and enter the following code:\" +\n                Environment.NewLine +\n                dcr.UserCode;\n            Context.Terminal.WriteLine(deviceMessage);\n\n            return await oauthClient.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None);\n        }\n\n        private Task ShowDeviceCodeViaHelperAsync(\n            OAuth2DeviceCodeResult dcr, string command, string args, CancellationToken ct)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"device\");\n            promptArgs.AppendFormat(\" --code {0} \", QuoteCmdArg(dcr.UserCode));\n            promptArgs.AppendFormat(\" --url {0}\", QuoteCmdArg(dcr.VerificationUri.ToString()));\n\n            return InvokeHelperAsync(command, promptArgs.ToString(), null, ct);\n        }\n\n        private bool TryFindHelperCommand(out string command, out string args)\n        {\n            return TryFindHelperCommand(\n                GitHubConstants.EnvironmentVariables.AuthenticationHelper,\n                GitHubConstants.GitConfiguration.Credential.AuthenticationHelper,\n                GitHubConstants.DefaultAuthenticationHelper,\n                out command,\n                out args);\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient => _httpClient ?? (_httpClient = Context.HttpClientFactory.CreateClient());\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubConstants.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace GitHub\n{\n    public static class GitHubConstants\n    {\n        public const string GitHubBaseUrlHost = \"github.com\";\n        public const string GistBaseUrlHost = \"gist.\" + GitHubBaseUrlHost;\n\n        public const string DefaultAuthenticationHelper = \"GitHub.UI\";\n\n        // https://github.com/settings/connections/applications/0120e057bd645470c1ed\n        public const string OAuthClientId = \"0120e057bd645470c1ed\";\n\n        // [SuppressMessage(\"Microsoft.Security\", \"CS002:SecretInNextLine\", Justification=\"OAuth2 public client application 'secrets' are required and permitted to be public\")]\n        public const string OAuthClientSecret = \"18867509d956965542b521a529a79bb883344c90\";\n        public static readonly Uri OAuthRedirectUri = new Uri(\"http://127.0.0.1/\"); // Note that the trailing slash is important!\n        public static readonly Uri OAuthLegacyRedirectUri = new Uri(\"http://localhost/\"); // Note that the trailing slash is important!\n        public static readonly Uri OAuthAuthorizationEndpointRelativeUri = new Uri(\"/login/oauth/authorize\", UriKind.Relative);\n        public static readonly Uri OAuthTokenEndpointRelativeUri = new Uri(\"/login/oauth/access_token\", UriKind.Relative);\n        public static readonly Uri OAuthDeviceEndpointRelativeUri = new Uri(\"/login/device/code\", UriKind.Relative);\n\n        /// <summary>\n        /// GitHub user names that contain underscores but are not EMU logins.\n        /// </summary>\n        public static readonly IReadOnlyList<string> InvalidUnderscoreLogins = new[] { \"pj_nitin\", \"up_the_irons\" };\n\n        /// <summary>\n        /// The GitHub required HTTP accepts header value\n        /// </summary>\n        public const string GitHubApiAcceptsHeaderValue = \"application/vnd.github.v3+json\";\n        public const string GitHubOptHeader = \"X-GitHub-OTP\";\n\n        /// <summary>\n        /// Minimum GitHub Enterprise Server version that supports OAuth authentication with GCM.\n        /// </summary>\n        public static readonly Version MinimumOnPremOAuthVersion = new Version(\"3.2\");\n\n        /// <summary>\n        /// Supported authentication modes for GitHub.com.\n        /// </summary>\n        /// <remarks>\n        /// As of 13th November 2020, GitHub.com does not support username/password (basic) authentication to the APIs.\n        /// See https://developer.github.com/changes/2020-02-14-deprecating-oauth-auth-endpoint for more information.\n        /// </remarks>\n        public const AuthenticationModes DotComAuthenticationModes = AuthenticationModes.OAuth | AuthenticationModes.Pat;\n\n        public static class TokenScopes\n        {\n            public const string Gist = \"gist\";\n            public const string Repo = \"repo\";\n        }\n\n        public static class OAuthScopes\n        {\n            public const string Gist = \"gist\";\n            public const string Repo = \"repo\";\n            public const string Workflow = \"workflow\";\n        }\n\n        public static class EnvironmentVariables\n        {\n            public const string AuthenticationHelper = \"GCM_GITHUB_HELPER\";\n            public const string AuthenticationModes = \"GCM_GITHUB_AUTHMODES\";\n            public const string DevOAuthClientId = \"GCM_DEV_GITHUB_CLIENTID\";\n            public const string DevOAuthClientSecret = \"GCM_DEV_GITHUB_CLIENTSECRET\";\n            public const string DevOAuthRedirectUri = \"GCM_DEV_GITHUB_REDIRECTURI\";\n            public const string AccountFiltering = \"GCM_GITHUB_ACCOUNTFILTERING\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string AuthenticationHelper = \"gitHubHelper\";\n                public const string AuthenticationModes = \"gitHubAuthModes\";\n                public const string DevOAuthClientId = \"gitHubDevClientId\";\n                public const string DevOAuthClientSecret = \"gitHubDevClientSecret\";\n                public const string DevOAuthRedirectUri = \"gitHubDevRedirectUri\";\n                public const string AccountFiltering = \"githubAccountFiltering\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubHostProvider.Commands.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Commands;\n\nnamespace GitHub;\n\npublic partial class GitHubHostProvider : ICommandProvider\n{\n    public ProviderCommand CreateCommand()\n    {\n        var rootCmd = new ProviderCommand(this);\n\n        var urlOpt = new Option<Uri>(\"--url\",\n            \"URL of the GitHub instance to target, otherwise use GitHub.com\");\n\n        //\n        // list [--url <url>]\n        //\n        var listCmd = new Command(\"list\", \"List all known GitHub accounts.\");\n        listCmd.AddOption(urlOpt);\n        listCmd.SetHandler(ListAccounts, urlOpt);\n\n        //\n        // login [--url <url>] [--username <username>] [--device | --browser/--web | --pat/--token <token>]\n        //\n        var loginCmd = new Command(\"login\", \"Add a GitHub account.\");\n        var userNameOpt = new Option<string>(\"--username\", \"User name to authenticate with\");\n        var deviceOpt = new Option<bool>(\"--device\", \"Use device flow to authenticate\");\n        var browserOpt = new Option<bool>(new[]{\"--web\", \"--browser\"}, \"Use a web browser to authenticate\");\n        var tokenOpt = new Option<string>(new[] {\"--pat\", \"--token\"}, \"Use personal access token to authenticate\");\n        var forceOpt = new Option<bool>(\"--force\", \"Force re-authentication even if a credential already exists for the account\");\n        loginCmd.AddOption(urlOpt);\n        loginCmd.AddOption(userNameOpt);\n        loginCmd.AddOptionSet(OptionArity.ZeroOrOne, deviceOpt, browserOpt, tokenOpt);\n        loginCmd.AddOption(forceOpt);\n        loginCmd.SetHandler(AddAccountAsync, urlOpt, userNameOpt, deviceOpt, browserOpt, tokenOpt, forceOpt);\n\n        //\n        // logout <account> [--url <url>]\n        //\n        var logoutCmd = new Command(\"logout\", \"Remove a GitHub account.\");\n        var accountArg = new Argument<string>(\"account\", \"Account to remove\")\n        {\n            Arity = ArgumentArity.ExactlyOne\n        };\n        logoutCmd.AddArgument(accountArg);\n        logoutCmd.AddOption(urlOpt);\n        logoutCmd.SetHandler(RemoveAccount, accountArg, urlOpt);\n\n        rootCmd.AddCommand(listCmd);\n        rootCmd.AddCommand(loginCmd);\n        rootCmd.AddCommand(logoutCmd);\n\n        return rootCmd;\n    }\n\n    private void ListAccounts(Uri url)\n    {\n        string service = url is null || IsGitHubDotCom(url)\n            ? $\"https://{GitHubConstants.GitHubBaseUrlHost}\"\n            : GetServiceName(url);\n\n        IList<string> accounts = _context.CredentialStore.GetAccounts(service);\n\n        foreach (string account in accounts)\n        {\n            _context.Streams.Out.WriteLine(account);\n        }\n    }\n\n    private async Task<int> AddAccountAsync(Uri url, string userName, bool device, bool browser, string token, bool force)\n    {\n        // Default to GitHub.com\n        url ??= new Uri($\"https://{GitHubConstants.GitHubBaseUrlHost}\");\n\n        // Prefer the username specified on the command-line\n        userName ??= url.GetUserName();\n\n        string service = GetServiceName(url);\n\n        // If we've already got a credential for this account then we can skip the login flow\n        // (so long as the user isn't explicitly forcing a re-authentication).\n        if (!string.IsNullOrWhiteSpace(userName) && !force)\n        {\n            IList<string> existingAccounts = _context.CredentialStore.GetAccounts(service);\n            if (existingAccounts.Any(x => StringComparer.OrdinalIgnoreCase.Equals(x, userName)))\n            {\n                string prettyUrl = url.AbsoluteUri.TrimEnd('/');\n                _context.Streams.Out.WriteLine(\n                    $\"Account '{userName}' already has credentials for {prettyUrl}; use --force to re-authenticate\"\n                );\n                return 0;\n            }\n        }\n\n        ICredential credential;\n        if (token is not null)\n        {\n            // Resolve the GitHub user handle if the user didn't supply one\n            if (string.IsNullOrEmpty(userName))\n            {\n                GitHubUserInfo userInfo = await _gitHubApi.GetUserInfoAsync(url, token);\n                userName = userInfo.Login;\n            }\n\n            credential = new GitCredential(userName, token);\n        }\n        else if (device || browser)\n        {\n            credential = await GenerateOAuthCredentialAsync(url, loginHint: userName, useBrowser: browser);\n        }\n        else\n        {\n            credential = await GenerateCredentialAsync(url, userName);\n        }\n\n        _context.CredentialStore.AddOrUpdate(service, credential.Account, credential.Password);\n\n        return 0;\n    }\n\n    private Task<int> RemoveAccount(string account, Uri url)\n    {\n        string service = url is null || IsGitHubDotCom(url)\n            ? $\"https://{GitHubConstants.GitHubBaseUrlHost}\"\n            : GetServiceName(url);\n\n        bool result = _context.CredentialStore.Remove(service, account);\n\n        if (!result)\n        {\n            _context.Streams.Error.WriteLine($\"warning: no such account '{account}' found.\");\n            return Task.FromResult(-1);\n        }\n\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubHostProvider.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitHub.Diagnostics;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Diagnostics;\n\nnamespace GitHub\n{\n    public partial class GitHubHostProvider : DisposableObject, IHostProvider, IDiagnosticProvider\n    {\n        private static readonly string[] GitHubOAuthScopes =\n        {\n            GitHubConstants.OAuthScopes.Repo,\n            GitHubConstants.OAuthScopes.Gist,\n            GitHubConstants.OAuthScopes.Workflow,\n        };\n\n        private static readonly string[] GitHubCredentialScopes =\n        {\n            GitHubConstants.TokenScopes.Gist,\n            GitHubConstants.TokenScopes.Repo\n        };\n\n        private readonly IGitHubRestApi _gitHubApi;\n        private readonly IGitHubAuthentication _gitHubAuth;\n        private readonly ICommandContext _context;\n\n        public GitHubHostProvider(ICommandContext context)\n            : this(context, new GitHubRestApi(context), new GitHubAuthentication(context)) { }\n\n        public GitHubHostProvider(ICommandContext context, IGitHubRestApi gitHubApi, IGitHubAuthentication gitHubAuth)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n            EnsureArgument.NotNull(gitHubApi, nameof(gitHubApi));\n            EnsureArgument.NotNull(gitHubAuth, nameof(gitHubAuth));\n\n            _context = context;\n            _gitHubApi = gitHubApi;\n            _gitHubAuth = gitHubAuth;\n        }\n\n        public string Id => \"github\";\n\n        public string Name => \"GitHub\";\n\n        public IEnumerable<string> SupportedAuthorityIds => GitHubAuthentication.AuthorityIds;\n\n        public bool IsSupported(InputArguments input)\n        {\n            if (input is null)\n            {\n                return false;\n            }\n\n            // We do not support unencrypted HTTP communications to GitHub,\n            // but we report `true` here for HTTP so that we can show a helpful\n            // error message for the user in `CreateCredentialAsync`.\n            if (!StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\") &&\n                !StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"https\"))\n            {\n                return false;\n            }\n\n            // Split port number and hostname from host input argument\n            if (!input.TryGetHostAndPort(out string hostName, out _))\n            {\n                return false;\n            }\n\n            if (StringComparer.OrdinalIgnoreCase.Equals(hostName, GitHubConstants.GitHubBaseUrlHost) ||\n                StringComparer.OrdinalIgnoreCase.Equals(hostName, GitHubConstants.GistBaseUrlHost))\n            {\n                return true;\n            }\n\n            string[] domains = hostName.Split(new char[] { '.' });\n\n            // github[.subdomain].domain.tld\n            if (domains.Length >= 3 &&\n                StringComparer.OrdinalIgnoreCase.Equals(domains[0], \"github\"))\n            {\n                return true;\n            }\n\n            // gist.github[.subdomain].domain.tld\n            if (domains.Length >= 4 &&\n                StringComparer.OrdinalIgnoreCase.Equals(domains[0], \"gist\") &&\n                StringComparer.OrdinalIgnoreCase.Equals(domains[1], \"github\"))\n            {\n                return true;\n            }\n\n            return false;\n        }\n\n        public bool IsSupported(HttpResponseMessage response)\n        {\n            if (response is null)\n            {\n                return false;\n            }\n\n            // Look for a known GitHub.com/GHES header\n            return response.Headers.Contains(\"X-GitHub-Request-Id\");\n        }\n\n        internal static /* for testing purposes */ string GetServiceName(InputArguments input)\n        {\n            // Get the remote URI without user information\n            var baseUri = input.GetRemoteUri(includeUser: false);\n\n            return GetServiceName(baseUri);\n        }\n\n        private static string GetServiceName(Uri baseUri)\n        {\n            // Normalise the URI\n            string url = NormalizeUri(baseUri).AbsoluteUri;\n\n            // Trim trailing slash\n            return url.TrimEnd('/');\n        }\n\n        public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n            Uri remoteUri = input.GetRemoteUri();\n\n            // If we have a specific username then we can try and find an existing credential for that account.\n            // If not, we should check what accounts are available in the store and prompt the user if there\n            // are multiple options.\n            string userName = input.UserName;\n            bool addAccount = false;\n            bool filtered = false;\n            if (string.IsNullOrWhiteSpace(userName))\n            {\n                IList<string> accounts = _context.CredentialStore.GetAccounts(service);\n                _context.Trace.WriteLine($\"Found {accounts.Count} accounts in the store for service={service}{(accounts.Count > 0 ? \":\" : \".\")}\");\n                foreach (string account in accounts)\n                {\n                    _context.Trace.WriteLine($\"  {account}\");\n                }\n\n                filtered = FilterAccounts(remoteUri, input.WwwAuth, ref accounts);\n\n                switch (accounts.Count)\n                {\n                    case 1:\n                        _context.Trace.WriteLine(\"Only one account available - using that one!\");\n                        userName = accounts[0];\n                        break;\n\n                    case > 1:\n                        _context.Trace.WriteLine(\"Multiple accounts available - prompting user to select one...\");\n                        userName = await _gitHubAuth.SelectAccountAsync(remoteUri, accounts);\n                        addAccount = userName is null;\n                        break;\n                }\n            }\n\n            // Always try and locate an existing credential in the OS credential store unless we're being\n            // told to explicitly add a new account OR have specifically filtered out irrelevant accounts.\n            // If the account lookup failed for another reason we should still try to lookup an existing credential.\n            ICredential credential = null;\n            if (addAccount)\n            {\n                _context.Trace.WriteLine(\"Adding a new account!\");\n            }\n            else if (!string.IsNullOrWhiteSpace(userName) || !filtered)\n            {\n                _context.Trace.WriteLine($\"Looking for existing credential in store with service={service} account={userName}...\");\n                credential = _context.CredentialStore.Get(service, userName);\n            }\n\n            if (credential == null)\n            {\n                _context.Trace.WriteLine(\"No existing credentials found.\");\n\n                // No existing credential was found, create a new one\n                _context.Trace.WriteLine(\"Creating new credential...\");\n                credential = await GenerateCredentialAsync(remoteUri, userName);\n                _context.Trace.WriteLine(\"Credential created.\");\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"Existing credential found.\");\n            }\n\n            return new GetCredentialResult(credential);\n        }\n\n        private bool FilterAccounts(Uri remoteUri, IEnumerable<string> wwwAuth, ref IList<string> accounts)\n        {\n            if (!IsGitHubDotCom(remoteUri))\n            {\n                _context.Trace.WriteLine(\"No account filtering outside of GitHub.com.\");\n            }\n\n            // Allow the user to disable account filtering until this feature stabilises.\n            // Default to enabled.\n            bool enableFiltering = !_context.Settings.TryGetSetting(\n                GitHubConstants.EnvironmentVariables.AccountFiltering,\n                Constants.GitConfiguration.Credential.SectionName,\n                GitHubConstants.GitConfiguration.Credential.AccountFiltering,\n                out string enableFilteringStr\n            ) || enableFilteringStr.ToBooleanyOrDefault(true);\n\n            if (!enableFiltering)\n            {\n                _context.Trace.WriteLine(\"Account filtering is disabled.\");\n                return false;\n            }\n\n            _context.Trace.WriteLine(\"Account filtering is enabled.\");\n\n            // If we have a WWW-Authenticate header then we can try and use any domain hint information\n            // to filter the list of accounts to only those that are valid for that domain.\n            // We only expect one challenge header to be returned, but if we're given more we just select the first.\n            GitHubAuthChallenge authChallenge = GitHubAuthChallenge.FromHeaders(wwwAuth).FirstOrDefault();\n            if (authChallenge is not null)\n            {\n                _context.Trace.WriteLine(\"Filtering based on WWW-Authenticate header information...\");\n                accounts = accounts.Where(authChallenge.IsDomainMember).ToList();\n\n                _context.Trace.WriteLine(string.IsNullOrWhiteSpace(authChallenge.Domain)\n                    ? $\"Matched {accounts.Count} accounts with public domain:\"\n                    : $\"Matched {accounts.Count} accounts with domain={authChallenge.Domain}:\");\n                foreach (string account in accounts)\n                {\n                    _context.Trace.WriteLine($\"  {account}\");\n                }\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public virtual Task StoreCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n\n            // WIA-authentication is signaled to Git as an empty username/password pair\n            // and we will get called to 'store' these WIA credentials.\n            // We avoid storing empty credentials.\n            if (string.IsNullOrWhiteSpace(input.UserName) && string.IsNullOrWhiteSpace(input.Password))\n            {\n                _context.Trace.WriteLine(\"Not storing empty credential.\");\n            }\n            else\n            {\n                // Add or update the credential in the store.\n                _context.Trace.WriteLine($\"Storing credential with service={service} account={input.UserName}...\");\n                _context.CredentialStore.AddOrUpdate(service, input.UserName, input.Password);\n                _context.Trace.WriteLine(\"Credential was successfully stored.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public virtual Task EraseCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n\n            // Try to locate an existing credential\n            _context.Trace.WriteLine($\"Erasing stored credential in store with service={service} account={input.UserName}...\");\n            if (_context.CredentialStore.Remove(service, input.UserName))\n            {\n                _context.Trace.WriteLine(\"Credential was successfully erased.\");\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"No credential was erased.\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        internal /* for testing purposes */  async Task<ICredential> GenerateCredentialAsync(Uri remoteUri, string userName)\n        {\n            ThrowIfDisposed();\n\n            // We should not allow unencrypted communication and should inform the user\n            if (!_context.Settings.AllowUnsafeRemotes &&\n                StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, \"http\"))\n            {\n                throw new Trace2Exception(_context.Trace2,\n                    \"Unencrypted HTTP is not recommended for GitHub. \" +\n                    \"Ensure the repository remote URL is using HTTPS \" +\n                    $\"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.\");\n            }\n\n            string service = GetServiceName(remoteUri);\n\n            AuthenticationModes authModes = await GetSupportedAuthenticationModesAsync(remoteUri);\n\n            AuthenticationPromptResult promptResult = await _gitHubAuth.GetAuthenticationAsync(remoteUri, userName, authModes);\n\n            switch (promptResult.AuthenticationMode)\n            {\n                case AuthenticationModes.Basic:\n                    GitCredential patCredential = await GeneratePersonalAccessTokenAsync(remoteUri, promptResult.Credential);\n\n                    // HACK: Store the PAT immediately in case this PAT is not valid for SSO.\n                    // We don't know if this PAT is valid for SAML SSO and if it's not Git will fail\n                    // with a 403 and call neither 'store' or 'erase'. The user is expected to fiddle with\n                    // the PAT permissions manually on the web and then retry the Git operation.\n                    // We must store the PAT now so they can resume/repeat the operation with the same,\n                    // now SSO authorized, PAT.\n                    // See: https://github.com/git-ecosystem/git-credential-manager/issues/133\n                    _context.CredentialStore.AddOrUpdate(service, patCredential.Account, patCredential.Password);\n                    return patCredential;\n\n                case AuthenticationModes.Browser:\n                    return await GenerateOAuthCredentialAsync(remoteUri, loginHint: userName, useBrowser: true);\n\n                case AuthenticationModes.Device:\n                    return await GenerateOAuthCredentialAsync(remoteUri, loginHint: userName, useBrowser: false);\n\n                case AuthenticationModes.Pat:\n                    // The token returned by the user should be good to use directly as the password for Git\n                    string token = promptResult.Credential.Password;\n\n                    // Resolve the GitHub user handle if we don't have a specific username already from the\n                    // initial request. The reason for this is GitHub requires a (any?) value for the username\n                    // when Git makes calls to GitHub.\n                    userName = promptResult.Credential.Account;\n                    if (userName is null)\n                    {\n                        GitHubUserInfo userInfo = await _gitHubApi.GetUserInfoAsync(remoteUri, token);\n                        userName = userInfo.Login;\n                    }\n\n                    return new GitCredential(userName, token);\n\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(promptResult));\n            }\n        }\n\n        private async Task<GitCredential> GenerateOAuthCredentialAsync(Uri targetUri, string loginHint, bool useBrowser)\n        {\n            OAuth2TokenResult result = useBrowser\n                ? await _gitHubAuth.GetOAuthTokenViaBrowserAsync(targetUri, GitHubOAuthScopes, loginHint)\n                : await _gitHubAuth.GetOAuthTokenViaDeviceCodeAsync(targetUri, GitHubOAuthScopes);\n\n            // Resolve the GitHub user handle\n            GitHubUserInfo userInfo = await _gitHubApi.GetUserInfoAsync(targetUri, result.AccessToken);\n\n            return new GitCredential(userInfo.Login, result.AccessToken);\n        }\n\n        private async Task<GitCredential> GeneratePersonalAccessTokenAsync(Uri targetUri, ICredential credentials)\n        {\n            AuthenticationResult result = await _gitHubApi.CreatePersonalTokenAsync(\n                targetUri, credentials, null, GitHubCredentialScopes);\n\n            string token = null;\n\n            if (result.Type == GitHubAuthenticationResultType.Success)\n            {\n                _context.Trace.WriteLine($\"Token acquisition for '{targetUri}' succeeded\");\n\n                token = result.Token;\n            }\n            else if (result.Type == GitHubAuthenticationResultType.TwoFactorApp ||\n                     result.Type == GitHubAuthenticationResultType.TwoFactorSms)\n            {\n                bool isSms = result.Type == GitHubAuthenticationResultType.TwoFactorSms;\n\n                string authCode = await _gitHubAuth.GetTwoFactorCodeAsync(targetUri, isSms);\n\n                result = await _gitHubApi.CreatePersonalTokenAsync(targetUri, credentials, authCode, GitHubCredentialScopes);\n\n                if (result.Type == GitHubAuthenticationResultType.Success)\n                {\n                    _context.Trace.WriteLine($\"Token acquisition for '{targetUri}' succeeded.\");\n\n                    token = result.Token;\n                }\n            }\n\n            if (token != null)\n            {\n                // Resolve the GitHub user handle\n                GitHubUserInfo userInfo = await _gitHubApi.GetUserInfoAsync(targetUri, token);\n\n                return new GitCredential(userInfo.Login, token);\n            }\n\n            var format = \"Interactive logon for '{0}' failed.\";\n            var message = string.Format(format, targetUri);\n            throw new Trace2Exception(_context.Trace2, message, format);\n        }\n\n        internal async Task<AuthenticationModes> GetSupportedAuthenticationModesAsync(Uri targetUri)\n        {\n            // Check for an explicit override for supported authentication modes\n            if (_context.Settings.TryGetSetting(\n                GitHubConstants.EnvironmentVariables.AuthenticationModes,\n                Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.AuthenticationModes,\n                out string authModesStr))\n            {\n                if (Enum.TryParse(authModesStr, true, out AuthenticationModes authModes) && authModes != AuthenticationModes.None)\n                {\n                    _context.Trace.WriteLine($\"Supported authentication modes override present: {authModes}\");\n                    return authModes;\n                }\n                else\n                {\n                    _context.Trace.WriteLine($\"Invalid value for supported authentication modes override setting: '{authModesStr}'\");\n                }\n            }\n\n            // GitHub.com should use OAuth or manual PAT based authentication only, never basic auth as of 13th November 2020\n            // https://developer.github.com/changes/2020-02-14-deprecating-oauth-auth-endpoint\n            if (IsGitHubDotCom(targetUri))\n            {\n                _context.Trace.WriteLine($\"{targetUri} is github.com - authentication schemes: '{GitHubConstants.DotComAuthenticationModes}'\");\n                return GitHubConstants.DotComAuthenticationModes;\n            }\n\n            // For GitHub Enterprise we must do some detection of supported modes\n            _context.Trace.WriteLine($\"{targetUri} is GitHub Enterprise - checking for supported authentication schemes...\");\n\n            try\n            {\n                GitHubMetaInfo metaInfo = await _gitHubApi.GetMetaInfoAsync(targetUri);\n\n                // All Enterprise/AE instances support PATs\n                var modes = AuthenticationModes.Pat;\n\n                // If the server says it supports basic auth, we can use that too!\n                if (metaInfo.VerifiablePasswordAuthentication)\n                {\n                    modes |= AuthenticationModes.Basic;\n                }\n\n                // If the version is unknown, we *assume* it supports OAuth.\n                // If the server version at least the minimum required, we *know* we can use OAuth.\n                if (!Version.TryParse(metaInfo.InstalledVersion, out var version) ||\n                    version >= GitHubConstants.MinimumOnPremOAuthVersion)\n                {\n                    modes |= AuthenticationModes.OAuth;\n                }\n\n                _context.Trace.WriteLine($\"GitHub Enterprise instance has version '{metaInfo.InstalledVersion}' and supports authentication schemes: {modes}\");\n                return modes;\n            }\n            catch (Exception ex)\n            {\n                var format = \"Failed to query '{0}' for supported authentication schemes.\";\n                var message = string.Format(format, targetUri);\n\n                _context.Trace.WriteLine(message);\n                _context.Trace.WriteException(ex);\n                _context.Trace2.WriteError(message, format);\n\n                _context.Terminal.WriteLine($\"warning: {message}\");\n\n                // Fall-back to offering all modes so the user is never blocked from authenticating by at least one mode\n                return AuthenticationModes.All;\n            }\n        }\n\n        protected override void ReleaseManagedResources()\n        {\n            _gitHubApi.Dispose();\n            _gitHubAuth.Dispose();\n            base.ReleaseManagedResources();\n        }\n\n        public IEnumerable<IDiagnostic> GetDiagnostics()\n        {\n            yield return new GitHubApiDiagnostic(_gitHubApi, _context);\n        }\n\n        #region Private Methods\n\n        public static bool IsGitHubDotCom(string targetUrl)\n        {\n            return Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri uri) && IsGitHubDotCom(uri);\n        }\n\n        public static bool IsGitHubDotCom(Uri targetUri)\n        {\n            EnsureArgument.AbsoluteUri(targetUri, nameof(targetUri));\n\n            // github.com or gist.github.com are both considered dotcom\n            return StringComparer.OrdinalIgnoreCase.Equals(targetUri.Host, GitHubConstants.GitHubBaseUrlHost) ||\n                   StringComparer.OrdinalIgnoreCase.Equals(targetUri.Host, GitHubConstants.GistBaseUrlHost);\n        }\n\n        internal static Uri NormalizeUri(Uri uri)\n        {\n            if (uri is null)\n            {\n                throw new ArgumentNullException(nameof(uri));\n            }\n\n            // Special case for gist.github.com which are git backed repositories under the hood.\n            // Credentials for these repositories are the same as the one stored with \"github.com\".\n            // Same for gist.github[.subdomain].domain.tld. The general form was already checked via IsSupported.\n            int firstDot = uri.DnsSafeHost.IndexOf(\".\", StringComparison.Ordinal);\n            if (firstDot > -1 && uri.DnsSafeHost.Substring(0, firstDot).Equals(\"gist\", StringComparison.OrdinalIgnoreCase))\n            {\n                return new Uri(\"https://\" + uri.DnsSafeHost.Substring(firstDot+1));\n            }\n\n            return uri;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubOAuth2Client.cs",
    "content": "using System;\nusing System.Net.Http;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace GitHub\n{\n    public class GitHubOAuth2Client : OAuth2Client\n    {\n        public GitHubOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri, ITrace2 trace2)\n            : base(httpClient, CreateEndpoints(baseUri),\n                GetClientId(settings), trace2, GetRedirectUri(settings, baseUri), GetClientSecret(settings)) { }\n\n        private static OAuth2ServerEndpoints CreateEndpoints(Uri uri)\n        {\n            // Ensure that the base URI is normalized to support Gist subdomains\n            Uri baseUri = GitHubHostProvider.NormalizeUri(uri);\n\n            Uri authEndpoint = new Uri(baseUri, GitHubConstants.OAuthAuthorizationEndpointRelativeUri);\n            Uri tokenEndpoint = new Uri(baseUri, GitHubConstants.OAuthTokenEndpointRelativeUri);\n            Uri deviceAuthEndpoint = new Uri(baseUri, GitHubConstants.OAuthDeviceEndpointRelativeUri);\n\n            return new OAuth2ServerEndpoints(authEndpoint, tokenEndpoint)\n            {\n                DeviceAuthorizationEndpoint = deviceAuthEndpoint\n            };\n        }\n\n        private static string GetClientId(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                GitHubConstants.EnvironmentVariables.DevOAuthClientId,\n                Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.DevOAuthClientId,\n                out string clientId))\n            {\n                return clientId;\n            }\n\n            return GitHubConstants.OAuthClientId;\n        }\n\n        private static Uri GetRedirectUri(ISettings settings, Uri targetUri)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                GitHubConstants.EnvironmentVariables.DevOAuthRedirectUri,\n                Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.DevOAuthRedirectUri,\n                out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))\n            {\n                return redirectUri;\n            }\n\n            // Only GitHub.com supports the new OAuth redirect URI today\n            return GitHubHostProvider.IsGitHubDotCom(targetUri)\n                ? GitHubConstants.OAuthRedirectUri\n                : GitHubConstants.OAuthLegacyRedirectUri;\n        }\n\n        private static string GetClientSecret(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                GitHubConstants.EnvironmentVariables.DevOAuthClientSecret,\n                Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.DevOAuthClientSecret,\n                out string clientSecret))\n            {\n                return clientSecret;\n            }\n\n            return GitHubConstants.OAuthClientSecret;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubResources.Designer.cs",
    "content": "﻿//------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n//------------------------------------------------------------------------------\n\nnamespace GitHub {\n    using System;\n    \n    \n    [System.CodeDom.Compiler.GeneratedCodeAttribute(\"System.Resources.Tools.StronglyTypedResourceBuilder\", \"4.0.0.0\")]\n    [System.Diagnostics.DebuggerNonUserCodeAttribute()]\n    [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]\n    public class GitHubResources {\n        \n        private static System.Resources.ResourceManager resourceMan;\n        \n        private static System.Globalization.CultureInfo resourceCulture;\n        \n        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(\"Microsoft.Performance\", \"CA1811:AvoidUncalledPrivateCode\")]\n        internal GitHubResources() {\n        }\n        \n        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]\n        public static System.Resources.ResourceManager ResourceManager {\n            get {\n                if (object.Equals(null, resourceMan)) {\n                    System.Resources.ResourceManager temp = new System.Resources.ResourceManager(\"GitHub.GitHubResources\", typeof(GitHubResources).Assembly);\n                    resourceMan = temp;\n                }\n                return resourceMan;\n            }\n        }\n        \n        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]\n        public static System.Globalization.CultureInfo Culture {\n            get {\n                return resourceCulture;\n            }\n            set {\n                resourceCulture = value;\n            }\n        }\n        \n        public static string AuthenticationResponseSuccessHtml {\n            get {\n                return ResourceManager.GetString(\"AuthenticationResponseSuccessHtml\", resourceCulture);\n            }\n        }\n        \n        public static string AuthenticationResponseFailureHtmlFormat {\n            get {\n                return ResourceManager.GetString(\"AuthenticationResponseFailureHtmlFormat\", resourceCulture);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/GitHubResources.resx",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<root>\n    <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n        <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n\n        </xsd:element>\n    </xsd:schema>\n    <resheader name=\"resmimetype\">\n        <value>text/microsoft-resx</value>\n    </resheader>\n    <resheader name=\"version\">\n        <value>1.3</value>\n    </resheader>\n    <resheader name=\"reader\">\n        <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n    </resheader>\n    <resheader name=\"writer\">\n        <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n    </resheader>\n    <data name=\"AuthenticationResponseSuccessHtml\" xml:space=\"preserve\"><![CDATA[<!DOCTYPE html><html><head>\n<meta charset=\"utf-8\">\n<title>Git Credential Manager - Authentication Succeeded</title>\n<style type=\"text/css\">\nbody {\n  color: #1B1F23;\n  background: #F6F8FA;\n  font-size: 14px;\n  font-family: -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  max-width: 620px;\n  margin: 28px auto;\n  text-align: center;\n}\nh1 {\n  font-size: 24px;\n  margin-bottom: 0;\n}\np {\n  margin-top: 0;\n}\n.icons {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n.icons .plus {\n  margin: 0 10px;\n  font-size: 24px;\n}\n.icons .gcm {\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAbdElEQVR4AeydAxDtOABFs7btwdq2bX3btm3btm3btm372W03WZv9+5O+82bOWM30ntz0NalIjx8/69ih++ITh9QON6+yIFg6c8Cf8y3H99Wzjj/zy04g/4dOqEahXdHerfsnVy3K4KSSF0uE9wEvXxzY9nmJ5fOyqnCrsPu+fOZv4c/1thPt376nfebknRLhXcCjFwapfbueDFUvuEcF+t/iz/qaExvdr7FjWedLhPcAD14UxKePriirvQqxKyiRWKeO3y0R4C08dDGgKn90QMfuKrRuEyj8mWMd2vewRIB38NDFgFq3q7CeLfx53nPU0kIiwBt44CLg25m/Z8tBLoQcCSAAIPxIAAEA4UcCCAAIPxLQDARA+CM9WgxxI8RIAAEA4UcCCAAIPxJAAED4kQACAMKPBLQAARD+5kN/ChkSAARA+JEAIADCjwQAARB+JAAIgPAjAUAAhN9ACQACIPzdmw0zOuhIAAEA4UcCCAAIPxJAAED4kQACAMKPBBAAEH4kgAAIfzfCn5YSQACgvrITbltrOsFHAuccBIAEkAACACSABBAAIAEkgAAACSABBABIAAkgAEACSAABABJAAggAkAASQACABJAAAgAkgAQQAO/2J5bNzSERbvOtBFrXnEnwf4k/7/uOdXDP4xLx3wAG4d/zi4090T5t+kmE29AEaAIGNAB29SEBJIAA2NKbThJAAgiA8CMBJIAACD8SQAIIgGO8kAASQACEHwkgAQRA+D0kASSAAAg/EkACCIDwIwEkgAAIPxJAAgiA8CMBJIAACD8SQAIIgPAjASSAAAg/EkACCIDwIwEkoDmEv1vTETrclNH+HXpIhNv8+lCRQIGPnFCD0msjXZuMjI3o1TI2qm8zRbRfu17hxuWXB4p/Ffd99RyHinAgiLdxN/z6SyAxb3Kxv3vD22dO3RafOa5MqGHZ1VIGSAABEH4zJOA+1qF9D0c6Nxrjy/ACEjAfBODGml//ZwLuk9q385lg5byHeCbgLdjPL6EJ/P2lRGxYj7a+r56lCXgEZn6awD8msWhmXn/ml2kCHoCZnybwr0iuXpTBl+F5mgANgJk/XZtAfMbY8jwToAEw86dxEzB7fGkC3p75ezQfenZvFJqAHY1cFSjyGW8M0gCY+dO1CSRXzM/Cy0JmwszvCjSBYKXcR9g7YB7M/IaSWDCtoERow/yphdk7YB6e/FBnOmAd3PuYROiCk0pe7M/5loMEzILabyDBctlOS4RuhFtWm8tWYrNgPz8PAl0jMWdScbPGkibAzM/63zVSe3c8y6Ei/ws0gHQNv0TbG8+Oxy436/wAJEDtNxA76L9RInRE1mODxpLlALXfQJxE/FKJ0JFAoU84Y9AgmPlNQ+7FV+MgETpi1mvBNAF29RmIHQpcLxE64s/7nkFjiQQMrP2gnrZLhG7YsegVZp0WxHLAwNoPavONROiGuvHNGkuaADO/gUQHdOwuEboRnz6mglljSRPgf34DCVbMdVQidMO8V4GRgInhh6+fc+wzJ++UCJ0I1Su5wayxZDmgae1vMeTPBxZiQ7u1lwidsMPB6/71twNoAjQAwv/PbiodXwhCAmZJgNpvMLEJg+tIhGZ8L4E8h/UfQ5YD1H6D8Wd7zbFOHrtHInTDDoeu1b8J0ASY+Q1HfdJb11eD9V8O0ASY+T1AdHDXjhKhIZo3AZoAM7/En+UVR51n58/+hrEbhOKzJ5SSCB3RVwI0gbSY+f1ZX3NCdYptUcKJTx5ePblu2WfWsUP3qX31jm2dLxE/x46Er7bPnLottX3j6/HZE0tGB3bqGm5WaXGgwEf63lAZnnfUp7okQkeQgJ4S8Gb45ak0oRqFdsWG92yd2rL2XXVirUS4gXVo38PxqaMqhVtUnS/Fol0TiPZr18vN6/2GvbOAjmO50vBd3g0zMzPnMTOZ2U70zMy2zAxaMzt+Zl5b8To6ZmaIZWZHZsZIloZ59v5Bq7ZnLKfUPdU1lXO+DkeKX/9fV1ffurcy+ZMEeja5aoKvjgS0Cr+nXc1QaNnsMfG7t77GkN1gLFZk+5q2voFtT6M6T5UbCp/gYpeKHmNIMRSTgJGAFuH3DWp3Mnri4FsMZQpIB68YqszNh5AC04YXxG9f/zZDKmEkoI4EXB1+fAKLnTv1DEOqgL2D4IJJs8rqP6OMCIwEUmEk4Mrwe9rVCkaP73+HIVWJ37v9Vf+I3L0q3FSyN5RNDUiMBBSQgKvCj+U13vGT0ch/MPQoJPh3uXz58g/XrVvXavbs2WOGDh26sm3btqebNGlytUGDBiVVq1ZNVK9ePYZ/3KxZs8udO3c+PHLkyPyFCxcO27Zt26/u3LnzZYYeleihPbU8raom3CwA39COR8ObV3RiqLL5kwR6ZUoCRgKuCT82+GKXzv6SoYri8/k+smnTpqbDhg1b0bBhw+IqVaokZWjatOmV8ePHzy8sLKwSjUb/naGKgM+K/nH9NrtWAIM7HPtTrcH6/+3JUGWDzVR8tTHBd75YyBWn+vzj+29MBP0fYuih8M8+dOjQ63l5ectr1aoVRHDtAEKZPHnyzEuXLv2IoYqAmgIUHblRAPjf0U8CRgJKhx/f88Orl/Zn6GHEYrF/2759e6OOHTseRUCdAq8OgwYNWnf8+PEXGHoYsYtFj5U1ed2VAgD4a6LP64B5HVB32V/7iQrPwDtx4sTzHTp0OIZAZpJ+/fptvnr16ncZSgeqED1ta4RdKQCzEtBqJVCpT/7grDGLK6t0N3p0XzWG0nH//v1Pjxo1ainCpwo1atQIz58/Py8cDv8nQ6lIlBZ/1tu10T1XCsDmlQD2TLy9ml4xwbd/JYCLNGL4paj1eDJ6eG9NhtJx7NixF3Nycm4hdCrSrl27k/jqwFAqEp7ST3k61PG5UgBOrAT6tTxngm/vSkCpJz+eKg9b9mOTD09YvHsjaCpTp04dHz4hMpQKNPTwtHjHFQLQTwJGAsqEHzxsww+f3sQlvxuYN2/eSIZSEb964Sc4juwWAejzOmBeB5RY9gP0lGcoFcFg8P0DBgzYgEC5kffee28KVi8MWYFpP3iaqi8AsxLQaSWgxJPf06ZGGKZnyApsqPXs2XMXguRmUDfAUCqCs8cuVFkA+knASCDj4Ud5b7oKv3g8/s8o6kGAdGDRokVDGbICJc7e3Jxb6gsgc68Dvt7NLpvgV97rQEaX/QC1/QylYtKkSbMQHJ1Yu3ZtG4asgM3xJUR9AZiVgA4rAfknv2R9f7rhFhs3bmzmRCVf8+bNL+Xm5u7t1KnTkUaNGt1zolbg3Llzv2DIiuC8ibNVFYDKK4GyX7+U9HZtWOzt9qu7f6Jzg5Kyhs+ZlUAaMvbkB+jNx5AVqKirXbt2wK4QooZg5cqVHYuLiz/LEADYqEM48b6O04F2/fwWLVpc8Pv9H2JIBOcePM3fUk4AKkkAf0bYOIYso0cLq6TbQ8Icxcj+HfUD0/KWe1q8bSTwABULPzanbGjmwZAVqOu3s7R37NixC0Oh0PsYSgcKeRBUu34PnCxkyIrItjVt3S4AUQIqgCawkMGfy42NBDISfpCuk09BQUE3u0I3Y8aMiQxVFI/H87E2bdqcsev14+TJk88xJJJkCXpaV4u7XQCiBFQCK1D8GWezBDISfjTRZMiKkpKSz9SrV6/MjsD17dt3K74qMPQoXL9+/duo6rPjd8LpxVS/U3jj77qrJAAdJZAIh94XnIP7PDsl4Hj4QboGnnj3tutpm/7sfnpQfmzXqmT9+vUtGRLBBilmEagiAF0lADBTAV9fskwC1gKwM/zo58eQFdiQww65XUd1GfpHwcrErvMHLVu2PJdqFRBc/JvfqCIA3SWAQ2ildZ7MKgn8//AvnDJD6gdKfPefO3fuKLuesthXYEiGLl26HLDr99u1a1ddhkTi1y//UB0B6C+ByN4tOeikrKMEMNSGoQcp90/wzmn3xJ5UQztQ62/Xuz9AHz+GZLDzIFL37t33MWQFBn2oIwD9JYBhq7oJAHhaVknGi+9+iSEACBeAKiK7h1rg0wtDVqCdl13hAn9r2SXBlClTZtj5O964ceObDImEVy8ZoI4A9JdAkl/HdG1Xjgzi/x9DgHDBpp/8d1G55f/gwYNX2xmu3bt312FIBnQXtvN3XLJkyQCGRBBgtQSgvwTQu1HHVwEQKlgwnCFAuEQP7qrrxA+O/eHYiwyJeL3ej9pZdQewi8+QDJghYHcXIYas8DR9QzEB6C+BwMSB67X8PNjg2SQmWDFEuPj6tzrrxIjuVFNr9+3bV83OYAEMARHO4z8SKA924qAQvjYwJIK5AtoJQJTA1lUdGFKF+LWLP8bvpaMEMPKeIcKmnHwjCrnin1mzZo0TgqDcawAmCcn+fJmvAeENy3N1FgAIzpswjyGV0LVkuCznlSSqTSm89rd9nPiBOEzEkBXi5zW7wBLe+gmbnp07d9Zz6qjwtGnTpjIkEj195FXdBRArOvE8QyoRXrest44CANEjv69OOFHlxA+DaBgSwbLczgk+It26dSssLS39FEMVAd2HxVOJdtKrV6+dDImgjbjOAkDFIzajGVKJ+K1r39FVAJhiTTgz7YhtUhz9xdBNpxtyYPjn0aNHX2EoFWhDtmzZst52b05aHVNmyAo0DtVVAP7hXQ4wpCLCJCdt8A1oc0a8qWwD03AYEkEQM9WZp3fv3tvXrFnT9uLFiz/GkJG7d+9+CfUCCxYsGC694y9BIBD4IEMi3u6/vq2dAIRNKRVBUHQUANrRk1O1zwmf52MMiWzZsiWnfAAM165d+w5DIr5B7U7qKoDwlpUdGVKRwNShK3X9HEi4OPHDsOPIkMjq1avblw+AIVW7MP/I3L26CiCyZ1NThlQkOHf8fB0FgIc/OdAiCZ1/AVmB92wT+oqVLQcmDVqnrQAKd9RnSEXweqLnp8CXk84UAeW8AsgKlL+WD4Dh4MGDbzEkEpgxMl9bAfx+Sw5DKiL0w9QGNFAVO9DaAm80ArJi+fLlueUDYEjVJgxNLXUVQGjV/wxkSEGEKkx98I/ru5XQBMF2AdR7GpAV2IUvHwDDhQsXfsKQiM6vAME54xYwpCLCsBZtwOwGQn0+luh213mn2gTE9NzyATDcvHnzGwyJ+EfouwmIT20MqQbygXMs2gkAvTnu3f4q8cX2LkBAbEQAAJa7JvTlexeiCIkhEW+vJld1FQB2pNGgkyGViBUdf0HLIqD+rYsYIlxwNNDuz4Gx86efZkgEtfkm+OWrFBmyAh1ddBUAiB7dV40hldD1C0D00J5aDBEuIFQwP8/WXd7dG5sxJIKzAHXr1vWY8DNM//79NzIkgkk4WLbpLAD/2D7bGFIFnE3wtKqa0C38+PLHEMAFENoE+fo0v2TbLm/+jIkMWYEDMCb86RuXxM4ce1H304Boyy2+KmaSSOH2RrqFHxvy6HPAEMDlb+APH8tMW+w+qsduhqzAyOz0wTA1ADiWqr0AFOoJgKe/jrv/YuclXMqB1sFoIWzDcU9AFkgcCNKLatWqxVMNDA1MG16gvQBA7SfEMdcZIbx1dXtduwA9CC4ihBvGBgmkbAmOXe9s3wcAPXv23MWQFRh3nQ0CABj9jSGeDGWCRMm9L2Levk7hx7AfhkRwscQOCWD8EkNWYGJvtgsARVEMicT/eOfLaNuWLQIAwQWTZzLkNNgLQ01CNoQf4JISvA6gG60T+wAHDhx4O5vDj8Yj6EnAkEh4bX5fV7cFl6hUY8hJAtNH5usffmsB2LMSENsRh4LvZ0gkxpWC77777s1sFQCajjJkBRqqZqMA8FVA/HxsF3jlQFiyKfwAF0clENm1oTlDFmT1waBTp049w5BIwlv2CQQhmwQglqyGViwaame/wEQw8EH0xsy28ANcHJUAbiaGrEArrPr165dmW/jRqJQhK6w//+kvABF/Xtf9ibKSzzBUmcQu/OEJT/vagWwMP8DFUQlgMyt2+dwvGLIiG2sCDh069DpDIliWetrWCBsB/P1YOY4NJyPh/2RIBpS/B6aPWIbxX9kafoCL4xIITBywgSErQqHQ+1APny3hHzJkyCqGLBAq0YwAAECXXsyZjN+9+Q2GHoXY2ZPPoa+C9CBcDcIPcHFcAninxacthiwQBnHoS40aNcLXr1//NkNWYCqNEUD61SQ6JaNnH/aWcOAM9xX2TQAEET11+DV8fg5MHry6fJWrCT/AJSMSwF80hlIxcODA9boLYOnSpf0ZskA4hmoEYJAJvw0CkK0TwBIs3RIO03swJEPX8OMAVJyLThgSwbu/t0fjG0YABjvDD3CxSQLyxz/RHRf18bqFv0GDBiUYQsKQFSiAkb85jABM+O0XgJQE8A6HnoQMpUC72oCaNWuGUrX9BglP6afQrtkIwCAffmkB2L8ngPFEiYD/QwylYvbs2WN0aff1sBHlFW/8aQRgwq+QAGQkgJueoVSga5DbDwsh/JiCxFAqIjvWtZK+QYwATPidF4D860B466oODKUCEpgzZ85otx70Wb9+fUuGUoHiqLL6zySNAAzy4XdeANIrAbQqip079QxD6Vi5cmVHPE3dEv46der4cNKRoVQk/N6PetpIV/wZAZjwKyAACQmgwgu9yhlKBwqF+G9lqoe/ZcuW586fP/9zhlKB8+f+vC4H5G8SIwATfjkBKPE6gNr3xP0/foGhdOAzmsrNRMeNG7dAnPMvghNuganDVsjfJEYAJvyqCUBiJeDt3KAEpZwMpQOFNJMnT56pWvgx7YihdPwp/DNHLZW/SYwATPhVFYDESgBHNK3OC4ioOFvQ4/F8jKFUYNkfmDp0pfxNYgRgwq+6ACRWAji8Eb964ScMpcJtAsCAD5xtl79JjABM+OXBRWkJoI1YpHBHfYascJMA4rdvfNPbuX6p/E1iBGDC7zYBSEgAbaGC8ybOTkYj/8HQg7hFAJG9W3LKGr0gcXMYAZjwayAAmYpBb9dG9xh6EDcIAK8xmbqp8Gcm20rLCEDP8ANc3CQBQA/iBgHELhY95vQN5elQx1fuoJUEaLTh7fHuTRNUvcIPcFFOAkYAEtR6LBn67axxyVj03xmqLNCjILyxoFtZw+dMaDUJP8BFXQlICAAdhbp3774vmwSAY8TR00deZcguYlfO/1yP1lom/AAXdSUgIYCRI0fmY9AmJJANAvC0eJs3+y7/kCG7SZQWf9a8ElRa+I0ArIuF5AXAkCABHQWAJ7/t4RdJ+H0f8XZpUGLCLB9+I4DUKwFpAQgS0E4A6LCMBqIMOQ0qNctyXjGhlg+/EYC1BOQFIEpANwGEfjc/j6FMET24u47M5GITfiOA9K8D8gKQl4CiAvB2bViMcwUMZRL/uL5bTcDlw28EkH4lIC0AcOzYsRftDP/o0aOXoHsRQwDYJQBxxz9TxIvvfunhk3ZM+I0A5FcC0gJAR167wo+fiTHnDJXDBgFgahBDqoA5eybs8uE3ArBeCagugNThB7JTfiyI7NncmCFVgKxN4CXDbwSQfiXgvADkw48ptN6OdXyVeWOhGi8RDr2PIZXAnD4T/DThNwKQXwkw9CDyArAv/Ght5mlXK1jps/JH9dzFkGqE8mdMNOG3JfxGAMDqWKu8ANwVfhBek9+XIdWInjj4lgm/EH4jAFvJmABGjBixTGrZLwFGXjOkGujjaMLvrvwYAciH3/4nv0Ci5N4XGVIRlCWb8KuPEYCbnvwCYmcklfC0qxky4VcbIwAbwo/DMXjyO3DeH5CqoAORCb/qGAHIh18AG5RO3XRi+a9KoBORCb/SGAHIhF8FAYhDU1TC0+xNE371MQJ4lLHeZ86ceZKhNDgqgNiFM08ypBrJSPg/0cHZhN8IwDEwc9/uFQAGkRYVFT3GUAocFUBk+5q2DKlG7FLRYyb8rsIIQF4CzgsgMC1vOUOqgQIlE35XYQQgLwHnBeBpVTWB4aIMqYR/eJcDJvyuwghAXgLOCwCIbcAyzZ+qAOs8acLvFowA5CWQSQEEJg9ezZAqhFYsHmzC7y7MVwAJMHeAIQAcFQCo/URSHJ+eKVCZ6Gn+lgm/izArAAlyc3P3or8gQwA4LQDgn9B/I0OZJrR87igTfpdhBCAf/kwLAN14o8cPvM1QpojfufHNsvrPmvC7CSMA+fCrIACAyrtE2f1PM+Q0Sa6O9PZqctWE31UYAciGXwUBiA1CUYXHkJMEpg5bYcLvOowAZMKvogCA/7+7FTp5TDi4cMoME34XYAQgfxbg8uXLP2QoDRkXAPANaHMGx5IZsgss+wPThheY8OuBEUAFaNq06ZXbt29/laEUKCEAgNHddhUJxe/d/qqvT/NLJvwaYAQgIQEFBSAODA3OmzAvEfB/iCFZkrHov4dWLh5c1vB5s9uvB0YAMhJQVgACmNwbyp85IVFW8hmGHplg4IPhdct64+yB+c6vFUYAshJQXQDiisA3tOPR8Nr8vrEr53+O93iGRHDAKH7r2nfCW1d18I/p/X/t3AOsXFEURuFTxU5qx3VQ27Zt20bUCWsjKMI6qm2Hte129Dw4t26jPbzJnvWSFT/P/43PGW//xry2HwAAQIBAAgC4A4JvYo9c/9xhbwLLJtx1nst3DjMVvJmH8QMAADgtXLjwtM38SgAAMX4A4BYAMX4A4DEAYvwAwLMAxPgBgNcBEOMHAF4JSIwfAHgvADF+AODdgMT4AYDzAIjxAwAnAhHjBwDOBCTGDwCcCkwJjJ8AINHmzp17wWZ+BQCMXwUAACB/etBmfpXBADB+AACAsO9bCe/wtoyT8WciAADgFHr9vKpvZHtGyvgzBQAAAAHGDwAAAAKMHwAAAAQYPwAAAAgwfgAAAP0IMH4AAAAQYPwAAAAgwPgBAADcR4DxEwB4PJ59NvNfSQOge/fuBTYTY+lAgPETAHTr1i10+vTpwTbzV3EAIP/a0kCA8Sc9ABAMVQ6AfPwgwPjdDwCkg5UDIB8/CDB+9wMA0XDlAMjHDwKM3/0AQDRgOQDy8YMA43c/ABA9cr/RfsT6aP+5c+f62UwKiwcBxk8A4JzNN3v27CvyUSd+za/plgDjBwAQEIwfBBg/AGhEQDB+EGD8AKARAcH4QYDxA4BSBATj148A4wcAEBCMHwQYPwCQC/1CgPETAIAA4ycAAAHGTwAAAoyfAAAEGD8BAAgwfgIAEGD8BAAgwPgJAECA8RMAgADjJwAAAcZPygEAgbFdUjP+3Rs22wzpSeEvRaHPHyoEFo15nKzhe/s1iuSeODTNZkhXSn8xioRChXMO7FzuHdAkofEHlox7EHr5pJbN6IuU/4IU/va5dPau9Vu9Q1vLh9+jjh3++Pv5Ny/2shm9UYb8ohQpKCiWf/ty9+yda7cHlk646xvTKeId2CzyrU+DiHdIy4h/5sDPwZWLTuUe2Tsv9PFdZZvRH0UBNA+2/vdRjH8AAAAASUVORK5CYII=\");\n  background-size: 52px;\n  height: 52px;\n  width: 52px;\n}\n.box {\n  border: 1px solid #E1E4E8;\n  background: white;\n  padding: 24px;\n  margin: 28px;\n}\n</style>\n</head><body>\n    <div class=\"icons\">\n        <svg height=\"52\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"52\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\"></path></svg>\n        <span class=\"plus\">+</span>\n        <span class=\"gcm\"/>\n    </div>\n    <div class=\"box\">\n        <h1>Authentication Succeeded</h1>\n        <p>You may now close this tab and return to the application.</p>\n    </div>\n</body></html>]]></data>\n\n    <data name=\"AuthenticationResponseFailureHtmlFormat\" xml:space=\"preserve\"><![CDATA[<!DOCTYPE html><html><head>\n<meta charset=\"utf-8\">\n<title>Git Credential Manager - Authentication Failed</title>\n<style type=\"text/css\">\nbody {{\n  color: #1B1F23;\n  background: #F6F8FA;\n  font-size: 14px;\n  font-family: -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  max-width: 620px;\n  margin: 28px auto;\n  text-align: center;\n}}\nh1 {{\n  font-size: 24px;\n  margin-bottom: 0;\n}}\ndt {{\n  font-weight: bold;\n}}\ndd {{\n  margin-left: 0;\n  margin-bottom: 10px;\n}}\n.icons {{\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}}\n.icons .plus {{\n  margin: 0 10px;\n  font-size: 24px;\n}}\n.icons .gcm {{\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAbdElEQVR4AeydAxDtOABFs7btwdq2bX3btm3btm3btm372W03WZv9+5O+82bOWM30ntz0NalIjx8/69ih++ITh9QON6+yIFg6c8Cf8y3H99Wzjj/zy04g/4dOqEahXdHerfsnVy3K4KSSF0uE9wEvXxzY9nmJ5fOyqnCrsPu+fOZv4c/1thPt376nfebknRLhXcCjFwapfbueDFUvuEcF+t/iz/qaExvdr7FjWedLhPcAD14UxKePriirvQqxKyiRWKeO3y0R4C08dDGgKn90QMfuKrRuEyj8mWMd2vewRIB38NDFgFq3q7CeLfx53nPU0kIiwBt44CLg25m/Z8tBLoQcCSAAIPxIAAEA4UcCCAAIPxLQDARA+CM9WgxxI8RIAAEA4UcCCAAIPxJAAED4kQACAMKPBLQAARD+5kN/ChkSAARA+JEAIADCjwQAARB+JAAIgPAjAUAAhN9ACQACIPzdmw0zOuhIAAEA4UcCCAAIPxJAAED4kQACAMKPBBAAEH4kgAAIfzfCn5YSQACgvrITbltrOsFHAuccBIAEkAACACSABBAAIAEkgAAACSABBABIAAkgAEACSAABABJAAggAkAASQACABJAAAgAkgAQQAO/2J5bNzSERbvOtBFrXnEnwf4k/7/uOdXDP4xLx3wAG4d/zi4090T5t+kmE29AEaAIGNAB29SEBJIAA2NKbThJAAgiA8CMBJIAACD8SQAIIgGO8kAASQACEHwkgAQRA+D0kASSAAAg/EkACCIDwIwEkgAAIPxJAAgiA8CMBJIAACD8SQAIIgPAjASSAAAg/EkACCIDwIwEkoDmEv1vTETrclNH+HXpIhNv8+lCRQIGPnFCD0msjXZuMjI3o1TI2qm8zRbRfu17hxuWXB4p/Ffd99RyHinAgiLdxN/z6SyAxb3Kxv3vD22dO3RafOa5MqGHZ1VIGSAABEH4zJOA+1qF9D0c6Nxrjy/ACEjAfBODGml//ZwLuk9q385lg5byHeCbgLdjPL6EJ/P2lRGxYj7a+r56lCXgEZn6awD8msWhmXn/ml2kCHoCZnybwr0iuXpTBl+F5mgANgJk/XZtAfMbY8jwToAEw86dxEzB7fGkC3p75ezQfenZvFJqAHY1cFSjyGW8M0gCY+dO1CSRXzM/Cy0JmwszvCjSBYKXcR9g7YB7M/IaSWDCtoERow/yphdk7YB6e/FBnOmAd3PuYROiCk0pe7M/5loMEzILabyDBctlOS4RuhFtWm8tWYrNgPz8PAl0jMWdScbPGkibAzM/63zVSe3c8y6Ei/ws0gHQNv0TbG8+Oxy436/wAJEDtNxA76L9RInRE1mODxpLlALXfQJxE/FKJ0JFAoU84Y9AgmPlNQ+7FV+MgETpi1mvBNAF29RmIHQpcLxE64s/7nkFjiQQMrP2gnrZLhG7YsegVZp0WxHLAwNoPavONROiGuvHNGkuaADO/gUQHdOwuEboRnz6mglljSRPgf34DCVbMdVQidMO8V4GRgInhh6+fc+wzJ++UCJ0I1Su5wayxZDmgae1vMeTPBxZiQ7u1lwidsMPB6/71twNoAjQAwv/PbiodXwhCAmZJgNpvMLEJg+tIhGZ8L4E8h/UfQ5YD1H6D8Wd7zbFOHrtHInTDDoeu1b8J0ASY+Q1HfdJb11eD9V8O0ASY+T1AdHDXjhKhIZo3AZoAM7/En+UVR51n58/+hrEbhOKzJ5SSCB3RVwI0gbSY+f1ZX3NCdYptUcKJTx5ePblu2WfWsUP3qX31jm2dLxE/x46Er7bPnLottX3j6/HZE0tGB3bqGm5WaXGgwEf63lAZnnfUp7okQkeQgJ4S8Gb45ak0oRqFdsWG92yd2rL2XXVirUS4gXVo38PxqaMqhVtUnS/Fol0TiPZr18vN6/2GvbOAjmO50vBd3g0zMzPnMTOZ2U70zMy2zAxaMzt+Zl5b8To6ZmaIZWZHZsZIloZ59v5Bq7ZnLKfUPdU1lXO+DkeKX/9fV1ffurcy+ZMEeja5aoKvjgS0Cr+nXc1QaNnsMfG7t77GkN1gLFZk+5q2voFtT6M6T5UbCp/gYpeKHmNIMRSTgJGAFuH3DWp3Mnri4FsMZQpIB68YqszNh5AC04YXxG9f/zZDKmEkoI4EXB1+fAKLnTv1DEOqgL2D4IJJs8rqP6OMCIwEUmEk4Mrwe9rVCkaP73+HIVWJ37v9Vf+I3L0q3FSyN5RNDUiMBBSQgKvCj+U13vGT0ch/MPQoJPh3uXz58g/XrVvXavbs2WOGDh26sm3btqebNGlytUGDBiVVq1ZNVK9ePYZ/3KxZs8udO3c+PHLkyPyFCxcO27Zt26/u3LnzZYYeleihPbU8raom3CwA39COR8ObV3RiqLL5kwR6ZUoCRgKuCT82+GKXzv6SoYri8/k+smnTpqbDhg1b0bBhw+IqVaokZWjatOmV8ePHzy8sLKwSjUb/naGKgM+K/nH9NrtWAIM7HPtTrcH6/+3JUGWDzVR8tTHBd75YyBWn+vzj+29MBP0fYuih8M8+dOjQ63l5ectr1aoVRHDtAEKZPHnyzEuXLv2IoYqAmgIUHblRAPjf0U8CRgJKhx/f88Orl/Zn6GHEYrF/2759e6OOHTseRUCdAq8OgwYNWnf8+PEXGHoYsYtFj5U1ed2VAgD4a6LP64B5HVB32V/7iQrPwDtx4sTzHTp0OIZAZpJ+/fptvnr16ncZSgeqED1ta4RdKQCzEtBqJVCpT/7grDGLK6t0N3p0XzWG0nH//v1Pjxo1ainCpwo1atQIz58/Py8cDv8nQ6lIlBZ/1tu10T1XCsDmlQD2TLy9ml4xwbd/JYCLNGL4paj1eDJ6eG9NhtJx7NixF3Nycm4hdCrSrl27k/jqwFAqEp7ST3k61PG5UgBOrAT6tTxngm/vSkCpJz+eKg9b9mOTD09YvHsjaCpTp04dHz4hMpQKNPTwtHjHFQLQTwJGAsqEHzxsww+f3sQlvxuYN2/eSIZSEb964Sc4juwWAejzOmBeB5RY9gP0lGcoFcFg8P0DBgzYgEC5kffee28KVi8MWYFpP3iaqi8AsxLQaSWgxJPf06ZGGKZnyApsqPXs2XMXguRmUDfAUCqCs8cuVFkA+knASCDj4Ud5b7oKv3g8/s8o6kGAdGDRokVDGbICJc7e3Jxb6gsgc68Dvt7NLpvgV97rQEaX/QC1/QylYtKkSbMQHJ1Yu3ZtG4asgM3xJUR9AZiVgA4rAfknv2R9f7rhFhs3bmzmRCVf8+bNL+Xm5u7t1KnTkUaNGt1zolbg3Llzv2DIiuC8ibNVFYDKK4GyX7+U9HZtWOzt9qu7f6Jzg5Kyhs+ZlUAaMvbkB+jNx5AVqKirXbt2wK4QooZg5cqVHYuLiz/LEADYqEM48b6O04F2/fwWLVpc8Pv9H2JIBOcePM3fUk4AKkkAf0bYOIYso0cLq6TbQ8Icxcj+HfUD0/KWe1q8bSTwABULPzanbGjmwZAVqOu3s7R37NixC0Oh0PsYSgcKeRBUu34PnCxkyIrItjVt3S4AUQIqgCawkMGfy42NBDISfpCuk09BQUE3u0I3Y8aMiQxVFI/H87E2bdqcsev14+TJk88xJJJkCXpaV4u7XQCiBFQCK1D8GWezBDISfjTRZMiKkpKSz9SrV6/MjsD17dt3K74qMPQoXL9+/duo6rPjd8LpxVS/U3jj77qrJAAdJZAIh94XnIP7PDsl4Hj4QboGnnj3tutpm/7sfnpQfmzXqmT9+vUtGRLBBilmEagiAF0lADBTAV9fskwC1gKwM/zo58eQFdiQww65XUd1GfpHwcrErvMHLVu2PJdqFRBc/JvfqCIA3SWAQ2ildZ7MKgn8//AvnDJD6gdKfPefO3fuKLuesthXYEiGLl26HLDr99u1a1ddhkTi1y//UB0B6C+ByN4tOeikrKMEMNSGoQcp90/wzmn3xJ5UQztQ62/Xuz9AHz+GZLDzIFL37t33MWQFBn2oIwD9JYBhq7oJAHhaVknGi+9+iSEACBeAKiK7h1rg0wtDVqCdl13hAn9r2SXBlClTZtj5O964ceObDImEVy8ZoI4A9JdAkl/HdG1Xjgzi/x9DgHDBpp/8d1G55f/gwYNX2xmu3bt312FIBnQXtvN3XLJkyQCGRBBgtQSgvwTQu1HHVwEQKlgwnCFAuEQP7qrrxA+O/eHYiwyJeL3ej9pZdQewi8+QDJghYHcXIYas8DR9QzEB6C+BwMSB67X8PNjg2SQmWDFEuPj6tzrrxIjuVFNr9+3bV83OYAEMARHO4z8SKA924qAQvjYwJIK5AtoJQJTA1lUdGFKF+LWLP8bvpaMEMPKeIcKmnHwjCrnin1mzZo0TgqDcawAmCcn+fJmvAeENy3N1FgAIzpswjyGV0LVkuCznlSSqTSm89rd9nPiBOEzEkBXi5zW7wBLe+gmbnp07d9Zz6qjwtGnTpjIkEj195FXdBRArOvE8QyoRXrest44CANEjv69OOFHlxA+DaBgSwbLczgk+It26dSssLS39FEMVAd2HxVOJdtKrV6+dDImgjbjOAkDFIzajGVKJ+K1r39FVAJhiTTgz7YhtUhz9xdBNpxtyYPjn0aNHX2EoFWhDtmzZst52b05aHVNmyAo0DtVVAP7hXQ4wpCLCJCdt8A1oc0a8qWwD03AYEkEQM9WZp3fv3tvXrFnT9uLFiz/GkJG7d+9+CfUCCxYsGC694y9BIBD4IEMi3u6/vq2dAIRNKRVBUHQUANrRk1O1zwmf52MMiWzZsiWnfAAM165d+w5DIr5B7U7qKoDwlpUdGVKRwNShK3X9HEi4OPHDsOPIkMjq1avblw+AIVW7MP/I3L26CiCyZ1NThlQkOHf8fB0FgIc/OdAiCZ1/AVmB92wT+oqVLQcmDVqnrQAKd9RnSEXweqLnp8CXk84UAeW8AsgKlL+WD4Dh4MGDbzEkEpgxMl9bAfx+Sw5DKiL0w9QGNFAVO9DaAm80ArJi+fLlueUDYEjVJgxNLXUVQGjV/wxkSEGEKkx98I/ru5XQBMF2AdR7GpAV2IUvHwDDhQsXfsKQiM6vAME54xYwpCLCsBZtwOwGQn0+luh213mn2gTE9NzyATDcvHnzGwyJ+EfouwmIT20MqQbygXMs2gkAvTnu3f4q8cX2LkBAbEQAAJa7JvTlexeiCIkhEW+vJld1FQB2pNGgkyGViBUdf0HLIqD+rYsYIlxwNNDuz4Gx86efZkgEtfkm+OWrFBmyAh1ddBUAiB7dV40hldD1C0D00J5aDBEuIFQwP8/WXd7dG5sxJIKzAHXr1vWY8DNM//79NzIkgkk4WLbpLAD/2D7bGFIFnE3wtKqa0C38+PLHEMAFENoE+fo0v2TbLm/+jIkMWYEDMCb86RuXxM4ce1H304Boyy2+KmaSSOH2RrqFHxvy6HPAEMDlb+APH8tMW+w+qsduhqzAyOz0wTA1ADiWqr0AFOoJgKe/jrv/YuclXMqB1sFoIWzDcU9AFkgcCNKLatWqxVMNDA1MG16gvQBA7SfEMdcZIbx1dXtduwA9CC4ihBvGBgmkbAmOXe9s3wcAPXv23MWQFRh3nQ0CABj9jSGeDGWCRMm9L2Levk7hx7AfhkRwscQOCWD8EkNWYGJvtgsARVEMicT/eOfLaNuWLQIAwQWTZzLkNNgLQ01CNoQf4JISvA6gG60T+wAHDhx4O5vDj8Yj6EnAkEh4bX5fV7cFl6hUY8hJAtNH5usffmsB2LMSENsRh4LvZ0gkxpWC77777s1sFQCajjJkBRqqZqMA8FVA/HxsF3jlQFiyKfwAF0clENm1oTlDFmT1waBTp049w5BIwlv2CQQhmwQglqyGViwaame/wEQw8EH0xsy28ANcHJUAbiaGrEArrPr165dmW/jRqJQhK6w//+kvABF/Xtf9ibKSzzBUmcQu/OEJT/vagWwMP8DFUQlgMyt2+dwvGLIiG2sCDh069DpDIliWetrWCBsB/P1YOY4NJyPh/2RIBpS/B6aPWIbxX9kafoCL4xIITBywgSErQqHQ+1APny3hHzJkyCqGLBAq0YwAAECXXsyZjN+9+Q2GHoXY2ZPPoa+C9CBcDcIPcHFcAninxacthiwQBnHoS40aNcLXr1//NkNWYCqNEUD61SQ6JaNnH/aWcOAM9xX2TQAEET11+DV8fg5MHry6fJWrCT/AJSMSwF80hlIxcODA9boLYOnSpf0ZskA4hmoEYJAJvw0CkK0TwBIs3RIO03swJEPX8OMAVJyLThgSwbu/t0fjG0YABjvDD3CxSQLyxz/RHRf18bqFv0GDBiUYQsKQFSiAkb85jABM+O0XgJQE8A6HnoQMpUC72oCaNWuGUrX9BglP6afQrtkIwCAffmkB2L8ngPFEiYD/QwylYvbs2WN0aff1sBHlFW/8aQRgwq+QAGQkgJueoVSga5DbDwsh/JiCxFAqIjvWtZK+QYwATPidF4D860B466oODKUCEpgzZ85otx70Wb9+fUuGUoHiqLL6zySNAAzy4XdeANIrAbQqip079QxD6Vi5cmVHPE3dEv46der4cNKRoVQk/N6PetpIV/wZAZjwKyAACQmgwgu9yhlKBwqF+G9lqoe/ZcuW586fP/9zhlKB8+f+vC4H5G8SIwATfjkBKPE6gNr3xP0/foGhdOAzmsrNRMeNG7dAnPMvghNuganDVsjfJEYAJvyqCUBiJeDt3KAEpZwMpQOFNJMnT56pWvgx7YihdPwp/DNHLZW/SYwATPhVFYDESgBHNK3OC4ioOFvQ4/F8jKFUYNkfmDp0pfxNYgRgwq+6ACRWAji8Eb964ScMpcJtAsCAD5xtl79JjABM+OXBRWkJoI1YpHBHfYascJMA4rdvfNPbuX6p/E1iBGDC7zYBSEgAbaGC8ybOTkYj/8HQg7hFAJG9W3LKGr0gcXMYAZjwayAAmYpBb9dG9xh6EDcIAK8xmbqp8Gcm20rLCEDP8ANc3CQBQA/iBgHELhY95vQN5elQx1fuoJUEaLTh7fHuTRNUvcIPcFFOAkYAEtR6LBn67axxyVj03xmqLNCjILyxoFtZw+dMaDUJP8BFXQlICAAdhbp3774vmwSAY8TR00deZcguYlfO/1yP1lom/AAXdSUgIYCRI0fmY9AmJJANAvC0eJs3+y7/kCG7SZQWf9a8ElRa+I0ArIuF5AXAkCABHQWAJ7/t4RdJ+H0f8XZpUGLCLB9+I4DUKwFpAQgS0E4A6LCMBqIMOQ0qNctyXjGhlg+/EYC1BOQFIEpANwGEfjc/j6FMET24u47M5GITfiOA9K8D8gKQl4CiAvB2bViMcwUMZRL/uL5bTcDlw28EkH4lIC0AcOzYsRftDP/o0aOXoHsRQwDYJQBxxz9TxIvvfunhk3ZM+I0A5FcC0gJAR167wo+fiTHnDJXDBgFgahBDqoA5eybs8uE3ArBeCagugNThB7JTfiyI7NncmCFVgKxN4CXDbwSQfiXgvADkw48ptN6OdXyVeWOhGi8RDr2PIZXAnD4T/DThNwKQXwkw9CDyArAv/Ght5mlXK1jps/JH9dzFkGqE8mdMNOG3JfxGAMDqWKu8ANwVfhBek9+XIdWInjj4lgm/EH4jAFvJmABGjBixTGrZLwFGXjOkGujjaMLvrvwYAciH3/4nv0Ci5N4XGVIRlCWb8KuPEYCbnvwCYmcklfC0qxky4VcbIwAbwo/DMXjyO3DeH5CqoAORCb/qGAHIh18AG5RO3XRi+a9KoBORCb/SGAHIhF8FAYhDU1TC0+xNE371MQJ4lLHeZ86ceZKhNDgqgNiFM08ypBrJSPg/0cHZhN8IwDEwc9/uFQAGkRYVFT3GUAocFUBk+5q2DKlG7FLRYyb8rsIIQF4CzgsgMC1vOUOqgQIlE35XYQQgLwHnBeBpVTWB4aIMqYR/eJcDJvyuwghAXgLOCwCIbcAyzZ+qAOs8acLvFowA5CWQSQEEJg9ezZAqhFYsHmzC7y7MVwAJMHeAIQAcFQCo/URSHJ+eKVCZ6Gn+lgm/izArAAlyc3P3or8gQwA4LQDgn9B/I0OZJrR87igTfpdhBCAf/kwLAN14o8cPvM1QpojfufHNsvrPmvC7CSMA+fCrIACAyrtE2f1PM+Q0Sa6O9PZqctWE31UYAciGXwUBiA1CUYXHkJMEpg5bYcLvOowAZMKvogCA/7+7FTp5TDi4cMoME34XYAQgfxbg8uXLP2QoDRkXAPANaHMGx5IZsgss+wPThheY8OuBEUAFaNq06ZXbt29/laEUKCEAgNHddhUJxe/d/qqvT/NLJvwaYAQgIQEFBSAODA3OmzAvEfB/iCFZkrHov4dWLh5c1vB5s9uvB0YAMhJQVgACmNwbyp85IVFW8hmGHplg4IPhdct64+yB+c6vFUYAshJQXQDiisA3tOPR8Nr8vrEr53+O93iGRHDAKH7r2nfCW1d18I/p/X/t3AOsXFEURuFTxU5qx3VQ27Zt20bUCWsjKMI6qm2Hte129Dw4t26jPbzJnvWSFT/P/43PGW//xry2HwAAQIBAAgC4A4JvYo9c/9xhbwLLJtx1nst3DjMVvJmH8QMAADgtXLjwtM38SgAAMX4A4BYAMX4A4DEAYvwAwLMAxPgBgNcBEOMHAF4JSIwfAHgvADF+AODdgMT4AYDzAIjxAwAnAhHjBwDOBCTGDwCcCkwJjJ8AINHmzp17wWZ+BQCMXwUAACB/etBmfpXBADB+AACAsO9bCe/wtoyT8WciAADgFHr9vKpvZHtGyvgzBQAAAAHGDwAAAAKMHwAAAAQYPwAAAAgwfgAAAP0IMH4AAAAQYPwAAAAgwPgBAADcR4DxEwB4PJ59NvNfSQOge/fuBTYTY+lAgPETAHTr1i10+vTpwTbzV3EAIP/a0kCA8Sc9ABAMVQ6AfPwgwPjdDwCkg5UDIB8/CDB+9wMA0XDlAMjHDwKM3/0AQDRgOQDy8YMA43c/ABA9cr/RfsT6aP+5c+f62UwKiwcBxk8A4JzNN3v27CvyUSd+za/plgDjBwAQEIwfBBg/AGhEQDB+EGD8AKARAcH4QYDxA4BSBATj148A4wcAEBCMHwQYPwCQC/1CgPETAIAA4ycAAAHGTwAAAoyfAAAEGD8BAAgwfgIAEGD8BAAgwPgJAECA8RMAgADjJwAAAcZPygEAgbFdUjP+3Rs22wzpSeEvRaHPHyoEFo15nKzhe/s1iuSeODTNZkhXSn8xioRChXMO7FzuHdAkofEHlox7EHr5pJbN6IuU/4IU/va5dPau9Vu9Q1vLh9+jjh3++Pv5Ny/2shm9UYb8ohQpKCiWf/ty9+yda7cHlk646xvTKeId2CzyrU+DiHdIy4h/5sDPwZWLTuUe2Tsv9PFdZZvRH0UBNA+2/vdRjH8AAAAASUVORK5CYII=\");\n  background-size: 52px;\n  height: 52px;\n  width: 52px;\n}}\n.box {{\n  border: 1px solid #E1E4E8;\n  background: white;\n  padding: 24px;\n  margin: 28px;\n}}\n</style>\n</head><body>\n    <div class=\"icons\">\n        <svg height=\"52\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"52\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\"></path></svg>\n        <span class=\"plus\">+</span>\n        <span class=\"gcm\"/>\n    </div>\n    <div class=\"box\">\n        <h1>Authentication Failed</h1>\n        <dl>\n            <dt>Error</dt>\n            <dd>{0}</dd>\n            <dt>Description</dt>\n            <dd>{1}</dd>\n            <dt>URL</dt>\n            <dd>{2}</dd>\n        </dl>\n    </div>\n</body></html>]]></data>\n</root>\n"
  },
  {
    "path": "src/shared/GitHub/GitHubRestApi.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace GitHub\n{\n    public interface IGitHubRestApi : IDisposable\n    {\n        Task<AuthenticationResult> CreatePersonalAccessTokenAsync(\n            Uri targetUri,\n            string username,\n            string password,\n            string authenticationCode,\n            IEnumerable<string> scopes);\n\n        Task<GitHubUserInfo> GetUserInfoAsync(Uri targetUri, string accessToken);\n\n        Task<GitHubMetaInfo> GetMetaInfoAsync(Uri targetUri);\n    }\n\n    public class GitHubRestApi : IGitHubRestApi\n    {\n        /// <summary>\n        /// The maximum wait time for a network request before timing out\n        /// </summary>\n        private const int RequestTimeout = 15 * 1000; // 15 second limit\n\n        private readonly ICommandContext _context;\n\n        public GitHubRestApi(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n        }\n\n        #region IGitHubApi\n\n        public async Task<AuthenticationResult> CreatePersonalAccessTokenAsync(Uri targetUri, string username, string password, string authenticationCode, IEnumerable<string> scopes)\n        {\n            EnsureArgument.AbsoluteUri(targetUri, nameof(targetUri));\n            EnsureArgument.NotNull(scopes, nameof(scopes));\n\n            Uri requestUri = GetApiRequestUri(targetUri, \"authorizations\");\n\n            _context.Trace.WriteLine($\"HTTP: POST {requestUri}\");\n            using (HttpContent content = GetTokenJsonContent(targetUri, scopes))\n            using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))\n            {\n                // Set the request content as well as auth and 2FA headers\n                request.Content = content;\n                request.AddBasicAuthenticationHeader(username, password);\n                if (!string.IsNullOrWhiteSpace(authenticationCode))\n                {\n                    request.Headers.Add(GitHubConstants.GitHubOptHeader, authenticationCode);\n                }\n\n                // Send the request!\n                using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n                {\n                    _context.Trace.WriteLine($\"HTTP: Response {(int) response.StatusCode} [{response.StatusCode}]\");\n\n                    switch (response.StatusCode)\n                    {\n                        case HttpStatusCode.OK:\n                        case HttpStatusCode.Created:\n                            return await ParseSuccessResponseAsync(targetUri, response);\n\n                        case HttpStatusCode.Unauthorized:\n                            return ParseUnauthorizedResponse(targetUri, authenticationCode, response);\n\n                        case HttpStatusCode.Forbidden:\n                            return await ParseForbiddenResponseAsync(targetUri, password, response);\n\n                        default:\n                            _context.Trace.WriteLine($\"Authentication failed for '{targetUri}'.\");\n                            return new AuthenticationResult(GitHubAuthenticationResultType.Failure);\n                    }\n                }\n            }\n        }\n\n        public async Task<GitHubUserInfo> GetUserInfoAsync(Uri targetUri, string accessToken)\n        {\n            Uri requestUri = GetApiRequestUri(targetUri, \"user\");\n\n            _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n            using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri))\n            {\n                request.AddBearerAuthenticationHeader(accessToken);\n\n                using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n                {\n                    _context.Trace.WriteLine($\"HTTP: Response {(int) response.StatusCode} [{response.StatusCode}]\");\n\n                    response.EnsureSuccessStatusCode();\n\n                    string json = await response.Content.ReadAsStringAsync();\n\n                    return JsonSerializer.Deserialize<GitHubUserInfo>(json);\n                }\n            }\n        }\n\n        public async Task<GitHubMetaInfo> GetMetaInfoAsync(Uri targetUri)\n        {\n            Uri requestUri = GetApiRequestUri(targetUri, \"meta\");\n\n            _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n            using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri))\n            using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n            {\n                _context.Trace.WriteLine($\"HTTP: Response {(int) response.StatusCode} [{response.StatusCode}]\");\n\n                response.EnsureSuccessStatusCode();\n\n                string json = await response.Content.ReadAsStringAsync();\n\n                return JsonSerializer.Deserialize<GitHubMetaInfo>(json);\n            }\n        }\n\n        #endregion\n\n        #region Private Methods\n\n        private async Task<AuthenticationResult> ParseForbiddenResponseAsync(Uri targetUri, string password, HttpResponseMessage response)\n        {\n            // This API only supports Basic authentication. If a valid OAuth token is supplied\n            // as the password, then a Forbidden response is returned instead of an Unauthorized.\n            // In that case, the supplied password is an OAuth token and is valid and we don't need\n            // to create a new personal access token.\n            var contentBody = await response.Content.ReadAsStringAsync();\n            if (contentBody.Contains(\"This API can only be accessed with username and password Basic Auth\"))\n            {\n                _context.Trace.WriteLine($\"Authentication success: user supplied personal access token for '{targetUri}'.\");\n\n                return new AuthenticationResult(GitHubAuthenticationResultType.Success, password);\n            }\n\n            _context.Trace.WriteLine($\"Authentication failed for '{targetUri}'.\");\n            return new AuthenticationResult(GitHubAuthenticationResultType.Failure);\n        }\n\n        private AuthenticationResult ParseUnauthorizedResponse(Uri targetUri, string authenticationCode, HttpResponseMessage response)\n        {\n            if (string.IsNullOrWhiteSpace(authenticationCode)\n                && response.Headers.Any(x => StringComparer.OrdinalIgnoreCase.Equals(GitHubConstants.GitHubOptHeader, x.Key)))\n            {\n                var mfakvp = response.Headers.First(x =>\n                    StringComparer.OrdinalIgnoreCase.Equals(GitHubConstants.GitHubOptHeader, x.Key) &&\n                    x.Value != null && x.Value.Any());\n\n                if (mfakvp.Value.First().Contains(\"app\"))\n                {\n                    _context.Trace.WriteLine($\"Two-factor app authentication code required for '{targetUri}'.\");\n                    return new AuthenticationResult(GitHubAuthenticationResultType.TwoFactorApp);\n                }\n                else\n                {\n                    _context.Trace.WriteLine($\"Two-factor SMS authentication code required for '{targetUri}'.\");\n                    return new AuthenticationResult(GitHubAuthenticationResultType.TwoFactorSms);\n                }\n            }\n            else\n            {\n                _context.Trace.WriteLine($\"Authentication failed for '{targetUri}'.\");\n                return new AuthenticationResult(GitHubAuthenticationResultType.Failure);\n            }\n        }\n\n        private async Task<AuthenticationResult> ParseSuccessResponseAsync(Uri targetUri, HttpResponseMessage response)\n        {\n            string token = null;\n            string responseText = await response.Content.ReadAsStringAsync();\n\n            Match tokenMatch;\n            if ((tokenMatch = Regex.Match(responseText, @\"\\s*\"\"token\"\"\\s*:\\s*\"\"([^\"\"]+)\"\"\\s*\",\n                    RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success\n                && tokenMatch.Groups.Count > 1)\n            {\n                token = tokenMatch.Groups[1].Value;\n            }\n\n            if (token == null)\n            {\n                _context.Trace.WriteLine($\"Authentication for '{targetUri}' failed.\");\n                return new AuthenticationResult(GitHubAuthenticationResultType.Failure);\n            }\n            else\n            {\n                _context.Trace.WriteLine($\"Authentication success: new personal access token for '{targetUri}' created.\");\n                return new AuthenticationResult(GitHubAuthenticationResultType.Success, token);\n            }\n        }\n\n        internal /* for testing */ static Uri GetApiRequestUri(Uri targetUri, string apiUrl)\n        {\n            if (GitHubHostProvider.IsGitHubDotCom(targetUri))\n            {\n                return new Uri($\"https://api.github.com/{apiUrl}\");\n            }\n            else\n            {\n                // If we're here, it's GitHub Enterprise via a configured authority\n                var baseUrl = targetUri.GetLeftPart(UriPartial.Authority);\n\n                RegexOptions reOptions = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;\n\n                // Check for 'raw.' in the hostname and remove it to get the correct GHE API URL\n                baseUrl = Regex.Replace(baseUrl, @\"^(https?://)raw\\.\", \"$1\", reOptions);\n\n                // Likewise check for `gist.` in the hostname and remove it to get the correct GHE API URL\n                baseUrl = Regex.Replace(baseUrl, @\"^(https?://)gist\\.\", \"$1\", reOptions);\n\n                return new Uri(baseUrl + $\"/api/v3/{apiUrl}\");\n            }\n        }\n\n        private HttpContent GetTokenJsonContent(Uri targetUri, IEnumerable<string> scopes)\n        {\n            const string HttpJsonContentType = \"application/x-www-form-urlencoded\";\n            const string JsonContentFormat = @\"{{ \"\"scopes\"\": {0}, \"\"note\"\": \"\"git: {1} on {2} at {3:dd-MMM-yyyy HH:mm}\"\" }}\";\n\n            var quotedScopes = scopes.Select(x => $\"\\\"{x}\\\"\");\n            string scopesJson = $\"[{string.Join(\", \", quotedScopes)}]\";\n\n            string jsonContent = string.Format(JsonContentFormat, scopesJson, targetUri, Environment.MachineName, DateTime.Now);\n\n            return new StringContent(jsonContent, Encoding.UTF8, HttpJsonContentType);\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient\n        {\n            get\n            {\n                if (_httpClient is null)\n                {\n                    _httpClient = _context.HttpClientFactory.CreateClient();\n\n                    // Set the common headers and timeout\n                    _httpClient.Timeout = TimeSpan.FromMilliseconds(RequestTimeout);\n                    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(GitHubConstants.GitHubApiAcceptsHeaderValue));\n                }\n\n                return _httpClient;\n            }\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n\n        #endregion\n    }\n\n    public class GitHubUserInfo\n    {\n        [JsonPropertyName(\"login\")]\n        public string Login { get; set; }\n    }\n\n    public class GitHubMetaInfo\n    {\n        [JsonPropertyName(\"installed_version\")]\n        public string InstalledVersion { get; set; }\n\n        [JsonPropertyName(\"verifiable_password_authentication\")]\n        public bool VerifiablePasswordAuthentication { get; set; }\n    }\n\n    public static class GitHubRestApiExtensions\n    {\n        public static Task<AuthenticationResult> CreatePersonalTokenAsync(\n            this IGitHubRestApi api,\n            Uri targetUri,\n            ICredential credentials,\n            string authenticationCode,\n            IEnumerable<string> scopes)\n        {\n            return api.CreatePersonalAccessTokenAsync(\n                targetUri,\n                credentials?.Account,\n                credentials?.Password,\n                authenticationCode,\n                scopes);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/InternalsVisibleTo.cs",
    "content": "using System.Runtime.CompilerServices;\n\n[assembly:InternalsVisibleTo(\"GitHub.Tests\")]\n"
  },
  {
    "path": "src/shared/GitHub/UI/Commands/CredentialsCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitHub.UI.ViewModels;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace GitHub.UI.Commands\n{\n    public abstract class CredentialsCommand : HelperCommand\n    {\n        protected CredentialsCommand(ICommandContext context)\n            : base(context, \"prompt\", \"Show authentication prompt.\")\n        {\n            var url = new Option<string>(\"--enterprise-url\", \"GitHub Enterprise URL.\");\n            AddOption(url);\n\n            var userName = new Option<string>(\"--username\", \"Username or email.\");\n            AddOption(userName);\n\n            var basic = new Option<bool>(\"--basic\", \"Enable username/password (basic) authentication.\");\n            AddOption(basic);\n\n            var browser = new Option<bool>(\"--browser\", \"Enable browser-based OAuth authentication.\");\n            AddOption(browser);\n\n            var device = new Option<bool>(\"--device\", \"Enable device code OAuth authentication.\");\n            AddOption(device);\n\n            var pat = new Option<bool>(\"--pat\", \"Enable personal access token authentication.\");\n            AddOption(pat);\n\n            var all = new Option<bool>(\"--all\", \"Enable all available authentication options.\");\n            AddOption(all);\n\n            this.SetHandler(ExecuteAsync, url, userName, basic, browser, device, pat, all);\n        }\n\n        private async Task<int> ExecuteAsync(string userName, string enterpriseUrl,\n            bool basic, bool browser, bool device, bool pat, bool all)\n        {\n            var viewModel = new CredentialsViewModel(Context.SessionManager, Context.ProcessManager)\n            {\n                ShowBrowserLogin = all || browser,\n                ShowDeviceLogin  = all || device,\n                ShowTokenLogin   = all || pat,\n                ShowBasicLogin   = all || basic,\n            };\n\n            if (!GitHubHostProvider.IsGitHubDotCom(enterpriseUrl))\n            {\n                viewModel.EnterpriseUrl = enterpriseUrl;\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                viewModel.UserName = userName;\n            }\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            var result = new Dictionary<string, string>();\n\n            switch (viewModel.SelectedMode)\n            {\n                case AuthenticationModes.Basic:\n                    result[\"mode\"] = \"basic\";\n                    result[\"username\"] = viewModel.UserName;\n                    result[\"password\"] = viewModel.Password;\n                    break;\n\n                case AuthenticationModes.Browser:\n                    result[\"mode\"] = \"browser\";\n                    break;\n\n                case AuthenticationModes.Device:\n                    result[\"mode\"] = \"device\";\n                    break;\n\n                case AuthenticationModes.Pat:\n                    result[\"mode\"] = \"pat\";\n                    result[\"pat\"] = viewModel.Token;\n                    break;\n\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n\n            WriteResult(result);\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Commands/DeviceCommand.cs",
    "content": "using System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitHub.UI.ViewModels;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace GitHub.UI.Commands\n{\n    public abstract class DeviceCodeCommand : HelperCommand\n    {\n        protected DeviceCodeCommand(ICommandContext context)\n            : base(context, \"device\", \"Show device code prompt.\")\n        {\n            var code = new Option<string>(\"--code\", \"User code.\");\n            AddOption(code);\n\n            var url = new Option<string>(\"--url\", \"Verification URL.\");\n            AddOption(url);\n\n            this.SetHandler(ExecuteAsync, code, url);\n        }\n\n        private async Task<int> ExecuteAsync(string code, string url)\n        {\n            var viewModel = new DeviceCodeViewModel(Context.SessionManager)\n            {\n                UserCode = code,\n                VerificationUrl = url,\n            };\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(DeviceCodeViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Commands/SelectAccountCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitHub.UI.ViewModels;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace GitHub.UI.Commands\n{\n    public abstract class SelectAccountCommand : HelperCommand\n    {\n        protected SelectAccountCommand(ICommandContext context)\n            : base(context, \"select-account\", \"Show account selection prompt. Accounts are read line-by-line from standard input.\")\n        {\n            var url = new Option<string>(new[] { \"--enterprise-url\" }, \"Enterprise URL.\");\n            AddOption(url);\n\n            var noHelp = new Option<bool>(new[] { \"--no-help\" }, \"Hide the help link.\");\n            AddOption(noHelp);\n\n            this.SetHandler(ExecuteAsync, url, noHelp);\n        }\n\n        private async Task<int> ExecuteAsync(string enterpriseUrl, bool noHelp)\n        {\n            // Read accounts from standard input\n            IList<string> accounts = ReadAccounts();\n\n            var viewModel = new SelectAccountViewModel(Context.SessionManager, accounts)\n            {\n                EnterpriseUrl = enterpriseUrl,\n                ShowHelpLink = !noHelp\n            };\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Exception(\"User cancelled dialog.\");\n            }\n\n            WriteResult(new Dictionary<string, string>\n            {\n                [\"account\"] = viewModel.SelectedAccount?.UserName\n            });\n\n            return 0;\n        }\n\n        private IList<string> ReadAccounts()\n        {\n            var accounts = new List<string>();\n\n            string line;\n            while (!string.IsNullOrWhiteSpace(line = Context.Streams.In.ReadLine()))\n            {\n                accounts.Add(line.Trim());\n            }\n\n            return accounts;\n        }\n\n        protected abstract Task ShowAsync(SelectAccountViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Commands/TwoFactorCommand.cs",
    "content": "using System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitHub.UI.ViewModels;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace GitHub.UI.Commands\n{\n    public abstract class TwoFactorCommand : HelperCommand\n    {\n        protected TwoFactorCommand(ICommandContext context)\n            : base(context, \"2fa\", \"Show two-factor prompt.\")\n        {\n            var sms = new Option<bool>(\"--sms\", \"Two-factor code was sent via SMS.\");\n            AddOption(sms);\n\n            this.SetHandler(ExecuteAsync, sms);\n        }\n\n        private async Task<int> ExecuteAsync(bool sms)\n        {\n            var viewModel = new TwoFactorViewModel(Context.SessionManager, Context.ProcessManager)\n            {\n                IsSms = sms\n            };\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            WriteResult(new Dictionary<string, string>\n            {\n                [\"code\"] = viewModel.Code\n            });\n\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(TwoFactorViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Controls/HorizontalShadowDivider.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"800\" d:DesignHeight=\"450\"\n             x:Class=\"GitHub.UI.Controls.HorizontalShadowDivider\">\n    <UserControl.Resources>\n        <ResourceDictionary>\n            <SolidColorBrush x:Key=\"HorizontalDividerBorderBrush\" Color=\"DarkGray\" />\n            <RadialGradientBrush x:Key=\"HorizontalDividerShadowBrush\">\n                <GradientStop Color=\"Black\" />\n                <GradientStop Color=\"Transparent\" Offset=\"1\" />\n            </RadialGradientBrush>\n        </ResourceDictionary>\n    </UserControl.Resources>\n    <DockPanel>\n        <Rectangle Height=\"1\"\n                   Fill=\"{StaticResource HorizontalDividerBorderBrush}\"\n                   VerticalAlignment=\"Top\"\n                   IsHitTestVisible=\"False\"/>\n        <Rectangle Height=\"4\"\n                   StrokeThickness=\"0\"\n                   VerticalAlignment=\"Top\"\n                   Margin=\"0,-2,0,0\"\n                   Opacity=\"0.25\"\n                   IsHitTestVisible=\"False\"\n                   Fill=\"{StaticResource HorizontalDividerShadowBrush}\">\n            <Rectangle.Clip>\n                <RectangleGeometry Rect=\"0,2,10000,2\" />\n            </Rectangle.Clip>\n        </Rectangle>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitHub/UI/Controls/HorizontalShadowDivider.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitHub.UI.Controls\n{\n    public partial class HorizontalShadowDivider : UserControl\n    {\n        public HorizontalShadowDivider()\n        {\n            InitializeComponent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Controls/SixDigitInput.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"800\" d:DesignHeight=\"450\"\n             x:Class=\"GitHub.UI.Controls.SixDigitInput\">\n    <UserControl.Styles>\n        <Style Selector=\"TextBox\">\n            <Setter Property=\"Width\" Value=\"36\"/>\n            <Setter Property=\"Height\" Value=\"46\"/>\n            <Setter Property=\"MinWidth\" Value=\"36\"/>\n            <Setter Property=\"Padding\" Value=\"6\"/>\n            <Setter Property=\"Margin\" Value=\"0,0,8,0\"/>\n            <Setter Property=\"HorizontalContentAlignment\" Value=\"Center\"/>\n            <Setter Property=\"VerticalContentAlignment\" Value=\"Center\"/>\n            <Setter Property=\"FontSize\" Value=\"20\"/>\n            <Setter Property=\"MaxLength\" Value=\"1\"/>\n        </Style>\n    </UserControl.Styles>\n    <StackPanel Orientation=\"Horizontal\">\n        <TextBox x:Name=\"_one\"/>\n        <TextBox x:Name=\"_two\"/>\n        <TextBox x:Name=\"_three\"/>\n        <TextBox x:Name=\"_four\"/>\n        <TextBox x:Name=\"_five\"/>\n        <TextBox x:Name=\"_six\" Margin=\"0\"/>\n    </StackPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitHub/UI/Controls/SixDigitInput.axaml.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Data;\nusing Avalonia.Input;\nusing Avalonia.Interactivity;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager;\nusing GitCredentialManager.UI.Controls;\n\nnamespace GitHub.UI.Controls\n{\n    public partial class SixDigitInput : UserControl, IFocusable\n    {\n        public static readonly DirectProperty<SixDigitInput, string> TextProperty =\n            AvaloniaProperty.RegisterDirect<SixDigitInput, string>(\n                nameof(Text),\n                o => o.Text,\n                (o, v) => o.Text = v,\n                defaultBindingMode: BindingMode.TwoWay);\n\n        private bool _ignoreTextBoxUpdate;\n        private TextBox[] _textBoxes;\n        private string _text;\n\n        public SixDigitInput()\n        {\n            InitializeComponent();\n\n            _textBoxes = new[]\n            {\n                _one,\n                _two,\n                _three,\n                _four,\n                _five,\n                _six,\n            };\n\n            foreach (TextBox textBox in _textBoxes)\n            {\n                SetUpTextBox(textBox);\n            }\n        }\n\n        public string Text\n        {\n            get => _text;\n            set\n            {\n                SetAndRaise(TextProperty, ref _text, value);\n                if (!_ignoreTextBoxUpdate) SetTextBoxes(value);\n            }\n        }\n\n        private void SetTextBoxes(string text)\n        {\n            if (string.IsNullOrWhiteSpace(text))\n            {\n                foreach (TextBox textBox in _textBoxes)\n                {\n                    textBox.Text = string.Empty;\n                }\n            }\n            else\n            {\n                IEnumerable<char> digits = text.Where(char.IsDigit);\n                string digitsStr = string.Join(string.Empty, digits).PadRight(6);\n                for (int i = 0; i < digitsStr.Length; i++)\n                {\n                    _textBoxes[i].Text = digitsStr.Substring(i, 1);\n                }\n            }\n        }\n\n        public void SetFocus()\n        {\n            // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n            if (!PlatformUtils.IsMacOS())\n                _textBoxes[0].Focus(NavigationMethod.Tab, KeyModifiers.None);\n        }\n\n        private void SetUpTextBox(TextBox textBox)\n        {\n            textBox.AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);\n\n            void OnPreviewKeyDown(object sender, KeyEventArgs e)\n            {\n                // Handle paste\n                if (TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration.Paste.Any(x => x.Matches(e)) ?? false)\n                {\n                    OnPaste();\n                    e.Handled = true;\n                }\n                // Handle keyboard navigation\n                else if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Back)\n                {\n                    e.Handled = e.Key == Key.Right ? MoveNext() : MovePrevious();\n                    if (e.Key == Key.Back)\n                    {\n                        textBox.Text = string.Empty;\n                    }\n                }\n                // Only allow 0-9, Tab, Escape, and Delete\n                else if (e.Key != Key.D0 &&\n                         e.Key != Key.D1 &&\n                         e.Key != Key.D2 &&\n                         e.Key != Key.D3 &&\n                         e.Key != Key.D4 &&\n                         e.Key != Key.D5 &&\n                         e.Key != Key.D6 &&\n                         e.Key != Key.D7 &&\n                         e.Key != Key.D8 &&\n                         e.Key != Key.D9 &&\n                         e.Key != Key.NumPad0 &&\n                         e.Key != Key.NumPad1 &&\n                         e.Key != Key.NumPad2 &&\n                         e.Key != Key.NumPad3 &&\n                         e.Key != Key.NumPad4 &&\n                         e.Key != Key.NumPad5 &&\n                         e.Key != Key.NumPad6 &&\n                         e.Key != Key.NumPad7 &&\n                         e.Key != Key.NumPad8 &&\n                         e.Key != Key.NumPad9 &&\n                         e.Key != Key.Tab &&\n                         e.Key != Key.Escape &&\n                         e.Key != Key.Delete)\n                {\n                    e.Handled = true;\n                }\n            };\n\n            textBox.PropertyChanged += (s, e) =>\n            {\n                if (e.Property.Name == nameof(TextBox.Text))\n                {\n                    try\n                    {\n                        _ignoreTextBoxUpdate = true;\n                        Text = string.Join(string.Empty, _textBoxes.Select(x => x.Text));\n                    }\n                    finally\n                    {\n                        _ignoreTextBoxUpdate = false;\n                    }\n\n                    if (e.NewValue is string value && value.Length > 0)\n                    {\n                        MoveNext();\n                    }\n                }\n            };\n        }\n\n        private void OnPaste()\n        {\n            Text = TopLevel.GetTopLevel(this)?.Clipboard?.GetTextAsync().GetAwaiter().GetResult();\n        }\n\n        private bool MoveNext() => MoveFocus(true);\n\n        private bool MovePrevious() => MoveFocus(false);\n\n        private bool MoveFocus(bool next)\n        {\n            // Get currently focused text box\n            if (TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() is TextBox textBox)\n            {\n                int textBoxIndex = Array.IndexOf(_textBoxes, textBox);\n                if (textBoxIndex > -1)\n                {\n                    int nextIndex = next\n                        ? Math.Min(_textBoxes.Length - 1, textBoxIndex + 1)\n                        : Math.Max(0, textBoxIndex - 1);\n\n                    _textBoxes[nextIndex].Focus(NavigationMethod.Tab, KeyModifiers.None);\n                    return true;\n                }\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/ViewModels/CredentialsViewModel.cs",
    "content": "using System.ComponentModel;\nusing System.Windows.Input;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitHub.UI.ViewModels\n{\n    public class CredentialsViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n        private readonly IProcessManager _processManager;\n\n        private string _enterpriseUrl;\n        private string _token;\n        private string _userName;\n        private string _password;\n        private bool _showBrowserLogin;\n        private bool _showDeviceLogin;\n        private bool _showTokenLogin;\n        private bool _showBasicLogin;\n        private ICommand _signUpCommand;\n        private ICommand _signInBrowserCommand;\n        private ICommand _signInDeviceCommand;\n        private RelayCommand _signInBasicCommand;\n        private RelayCommand _signInTokenCommand;\n\n        public CredentialsViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public CredentialsViewModel(ISessionManager sessionManager, IProcessManager processManager)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n            EnsureArgument.NotNull(processManager, nameof(processManager));\n\n            _sessionManager = sessionManager;\n            _processManager = processManager;\n\n            Title = \"Connect to GitHub\";\n            SignUpCommand = new RelayCommand(SignUp);\n            SignInBrowserCommand = new RelayCommand(SignInBrowser);\n            SignInDeviceCommand = new RelayCommand(SignInDevice);\n            SignInTokenCommand = new RelayCommand(SignInToken, CanSignInToken);\n            SignInBasicCommand = new RelayCommand(SignInBasic, CanSignInBasic);\n\n            PropertyChanged += OnPropertyChanged;\n        }\n\n        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)\n        {\n            switch (e.PropertyName)\n            {\n                case nameof(UserName):\n                case nameof(Password):\n                    SignInBasicCommand.RaiseCanExecuteChanged();\n                    break;\n\n                case nameof(Token):\n                    SignInTokenCommand.RaiseCanExecuteChanged();\n                    break;\n            }\n        }\n\n        private void SignUp()\n        {\n            _sessionManager.OpenBrowser(\"https://github.com/pricing\");\n        }\n\n        private void SignInBrowser()\n        {\n            SelectedMode = AuthenticationModes.Browser;\n            Accept();\n        }\n\n        private void SignInDevice()\n        {\n            SelectedMode = AuthenticationModes.Device;\n            Accept();\n        }\n\n        private bool CanSignInToken()\n        {\n            return !string.IsNullOrWhiteSpace(Token);\n        }\n\n        private void SignInToken()\n        {\n            SelectedMode = AuthenticationModes.Pat;\n            Accept();\n        }\n\n        private bool CanSignInBasic()\n        {\n            return !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrEmpty(Password);\n        }\n\n        private void SignInBasic()\n        {\n            SelectedMode = AuthenticationModes.Basic;\n            Accept();\n        }\n\n        public string Token\n        {\n            get => _token;\n            set => SetAndRaisePropertyChanged(ref _token, value);\n        }\n\n        public string UserName\n        {\n            get => _userName;\n            set => SetAndRaisePropertyChanged(ref _userName, value);\n        }\n\n        public string Password\n        {\n            get => _password;\n            set => SetAndRaisePropertyChanged(ref _password, value);\n        }\n\n        public string EnterpriseUrl\n        {\n            get => _enterpriseUrl;\n            set => SetAndRaisePropertyChanged(ref _enterpriseUrl, value);\n        }\n\n        public string OAuthModeTitle\n        {\n            get\n            {\n                if (ShowBrowserLogin && ShowDeviceLogin)\n                    return \"Browser/Device\";\n                if (ShowBrowserLogin) return \"Browser\";\n                if (ShowDeviceLogin)  return \"Device\";\n                return \"OAuth\";\n            }\n        }\n\n        public bool ShowBrowserLogin\n        {\n            get => _showBrowserLogin;\n            set\n            {\n                SetAndRaisePropertyChanged(ref _showBrowserLogin, value);\n                RaisePropertyChanged(OAuthModeTitle);\n            }\n        }\n\n        public bool ShowDeviceLogin\n        {\n            get => _showDeviceLogin;\n            set\n            {\n                SetAndRaisePropertyChanged(ref _showDeviceLogin, value);\n                RaisePropertyChanged(OAuthModeTitle);\n            }\n        }\n\n        public bool ShowTokenLogin\n        {\n            get => _showTokenLogin;\n            set => SetAndRaisePropertyChanged(ref _showTokenLogin, value);\n        }\n\n        public bool ShowBasicLogin\n        {\n            get => _showBasicLogin;\n            set => SetAndRaisePropertyChanged(ref _showBasicLogin, value);\n        }\n\n        public ICommand SignUpCommand\n        {\n            get => _signUpCommand;\n            set => SetAndRaisePropertyChanged(ref _signUpCommand, value);\n        }\n\n        public ICommand SignInBrowserCommand\n        {\n            get => _signInBrowserCommand;\n            set => SetAndRaisePropertyChanged(ref _signInBrowserCommand, value);\n        }\n\n        public ICommand SignInDeviceCommand\n        {\n            get => _signInDeviceCommand;\n            set => SetAndRaisePropertyChanged(ref _signInDeviceCommand, value);\n        }\n\n        public RelayCommand SignInTokenCommand\n        {\n            get => _signInTokenCommand;\n            set => SetAndRaisePropertyChanged(ref _signInTokenCommand, value);\n        }\n\n        public RelayCommand SignInBasicCommand\n        {\n            get => _signInBasicCommand;\n            set => SetAndRaisePropertyChanged(ref _signInBasicCommand, value);\n        }\n\n        public AuthenticationModes SelectedMode { get; private set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/ViewModels/DeviceCodeViewModel.cs",
    "content": "using System.Windows.Input;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitHub.UI.ViewModels\n{\n    public class DeviceCodeViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n\n        private ICommand _verificationUrlCommand;\n        private string _verificationUrl;\n        private string _userCode;\n\n        public DeviceCodeViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public DeviceCodeViewModel(ISessionManager sessionManager)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n\n            _sessionManager = sessionManager;\n\n            Title = \"Device code authentication\";\n            VerificationUrlCommand = new RelayCommand(OpenVerificationUrl);\n        }\n\n        private void OpenVerificationUrl()\n        {\n            _sessionManager.OpenBrowser(VerificationUrl);\n        }\n\n        public string UserCode\n        {\n            get => _userCode;\n            set => SetAndRaisePropertyChanged(ref _userCode, value);\n        }\n\n        public string VerificationUrl\n        {\n            get => _verificationUrl;\n            set => SetAndRaisePropertyChanged(ref _verificationUrl, value);\n        }\n\n        public ICommand VerificationUrlCommand\n        {\n            get => _verificationUrlCommand;\n            set => SetAndRaisePropertyChanged(ref _verificationUrlCommand, value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/ViewModels/SelectAccountViewModel.cs",
    "content": "using System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.ComponentModel;\nusing System.Linq;\nusing System.Windows.Input;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitHub.UI.ViewModels\n{\n    public class SelectAccountViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n\n        private AccountViewModel _selectedAccount;\n        private string _enterpriseUrl;\n        private ObservableCollection<AccountViewModel> _accounts;\n        private RelayCommand _continueCommand;\n        private ICommand _newAccountCommand;\n        private ICommand _learnMoreCommand;\n        private bool _showHelpLink = true;\n\n        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)\n        {\n            switch (e.PropertyName)\n            {\n                case nameof(SelectedAccount):\n                    ContinueCommand.RaiseCanExecuteChanged();\n                    break;\n            }\n        }\n\n        public SelectAccountViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public SelectAccountViewModel(ISessionManager sessionManager, IEnumerable<string> accounts = null)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n\n            _sessionManager = sessionManager;\n\n            Title = \"Select an account\";\n            ContinueCommand = new RelayCommand(Accept, CanContinue);\n            NewAccountCommand = new RelayCommand(NewAccount);\n            LearnMoreCommand = new RelayCommand(LearnMore);\n            Accounts = new ObservableCollection<AccountViewModel>();\n\n            foreach (string account in accounts ?? Enumerable.Empty<string>())\n            {\n                Accounts.Add(\n                    new AccountViewModel\n                    {\n                        UserName = account\n                    }\n                );\n            }\n\n            PropertyChanged += OnPropertyChanged;\n        }\n\n        private void NewAccount()\n        {\n            SelectedAccount = null;\n            Accept();\n        }\n\n        private void LearnMore()\n        {\n            _sessionManager.OpenBrowser(Constants.HelpUrls.GcmMultipleUsers);\n        }\n\n        private bool CanContinue()\n        {\n            return SelectedAccount != null;\n        }\n\n        public AccountViewModel SelectedAccount\n        {\n            get => _selectedAccount;\n            set => SetAndRaisePropertyChanged(ref _selectedAccount, value);\n        }\n\n        public string EnterpriseUrl\n        {\n            get => _enterpriseUrl;\n            set => SetAndRaisePropertyChanged(ref _enterpriseUrl, value);\n        }\n\n        public ObservableCollection<AccountViewModel> Accounts\n        {\n            get => _accounts;\n            set => SetAndRaisePropertyChanged(ref _accounts, value);\n        }\n\n        public RelayCommand ContinueCommand\n        {\n            get => _continueCommand;\n            set => SetAndRaisePropertyChanged(ref _continueCommand, value);\n        }\n\n        public ICommand NewAccountCommand\n        {\n            get => _newAccountCommand;\n            set => SetAndRaisePropertyChanged(ref _newAccountCommand, value);\n        }\n\n        public ICommand LearnMoreCommand\n        {\n            get => _learnMoreCommand;\n            set => SetAndRaisePropertyChanged(ref _learnMoreCommand, value);\n        }\n\n        public bool ShowHelpLink\n        {\n            get => _showHelpLink;\n            set => SetAndRaisePropertyChanged(ref _showHelpLink, value);\n        }\n    }\n\n    public class AccountViewModel : ViewModel\n    {\n        private string _userName;\n\n        public string UserName\n        {\n            get => _userName;\n            set => SetAndRaisePropertyChanged(ref _userName, value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/ViewModels/TwoFactorViewModel.cs",
    "content": "using System.ComponentModel;\nusing System.Windows.Input;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitHub.UI.ViewModels\n{\n    public class TwoFactorViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n        private readonly IProcessManager _processManager;\n\n        private string _code;\n        private ICommand _learnMoreCommand;\n        private RelayCommand _verifyCommand;\n        private bool _isSms;\n\n        public TwoFactorViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public TwoFactorViewModel(ISessionManager sessionManager, IProcessManager processManager)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n            EnsureArgument.NotNull(processManager, nameof(processManager));\n\n            _sessionManager = sessionManager;\n            _processManager = processManager;\n\n            Title = \"Two-factor authentication required\";\n            LearnMoreCommand = new RelayCommand(LearnMore);\n            VerifyCommand = new RelayCommand(Accept, CanVerify);\n\n            PropertyChanged += OnPropertyChanged;\n        }\n\n        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)\n        {\n            switch (e.PropertyName)\n            {\n                case nameof(Code):\n                    VerifyCommand.RaiseCanExecuteChanged();\n                    break;\n            }\n        }\n\n        private void LearnMore()\n        {\n            _sessionManager.OpenBrowser(\"https://aka.ms/vs-core-github-auth-help\");\n        }\n        private bool CanVerify()\n        {\n            return !string.IsNullOrWhiteSpace(Code) && Code.Length == 6;\n        }\n\n        public bool IsSms\n        {\n            get => _isSms;\n            set\n            {\n                SetAndRaisePropertyChanged(ref _isSms, value);\n                RaisePropertyChanged(nameof(Description));\n            }\n        }\n\n        public string Description => IsSms\n            ? \"We sent you a message via SMS with your authentication code.\"\n            : \"Open the two-factor authentication app on your device to view your authentication code.\";\n\n        public string Code\n        {\n            get => _code;\n            set => SetAndRaisePropertyChanged(ref _code, value);\n        }\n\n        public ICommand LearnMoreCommand\n        {\n            get => _learnMoreCommand;\n            set => SetAndRaisePropertyChanged(ref _learnMoreCommand, value);\n        }\n\n        public RelayCommand VerifyCommand\n        {\n            get => _verifyCommand;\n            set => SetAndRaisePropertyChanged(ref _verifyCommand, value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/CredentialsView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:controls=\"clr-namespace:GitHub.UI.Controls\"\n             xmlns:vm=\"clr-namespace:GitHub.UI.ViewModels\"\n             xmlns:converters=\"clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitHub.UI.Views.CredentialsView\">\n    <Design.DataContext>\n        <vm:CredentialsViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"0,0,0,15\">\n            <!-- TODO: replace with GitHub logo -->\n            <TextBlock Text=\"GitHub\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"20\"\n                       FontWeight=\"Bold\"/>\n            <TextBlock Text=\"Sign in\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"24\"\n                       FontWeight=\"Light\"\n                       Margin=\"0,0,0,10\" />\n            <controls:HorizontalShadowDivider/>\n            <StackPanel IsVisible=\"{Binding EnterpriseUrl, Converter={x:Static StringConverters.IsNotNullOrEmpty}}\"\n                        Margin=\"0,10,0,0\">\n                <TextBlock Text=\"GitHub Enterprise\" HorizontalAlignment=\"Center\"/>\n                <TextBlock Text=\"{Binding EnterpriseUrl}\"\n                           HorizontalAlignment=\"Center\"/>\n            </StackPanel>\n        </StackPanel>\n\n        <WrapPanel DockPanel.Dock=\"Bottom\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                   Margin=\"0,20,0,0\">\n            <TextBlock Text=\"Don't have an account?\" Margin=\"0,0,5,0\" />\n            <Button Content=\"Sign up\"\n                    Command=\"{Binding SignUpCommand}\"\n                    Classes=\"hyperlink\"/>\n        </WrapPanel>\n\n        <TabControl x:Name=\"_authModesTabControl\"\n                    VerticalContentAlignment=\"Center\"\n                    AutoScrollToSelectedItem=\"True\">\n            <TabControl.Styles>\n                <Style Selector=\"TabItem\">\n                    <Setter Property=\"MinHeight\" Value=\"30\" />\n                </Style>\n                <Style Selector=\"DockPanel > ItemsPresenter > WrapPanel\">\n                    <Setter Property=\"HorizontalAlignment\" Value=\"Center\"/>\n                </Style>\n            </TabControl.Styles>\n\n            <TabItem IsVisible=\"{Binding $self.IsEnabled}\">\n                <TabItem.IsEnabled>\n                    <MultiBinding Converter=\"{x:Static converters:BoolConvertersEx.Or}\">\n                        <Binding Path=\"ShowBrowserLogin\" />\n                        <Binding Path=\"ShowDeviceLogin\" />\n                    </MultiBinding>\n                </TabItem.IsEnabled>\n                <TabItem.Header>\n                    <TextBlock Text=\"{Binding OAuthModeTitle}\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,10\">\n                    <Button x:Name=\"_signInBrowserButton\"\n                            Content=\"Sign in with your browser\"\n                            IsDefault=\"True\"\n                            Command=\"{Binding SignInBrowserCommand}\"\n                            IsVisible=\"{Binding ShowBrowserLogin}\"\n                            HorizontalAlignment=\"Center\"\n                            Margin=\"0,0,0,10\"\n                            Classes=\"accent\"/>\n                    <Button x:Name=\"_signInDeviceButton\"\n                            Content=\"Sign in with a code\"\n                            Command=\"{Binding SignInDeviceCommand}\"\n                            IsVisible=\"{Binding ShowDeviceLogin}\"\n                            HorizontalAlignment=\"Center\"\n                            Margin=\"0,10,0,10\"/>\n                </StackPanel>\n            </TabItem>\n\n            <TabItem IsEnabled=\"{Binding ShowTokenLogin}\"\n                     IsVisible=\"{Binding $self.IsEnabled}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"Token\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,10\">\n                    <TextBox x:Name=\"_tokenTextBox\"\n                             Watermark=\"Personal access token\" Margin=\"0,0,0,10\"\n                             PasswordChar=\"●\"\n                             Text=\"{Binding Token}\"/>\n                    <Button Content=\"Sign in\"\n                            IsDefault=\"True\"\n                            Command=\"{Binding SignInTokenCommand}\"\n                            HorizontalAlignment=\"Center\"\n                            Classes=\"accent\"/>\n                </StackPanel>\n            </TabItem>\n\n            <TabItem IsEnabled=\"{Binding ShowBasicLogin}\"\n                     IsVisible=\"{Binding $self.IsEnabled}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"Password\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,10\">\n                    <TextBox x:Name=\"_userNameTextBox\"\n                             Watermark=\"Username or email\" Margin=\"0,0,0,10\"\n                             Text=\"{Binding UserName}\"/>\n                    <TextBox x:Name=\"_passwordTextBox\"\n                             Watermark=\"Password\" Margin=\"0,0,0,10\"\n                             PasswordChar=\"●\"\n                             Text=\"{Binding Password}\"/>\n                    <Button Content=\"Sign in\"\n                            IsDefault=\"True\"\n                            Command=\"{Binding SignInBasicCommand}\"\n                            HorizontalAlignment=\"Center\"\n                            Classes=\"accent\"/>\n                </StackPanel>\n            </TabItem>\n        </TabControl>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/CredentialsView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager;\nusing GitHub.UI.ViewModels;\nusing GitCredentialManager.UI.Controls;\n\nnamespace GitHub.UI.Views\n{\n    public partial class CredentialsView : UserControl, IFocusable\n    {\n        public CredentialsView()\n        {\n            InitializeComponent();\n        }\n\n        public void SetFocus()\n        {\n            if (!(DataContext is CredentialsViewModel vm))\n            {\n                return;\n            }\n\n            // Select the best available authentication mechanism that is visible\n            // and focus on the button/text box\n            if (vm.ShowBrowserLogin)\n            {\n                _authModesTabControl.SelectedIndex = 0;\n                _signInBrowserButton.Focus();\n            }\n            else if (vm.ShowDeviceLogin)\n            {\n                _authModesTabControl.SelectedIndex = 0;\n                _signInDeviceButton.Focus();\n            }\n            else if (vm.ShowTokenLogin)\n            {\n                _authModesTabControl.SelectedIndex = 1;\n                // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                if (!PlatformUtils.IsMacOS())\n                    _tokenTextBox.Focus();\n\n            }\n            else if (vm.ShowBasicLogin)\n            {\n                _authModesTabControl.SelectedIndex = 2;\n                if (string.IsNullOrWhiteSpace(vm.UserName))\n                {\n                    // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                    if (!PlatformUtils.IsMacOS())\n                        _userNameTextBox.Focus();\n                }\n                else\n                {\n                    // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                    if (!PlatformUtils.IsMacOS())\n                        _passwordTextBox.Focus();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/DeviceCodeView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:controls=\"clr-namespace:GitHub.UI.Controls\"\n             xmlns:vm=\"clr-namespace:GitHub.UI.ViewModels\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitHub.UI.Views.DeviceCodeView\">\n    <Design.DataContext>\n        <vm:DeviceCodeViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"0,0,0,15\">\n            <!-- TODO: replace with GitHub logo -->\n            <TextBlock Text=\"GitHub\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"20\"\n                       FontWeight=\"Bold\"/>\n            <TextBlock Text=\"Device authentication\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"24\"\n                       FontWeight=\"Light\"\n                       Margin=\"0,0,0,10\" />\n            <controls:HorizontalShadowDivider/>\n        </StackPanel>\n\n        <StackPanel Orientation=\"Vertical\" VerticalAlignment=\"Center\">\n            <TextBlock Text=\"Visit the URL below, sign in, and enter the following device code to continue.\"\n                       Margin=\"0,0,0,20\"\n                       TextWrapping=\"Wrap\" TextAlignment=\"Center\"/>\n            <TextBox Text=\"{Binding UserCode}\"\n                     Margin=\"0,0,0,20\"\n                     HorizontalAlignment=\"Center\"\n                     FontSize=\"24\"\n                     TextAlignment=\"Center\"\n                     Classes=\"label monospace\"/>\n            <Button Content=\"{Binding VerificationUrl}\"\n                    Command=\"{Binding VerificationUrlCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"hyperlink\" />\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/DeviceCodeView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\n\nnamespace GitHub.UI.Views\n{\n    public partial class DeviceCodeView : UserControl\n    {\n        public DeviceCodeView()\n        {\n            InitializeComponent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/SelectAccountView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:controls=\"clr-namespace:GitHub.UI.Controls\"\n             xmlns:vm=\"clr-namespace:GitHub.UI.ViewModels\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitHub.UI.Views.SelectAccountView\">\n    <Design.DataContext>\n        <vm:SelectAccountViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"0,0,0,15\">\n            <!-- TODO: replace with GitHub logo -->\n            <TextBlock Text=\"GitHub\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"20\"\n                       FontWeight=\"Bold\"/>\n            <TextBlock Text=\"Select an account\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"24\"\n                       FontWeight=\"Light\"\n                       Margin=\"0,0,0,10\" />\n            <controls:HorizontalShadowDivider/>\n            <StackPanel IsVisible=\"{Binding EnterpriseUrl, Converter={x:Static StringConverters.IsNotNullOrEmpty}}\"\n                        Margin=\"0,10,0,0\">\n                <TextBlock Text=\"GitHub Enterprise\" HorizontalAlignment=\"Center\"/>\n                <TextBlock Text=\"{Binding EnterpriseUrl}\"\n                           HorizontalAlignment=\"Center\"/>\n            </StackPanel>\n        </StackPanel>\n\n        <StackPanel DockPanel.Dock=\"Top\" Orientation=\"Horizontal\"\n                    HorizontalAlignment=\"Center\"\n                    Margin=\"0,5,0,10\"\n                    IsVisible=\"{Binding ShowHelpLink}\">\n            <Image Source=\"{DynamicResource HelpIcon}\"\n                   Width=\"16\" Height=\"16\"\n                   Margin=\"0,0,5,0\"/>\n            <Button Content=\"Why am I being asked to select an account?\"\n                    Command=\"{Binding LearnMoreCommand}\"\n                    Classes=\"hyperlink\"/>\n        </StackPanel>\n\n        <StackPanel Orientation=\"Vertical\" VerticalAlignment=\"Center\">\n            <ListBox ItemsSource=\"{Binding Accounts}\"\n                     SelectedItem=\"{Binding SelectedAccount}\"\n                     Margin=\"20,0,20,10\"\n                     MaxHeight=\"200\"\n                     Background=\"Transparent\"\n                     AutoScrollToSelectedItem=\"True\"\n                     DoubleTapped=\"ListBox_OnDoubleTapped\">\n                <ListBox.ItemTemplate>\n                    <DataTemplate DataType=\"vm:AccountViewModel\">\n                        <DockPanel LastChildFill=\"True\">\n                            <Image DockPanel.Dock=\"Left\"\n                                   VerticalAlignment=\"Center\"\n                                   Source=\"{DynamicResource PersonIcon}\"\n                                   Width=\"24\" Height=\"24\"/>\n                            <TextBlock Text=\"{Binding UserName}\"\n                                       Margin=\"10,0\"\n                                       VerticalAlignment=\"Center\"/>\n                        </DockPanel>\n                    </DataTemplate>\n                </ListBox.ItemTemplate>\n            </ListBox>\n            <Button Content=\"Continue\"\n                    Command=\"{Binding ContinueCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    IsDefault=\"true\"\n                    Classes=\"accent\"\n                    Margin=\"0,0,0,15\"\n                    Padding=\"20,10\"/>\n            <Button Content=\"Add a new account\"\n                    Command=\"{Binding NewAccountCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"hyperlink\"/>\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/SelectAccountView.axaml.cs",
    "content": "using Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Input;\nusing Avalonia.Markup.Xaml;\nusing GitHub.UI.ViewModels;\n\nnamespace GitHub.UI.Views;\n\npublic partial class SelectAccountView : UserControl\n{\n    public SelectAccountView()\n    {\n        InitializeComponent();\n    }\n\n    private void ListBox_OnDoubleTapped(object sender, TappedEventArgs e)\n    {\n        if (DataContext is SelectAccountViewModel { SelectedAccount: not null } vm)\n        {\n            vm.ContinueCommand.Execute(null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/TwoFactorView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:controls=\"clr-namespace:GitHub.UI.Controls\"\n             xmlns:vm=\"clr-namespace:GitHub.UI.ViewModels\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitHub.UI.Views.TwoFactorView\">\n    <Design.DataContext>\n        <vm:TwoFactorViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"0,0,0,15\">\n            <!-- TODO: replace with GitHub logo -->\n            <TextBlock Text=\"GitHub\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"20\"\n                       FontWeight=\"Bold\"/>\n            <TextBlock Text=\"Two-factor authentication\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"24\"\n                       FontWeight=\"Light\"\n                       Margin=\"0,0,0,10\" />\n            <controls:HorizontalShadowDivider/>\n        </StackPanel>\n\n        <StackPanel Orientation=\"Vertical\" VerticalAlignment=\"Center\">\n            <TextBlock Text=\"{Binding Description}\" Margin=\"0,0,0,20\"\n                       TextWrapping=\"Wrap\" TextAlignment=\"Center\"/>\n            <controls:SixDigitInput x:Name=\"_codeInput\"\n                                    Text=\"{Binding Code}\"\n                                    Margin=\"0,0,0,20\"\n                                    HorizontalAlignment=\"Center\"/>\n            <Button Content=\"Verify\"\n                    IsDefault=\"True\"\n                    Command=\"{Binding VerifyCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"accent\"/>\n            <Button Content=\"Learn more\"\n                    Command=\"{Binding LearnMoreCommand}\"\n                    HorizontalAlignment=\"Center\"\n                    Classes=\"hyperlink\"\n                    Margin=\"0,20,0,0\"/>\n        </StackPanel>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitHub/UI/Views/TwoFactorView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager;\nusing GitHub.UI.Controls;\nusing GitCredentialManager.UI.Controls;\n\nnamespace GitHub.UI.Views\n{\n    public partial class TwoFactorView : UserControl, IFocusable\n    {\n        public TwoFactorView()\n        {\n            InitializeComponent();\n        }\n\n        public void SetFocus()\n        {\n            // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n            if (!PlatformUtils.IsMacOS())\n                _codeInput.SetFocus();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub.Tests/GitHub.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.2\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.11.1\" />\n    <PackageReference Include=\"ReportGenerator\" Version=\"5.3.10\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <DotNetCliToolReference Include=\"dotnet-xunit\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\GitHub\\GitHub.csproj\" />\n    <ProjectReference Include=\"..\\TestInfrastructure\\TestInfrastructure.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/GitHub.Tests/GitHubAuthChallengeTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace GitHub.Tests;\n\npublic class GitHubAuthChallengeTests\n{\n    [Fact]\n    public void GitHubAuthChallenge_FromHeaders_CaseInsensitive()\n    {\n        var headers = new[]\n        {\n            \"BASIC REALM=\\\"GITHUB\\\"\",\n            \"basic realm=\\\"github\\\"\",\n            \"bAsIc ReAlM=\\\"gItHuB\\\"\",\n        };\n\n        IList<GitHubAuthChallenge> challenges = GitHubAuthChallenge.FromHeaders(headers);\n        Assert.Equal(3, challenges.Count);\n\n        foreach (var challenge in challenges)\n        {\n            Assert.Null(challenge.Domain);\n            Assert.Null(challenge.Enterprise);\n        }\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_FromHeaders_MultipleRealms_ReturnsGitHubOnly()\n    {\n        var headers = new[]\n        {\n            \"Basic realm=\\\"contoso\\\"\",\n            \"Basic realm=\\\"GitHub\\\"\",\n            \"Basic realm=\\\"fabrikam\\\"\",\n        };\n\n        IList<GitHubAuthChallenge> challenges = GitHubAuthChallenge.FromHeaders(headers);\n        Assert.Single(challenges);\n\n        Assert.Null(challenges[0].Domain);\n        Assert.Null(challenges[0].Enterprise);\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_FromHeaders_NoMatchingRealms_ReturnsEmpty()\n    {\n        var headers = new[]\n        {\n            \"Basic realm=\\\"contoso\\\"\",\n            \"Basic realm=\\\"fabrikam\\\"\",\n            \"Basic realm=\\\"example\\\"\",\n        };\n\n        IList<GitHubAuthChallenge> challenges = GitHubAuthChallenge.FromHeaders(headers);\n        Assert.Empty(challenges);\n    }\n\n    [Theory]\n    [InlineData(\"Basic realm=\\\"GitHub\\\" enterprise_hint=\\\"contoso-corp\\\" domain_hint=\\\"contoso\\\"\", \"contoso\", \"contoso-corp\")]\n    [InlineData(\"Basic realm=\\\"GitHub\\\" domain_hint=\\\"contoso\\\"\", \"contoso\", null)]\n    [InlineData(\"Basic realm=\\\"GitHub\\\" enterprise_hint=\\\"contoso-corp\\\"\", null, \"contoso-corp\")]\n    [InlineData(\"Basic realm=\\\"GitHub\\\" domain_hint=\\\"fab\\\" enterprise_hint=\\\"fabirkamopensource\\\"\", \"fab\", \"fabirkamopensource\")]\n    [InlineData(\"Basic enterprise_hint=\\\"iana\\\" realm=\\\"GitHub\\\" domain_hint=\\\"example\\\"\", \"example\", \"iana\")]\n    [InlineData(\"Basic domain_hint=\\\"test\\\" enterprise_hint=\\\"test-inc\\\" realm=\\\"GitHub\\\"\", \"test\", \"test-inc\")]\n    public void GitHubAuthChallenge_FromHeaders_Hints_ReturnsWithHints(string header, string domain, string enterprise)\n    {\n        IList<GitHubAuthChallenge> challenges = GitHubAuthChallenge.FromHeaders(new[] { header });\n        Assert.Single(challenges);\n\n        Assert.Equal(domain, challenges[0].Domain);\n        Assert.Equal(enterprise, challenges[0].Enterprise);\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_FromHeaders_EmptyHeaders_ReturnsEmpty()\n    {\n        string[] headers = Array.Empty<string>();\n        IList<GitHubAuthChallenge> challenges = GitHubAuthChallenge.FromHeaders(headers);\n        Assert.Empty(challenges);\n    }\n\n    [Theory]\n    [InlineData(null, false)]\n    [InlineData(\"\", false)]\n    [InlineData(\" \", false)]\n    [InlineData(\"alice\", true)]\n    [InlineData(\"alice_contoso\", false)]\n    [InlineData(\"alice_CONTOSO\", false)]\n    [InlineData(\"alice_contoso_alt\", false)]\n    [InlineData(\"pj_nitin\", true)]\n    [InlineData(\"up_the_irons\", true)]\n    public void GitHubAuthChallenge_IsDomainMember_NoHint(string userName, bool expected)\n    {\n        var challenge = new GitHubAuthChallenge();\n        Assert.Equal(expected, challenge.IsDomainMember(userName));\n    }\n\n    [Theory]\n    [InlineData(null, false)]\n    [InlineData(\"\", false)]\n    [InlineData(\" \", false)]\n    [InlineData(\"alice\", false)]\n    [InlineData(\"alice_contoso\", true)]\n    [InlineData(\"alice_CONTOSO\", true)]\n    [InlineData(\"alice_contoso_alt\", false)]\n    [InlineData(\"pj_nitin\", false)]\n    [InlineData(\"up_the_irons\", false)]\n    public void GitHubAuthChallenge_IsDomainMember_DomainHint(string userName, bool expected)\n    {\n        var realm = new GitHubAuthChallenge(\"contoso\", \"contoso-corp\");\n        Assert.Equal(expected, realm.IsDomainMember(userName));\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_Equals_Null_ReturnsFalse()\n    {\n        var challenge = new GitHubAuthChallenge(\"contoso\", \"contoso-corp\");\n        Assert.False(challenge.Equals(null));\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_Equals_SameInstance_ReturnsTrue()\n    {\n        var challenge = new GitHubAuthChallenge(\"contoso\", \"contoso-corp\");\n        Assert.True(challenge.Equals(challenge));\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_Equals_DifferentInstance_ReturnsTrue()\n    {\n        var challenge1 = new GitHubAuthChallenge(\"contoso\", \"constoso-corp\");\n        var challenge2 = new GitHubAuthChallenge(\"contoso\", \"constoso-corp\");\n        Assert.True(challenge1.Equals(challenge2));\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_Equals_DifferentCase_ReturnsTrue()\n    {\n        var challenge1 = new GitHubAuthChallenge(\"contoso\", \"contoso-corp\");\n        var challenge2 = new GitHubAuthChallenge(\"CONTOSO\", \"CONTOSO-CORP\");\n        Assert.True(challenge1.Equals(challenge2));\n    }\n\n    [Fact]\n    public void GitHubAuthChallenge_Equals_DifferentShortCode_ReturnsFalse()\n    {\n        var challenge1 = new GitHubAuthChallenge(\"contoso\", \"constoso-corp\");\n        var challenge2 = new GitHubAuthChallenge(\"fab\", \"fabrikamopensource\");\n        Assert.False(challenge1.Equals(challenge2));\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub.Tests/GitHubAuthenticationTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Threading.Tasks;\nusing Avalonia.Animation;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Moq.Protected;\nusing Xunit;\n\nnamespace GitHub.Tests\n{\n    public class GitHubAuthenticationTests\n    {\n        [Fact]\n        public async Task GitHubAuthentication_GetAuthenticationAsync_AuthenticationModesNone_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var auth = new GitHubAuthentication(context);\n            await Assert.ThrowsAsync<ArgumentException>(\"modes\",\n                () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.None)\n            );\n        }\n\n        [Theory]\n        [InlineData(AuthenticationModes.Browser)]\n        [InlineData(AuthenticationModes.Device)]\n        public async Task GitHubAuthentication_GetAuthenticationAsync_SingleChoice_TerminalAndInteractionNotRequired(GitHub.AuthenticationModes modes)\n        {\n            var context = new TestCommandContext();\n            context.Settings.IsTerminalPromptsEnabled = false;\n            context.Settings.IsInteractionAllowed = false;\n            context.SessionManager.IsDesktopSession = true; // necessary for browser\n            context.FileSystem.Files[\"/usr/local/bin/GitHub.UI\"] = new byte[0];\n            context.FileSystem.Files[@\"C:\\Program Files\\Git Credential Manager Core\\GitHub.UI.exe\"] = new byte[0];\n            var auth = new GitHubAuthentication(context);\n            var result = await auth.GetAuthenticationAsync(null, null, modes);\n            Assert.Equal(modes, result.AuthenticationMode);\n        }\n\n        [Fact]\n        public async Task GitHubAuthentication_GetAuthenticationAsync_TerminalPromptsDisabled_Throws()\n        {\n            var context = new TestCommandContext();\n            context.Settings.IsTerminalPromptsEnabled = false;\n            var auth = new GitHubAuthentication(context);\n            var exception = await Assert.ThrowsAsync<Trace2InvalidOperationException>(\n                () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All)\n            );\n            Assert.Equal(\"Cannot prompt because terminal prompts have been disabled.\", exception.Message);\n        }\n\n        // reproduces https://github.com/git-ecosystem/git-credential-manager/issues/453\n        [Fact]\n        public async Task GitHubAuthentication_GetAuthenticationAsync_Terminal()\n        {\n            var context = new TestCommandContext();\n            context.FileSystem.Files[\"/usr/local/bin/GitHub.UI\"] = new byte[0];\n            context.FileSystem.Files[@\"C:\\Program Files\\Git Credential Manager Core\\GitHub.UI.exe\"] = new byte[0];\n            var auth = new GitHubAuthentication(context);\n            context.Terminal.Prompts[\"option (enter for default)\"] = \"\";\n            var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);\n            Assert.Equal(AuthenticationModes.Device, result.AuthenticationMode);\n        }\n\n        [Fact]\n        public async Task GitHubAuthentication_GetAuthenticationAsync_AuthenticationModesAll_RequiresInteraction()\n        {\n            var context = new TestCommandContext();\n            context.Settings.IsInteractionAllowed = false;\n            var auth = new GitHubAuthentication(context);\n            var exception = await Assert.ThrowsAsync<Trace2InvalidOperationException>(\n                () => auth.GetAuthenticationAsync(new Uri(\"https://github.com\"), null, AuthenticationModes.All)\n            );\n            Assert.Equal(\"Cannot prompt because user interactivity has been disabled.\", exception.Message);\n        }\n\n        [Fact]\n        public async Task GitHubAuthentication_GetAuthenticationAsync_Helper_Basic()\n        {\n            const string unixHelperPath = \"/usr/local/bin/GitHub.UI\";\n            const string windowsHelperPath = @\"C:\\Program Files\\Git Credential Manager\\GitHub.UI.exe\";\n            string helperPath = PlatformUtils.IsWindows() ? windowsHelperPath : unixHelperPath;\n\n            var context = new TestCommandContext();\n            context.FileSystem.Files[helperPath] = Array.Empty<byte>();\n            context.SessionManager.IsDesktopSession = true;\n            context.Environment.Variables[GitHubConstants.EnvironmentVariables.AuthenticationHelper] = helperPath;\n            var auth = new Mock<GitHubAuthentication>(MockBehavior.Strict, context);\n            auth.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), \"prompt --all\", It.IsAny<StreamReader>(), It.IsAny<System.Threading.CancellationToken>()))\n            .Returns(Task.FromResult<IDictionary<string, string>>(\n                new Dictionary<string, string>\n                {\n                    [\"mode\"] = \"basic\",\n                    [\"username\"] = \"tim\",\n                    [\"password\"] = \"hunter2\"\n                }));\n            var result = await auth.Object.GetAuthenticationAsync(new Uri(\"https://github.com\"), null, AuthenticationModes.All);\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(\"tim\", result.Credential.Account);\n            Assert.Equal(\"hunter2\", result.Credential.Password);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub.Tests/GitHubHostProviderTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace GitHub.Tests\n{\n    public class GitHubHostProviderTests\n    {\n        [Theory]\n        [InlineData(\"https://github.com\", true)]\n        [InlineData(\"https://gitHUB.CoM\", true)]\n        [InlineData(\"https://GITHUB.COM\", true)]\n        [InlineData(\"https://gist.github.com\", true)]\n        [InlineData(\"https://foogithub.com\", false)]\n        [InlineData(\"https://api.github.com\", false)]\n        [InlineData(\"https://api.gist.github.com\", false)]\n        [InlineData(\"https://foogist.github.com\", false)]\n        public void GitHubHostProvider_IsGitHubDotCom(string input, bool expected)\n        {\n            Assert.Equal(expected, GitHubHostProvider.IsGitHubDotCom(new Uri(input)));\n        }\n\n\n        [Theory]\n        // We report that we support unencrypted HTTP here so that we can fail and\n        // show a helpful error message in the call to `GenerateCredentialAsync` instead.\n        [InlineData(\"http\", \"github.com\", true)]\n        [InlineData(\"http\", \"gist.github.com\", true)]\n        [InlineData(\"ssh\", \"github.com\", false)]\n        [InlineData(\"https\", \"example.com\", false)]\n\n        [InlineData(\"https\", \"github.com\", true)]\n        [InlineData(\"https\", \"github.con\", false)] // No support of phony similar tld.\n        [InlineData(\"https\", \"gist.github.con\", false)] // No support of phony similar tld.\n        [InlineData(\"https\", \"foogithub.com\", false)] // No support of non github.com domains.\n        [InlineData(\"https\", \"api.github.com\", false)] // No support of github.com subdomains.\n        [InlineData(\"https\", \"gist.github.com\", true)] // Except gists.\n        [InlineData(\"https\", \"GiST.GitHub.Com\", true)]\n        [InlineData(\"https\", \"GitHub.Com\", true)]\n\n        [InlineData(\"http\", \"github.my-company-server.com\", true)]\n        [InlineData(\"http\", \"gist.github.my-company-server.com\", true)]\n        [InlineData(\"https\", \"github.my-company-server.com\", true)]\n        [InlineData(\"https\", \"gist.github.my-company-server.com\", true)]\n        [InlineData(\"https\", \"gist.my-company-server.com\", false)]\n        [InlineData(\"https\", \"my-company-server.com\", false)]\n        [InlineData(\"https\", \"github.my.company.server.com\", true)]\n        [InlineData(\"https\", \"foogithub.my-company-server.com\", false)]\n        [InlineData(\"https\", \"api.github.my-company-server.com\", false)]\n        [InlineData(\"https\", \"gist.github.my.company.server.com\", true)]\n        [InlineData(\"https\", \"GitHub.My-Company-Server.Com\", true)]\n        [InlineData(\"https\", \"GiST.GitHub.My-Company-Server.com\", true)]\n        public void GitHubHostProvider_IsSupported(string protocol, string host, bool expected)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"] = host,\n            });\n\n            var provider = new GitHubHostProvider(new TestCommandContext());\n            Assert.Equal(expected, provider.IsSupported(input));\n        }\n\n        [Theory]\n        [InlineData(\"https\", \"github.com\", \"https://github.com\")]\n        [InlineData(\"https\", \"GitHub.Com\", \"https://github.com\")]\n        [InlineData(\"https\", \"gist.github.com\", \"https://github.com\")]\n        [InlineData(\"https\", \"GiST.GitHub.Com\", \"https://github.com\")]\n        [InlineData(\"https\", \"github.my-company-server.com\", \"https://github.my-company-server.com\")]\n        [InlineData(\"https\", \"GitHub.My-Company-Server.Com\", \"https://github.my-company-server.com\")]\n        [InlineData(\"https\", \"gist.github.my-company-server.com\", \"https://github.my-company-server.com\")]\n        [InlineData(\"https\", \"GiST.GitHub.My-Company-Server.Com\", \"https://github.my-company-server.com\")]\n        [InlineData(\"https\", \"github.my.company.server.com\", \"https://github.my.company.server.com\")]\n        [InlineData(\"https\", \"GitHub.My.Company.Server.Com\", \"https://github.my.company.server.com\")]\n        [InlineData(\"https\", \"gist.github.my.company.server.com\", \"https://github.my.company.server.com\")]\n        [InlineData(\"https\", \"GiST.GitHub.My.Company.Server.Com\", \"https://github.my.company.server.com\")]\n        public void GitHubHostProvider_GetCredentialServiceUrl(string protocol, string host, string expectedService)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"] = host,\n            });\n\n            var provider = new GitHubHostProvider(new TestCommandContext());\n            Assert.Equal(expectedService, GitHubHostProvider.GetServiceName(input));\n        }\n\n\n        [Theory]\n        [InlineData(\"https://example.com\", \"browser\", AuthenticationModes.Browser)]\n        [InlineData(\"https://github.com\", \"NOT-A-REAL-VALUE\", GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://GitHub.Com\", \"NOT-A-REAL-VALUE\", GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://github.com\", \"none\", GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://GitHub.Com\", \"none\", GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://github.com\", null, GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://GitHub.Com\", null, GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://gist.github.com\", null, GitHubConstants.DotComAuthenticationModes)]\n        [InlineData(\"https://GIST.GITHUB.COM\", null, GitHubConstants.DotComAuthenticationModes)]\n        public async Task GitHubHostProvider_GetSupportedAuthenticationModes(string uriString, string gitHubAuthModes, AuthenticationModes expectedModes)\n        {\n            var targetUri = new Uri(uriString);\n\n            var context = new TestCommandContext { };\n            if (gitHubAuthModes != null)\n                context.Environment.Variables.Add(GitHubConstants.EnvironmentVariables.AuthenticationModes, gitHubAuthModes);\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            AuthenticationModes actualModes = await provider.GetSupportedAuthenticationModesAsync(targetUri);\n\n            Assert.Equal(expectedModes, actualModes);\n        }\n\n        [Theory]\n        [InlineData(\"https://example.com\", null, \"0.1\", false, AuthenticationModes.Pat)]\n        [InlineData(\"https://example.com\", null, \"0.1\", true, AuthenticationModes.Basic | AuthenticationModes.Pat)]\n        [InlineData(\"https://example.com\", null, \"100.0\", false, AuthenticationModes.OAuth | AuthenticationModes.Pat)]\n        [InlineData(\"https://example.com\", null, \"100.0\", true, AuthenticationModes.All)]\n        [InlineData(\"https://example.com\", null, null, false, AuthenticationModes.OAuth | AuthenticationModes.Pat)]\n        [InlineData(\"https://example.com\", null, \"\", false, AuthenticationModes.OAuth | AuthenticationModes.Pat)]\n        [InlineData(\"https://example.com\", null, \" \", false, AuthenticationModes.OAuth | AuthenticationModes.Pat)]\n        public async Task GitHubHostProvider_GetSupportedAuthenticationModes_WithMetadata(string uriString, string gitHubAuthModes,\n            string installedVersion, bool verifiablePasswordAuthentication, AuthenticationModes expectedModes)\n        {\n            var targetUri = new Uri(uriString);\n\n            var context = new TestCommandContext { };\n            if (gitHubAuthModes != null)\n                context.Environment.Variables.Add(GitHubConstants.EnvironmentVariables.AuthenticationModes, gitHubAuthModes);\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n\n            var metaInfo = new GitHubMetaInfo\n            {\n                InstalledVersion = installedVersion,\n                VerifiablePasswordAuthentication = verifiablePasswordAuthentication\n            };\n            ghApiMock.Setup(x => x.GetMetaInfoAsync(targetUri)).ReturnsAsync(metaInfo);\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            AuthenticationModes actualModes = await provider.GetSupportedAuthenticationModesAsync(targetUri);\n\n            Assert.Equal(expectedModes, actualModes);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GetCredentialAsync_NoCredentials_NoUserNoHeaders_PromptsUser()\n        {\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = \"https\",\n                    [\"host\"] = \"github.com\",\n                }\n            );\n\n            var newCredential = new GitCredential(\"alice\", \"password\");\n\n            var context = new TestCommandContext();\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n            ghAuthMock.Setup(x => x.GetAuthenticationAsync(\n                    It.IsAny<Uri>(), It.IsAny<string>(), It.IsAny<AuthenticationModes>()))\n                .ReturnsAsync(new AuthenticationPromptResult(AuthenticationModes.Pat, newCredential));\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(credential.Account, newCredential.Account);\n            Assert.Equal(credential.Password, newCredential.Password);\n            ghAuthMock.Verify(x => x.GetAuthenticationAsync(\n                new Uri(\"https://github.com\"), null, It.IsAny<AuthenticationModes>()),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GetCredentialAsync_InputUser_ReturnsCredentialForUser()\n        {\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = \"https\",\n                    [\"host\"]     = \"github.com\",\n                    [\"username\"] = \"alice\"\n                }\n            );\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(\"https://github.com\", \"alice\", \"letmein123\");\n            context.CredentialStore.Add(\"https://github.com\", \"bob\", \"secret123\");\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(result);\n            Assert.Equal(\"alice\", credential.Account);\n            Assert.Equal(\"letmein123\", credential.Password);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GetCredentialAsync_OneDomainAccount_ReturnsCredentialForRealmAccount()\n        {\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = \"https\",\n                    [\"host\"]     = \"github.com\",\n                    [\"wwwauth\"]  = \"Basic realm=\\\"GitHub\\\" domain_hint=\\\"contoso\\\"\",\n                }\n            );\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(\"https://github.com\", \"alice\", \"letmein123\");\n            context.CredentialStore.Add(\"https://github.com\", \"bob_contoso\", \"secret123\");\n            context.CredentialStore.Add(\"https://github.com\", \"test_fabrikam\", \"hidden_value\");\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(result);\n            Assert.Equal(\"bob_contoso\", credential.Account);\n            Assert.Equal(\"secret123\", credential.Password);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GetCredentialAsync_MultipleDomainAccounts_PromptForAccountAndReturnCredentialForAccount()\n        {\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = \"https\",\n                    [\"host\"]     = \"github.com\",\n                    [\"wwwauth\"]  = \"Basic realm=\\\"GitHub\\\" domain_hint=\\\"contoso\\\"\",\n                }\n            );\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(\"https://github.com\", \"alice\", \"letmein123\");\n            context.CredentialStore.Add(\"https://github.com\", \"bob_contoso\", \"secret123\");\n            context.CredentialStore.Add(\"https://github.com\", \"john_contoso\", \"who_knows\");\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n\n            ghAuthMock.Setup(x => x.SelectAccountAsync(It.IsAny<Uri>(), It.IsAny<IEnumerable<string>>()))\n                .ReturnsAsync(\"john_contoso\");\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(result);\n            Assert.Equal(\"john_contoso\", credential.Account);\n            Assert.Equal(\"who_knows\", credential.Password);\n\n            ghAuthMock.Verify(x => x.SelectAccountAsync(\n                    new Uri(\"https://github.com\"), new[] { \"bob_contoso\", \"john_contoso\" }),\n                Times.Once\n            );\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GetCredentialAsync_MultipleDomainAccounts_PromptForAccountNewAccount()\n        {\n            var input = new InputArguments(\n                new Dictionary<string, string>\n                {\n                    [\"protocol\"] = \"https\",\n                    [\"host\"]     = \"github.com\",\n                    [\"wwwauth\"]  = \"Basic realm=\\\"GitHub\\\" domain_hint=\\\"contoso\\\"\",\n                }\n            );\n\n            var newCredential = new GitCredential(\"alice\", \"password\");\n\n            var context = new TestCommandContext();\n            context.CredentialStore.Add(\"https://github.com\", \"alice\", \"letmein123\");\n            context.CredentialStore.Add(\"https://github.com\", \"bob_contoso\", \"secret123\");\n            context.CredentialStore.Add(\"https://github.com\", \"john_contoso\", \"who_knows\");\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n\n            ghAuthMock.Setup(x => x.SelectAccountAsync(It.IsAny<Uri>(), It.IsAny<IEnumerable<string>>()))\n                .ReturnsAsync((string)null);\n\n            ghAuthMock.Setup(x => x.GetAuthenticationAsync(\n                    It.IsAny<Uri>(), It.IsAny<string>(), It.IsAny<AuthenticationModes>()))\n                .ReturnsAsync(new AuthenticationPromptResult(AuthenticationModes.Pat, newCredential));\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.Equal(newCredential.Account, credential.Account);\n            Assert.Equal(newCredential.Password, credential.Password);\n\n            ghAuthMock.Verify(x => x.GetAuthenticationAsync(\n                    new Uri(\"https://github.com\"), null, It.IsAny<AuthenticationModes>()),\n                Times.Once);\n            ghAuthMock.Verify(x => x.SelectAccountAsync(\n                    new Uri(\"https://github.com\"), new[] { \"bob_contoso\", \"john_contoso\" }),\n                Times.Once\n            );\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GenerateCredentialAsync_UnencryptedHttp_ThrowsException()\n        {\n            var remoteUri = new Uri(\"http://github.com\");\n\n            var context = new TestCommandContext();\n            var ghApi = Mock.Of<IGitHubRestApi>();\n            var ghAuth = Mock.Of<IGitHubAuthentication>();\n\n            var provider = new GitHubHostProvider(context, ghApi, ghAuth);\n\n            await Assert.ThrowsAsync<Trace2Exception>(() => provider.GenerateCredentialAsync(remoteUri, null));\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GenerateCredentialAsync_Browser_ReturnsCredential()\n        {\n            var remoteUri = new Uri(\"https://github.com\");\n            var expectedTargetUri = new Uri(\"https://github.com/\");\n            IEnumerable<string> expectedOAuthScopes = new[]\n            {\n                GitHubConstants.OAuthScopes.Repo,\n                GitHubConstants.OAuthScopes.Gist,\n                GitHubConstants.OAuthScopes.Workflow,\n            };\n\n            var expectedUserName = \"john.doe\";\n            var tokenValue = \"OAUTH-TOKEN\";\n            var response = new OAuth2TokenResult(tokenValue, \"bearer\");\n\n            var context = new TestCommandContext();\n\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n            ghAuthMock.Setup(x => x.GetAuthenticationAsync(expectedTargetUri, null, It.IsAny<AuthenticationModes>()))\n                      .ReturnsAsync(new AuthenticationPromptResult(AuthenticationModes.Browser));\n\n            ghAuthMock.Setup(x => x.GetOAuthTokenViaBrowserAsync(expectedTargetUri, It.IsAny<IEnumerable<string>>(), It.IsAny<string>()))\n                      .ReturnsAsync(response);\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            ghApiMock.Setup(x => x.GetUserInfoAsync(expectedTargetUri, tokenValue))\n                     .ReturnsAsync(new GitHubUserInfo{Login = expectedUserName});\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            ICredential credential = await provider.GenerateCredentialAsync(remoteUri, null);\n\n            Assert.NotNull(credential);\n            Assert.Equal(expectedUserName, credential.Account);\n            Assert.Equal(tokenValue, credential.Password);\n\n            ghAuthMock.Verify(\n                x => x.GetOAuthTokenViaBrowserAsync(\n                    expectedTargetUri, expectedOAuthScopes, null),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GenerateCredentialAsync_Browser_LoginHint_IncludesHintAndReturnsCredential()\n        {\n            var expectedTargetUri = new Uri(\"https://github.com/\");\n            IEnumerable<string> expectedOAuthScopes = new[]\n            {\n                GitHubConstants.OAuthScopes.Repo,\n                GitHubConstants.OAuthScopes.Gist,\n                GitHubConstants.OAuthScopes.Workflow,\n            };\n\n            var expectedUserName = \"john.doe\";\n            var tokenValue = \"OAUTH-TOKEN\";\n            var response = new OAuth2TokenResult(tokenValue, \"bearer\");\n\n            var context = new TestCommandContext();\n\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n            ghAuthMock.Setup(x => x.GetAuthenticationAsync(expectedTargetUri, expectedUserName, It.IsAny<AuthenticationModes>()))\n                      .ReturnsAsync(new AuthenticationPromptResult(AuthenticationModes.Browser));\n\n            ghAuthMock.Setup(x => x.GetOAuthTokenViaBrowserAsync(expectedTargetUri, It.IsAny<IEnumerable<string>>(), It.IsAny<string>()))\n                      .ReturnsAsync(response);\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            ghApiMock.Setup(x => x.GetUserInfoAsync(expectedTargetUri, tokenValue))\n                     .ReturnsAsync(new GitHubUserInfo{Login = expectedUserName});\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            ICredential credential = await provider.GenerateCredentialAsync(expectedTargetUri, expectedUserName);\n\n            Assert.NotNull(credential);\n            Assert.Equal(expectedUserName, credential.Account);\n            Assert.Equal(tokenValue, credential.Password);\n\n            ghAuthMock.Verify(\n                x => x.GetOAuthTokenViaBrowserAsync(\n                    expectedTargetUri, expectedOAuthScopes, expectedUserName),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GenerateCredentialAsync_Basic_1FAOnly_ReturnsCredential()\n        {\n            var remoteUri = new Uri(\"https://github.com\");\n            var expectedTargetUri = new Uri(\"https://github.com/\");\n            var expectedUserName = \"john.doe\";\n            var expectedPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            IEnumerable<string> expectedPatScopes = new[]\n            {\n                GitHubConstants.TokenScopes.Gist,\n                GitHubConstants.TokenScopes.Repo,\n            };\n\n            var patValue = \"PERSONAL-ACCESS-TOKEN\";\n            var response = new AuthenticationResult(GitHubAuthenticationResultType.Success, patValue);\n\n            var context = new TestCommandContext();\n\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n            ghAuthMock.Setup(x => x.GetAuthenticationAsync(expectedTargetUri, null, It.IsAny<AuthenticationModes>()))\n                      .ReturnsAsync(new AuthenticationPromptResult(\n                          AuthenticationModes.Basic, new GitCredential(expectedUserName, expectedPassword)));\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            ghApiMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedTargetUri, expectedUserName, expectedPassword, null, It.IsAny<IEnumerable<string>>()))\n                     .ReturnsAsync(response);\n            ghApiMock.Setup(x => x.GetUserInfoAsync(expectedTargetUri, patValue))\n                     .ReturnsAsync(new GitHubUserInfo{Login = expectedUserName});\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            ICredential credential = await provider.GenerateCredentialAsync(remoteUri, null);\n\n            Assert.NotNull(credential);\n            Assert.Equal(expectedUserName, credential.Account);\n            Assert.Equal(patValue, credential.Password);\n\n            ghApiMock.Verify(\n                x => x.CreatePersonalAccessTokenAsync(\n                    expectedTargetUri, expectedUserName, expectedPassword, null, expectedPatScopes),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task GitHubHostProvider_GenerateCredentialAsync_Basic_2FARequired_ReturnsCredential()\n        {\n            var remoteUri = new Uri(\"https://github.com\");\n            var expectedTargetUri = new Uri(\"https://github.com/\");\n            var expectedUserName = \"john.doe\";\n            var expectedPassword = \"letmein123\";  // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            var expectedAuthCode = \"123456\";\n            IEnumerable<string> expectedPatScopes = new[]\n            {\n                GitHubConstants.TokenScopes.Gist,\n                GitHubConstants.TokenScopes.Repo,\n            };\n\n            var patValue = \"PERSONAL-ACCESS-TOKEN\";\n            var response1 = new AuthenticationResult(GitHubAuthenticationResultType.TwoFactorApp);\n            var response2 = new AuthenticationResult(GitHubAuthenticationResultType.Success, patValue);\n\n            var context = new TestCommandContext();\n\n            var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);\n            ghAuthMock.Setup(x => x.GetAuthenticationAsync(expectedTargetUri, null, It.IsAny<AuthenticationModes>()))\n                      .ReturnsAsync(new AuthenticationPromptResult(\n                          AuthenticationModes.Basic, new GitCredential(expectedUserName, expectedPassword)));\n            ghAuthMock.Setup(x => x.GetTwoFactorCodeAsync(expectedTargetUri, false))\n                      .ReturnsAsync(expectedAuthCode);\n\n            var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);\n            ghApiMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedTargetUri, expectedUserName, expectedPassword, null, It.IsAny<IEnumerable<string>>()))\n                        .ReturnsAsync(response1);\n            ghApiMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedTargetUri, expectedUserName, expectedPassword, expectedAuthCode, It.IsAny<IEnumerable<string>>()))\n                        .ReturnsAsync(response2);\n            ghApiMock.Setup(x => x.GetUserInfoAsync(expectedTargetUri, patValue))\n                     .ReturnsAsync(new GitHubUserInfo{Login = expectedUserName});\n\n            var provider = new GitHubHostProvider(context, ghApiMock.Object, ghAuthMock.Object);\n\n            ICredential credential = await provider.GenerateCredentialAsync(remoteUri, null);\n\n            Assert.NotNull(credential);\n            Assert.Equal(expectedUserName, credential.Account);\n            Assert.Equal(patValue, credential.Password);\n\n            ghApiMock.Verify(\n                x => x.CreatePersonalAccessTokenAsync(\n                    expectedTargetUri, expectedUserName, expectedPassword, null, expectedPatScopes),\n                Times.Once);\n            ghApiMock.Verify(\n                x => x.CreatePersonalAccessTokenAsync(\n                    expectedTargetUri, expectedUserName, expectedPassword, expectedAuthCode, expectedPatScopes),\n                Times.Once);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub.Tests/GitHubRestApiTests.cs",
    "content": "using System;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Tests;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitHub.Tests\n{\n    public class GitHubRestApiTests\n    {\n        [Theory]\n        [InlineData(\"https://github.com\", \"user\", \"https://api.github.com/user\")]\n        [InlineData(\"https://github.com\", \"users/123\", \"https://api.github.com/users/123\")]\n        [InlineData(\"https://gItHuB.cOm\", \"uSeRs/123\", \"https://api.github.com/uSeRs/123\")]\n        [InlineData(\"https://gist.github.com\", \"user\", \"https://api.github.com/user\")]\n        [InlineData(\"https://github.example.com\", \"user\", \"https://github.example.com/api/v3/user\")]\n        [InlineData(\"https://raw.github.example.com\", \"user\", \"https://github.example.com/api/v3/user\")]\n        [InlineData(\"https://gist.github.example.com\", \"user\", \"https://github.example.com/api/v3/user\")]\n        public void GitHubRestApi_GetApiRequestUri(string targetUrl, string apiUrl, string expected)\n        {\n            Uri actualUri = GitHubRestApi.GetApiRequestUri(new Uri(targetUrl), apiUrl);\n            Assert.Equal(expected, actualUri.ToString());\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_NullUri_ThrowsException()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var api = new GitHubRestApi(context);\n\n            await Assert.ThrowsAsync<ArgumentNullException>(\n                () => api.CreatePersonalAccessTokenAsync(null, testUserName, testPassword, testAuthCode, testScopes)\n            );\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_NoNetwork_ThrowsException()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n\n            var httpHandler = new TestHttpMessageHandler {SimulateNoNetwork = true};\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            await Assert.ThrowsAsync<HttpRequestException>(\n                () => api.CreatePersonalAccessTokenAsync(uri, testUserName, testPassword, testAuthCode, testScopes)\n            );\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_ValidRequestOK_ReturnsToken()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            const string expectedTokenValue = \"GITHUB_TOKEN_VALUE\";\n            string tokenResponseJson = $\"{{ \\\"token\\\": \\\"{expectedTokenValue}\\\" }}\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new StringContent(tokenResponseJson)\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, testAuthCode);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, testAuthCode, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Success, authResult.Type);\n            Assert.NotNull(authResult.Token);\n            Assert.Equal(expectedTokenValue, authResult.Token);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_ValidRequestOKBadJson_ReturnsFailure()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            const string tokenResponseJson = \"ThisIsBadJSON\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new StringContent(tokenResponseJson)\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, testAuthCode);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, testAuthCode, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Failure, authResult.Type);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_ValidRequestCreated_ReturnsToken()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            const string expectedTokenValue = \"GITHUB_TOKEN_VALUE\";\n            string tokenResponseJson = $\"{{ \\\"token\\\": \\\"{expectedTokenValue}\\\" }}\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Created)\n            {\n                Content = new StringContent(tokenResponseJson)\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, testAuthCode);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, testAuthCode, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Success, authResult.Type);\n            Assert.NotNull(authResult.Token);\n            Assert.Equal(expectedTokenValue, authResult.Token);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_Valid1FANoAppAuthCode_ReturnsApp2FARequired()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            httpResponse.Headers.Add(GitHubConstants.GitHubOptHeader, \"app\");\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, null);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, null, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.TwoFactorApp, authResult.Type);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_Valid1FANoSmsAuthCode_ReturnsSms2FARequired()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            httpResponse.Headers.Add(GitHubConstants.GitHubOptHeader, \"sms\");\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, null);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, null, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.TwoFactorSms, authResult.Type);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_ValidOAuthToken_ReturnsOAuthToken()\n        {\n            const string testUserName = \"john.doe\";\n            const string testOAuthToken = \"TestOAuthToken\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Forbidden)\n            {\n                Content = new StringContent(\"This API can only be accessed with username and password Basic Auth\")\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testOAuthToken);\n                AssertAuthCode(request, null);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testOAuthToken, null, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Success, authResult.Type);\n            Assert.NotNull(authResult.Token);\n            Assert.Equal(testOAuthToken, authResult.Token);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_Unauthorized_ReturnsFailure()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized)\n            {\n                Content = new StringContent(string.Empty)\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, testAuthCode);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, testAuthCode, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Failure, authResult.Type);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_Forbidden_ReturnsFailure()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Forbidden)\n            {\n                Content = new StringContent(string.Empty)\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, testAuthCode);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, testAuthCode, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Failure, authResult.Type);\n        }\n\n        [Fact]\n        public async Task GitHubRestApi_AcquireTokenAsync_UnknownResponse_ReturnsFailure()\n        {\n            const string testUserName = \"john.doe\";\n            const string testPassword = \"letmein123\"; // [SuppressMessage(\"Microsoft.Security\", \"CS001:SecretInline\", Justification=\"Fake credential\")]\n            const string testAuthCode = \"1234\";\n            string[] testScopes = { \"scope1\", \"scope2\" };\n\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://github.com\");\n\n            var expectedRequestUri = new Uri(\"https://api.github.com/authorizations\");\n\n            // https://www.rfc-editor.org/rfc/rfc2324#section-2.3.2\n            const HttpStatusCode httpIAmATeaPot = (HttpStatusCode) 418;\n            var httpResponse = new HttpResponseMessage(httpIAmATeaPot)\n            {\n                Content = new StringContent(\"I am a tea pot (short and stout).\")\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Post, expectedRequestUri, request =>\n            {\n                RestTestUtilities.AssertBasicAuth(request, testUserName, testPassword);\n                AssertAuthCode(request, testAuthCode);\n                return httpResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new GitHubRestApi(context);\n\n            AuthenticationResult authResult = await api.CreatePersonalAccessTokenAsync(\n                uri, testUserName, testPassword, testAuthCode, testScopes);\n\n            Assert.Equal(GitHubAuthenticationResultType.Failure, authResult.Type);\n        }\n\n        #region Helpers\n\n        private void AssertAuthCode(HttpRequestMessage request, string authCode)\n        {\n            if (authCode is null)\n            {\n                Assert.False(request.Headers.Contains(GitHubConstants.GitHubOptHeader));\n            }\n            else\n            {\n                Assert.True(request.Headers.TryGetValues(GitHubConstants.GitHubOptHeader, out var values));\n\n                string actualAuthCode = values.Single();\n                Assert.Equal(authCode, actualAuthCode);\n            }\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/GitHub.UI.Avalonia/Commands/SelectAccountCommandImpl.cs",
    "content": "using System.Threading;\nusing System.Threading.Tasks;\nusing GitHub.UI.ViewModels;\nusing GitHub.UI.Views;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace GitHub.UI.Commands\n{\n    public class SelectAccountCommandImpl : SelectAccountCommand\n    {\n        public SelectAccountCommandImpl(ICommandContext context) : base(context) { }\n\n        protected override Task ShowAsync(SelectAccountViewModel viewModel, CancellationToken ct)\n        {\n            return AvaloniaUi.ShowViewAsync<SelectAccountView>(viewModel, GetParentHandle(), ct);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/GitLab.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"'$(OSPlatform)'=='windows'\">net8.0;net472</TargetFrameworks>\n    <AssemblyName>GitLab</AssemblyName>\n    <RootNamespace>GitLab</RootNamespace>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' == 'net472'\">\n    <Reference Include=\"System.Net.Http\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/GitLab/GitLabAuthentication.cs",
    "content": "using System;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.UI;\nusing GitLab.UI.ViewModels;\nusing GitLab.UI.Views;\n\nnamespace GitLab\n{\n    public interface IGitLabAuthentication : IDisposable\n    {\n        Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes);\n\n        Task<OAuth2TokenResult> GetOAuthTokenViaBrowserAsync(Uri targetUri, IEnumerable<string> scopes);\n\n        Task<OAuth2TokenResult> GetOAuthTokenViaRefresh(Uri targetUri, string refreshToken);\n    }\n\n    public class AuthenticationPromptResult\n    {\n        public AuthenticationPromptResult(AuthenticationModes mode)\n        {\n            AuthenticationMode = mode;\n        }\n\n        public AuthenticationPromptResult(AuthenticationModes mode, ICredential credential)\n            : this(mode)\n        {\n            Credential = credential;\n        }\n\n        public AuthenticationModes AuthenticationMode { get; }\n\n        public ICredential Credential { get; set; }\n    }\n\n    [Flags]\n    public enum AuthenticationModes\n    {\n        None = 0,\n        Basic = 1,\n        Browser = 1 << 1,\n        Pat = 1 << 2,\n\n        All = Basic | Browser | Pat\n    }\n\n    public class GitLabAuthentication : AuthenticationBase, IGitLabAuthentication\n    {\n        public GitLabAuthentication(ICommandContext context)\n            : base(context) { }\n\n        public async Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            // If we cannot start a browser then don't offer the option\n            if (!Context.SessionManager.IsWebBrowserAvailable)\n            {\n                modes = modes & ~AuthenticationModes.Browser;\n            }\n\n            // We need at least one mode!\n            if (modes == AuthenticationModes.None)\n            {\n                throw new ArgumentException(@$\"Must specify at least one {nameof(AuthenticationModes)}\", nameof(modes));\n            }\n\n            ThrowIfUserInteractionDisabled();\n\n            if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)\n            {\n                if (TryFindHelperCommand(out string helperCommand, out string args))\n                {\n                    return await GetAuthenticationViaHelperAsync(targetUri, userName, modes, helperCommand, args);\n                }\n\n                return await GetAuthenticationViaUiAsync(targetUri, userName, modes);\n            }\n\n            return GetAuthenticationViaTty(targetUri, userName, modes);\n        }\n\n        private async Task<AuthenticationPromptResult> GetAuthenticationViaUiAsync(\n            Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            var viewModel = new CredentialsViewModel(Context.SessionManager)\n            {\n                ShowBrowserLogin = (modes & AuthenticationModes.Browser) != 0,\n                ShowTokenLogin   = (modes & AuthenticationModes.Pat) != 0,\n                ShowBasicLogin   = (modes & AuthenticationModes.Basic) != 0,\n            };\n\n            if (!GitLabConstants.IsGitLabDotCom(targetUri))\n            {\n                viewModel.Url = targetUri.ToString();\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                viewModel.UserName = userName;\n                viewModel.TokenUserName = userName;\n            }\n\n            await AvaloniaUi.ShowViewAsync<CredentialsView>(viewModel, GetParentWindowHandle(), CancellationToken.None);\n\n            ThrowIfWindowCancelled(viewModel);\n\n            switch (viewModel.SelectedMode)\n            {\n                case AuthenticationModes.Basic:\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Basic,\n                        new GitCredential(viewModel.UserName, viewModel.Password)\n                    );\n\n                case AuthenticationModes.Browser:\n                    return new AuthenticationPromptResult(AuthenticationModes.Browser);\n\n                case AuthenticationModes.Pat:\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Pat,\n                        new GitCredential(viewModel.TokenUserName, viewModel.Token)\n                    );\n\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n        }\n\n        private AuthenticationPromptResult GetAuthenticationViaTty(Uri targetUri, string userName, AuthenticationModes modes)\n        {\n            ThrowIfTerminalPromptsDisabled();\n\n            switch (modes)\n            {\n                case AuthenticationModes.Basic:\n                    Context.Terminal.WriteLine(\"Enter GitLab credentials for '{0}'...\", targetUri);\n\n                    if (string.IsNullOrWhiteSpace(userName))\n                    {\n                        userName = Context.Terminal.Prompt(\"Username\");\n                    }\n                    else\n                    {\n                        Context.Terminal.WriteLine(\"Username: {0}\", userName);\n                    }\n\n                    string password = Context.Terminal.PromptSecret(\"Password\");\n                    return new AuthenticationPromptResult(AuthenticationModes.Basic, new GitCredential(userName, password));\n\n                case AuthenticationModes.Pat:\n                    Context.Terminal.WriteLine(\"Enter GitLab credentials for '{0}'...\", targetUri);\n\n                    if (string.IsNullOrWhiteSpace(userName))\n                    {\n                        userName = Context.Terminal.Prompt(\"Username\");\n                    }\n                    else\n                    {\n                        Context.Terminal.WriteLine(\"Username: {0}\", userName);\n                    }\n\n                    string token = Context.Terminal.PromptSecret(\"Personal access token\");\n                    return new AuthenticationPromptResult(AuthenticationModes.Pat, new GitCredential(userName, token));\n\n                case AuthenticationModes.Browser:\n                    return new AuthenticationPromptResult(AuthenticationModes.Browser);\n\n                case AuthenticationModes.None:\n                    throw new ArgumentOutOfRangeException(nameof(modes),\n                        @$\"At least one {nameof(AuthenticationModes)} must be supplied\");\n\n                default:\n                    var menuTitle = $\"Select an authentication method for '{targetUri}'\";\n                    var menu = new TerminalMenu(Context.Terminal, menuTitle);\n\n                    TerminalMenuItem browserItem = null;\n                    TerminalMenuItem basicItem = null;\n                    TerminalMenuItem patItem = null;\n\n                    if ((modes & AuthenticationModes.Browser) != 0) browserItem = menu.Add(\"Web browser\");\n                    if ((modes & AuthenticationModes.Pat) != 0) patItem = menu.Add(\"Personal access token\");\n                    if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add(\"Username/password\");\n\n                    // Default to the 'first' choice in the menu\n                    TerminalMenuItem choice = menu.Show(0);\n\n                    if (choice == browserItem) goto case AuthenticationModes.Browser;\n                    if (choice == basicItem) goto case AuthenticationModes.Basic;\n                    if (choice == patItem) goto case AuthenticationModes.Pat;\n\n                    throw new Exception();\n            }\n        }\n\n        private async Task<AuthenticationPromptResult> GetAuthenticationViaHelperAsync(\n            Uri targetUri, string userName, AuthenticationModes modes, string helperCommand, string args)\n        {\n            var promptArgs = new StringBuilder(args);\n            promptArgs.Append(\"prompt\");\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                promptArgs.AppendFormat(\" --username {0}\", QuoteCmdArg(userName));\n            }\n\n            promptArgs.AppendFormat(\" --url {0}\", QuoteCmdArg(targetUri.ToString()));\n\n            if ((modes & AuthenticationModes.Basic) != 0) promptArgs.Append(\" --basic\");\n            if ((modes & AuthenticationModes.Browser) != 0) promptArgs.Append(\" --browser\");\n            if ((modes & AuthenticationModes.Pat) != 0) promptArgs.Append(\" --pat\");\n\n            IDictionary<string, string> resultDict = await InvokeHelperAsync(helperCommand, promptArgs.ToString());\n\n            if (!resultDict.TryGetValue(\"mode\", out string responseMode))\n            {\n                throw new Trace2Exception(Context.Trace2, \"Missing 'mode' in response\");\n            }\n\n            switch (responseMode.ToLowerInvariant())\n            {\n                case \"pat\":\n                    if (!resultDict.TryGetValue(\"pat\", out string pat))\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing 'pat' in response\");\n                    }\n\n                    if (!resultDict.TryGetValue(\"username\", out string patUserName))\n                    {\n                        // Username is optional for PATs\n                    }\n\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Pat, new GitCredential(patUserName, pat));\n\n                case \"browser\":\n                    return new AuthenticationPromptResult(AuthenticationModes.Browser);\n\n                case \"basic\":\n                    if (!resultDict.TryGetValue(\"username\", out userName))\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing 'username' in response\");\n                    }\n\n                    if (!resultDict.TryGetValue(\"password\", out string password))\n                    {\n                        throw new Trace2Exception(Context.Trace2, \"Missing 'password' in response\");\n                    }\n\n                    return new AuthenticationPromptResult(\n                        AuthenticationModes.Basic, new GitCredential(userName, password));\n\n                default:\n                    throw new Trace2Exception(Context.Trace2,\n                        $\"Unknown mode value in response '{responseMode}'\");\n            }\n        }\n\n        public async Task<OAuth2TokenResult> GetOAuthTokenViaBrowserAsync(Uri targetUri, IEnumerable<string> scopes)\n        {\n            ThrowIfUserInteractionDisabled();\n\n            var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2);\n\n            // We require a desktop session to launch the user's default web browser\n            if (!Context.SessionManager.IsDesktopSession)\n            {\n                throw new Trace2InvalidOperationException(Context.Trace2,\n                    \"Browser authentication requires a desktop session\");\n            }\n\n            var browserOptions = new OAuth2WebBrowserOptions { };\n            var browser = new OAuth2SystemWebBrowser(Context.SessionManager, browserOptions);\n\n            // Write message to the terminal (if any is attached) for some feedback that we're waiting for a web response\n            Context.Terminal.WriteLine(\"info: please complete authentication in your browser...\");\n\n            OAuth2AuthorizationCodeResult authCodeResult =\n                await oauthClient.GetAuthorizationCodeAsync(scopes, browser, CancellationToken.None);\n\n            return await oauthClient.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);\n        }\n\n        public async Task<OAuth2TokenResult> GetOAuthTokenViaRefresh(Uri targetUri, string refreshToken)\n        {\n            var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2);\n            return await oauthClient.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None);\n        }\n\n        private bool TryFindHelperCommand(out string command, out string args)\n        {\n            return TryFindHelperCommand(\n                GitLabConstants.EnvironmentVariables.AuthenticationHelper,\n                GitLabConstants.GitConfiguration.Credential.AuthenticationHelper,\n                GitLabConstants.DefaultAuthenticationHelper,\n                out command,\n                out args);\n        }\n\n        private HttpClient _httpClient;\n        private HttpClient HttpClient => _httpClient ?? (_httpClient = Context.HttpClientFactory.CreateClient());\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/GitLabConstants.cs",
    "content": "using System;\n\nnamespace GitLab\n{\n    public static class GitLabConstants\n    {\n        public static readonly Uri GitLabDotCom = new Uri(\"https://gitlab.com\");\n\n        public const string DefaultAuthenticationHelper = \"GitLab.UI\";\n\n        // Owned by https://gitlab.com/gitcredentialmanager\n        public const string OAuthClientId = \"172b9f227872b5dde33f4d9b1db06a6a5515ae79508e7a00c973c85ce490671e\";\n\n        public static readonly Uri OAuthRedirectUri = new Uri(\"http://127.0.0.1/\");\n        // https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow\n        public static readonly Uri OAuthAuthorizationEndpointRelativeUri = new Uri(\"/oauth/authorize\", UriKind.Relative);\n        public static readonly Uri OAuthTokenEndpointRelativeUri = new Uri(\"/oauth/token\", UriKind.Relative);\n\n        public const AuthenticationModes DotComAuthenticationModes = AuthenticationModes.All;\n\n        public static class EnvironmentVariables\n        {\n            public const string DevOAuthClientId = \"GCM_DEV_GITLAB_CLIENTID\";\n            public const string DevOAuthClientSecret = \"GCM_DEV_GITLAB_CLIENTSECRET\";\n            public const string DevOAuthRedirectUri = \"GCM_DEV_GITLAB_REDIRECTURI\";\n            public const string AuthenticationModes = \"GCM_GITLAB_AUTHMODES\";\n            public const string AuthenticationHelper = \"GCM_GITLAB_HELPER\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string AuthenticationModes = \"gitLabAuthModes\";\n                public const string DevOAuthClientId = \"gitLabDevClientId\";\n                public const string DevOAuthClientSecret = \"gitLabDevClientSecret\";\n                public const string DevOAuthRedirectUri = \"gitLabDevRedirectUri\";\n                public const string AuthenticationHelper = \"gitLabHelper\";\n            }\n        }\n\n        public static class HelpUrls\n        {\n            public const string GitLab = \"https://aka.ms/gcm/gitlab\";\n        }\n\n        public static bool IsGitLabDotCom(Uri uri) => StringComparer.OrdinalIgnoreCase.Equals(uri.Host, GitLabDotCom.Host);\n\n        public static bool IsGitLabDotComClientId(string clientId) => StringComparer.OrdinalIgnoreCase.Equals(clientId, OAuthClientId);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/GitLabHostProvider.cs",
    "content": "﻿using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\nusing System.Net.Http.Headers;\nusing System.Linq;\n\nnamespace GitLab\n{\n    public class GitLabHostProvider : HostProvider\n    {\n        // https://docs.gitlab.com/ee/integration/oauth_provider.html#authorized-applications\n        private static readonly string[] GitLabOAuthScopes =\n        {\n            \"write_repository\",\n            \"read_repository\"\n        };\n\n        private readonly IGitLabAuthentication _gitLabAuth;\n\n        public GitLabHostProvider(ICommandContext context)\n            : this(context, new GitLabAuthentication(context)) { }\n\n        public GitLabHostProvider(ICommandContext context, IGitLabAuthentication gitLabAuth)\n            : base(context)\n        {\n            EnsureArgument.NotNull(gitLabAuth, nameof(gitLabAuth));\n\n            _gitLabAuth = gitLabAuth;\n        }\n\n        public override string Id => \"gitlab\";\n\n        public override string Name => \"GitLab\";\n\n        public override bool IsSupported(InputArguments input)\n        {\n            if (input is null)\n            {\n                return false;\n            }\n\n            // We do not support unencrypted HTTP communications to GitLab,\n            // but we report `true` here for HTTP so that we can show a helpful\n            // error message for the user in `CreateCredentialAsync`.\n            if (!StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\") &&\n                !StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"https\"))\n            {\n                return false;\n            }\n\n            if (GitLabConstants.IsGitLabDotCom(input.GetRemoteUri()))\n            {\n                return true;\n            }\n\n            // Split port number and hostname from host input argument\n            if (!input.TryGetHostAndPort(out string hostName, out _))\n            {\n                return false;\n            }\n\n            string[] domains = hostName.Split(new char[] { '.' });\n\n            // GitLab[.subdomain].domain.tld\n            if (domains.Length >= 3 &&\n                StringComparer.OrdinalIgnoreCase.Equals(domains[0], \"gitlab\"))\n            {\n                return true;\n            }\n\n            if (input.WwwAuth.Any(x => x.Contains(\"realm=\\\"GitLab\\\"\")))\n            {\n                return true;\n            }\n\n            return false;\n        }\n\n        public override bool IsSupported(HttpResponseMessage response)\n        {\n            if (response == null)\n            {\n                return false;\n            }\n\n            // as seen at eg. https://salsa.debian.org/apt-team/apt.git\n            // not always present https://gitlab.com/gitlab-org/gitlab/-/issues/349464\n            return response.Headers.Contains(\"X-Gitlab-Feature-Category\");\n        }\n\n        public override async Task<ICredential> GenerateCredentialAsync(InputArguments input)\n        {\n            ThrowIfDisposed();\n\n            // We should not allow unencrypted communication and should inform the user\n            if (!Context.Settings.AllowUnsafeRemotes &&\n                StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\"))\n            {\n                throw new Trace2Exception(Context.Trace2,\n                    \"Unencrypted HTTP is not recommended for GitLab. \" +\n                    \"Ensure the repository remote URL is using HTTPS \" +\n                    $\"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.\");\n            }\n\n            Uri remoteUri = input.GetRemoteUri();\n\n            AuthenticationModes authModes = GetSupportedAuthenticationModes(remoteUri);\n\n            AuthenticationPromptResult promptResult = await _gitLabAuth.GetAuthenticationAsync(remoteUri, input.UserName, authModes);\n\n            switch (promptResult.AuthenticationMode)\n            {\n                case AuthenticationModes.Basic:\n                case AuthenticationModes.Pat:\n                    return promptResult.Credential;\n\n                case AuthenticationModes.Browser:\n                    return await GenerateOAuthCredentialAsync(input);\n\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(promptResult));\n            }\n        }\n\n        internal AuthenticationModes GetSupportedAuthenticationModes(Uri targetUri)\n        {\n            // Check for an explicit override for supported authentication modes\n            if (Context.Settings.TryGetSetting(\n                GitLabConstants.EnvironmentVariables.AuthenticationModes,\n                Constants.GitConfiguration.Credential.SectionName, GitLabConstants.GitConfiguration.Credential.AuthenticationModes,\n                out string authModesStr))\n            {\n                if (Enum.TryParse(authModesStr, true, out AuthenticationModes authModes) && authModes != AuthenticationModes.None)\n                {\n                    Context.Trace.WriteLine($\"Supported authentication modes override present: {authModes}\");\n                    return authModes;\n                }\n                else\n                {\n                    Context.Trace.WriteLine($\"Invalid value for supported authentication modes override setting: '{authModesStr}'\");\n                }\n            }\n\n            // GitLab.com has well-known supported auth modes\n            if (GitLabConstants.IsGitLabDotCom(targetUri))\n            {\n                return GitLabConstants.DotComAuthenticationModes;\n            }\n\n            // Try to detect what auth modes are available for this non-GitLab.com host.\n            // Assume that PATs are always available to give at least one option to users!\n            var modes = AuthenticationModes.Pat;\n\n            // If there is a configured OAuth client ID (that isn't GitLab.com's client ID)\n            // then assume OAuth is possible.\n            string oauthClientId = GitLabOAuth2Client.GetClientId(Context.Settings);\n            if (!GitLabConstants.IsGitLabDotComClientId(oauthClientId))\n            {\n                modes |= AuthenticationModes.Browser;\n            }\n            else\n            {\n                // Tell the user that they may wish to configure OAuth for this GitLab instance\n                Context.Streams.Error.WriteLine(\n                    $\"warning: missing OAuth configuration for {targetUri.Host} - see {GitLabConstants.HelpUrls.GitLab} for more information\");\n            }\n\n            // Would like to query password_authentication_enabled_for_git, but can't unless logged in https://gitlab.com/gitlab-org/gitlab/-/issues/349463.\n            // For now assume password auth is always available.\n            bool supportsBasic = true;\n            if (supportsBasic)\n            {\n                modes |= AuthenticationModes.Basic;\n            }\n\n            return modes;\n        }\n\n        // <remarks>Stores OAuth tokens as a side effect</remarks>\n        public override async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)\n        {\n            string service = GetServiceName(input);\n            ICredential credential = Context.CredentialStore.Get(service, input.UserName);\n            if (credential?.Account == \"oauth2\" && await IsOAuthTokenExpired(input.GetRemoteUri(), credential.Password))\n            {\n                Context.Trace.WriteLine(\"Removing expired OAuth access token...\");\n                Context.CredentialStore.Remove(service, credential.Account);\n                credential = null;\n            }\n\n            if (credential != null)\n            {\n                return new GetCredentialResult(credential);\n            }\n\n            string refreshService = GetRefreshTokenServiceName(input);\n            string refreshToken = Context.CredentialStore.Get(refreshService, input.UserName)?.Password;\n            if (refreshToken != null)\n            {\n                Context.Trace.WriteLine(\"Refreshing OAuth token...\");\n                try\n                {\n                    credential = await RefreshOAuthCredentialAsync(input, refreshToken);\n                }\n                catch (Exception e)\n                {\n                    Context.Terminal.WriteLine($\"OAuth token refresh failed: {e.Message}\");\n                }\n            }\n\n            credential ??= await GenerateCredentialAsync(input);\n\n            if (credential is OAuthCredential oAuthCredential)\n            {\n                Context.Trace.WriteLine(\"Pre-emptively storing OAuth access and refresh tokens...\");\n                // freshly-generated OAuth credential\n                // store credential, since we know it to be valid (whereas Git will only store credential if git push succeeds)\n                Context.CredentialStore.AddOrUpdate(service, oAuthCredential.Account, oAuthCredential.AccessToken);\n                // store refresh token under a separate service\n                Context.CredentialStore.AddOrUpdate(refreshService, oAuthCredential.Account, oAuthCredential.RefreshToken);\n            }\n            return new GetCredentialResult(credential);\n        }\n\n        private async Task<bool> IsOAuthTokenExpired(Uri baseUri, string accessToken)\n        {\n            // https://docs.gitlab.com/ee/api/oauth2.html#retrieve-the-token-information\n            Uri infoUri = new Uri(baseUri, \"/oauth/token/info\");\n            using (HttpClient httpClient = Context.HttpClientFactory.CreateClient())\n            {\n                httpClient.Timeout = TimeSpan.FromSeconds(15);\n                httpClient.DefaultRequestHeaders.Authorization\n                         = new AuthenticationHeaderValue(\"Bearer\", accessToken);\n                try\n                {\n                    HttpResponseMessage response = await httpClient.GetAsync(infoUri);\n                    return response.StatusCode == System.Net.HttpStatusCode.Unauthorized;\n                }\n                catch (Exception e)\n                {\n                    Context.Terminal.WriteLine($\"OAuth token info request failed: {e.Message}\");\n                    return false;\n                }\n            }\n        }\n\n        internal class OAuthCredential : ICredential\n        {\n            public OAuthCredential(OAuth2TokenResult oAuth2TokenResult)\n            {\n                AccessToken = oAuth2TokenResult.AccessToken;\n                RefreshToken = oAuth2TokenResult.RefreshToken;\n            }\n\n            // username must be 'oauth2' https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token\n            public string Account => \"oauth2\";\n            public string AccessToken { get; }\n            public string RefreshToken { get; }\n            string ICredential.Password => AccessToken;\n        }\n\n        private async Task<OAuthCredential> GenerateOAuthCredentialAsync(InputArguments input)\n        {\n            OAuth2TokenResult result = await _gitLabAuth.GetOAuthTokenViaBrowserAsync(input.GetRemoteUri(), GitLabOAuthScopes);\n            return new OAuthCredential(result);\n        }\n\n        private async Task<OAuthCredential> RefreshOAuthCredentialAsync(InputArguments input, string refreshToken)\n        {\n            OAuth2TokenResult result = await _gitLabAuth.GetOAuthTokenViaRefresh(input.GetRemoteUri(), refreshToken);\n            return new OAuthCredential(result);\n        }\n\n        protected override void ReleaseManagedResources()\n        {\n            _gitLabAuth.Dispose();\n            base.ReleaseManagedResources();\n        }\n\n        private string GetRefreshTokenServiceName(InputArguments input)\n        {\n            var builder = new UriBuilder(GetServiceName(input));\n            builder.Host = \"oauth-refresh-token.\" + builder.Host;\n            return builder.Uri.ToString();\n        }\n\n        public override Task EraseCredentialAsync(InputArguments input)\n        {\n            // delete any refresh token too\n            Context.CredentialStore.Remove(GetRefreshTokenServiceName(input), \"oauth2\");\n            return base.EraseCredentialAsync(input);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/GitLabOAuth2Client.cs",
    "content": "using System;\nusing System.Net.Http;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace GitLab\n{\n    public class GitLabOAuth2Client : OAuth2Client\n    {\n        public GitLabOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri, ITrace2 trace2)\n            : base(httpClient, CreateEndpoints(baseUri),\n                GetClientId(settings), trace2, GetRedirectUri(settings), GetClientSecret(settings))\n        { }\n\n        private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri)\n        {\n            Uri authEndpoint = new Uri(baseUri, GitLabConstants.OAuthAuthorizationEndpointRelativeUri);\n            Uri tokenEndpoint = new Uri(baseUri, GitLabConstants.OAuthTokenEndpointRelativeUri);\n\n            return new OAuth2ServerEndpoints(authEndpoint, tokenEndpoint);\n        }\n\n        private static Uri GetRedirectUri(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                GitLabConstants.EnvironmentVariables.DevOAuthRedirectUri,\n                Constants.GitConfiguration.Credential.SectionName, GitLabConstants.GitConfiguration.Credential.DevOAuthRedirectUri,\n                out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))\n            {\n                return redirectUri;\n            }\n\n            return GitLabConstants.OAuthRedirectUri;\n        }\n\n        internal static string GetClientId(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                GitLabConstants.EnvironmentVariables.DevOAuthClientId,\n                Constants.GitConfiguration.Credential.SectionName, GitLabConstants.GitConfiguration.Credential.DevOAuthClientId,\n                out string clientId))\n            {\n                return clientId;\n            }\n\n            return GitLabConstants.OAuthClientId;\n        }\n\n        private static string GetClientSecret(ISettings settings)\n        {\n            // Check for developer override value\n            if (settings.TryGetSetting(\n                GitLabConstants.EnvironmentVariables.DevOAuthClientSecret,\n                Constants.GitConfiguration.Credential.SectionName, GitLabConstants.GitConfiguration.Credential.DevOAuthClientSecret,\n                out string clientSecret))\n            {\n                return clientSecret;\n            }\n\n            // no secret necessary\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/InternalsVisibleTo.cs",
    "content": "using System.Runtime.CompilerServices;\n\n[assembly: InternalsVisibleTo(\"GitLab.Tests\")]\n"
  },
  {
    "path": "src/shared/GitLab/UI/Assets/Images.axaml",
    "content": "<ResourceDictionary xmlns=\"https://github.com/avaloniaui\"\n                    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <DrawingImage x:Key=\"GitLabLogo\">\n        <DrawingGroup>\n            <GeometryDrawing Brush=\"#E24329\" Geometry=\"M442.097,243.57h-87.12l37.425-115.224c1.919-5.895,10.282-5.895,12.27,0L442.097,243.57L442.097,243.57zM292.778,434.892L292.778,434.892l62.199-191.322H230.669L292.778,434.892L292.778,434.892zM143.549,243.57h87.12l-37.494-115.224c-1.919-5.895-10.282-5.895-12.27,0L143.549,243.57L143.549,243.57z\"/>\n            <GeometryDrawing Brush=\"#FCA326\" Geometry=\"M442.097,243.57L442.097,243.57l18.873,58.126c1.714,5.278-0.137,11.104-4.661,14.394L292.778,434.892L442.097,243.57L442.097,243.57zM143.549,243.57L143.549,243.57l-18.941,58.126c-1.714,5.278,0.137,11.104,4.661,14.394l163.509,118.801L143.549,243.57L143.549,243.57z\"/>\n            <GeometryDrawing Brush=\"#FC6D26\">\n                <GeometryDrawing.Geometry>\n                    <CombinedGeometry GeometryCombineMode=\"Union\">\n                        <CombinedGeometry.Geometry1>\n                            <PolylineGeometry IsFilled=\"True\">\n                                <PolylineGeometry.Points>\n                                    <Point>292.778,434.892</Point>\n                                    <Point>354.977,243.57</Point>\n                                    <Point>442.097,243.57</Point>\n                                </PolylineGeometry.Points>\n                            </PolylineGeometry>\n                        </CombinedGeometry.Geometry1>\n                        <CombinedGeometry.Geometry2>\n                            <PolylineGeometry IsFilled=\"True\">\n                                <PolylineGeometry.Points>\n                                    <Point>292.778,434.892</Point>\n                                    <Point>143.549,243.57</Point>\n                                    <Point>230.669,243.57</Point>\n                                </PolylineGeometry.Points>\n                            </PolylineGeometry>\n                        </CombinedGeometry.Geometry2>\n                    </CombinedGeometry>\n                </GeometryDrawing.Geometry>\n            </GeometryDrawing>\n        </DrawingGroup>\n    </DrawingImage>\n    <DrawingImage x:Key=\"GitLabLogoType\">\n        <GeometryDrawing Brush=\"#8C929D\">\n            <GeometryGroup>\n                <PathGeometry>\n                    <PathGeometry.Transform>\n                        <TranslateTransform X=\"977.327440\" Y=\"143.286396\"/>\n                    </PathGeometry.Transform>\n                    M13,188.892c-5.5,5.7-14.6,11.4-27,11.4c-16.6,0-23.3-8.2-23.3-18.9\n                    c0-16.1,11.2-23.8,35-23.8c4.5,0,11.7,0.5,15.4,1.2v30.1H13z M-9.6,90.392c-17.6,0-33.8,6.2-46.4,16.7l7.7,13.4\n                    c8.9-5.2,19.8-10.4,35.5-10.4c17.9,0,25.8,9.2,25.8,24.6v7.9c-3.5-0.7-10.7-1.2-15.1-1.2c-38.2,0-57.6,13.4-57.6,41.4\n                    c0,25.1,15.4,37.7,38.7,37.7c15.7,0,30.8-7.2,36-18.9l4,15.9h15.4v-83.2C34.3,107.992,22.9,90.392-9.6,90.392L-9.6,90.392z\n                </PathGeometry>\n                <PathGeometry>\n                    <PathGeometry.Transform>\n                        <TranslateTransform X=\"1099.766904\" Y=\"143.128930\"/>\n                    </PathGeometry.Transform>\n                    M-17.7,201.192c-8.2,0-15.4-1-20.8-3.5v-67.3v-7.8c7.4-6.2,16.6-10.7,28.3-10.7\n                    c21.1,0,29.2,14.9,29.2,39C19,185.092,5.9,201.192-17.7,201.192 M-8.5,90.592c-19.5,0-30,13.3-30,13.3v-21l-0.1-27.8h-9.8h-11.5\n                    l0.1,158.5c10.7,4.5,25.3,6.9,41.2,6.9c40.7,0,60.3-26,60.3-70.9C41.6,114.092,23.5,90.592-8.5,90.592\n                </PathGeometry>\n                <PathGeometry>\n                    <PathGeometry.Transform>\n                        <TranslateTransform X=\"584.042117\" Y=\"143.630796\"/>\n                    </PathGeometry.Transform>\n                    M18.3,72.192c19.3,0,31.8,6.4,39.9,12.9l9.4-16.3c-12.7-11.2-29.9-17.2-48.3-17.2\n                    c-46.4,0-78.9,28.3-78.9,85.4c0,59.8,35.1,83.1,75.2,83.1c20.1,0,37.2-4.7,48.4-9.4l-0.5-63.9v-7.5v-12.6H4v20.1h38l0.5,48.5\n                    c-5,2.5-13.6,4.5-25.3,4.5c-32.2,0-53.8-20.3-53.8-63C-36.7,93.292-14.4,72.192,18.3,72.192\n                </PathGeometry>\n                <PathGeometry>\n                    <PathGeometry.Transform>\n                        <TranslateTransform X=\"793.569045\" Y=\"142.577463\"/>\n                    </PathGeometry.Transform>\n                    M-37.7,55.592H-59l0.1,27.3v11.2v6.5v11.4v65v0.2c0,26.3,11.4,43.9,43.9,43.9\n                    c4.5,0,8.9-0.4,13.1-1.2v-19.1c-3.1,0.5-6.4,0.7-9.9,0.7c-17.9,0-25.8-9.2-25.8-24.6v-65h35.7v-17.8h-35.7L-37.7,55.592\n                    L-37.7,55.592z\n                </PathGeometry>\n                <PathGeometry>\n                    M839.7,198.192h-21.8l0.1,162.5h88.3v-20.1h-66.5L839.7,198.192L839.7,198.192z\n                    M680.4,360.692h21.3v-124h-21.3V360.692L680.4,360.692z\n                    M680.4,219.592h21.3v-21.3h-21.3V219.592L680.4,219.592z\n                </PathGeometry>\n            </GeometryGroup>\n        </GeometryDrawing>\n    </DrawingImage>\n</ResourceDictionary>\n"
  },
  {
    "path": "src/shared/GitLab/UI/Commands/CredentialsCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitLab.UI.ViewModels;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\n\nnamespace GitLab.UI.Commands\n{\n    public abstract class CredentialsCommand : HelperCommand\n    {\n        protected CredentialsCommand(ICommandContext context)\n            : base(context, \"prompt\", \"Show authentication prompt.\")\n        {\n            var url = new Option<string>(\"--url\", \"GitLab instance URL.\");\n            AddOption(url);\n\n            var userName = new Option<string>(\"--username\", \"Username or email.\");\n            AddOption(userName);\n\n            var basic = new Option<bool>(\"--basic\", \"Enable username/password (basic) authentication.\");\n            AddOption(basic);\n\n            var browser = new Option<bool>(\"--browser\", \"Enable browser-based OAuth authentication.\");\n            AddOption(browser);\n\n            var pat = new Option<bool>(\"--pat\", \"Enable personal access token authentication.\");\n            AddOption(pat);\n\n            var all = new Option<bool>(\"--all\", \"Enable all available authentication options.\");\n            AddOption(all);\n\n            this.SetHandler(ExecuteAsync, url, userName, basic, browser, pat, all);\n        }\n\n        private async Task<int> ExecuteAsync(string userName, string url, bool basic, bool browser, bool pat, bool all)\n        {\n            var viewModel = new CredentialsViewModel(Context.SessionManager)\n            {\n                ShowBrowserLogin = all || browser,\n                ShowTokenLogin   = all || pat,\n                ShowBasicLogin   = all || basic,\n            };\n\n            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri) && !GitLabConstants.IsGitLabDotCom(uri))\n            {\n                viewModel.Url = url;\n            }\n\n            if (!string.IsNullOrWhiteSpace(userName))\n            {\n                viewModel.UserName = userName;\n                viewModel.TokenUserName = userName;\n            }\n\n            await ShowAsync(viewModel, CancellationToken.None);\n\n            if (!viewModel.WindowResult)\n            {\n                throw new Trace2Exception(Context.Trace2, \"User cancelled dialog.\");\n            }\n\n            var result = new Dictionary<string, string>();\n\n            switch (viewModel.SelectedMode)\n            {\n                case AuthenticationModes.Basic:\n                    result[\"mode\"] = \"basic\";\n                    result[\"username\"] = viewModel.UserName;\n                    result[\"password\"] = viewModel.Password;\n                    break;\n\n                case AuthenticationModes.Browser:\n                    result[\"mode\"] = \"browser\";\n                    break;\n\n                case AuthenticationModes.Pat:\n                    result[\"mode\"] = \"pat\";\n                    result[\"pat\"] = viewModel.Token;\n                    result[\"username\"] = viewModel.TokenUserName;\n                    break;\n\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n\n            WriteResult(result);\n            return 0;\n        }\n\n        protected abstract Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct);\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/UI/ViewModels/CredentialsViewModel.cs",
    "content": "using System.ComponentModel;\nusing System.Windows.Input;\nusing GitCredentialManager;\nusing GitCredentialManager.UI;\nusing GitCredentialManager.UI.ViewModels;\n\nnamespace GitLab.UI.ViewModels\n{\n    public class CredentialsViewModel : WindowViewModel\n    {\n        private readonly ISessionManager _sessionManager;\n\n        private string _url;\n        private string _token;\n        private string _tokenUserName;\n        private string _userName;\n        private string _password;\n        private bool _showBrowserLogin;\n        private bool _showTokenLogin;\n        private bool _showBasicLogin;\n        private ICommand _signUpCommand;\n        private ICommand _signInBrowserCommand;\n        private RelayCommand _signInBasicCommand;\n        private RelayCommand _signInTokenCommand;\n\n        public CredentialsViewModel()\n        {\n            // Constructor the XAML designer\n        }\n\n        public CredentialsViewModel(ISessionManager sessionManager)\n        {\n            EnsureArgument.NotNull(sessionManager, nameof(sessionManager));\n\n            _sessionManager = sessionManager;\n\n            Title = \"Connect to GitLab\";\n            SignUpCommand = new RelayCommand(SignUp);\n            SignInBrowserCommand = new RelayCommand(SignInBrowser);\n            SignInTokenCommand = new RelayCommand(SignInToken, CanSignInToken);\n            SignInBasicCommand = new RelayCommand(SignInBasic, CanSignInBasic);\n\n            PropertyChanged += OnPropertyChanged;\n        }\n\n        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)\n        {\n            switch (e.PropertyName)\n            {\n                case nameof(UserName):\n                case nameof(Password):\n                    SignInBasicCommand.RaiseCanExecuteChanged();\n                    break;\n\n                case nameof(Token):\n                    SignInTokenCommand.RaiseCanExecuteChanged();\n                    break;\n            }\n        }\n\n        private void SignUp()\n        {\n            _sessionManager.OpenBrowser(\"https://about.gitlab.com/\");\n        }\n\n        private void SignInBrowser()\n        {\n            SelectedMode = AuthenticationModes.Browser;\n            Accept();\n        }\n\n        private bool CanSignInToken()\n        {\n            return !string.IsNullOrWhiteSpace(Token);\n        }\n\n        private void SignInToken()\n        {\n            SelectedMode = AuthenticationModes.Pat;\n            Accept();\n        }\n\n        private bool CanSignInBasic()\n        {\n            return !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrEmpty(Password);\n        }\n\n        private void SignInBasic()\n        {\n            SelectedMode = AuthenticationModes.Basic;\n            Accept();\n        }\n\n        public string Token\n        {\n            get => _token;\n            set => SetAndRaisePropertyChanged(ref _token, value);\n        }\n\n        public string TokenUserName\n        {\n            get => _tokenUserName;\n            set => SetAndRaisePropertyChanged(ref _tokenUserName, value);\n        }\n\n        public string UserName\n        {\n            get => _userName;\n            set => SetAndRaisePropertyChanged(ref _userName, value);\n        }\n\n        public string Password\n        {\n            get => _password;\n            set => SetAndRaisePropertyChanged(ref _password, value);\n        }\n\n        public string Url\n        {\n            get => _url;\n            set => SetAndRaisePropertyChanged(ref _url, value);\n        }\n\n        public string OAuthModeTitle\n        {\n            get\n            {\n                if (ShowBrowserLogin) return \"Browser\";\n                return \"OAuth\";\n            }\n        }\n\n        public bool ShowBrowserLogin\n        {\n            get => _showBrowserLogin;\n            set\n            {\n                SetAndRaisePropertyChanged(ref _showBrowserLogin, value);\n                RaisePropertyChanged(OAuthModeTitle);\n            }\n        }\n\n        public bool ShowTokenLogin\n        {\n            get => _showTokenLogin;\n            set => SetAndRaisePropertyChanged(ref _showTokenLogin, value);\n        }\n\n        public bool ShowBasicLogin\n        {\n            get => _showBasicLogin;\n            set => SetAndRaisePropertyChanged(ref _showBasicLogin, value);\n        }\n\n        public ICommand SignUpCommand\n        {\n            get => _signUpCommand;\n            set => SetAndRaisePropertyChanged(ref _signUpCommand, value);\n        }\n\n        public ICommand SignInBrowserCommand\n        {\n            get => _signInBrowserCommand;\n            set => SetAndRaisePropertyChanged(ref _signInBrowserCommand, value);\n        }\n\n        public RelayCommand SignInTokenCommand\n        {\n            get => _signInTokenCommand;\n            set => SetAndRaisePropertyChanged(ref _signInTokenCommand, value);\n        }\n\n        public RelayCommand SignInBasicCommand\n        {\n            get => _signInBasicCommand;\n            set => SetAndRaisePropertyChanged(ref _signInBasicCommand, value);\n        }\n\n        public AuthenticationModes SelectedMode { get; private set; }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab/UI/Views/CredentialsView.axaml",
    "content": "<UserControl xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:vm=\"clr-namespace:GitLab.UI.ViewModels\"\n             xmlns:converters=\"clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore\"\n             mc:Ignorable=\"d\" d:DesignWidth=\"420\"\n             x:Class=\"GitLab.UI.Views.CredentialsView\"\n             x:Name=\"view\">\n    <UserControl.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceInclude Source=\"/UI/Assets/Images.axaml\"/>\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </UserControl.Resources>\n    <Design.DataContext>\n        <vm:CredentialsViewModel/>\n    </Design.DataContext>\n    <DockPanel>\n        <StackPanel DockPanel.Dock=\"Top\" Margin=\"0,0,0,15\">\n            <!--\n                Only show the GitLab logo when this is GitLab.com,\n                otherwise show just the logo type \"GitLab\".\n            -->\n            <StackPanel Orientation=\"Horizontal\"\n                        HorizontalAlignment=\"Center\"\n                        Margin=\"0,0,0,10\">\n                <Image Source=\"{DynamicResource GitLabLogo}\"\n                       Height=\"46\" VerticalAlignment=\"Center\" Margin=\"0,0,8,0\"\n                       IsVisible=\"{Binding Url, Converter={x:Static StringConverters.IsNullOrEmpty}}\"/>\n                <Image Source=\"{DynamicResource GitLabLogoType}\"\n                       Height=\"24\" VerticalAlignment=\"Center\"/>\n            </StackPanel>\n            <TextBlock Text=\"Sign in\"\n                       HorizontalAlignment=\"Center\"\n                       FontSize=\"16\"\n                       FontWeight=\"Light\"\n                       Margin=\"0,0,0,5\" />\n            <StackPanel IsVisible=\"{Binding Url, Converter={x:Static StringConverters.IsNotNullOrEmpty}}\"\n                        Margin=\"0,10,0,0\">\n                <TextBlock Text=\"{Binding Url}\" HorizontalAlignment=\"Center\"/>\n            </StackPanel>\n        </StackPanel>\n\n        <WrapPanel DockPanel.Dock=\"Bottom\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                   IsVisible=\"{Binding Url, Converter={x:Static StringConverters.IsNullOrEmpty}}\"\n                   Margin=\"0,20,0,0\">\n            <TextBlock Text=\"Don't have an account?\" Margin=\"0,0,5,0\" />\n            <Button Content=\"Sign up\"\n                    Command=\"{Binding SignUpCommand}\"\n                    Classes=\"hyperlink\"/>\n        </WrapPanel>\n\n        <TabControl x:Name=\"_authModesTabControl\"\n                    VerticalContentAlignment=\"Center\"\n                    AutoScrollToSelectedItem=\"True\">\n            <TabControl.Styles>\n                <Style Selector=\"TabItem\">\n                    <Setter Property=\"MinHeight\" Value=\"30\" />\n                </Style>\n                <Style Selector=\"DockPanel > ItemsPresenter > WrapPanel\">\n                    <Setter Property=\"HorizontalAlignment\" Value=\"Center\"/>\n                </Style>\n            </TabControl.Styles>\n\n            <TabItem IsEnabled=\"{Binding ShowBrowserLogin}\"\n                     IsVisible=\"{Binding $self.IsEnabled}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"{Binding OAuthModeTitle}\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,10\">\n                    <Button x:Name=\"_signInBrowserButton\"\n                            Content=\"Sign in with your browser\"\n                            IsDefault=\"True\"\n                            Command=\"{Binding SignInBrowserCommand}\"\n                            IsVisible=\"{Binding ShowBrowserLogin}\"\n                            HorizontalAlignment=\"Center\"\n                            Margin=\"0,0,0,10\"\n                            Classes=\"accent\"/>\n                </StackPanel>\n            </TabItem>\n\n            <TabItem IsEnabled=\"{Binding ShowTokenLogin}\"\n                     IsVisible=\"{Binding $self.IsEnabled}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"Token\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,10\">\n                    <TextBox x:Name=\"_patUserNameTextBox\"\n                             Watermark=\"Username or email (optional)\" Margin=\"0,0,0,10\"\n                             Text=\"{Binding TokenUserName}\"/>\n                    <TextBox x:Name=\"_tokenTextBox\"\n                             Watermark=\"Personal access token\" Margin=\"0,0,0,10\"\n                             PasswordChar=\"●\"\n                             Text=\"{Binding Token}\"/>\n                    <Button Content=\"Sign in\"\n                            IsDefault=\"True\"\n                            Command=\"{Binding SignInTokenCommand}\"\n                            HorizontalAlignment=\"Center\"\n                            Classes=\"accent\"/>\n                </StackPanel>\n            </TabItem>\n\n            <TabItem IsEnabled=\"{Binding ShowBasicLogin}\"\n                     IsVisible=\"{Binding $self.IsEnabled}\">\n                <TabItem.Header>\n                    <TextBlock Text=\"Password\" FontSize=\"12\" />\n                </TabItem.Header>\n                <StackPanel Margin=\"0,10\">\n                    <TextBox x:Name=\"_userNameTextBox\"\n                             Watermark=\"Username or email\" Margin=\"0,0,0,10\"\n                             Text=\"{Binding UserName}\"/>\n                    <TextBox x:Name=\"_passwordTextBox\"\n                             Watermark=\"Password\" Margin=\"0,0,0,10\"\n                             PasswordChar=\"●\"\n                             Text=\"{Binding Password}\"/>\n                    <Button Content=\"Sign in\"\n                            IsDefault=\"True\"\n                            Command=\"{Binding SignInBasicCommand}\"\n                            HorizontalAlignment=\"Center\"\n                            Classes=\"accent\"/>\n                </StackPanel>\n            </TabItem>\n        </TabControl>\n    </DockPanel>\n</UserControl>\n"
  },
  {
    "path": "src/shared/GitLab/UI/Views/CredentialsView.axaml.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Markup.Xaml;\nusing GitCredentialManager;\nusing GitLab.UI.ViewModels;\nusing GitCredentialManager.UI.Controls;\n\nnamespace GitLab.UI.Views\n{\n    public partial class CredentialsView : UserControl, IFocusable\n    {\n        public CredentialsView()\n        {\n            InitializeComponent();\n        }\n\n        public void SetFocus()\n        {\n            if (!(DataContext is CredentialsViewModel vm))\n            {\n                return;\n            }\n\n            // Select the best available authentication mechanism that is visible\n            // and focus on the button/text box\n            if (vm.ShowBrowserLogin)\n            {\n                _authModesTabControl.SelectedIndex = 0;\n                _signInBrowserButton.Focus();\n            }\n            else if (vm.ShowTokenLogin)\n            {\n                _authModesTabControl.SelectedIndex = 1;\n                // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                if (!PlatformUtils.IsMacOS())\n                    _tokenTextBox.Focus();\n            }\n            else if (vm.ShowBasicLogin)\n            {\n                _authModesTabControl.SelectedIndex = 2;\n                if (string.IsNullOrWhiteSpace(vm.UserName))\n                {\n                    // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                    if (!PlatformUtils.IsMacOS())\n                        _userNameTextBox.Focus();\n                }\n                else\n                {\n                    // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293\n                    if (!PlatformUtils.IsMacOS())\n                        _passwordTextBox.Focus();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab.Tests/GitLab.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.2\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.11.1\" />\n    <PackageReference Include=\"ReportGenerator\" Version=\"5.3.10\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <DotNetCliToolReference Include=\"dotnet-xunit\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\GitLab\\GitLab.csproj\" />\n    <ProjectReference Include=\"..\\TestInfrastructure\\TestInfrastructure.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/GitLab.Tests/GitLabAuthenticationTests.cs",
    "content": "using System;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitLab.Tests\n{\n    public class GitLabAuthenticationTests\n    {\n        [Fact]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_AuthenticationModesNone_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var auth = new GitLabAuthentication(context);\n            await Assert.ThrowsAsync<ArgumentException>(\"modes\",\n                () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.None)\n            );\n        }\n\n        [Theory]\n        [InlineData(AuthenticationModes.Browser)]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_SingleChoice_InteractionStillRequired(GitLab.AuthenticationModes modes)\n        {\n            var context = new TestCommandContext();\n            context.Settings.IsInteractionAllowed = true;\n            context.SessionManager.IsDesktopSession = true; // necessary for browser\n            context.Settings.IsGuiPromptsEnabled = false;\n            var auth = new GitLabAuthentication(context);\n            var result = await auth.GetAuthenticationAsync(null, null, modes);\n            Assert.Equal(modes, result.AuthenticationMode);\n        }\n\n        [Fact]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_TerminalPromptsDisabled_Throws()\n        {\n            var context = new TestCommandContext();\n            context.Settings.IsTerminalPromptsEnabled = false;\n            var auth = new GitLabAuthentication(context);\n            var exception = await Assert.ThrowsAsync<Trace2InvalidOperationException>(\n                () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All)\n            );\n            Assert.Equal(\"Cannot prompt because terminal prompts have been disabled.\", exception.Message);\n        }\n\n        [Fact]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_Terminal()\n        {\n            var context = new TestCommandContext();\n            var auth = new GitLabAuthentication(context);\n            context.SessionManager.IsDesktopSession = true;\n            context.Settings.IsGuiPromptsEnabled = false;\n            context.Terminal.Prompts[\"option (enter for default)\"] = \"\";\n            var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);\n            Assert.Equal(AuthenticationModes.Browser, result.AuthenticationMode);\n        }\n\n        [Fact]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_ChoosePat()\n        {\n            var context = new TestCommandContext();\n            var auth = new GitLabAuthentication(context);\n            context.Terminal.Prompts[\"option (enter for default)\"] = \"\";\n            context.Terminal.Prompts[\"Username\"] = \"username\";\n            context.Terminal.SecretPrompts[\"Personal access token\"] = \"token\";\n            var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);\n            Assert.Equal(AuthenticationModes.Pat, result.AuthenticationMode);\n            Assert.Equal(\"username\", result.Credential.Account);\n            Assert.Equal(\"token\", result.Credential.Password);\n        }\n\n        [Fact]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_ChooseBasic()\n        {\n            var context = new TestCommandContext();\n            var auth = new GitLabAuthentication(context);\n            context.Terminal.Prompts[\"option (enter for default)\"] = \"2\";\n            context.Terminal.Prompts[\"Username\"] = \"username\";\n            context.Terminal.SecretPrompts[\"Password\"] = \"password\";\n            var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);\n            Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);\n            Assert.Equal(\"username\", result.Credential.Account);\n            Assert.Equal(\"password\", result.Credential.Password);\n        }\n\n        [Fact]\n        public async Task GitLabAuthentication_GetAuthenticationAsync_AuthenticationModesAll_RequiresInteraction()\n        {\n            var context = new TestCommandContext();\n            context.Settings.IsInteractionAllowed = false;\n            var auth = new GitLabAuthentication(context);\n            var exception = await Assert.ThrowsAsync<Trace2InvalidOperationException>(\n                () => auth.GetAuthenticationAsync(new Uri(\"https://GitLab.com\"), null, AuthenticationModes.All)\n            );\n            Assert.Equal(\"Cannot prompt because user interactivity has been disabled.\", exception.Message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/GitLab.Tests/GitLabHostProviderTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitLab.Tests\n{\n    public class GitLabHostProviderTests\n    {\n        [Theory]\n        [InlineData(\"https\", \"gitlab.com\", true)]\n        [InlineData(\"http\", \"gitlab.com\", true)]\n        [InlineData(\"https\", \"gitlab.example.com\", true)]\n        [InlineData(\"https\", \"github.com\", false)]\n        [InlineData(\"https\", \"github.example.com\", false)]\n        public void GitLabHostProvider_IsSupported(string protocol, string host, bool expected)\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = protocol,\n                [\"host\"] = host,\n            });\n\n            var provider = new GitLabHostProvider(new TestCommandContext());\n            Assert.Equal(expected, provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void GitLabHostProvider_GetSupportedAuthenticationModes_DotCom_ReturnsDotComModes()\n        {\n            Uri targetUri = GitLabConstants.GitLabDotCom;\n            AuthenticationModes expected = GitLabConstants.DotComAuthenticationModes;\n\n            var context = new TestCommandContext();\n            var provider = new GitLabHostProvider(context);\n            AuthenticationModes actual = provider.GetSupportedAuthenticationModes(targetUri);\n\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void GitLabHostProvider_GetSupportedAuthenticationModes_Custom_NoOAuthConfig_ReturnsBasicPat()\n        {\n            var targetUri = new Uri(\"https://gitlab.example.com\");\n            var expected = AuthenticationModes.Basic\n                                          | AuthenticationModes.Pat;\n\n            var context = new TestCommandContext();\n            var provider = new GitLabHostProvider(context);\n            AuthenticationModes actual = provider.GetSupportedAuthenticationModes(targetUri);\n\n            Assert.Equal(expected, actual);\n        }\n\n        [Fact]\n        public void GitLabHostProvider_GetSupportedAuthenticationModes_Custom_WithOAuthConfig_ReturnsBasicPatBrowser()\n        {\n            var targetUri = new Uri(\"https://gitlab.example.com\");\n            var expected = AuthenticationModes.Basic\n                                          | AuthenticationModes.Pat\n                                          | AuthenticationModes.Browser;\n\n            var context = new TestCommandContext();\n            context.Environment.Variables[GitLabConstants.EnvironmentVariables.DevOAuthClientId] = \"abcdefg1234567\";\n\n            var provider = new GitLabHostProvider(context);\n            AuthenticationModes actual = provider.GetSupportedAuthenticationModes(targetUri);\n\n            Assert.Equal(expected, actual);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/AzureDevOpsAuthorityCache.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing GitCredentialManager;\n\nnamespace Microsoft.AzureRepos\n{\n    public interface IAzureDevOpsAuthorityCache\n    {\n        /// <summary>\n        /// Lookup the cached authority for the specified Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"orgName\">Azure DevOps organization name.</param>\n        /// <returns>Authority for the organization, or null if not found.</returns>\n        string GetAuthority(string orgName);\n\n        /// <summary>\n        /// Updates the cached authority for the specified Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"orgName\">Azure DevOps organization name.</param>\n        /// <param name=\"authority\">New authority value.</param>\n        void UpdateAuthority(string orgName, string authority);\n\n        /// <summary>\n        /// Erase the cached authority for the specified Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"orgName\">Azure DevOps organization name.</param>\n        void EraseAuthority(string orgName);\n\n        /// <summary>\n        /// Clear all cached authorities.\n        /// </summary>\n        void Clear();\n    }\n\n    public class AzureDevOpsAuthorityCache : IAzureDevOpsAuthorityCache\n    {\n        private readonly ITrace _trace;\n        private readonly IGit _git;\n\n        public AzureDevOpsAuthorityCache(ICommandContext context)\n            : this(context.Trace, context.Git) { }\n\n        public AzureDevOpsAuthorityCache(ITrace trace, IGit git)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            EnsureArgument.NotNull(git, nameof(git));\n\n            _trace = trace;\n            _git = git;\n        }\n\n        public string GetAuthority(string orgName)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            _trace.WriteLine($\"Looking up cached authority for organization '{orgName}'...\");\n\n            IGitConfiguration config = _git.GetConfiguration();\n\n            if (config.TryGet(GitConfigurationLevel.Global, GitConfigurationType.Raw,\n                GetAuthorityKey(orgName), out string authority))\n            {\n                return authority;\n            }\n\n            return null;\n        }\n\n        public void UpdateAuthority(string orgName, string authority)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            _trace.WriteLine($\"Updating cached authority for '{orgName}' to '{authority}'...\");\n\n            IGitConfiguration config = _git.GetConfiguration();\n            config.Set(GitConfigurationLevel.Global, GetAuthorityKey(orgName), authority);\n        }\n\n        public void EraseAuthority(string orgName)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            _trace.WriteLine($\"Removing cached authority for '{orgName}'...\");\n            IGitConfiguration config = _git.GetConfiguration();\n            config.Unset(GitConfigurationLevel.Global, GetAuthorityKey(orgName));\n        }\n\n        public void Clear()\n        {\n            _trace.WriteLine(\"Clearing all cached authorities...\");\n\n            IGitConfiguration config = _git.GetConfiguration();\n\n            var orgKeys = new HashSet<string>(GitConfigurationKeyComparer.Instance);\n            config.Enumerate(\n                GitConfigurationLevel.Global,\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.GitConfiguration.Credential.AzureAuthority,\n                entry =>\n            {\n                if (GitConfigurationKeyComparer.TrySplit(entry.Key, out _, out string scope, out _) &&\n                    Uri.TryCreate(scope, UriKind.Absolute, out Uri orgUrn) &&\n                    orgUrn.Scheme == AzureDevOpsConstants.UrnScheme)\n                {\n                    orgKeys.Add(entry.Key);\n                }\n\n                return true;\n            });\n\n            foreach (string orgKey in orgKeys)\n            {\n                config.Unset(GitConfigurationLevel.Global, orgKey);\n            }\n        }\n\n        private static string GetAuthorityKey(string orgName)\n        {\n            return string.Format(CultureInfo.InvariantCulture, \"{0}.{1}:{2}/{3}.{4}\",\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.UrnScheme, AzureDevOpsConstants.UrnOrgPrefix, orgName,\n                AzureDevOpsConstants.GitConfiguration.Credential.AzureAuthority);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs",
    "content": "using System;\n\nnamespace Microsoft.AzureRepos\n{\n    internal static class AzureDevOpsConstants\n    {\n        // AAD environment authority base URL\n        public const string AadAuthorityBaseUrl = \"https://login.microsoftonline.com\";\n\n        // Azure DevOps's app ID + default scopes\n        public const string AzureDevOpsResourceId = \"499b84ac-1321-427f-aa17-267ca6975798\";\n        public static readonly string[] AzureDevOpsDefaultScopes = {$\"{AzureDevOpsResourceId}/.default\"};\n\n        // Visual Studio's client ID\n        // We share this to be able to consume existing access tokens from the VS caches\n        public const string AadClientId = \"872cd9fa-d31f-45e0-9eab-6e460a02d1f1\";\n\n        // Redirect URI specified by the Visual Studio application configuration\n        public static readonly Uri AadRedirectUri = new Uri(\"http://localhost\");\n\n        public const string VstsHostSuffix = \".visualstudio.com\";\n        public const string AzureDevOpsHost = \"dev.azure.com\";\n\n        public const string VssResourceTenantHeader = \"X-VSS-ResourceTenant\";\n\n        public const string PatCredentialType = \"pat\";\n        public const string OAuthCredentialType = \"oauth\";\n\n        public const string UrnScheme = \"azrepos\";\n        public const string UrnOrgPrefix = \"org\";\n\n        public static class PersonalAccessTokenScopes\n        {\n            public const string ReposWrite = \"vso.code_write\";\n            public const string ArtifactsRead = \"vso.packaging\";\n        }\n\n        public static class EnvironmentVariables\n        {\n            public const string DevAadClientId = \"GCM_DEV_AZREPOS_CLIENTID\";\n            public const string DevAadRedirectUri = \"GCM_DEV_AZREPOS_REDIRECTURI\";\n            public const string DevAadAuthorityBaseUri = \"GCM_DEV_AZREPOS_AUTHORITYBASEURI\";\n            public const string CredentialType = \"GCM_AZREPOS_CREDENTIALTYPE\";\n            public const string ServicePrincipalId = \"GCM_AZREPOS_SERVICE_PRINCIPAL\";\n            public const string ServicePrincipalSecret = \"GCM_AZREPOS_SP_SECRET\";\n            public const string ServicePrincipalCertificateThumbprint = \"GCM_AZREPOS_SP_CERT_THUMBPRINT\";\n            public const string ServicePrincipalCertificateSendX5C = \"GCM_AZREPOS_SP_CERT_SEND_X5C\";\n            public const string ManagedIdentity = \"GCM_AZREPOS_MANAGEDIDENTITY\";\n        }\n\n        public static class GitConfiguration\n        {\n            public static class Credential\n            {\n                public const string DevAadClientId = \"azreposDevClientId\";\n                public const string DevAadRedirectUri = \"azreposDevRedirectUri\";\n                public const string DevAadAuthorityBaseUri = \"azreposDevAuthorityBaseUri\";\n                public const string CredentialType = \"azreposCredentialType\";\n                public const string AzureAuthority = \"azureAuthority\";\n                public const string ServicePrincipal = \"azreposServicePrincipal\";\n                public const string ServicePrincipalSecret = \"azreposServicePrincipalSecret\";\n                public const string ServicePrincipalCertificateThumbprint = \"azreposServicePrincipalCertificateThumbprint\";\n                public const string ServicePrincipalCertificateSendX5C = \"azreposServicePrincipalCertificateSendX5C\";\n                public const string ManagedIdentity = \"azreposManagedIdentity\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\n\nnamespace Microsoft.AzureRepos\n{\n    public interface IAzureDevOpsRestApi : IDisposable\n    {\n        Task<string> GetAuthorityAsync(Uri organizationUri);\n        Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, string accessToken, IEnumerable<string> scopes);\n    }\n\n    public class AzureDevOpsRestApi : IAzureDevOpsRestApi\n    {\n        private readonly ICommandContext _context;\n\n        public AzureDevOpsRestApi(ICommandContext context)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n\n            _context = context;\n        }\n\n        public async Task<string> GetAuthorityAsync(Uri organizationUri)\n        {\n            EnsureArgument.AbsoluteUri(organizationUri, nameof(organizationUri));\n\n            Uri authorityBase = GetAuthorityBaseUri();\n            var commonAuthority = new Uri(authorityBase, \"common\");\n\n            // We should be using \"/common\" or \"/consumer\" as the authority for MSA but since\n            // Azure DevOps uses MSA pass-through (an internal hack to support MSA and AAD\n            // accounts in the same auth stack), which actually need to consult the \"/organizations\"\n            // authority instead.\n            var msaAuthority = new Uri(authorityBase, \"organizations\");\n\n            _context.Trace.WriteLine($\"HTTP: HEAD {organizationUri}\");\n            using (HttpResponseMessage response = await HttpClient.HeadAsync(organizationUri))\n            {\n                _context.Trace.WriteLine(\"HTTP: Response code ignored.\");\n                _context.Trace.WriteLine(\"Inspecting headers...\");\n\n                // Check WWW-Authenticate headers first; we prefer these\n                foreach (var header in response.Headers.WwwAuthenticate)\n                {\n                    if (TryGetAuthorityFromHeader(header, out string authority))\n                    {\n                        _context.Trace.WriteLine(\n                            $\"Found WWW-Authenticate header with Bearer authority '{authority}'.\");\n                        return authority;\n                    }\n                }\n\n                // We didn't find a bearer WWW-Auth header; check for the X-VSS-ResourceTenant header\n                foreach (var header in response.Headers)\n                {\n                    if (StringComparer.OrdinalIgnoreCase.Equals(header.Key, AzureDevOpsConstants.VssResourceTenantHeader))\n                    {\n                        string[] tenantIds = header.Value.ToArray();\n                        Guid guid;\n\n                        // Take the first tenant ID that isn't an empty GUID\n                        var tenantId = tenantIds.FirstOrDefault(x => Guid.TryParse(x, out guid) && guid != Guid.Empty);\n                        if (tenantId != null)\n                        {\n                            _context.Trace.WriteLine($\"Found {AzureDevOpsConstants.VssResourceTenantHeader} header with AAD tenant ID '{tenantId}'.\");\n                            return new Uri(authorityBase, tenantId).ToString();\n                        }\n\n                        // If we have exactly one empty GUID then this is a MSA backed organization\n                        if (tenantIds.Length == 1 && Guid.TryParse(tenantIds[0], out guid) && guid == Guid.Empty)\n                        {\n                            _context.Trace.WriteLine($\"Found {AzureDevOpsConstants.VssResourceTenantHeader} header with MSA tenant ID (empty GUID).\");\n                            return msaAuthority.ToString();\n                        }\n                    }\n                }\n            }\n\n            // Use the common authority if we can't determine a specific one\n            _context.Trace.WriteLine($\"Unable to determine AAD/MSA tenant - falling back to common authority\");\n            return commonAuthority.ToString();\n        }\n\n        private Uri GetAuthorityBaseUri()\n        {\n            // Check for developer override value\n            if (_context.Settings.TryGetSetting(\n                    AzureDevOpsConstants.EnvironmentVariables.DevAadAuthorityBaseUri,\n                    Constants.GitConfiguration.Credential.SectionName, AzureDevOpsConstants.GitConfiguration.Credential.DevAadAuthorityBaseUri,\n                    out string redirectUriStr) &&\n                Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri authorityBase))\n            {\n                return authorityBase;\n            }\n\n            return new Uri(AzureDevOpsConstants.AadAuthorityBaseUrl);\n        }\n\n        public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, string accessToken, IEnumerable<string> scopes)\n        {\n            const string sessionTokenUrl = \"_apis/token/sessiontokens?api-version=1.0&tokentype=compact\";\n\n            EnsureArgument.AbsoluteUri(organizationUri, nameof(organizationUri));\n            if (!UriHelpers.IsAzureDevOpsHost(organizationUri.Host))\n            {\n                throw new ArgumentException($\"Provided URI '{organizationUri}' is not a valid Azure DevOps hostname\", nameof(organizationUri));\n            }\n            EnsureArgument.NotNull(accessToken, nameof(accessToken));\n\n            _context.Trace.WriteLine(\"Getting Azure DevOps Identity Service endpoint...\");\n            Uri identityServiceUri = await GetIdentityServiceUriAsync(organizationUri, accessToken);\n            _context.Trace.WriteLine($\"Identity Service endpoint is '{identityServiceUri}'.\");\n\n            Uri requestUri = new Uri(identityServiceUri, sessionTokenUrl);\n\n            _context.Trace.WriteLine($\"HTTP: POST {requestUri}\");\n            using (StringContent content = CreateAccessTokenRequestJson(organizationUri, scopes))\n            using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, requestUri, content, accessToken))\n            using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n            {\n                _context.Trace.WriteLine($\"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]\");\n\n                string responseText = await response.Content.ReadAsStringAsync();\n\n                if (!string.IsNullOrWhiteSpace(responseText))\n                {\n                    if (response.IsSuccessStatusCode)\n                    {\n                        if (TryGetFirstJsonStringField(responseText, \"token\", out string token))\n                        {\n                            return token;\n                        }\n                    }\n                    else\n                    {\n                        if (TryGetFirstJsonStringField(responseText, \"message\", out string errorMessage))\n                        {\n                            throw new Trace2Exception(_context.Trace2, $\"Failed to create PAT: {errorMessage}\");\n                        }\n                    }\n                }\n            }\n\n            throw new Trace2Exception(_context.Trace2, \"Failed to create PAT\");\n        }\n\n        #region Private Methods\n\n        private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, string accessToken)\n        {\n            const string locationServicePath = \"_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381\";\n            const string locationServiceQuery = \"api-version=1.0\";\n\n            Uri requestUri = new UriBuilder(organizationUri)\n            {\n                Path = UriHelpers.CombinePath(organizationUri.AbsolutePath, locationServicePath),\n                Query = locationServiceQuery,\n            }.Uri;\n\n            _context.Trace.WriteLine($\"HTTP: GET {requestUri}\");\n            using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Get, requestUri, bearerToken: accessToken))\n            using (HttpResponseMessage response = await HttpClient.SendAsync(request))\n            {\n                _context.Trace.WriteLine($\"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]\");\n                if (response.IsSuccessStatusCode)\n                {\n                    string responseText = await response.Content.ReadAsStringAsync();\n\n                    if (TryGetFirstJsonStringField(responseText, \"location\", out string identityServiceStr) &&\n                        Uri.TryCreate(identityServiceStr, UriKind.Absolute, out Uri identityService))\n                    {\n                        return identityService;\n                    }\n                }\n            }\n\n            throw new Trace2Exception(_context.Trace2, \"Failed to find location service\");\n        }\n\n        #endregion\n\n        #region Request and Response Helpers\n\n        private const RegexOptions CommonRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;\n\n        private HttpClient _httpClient;\n\n        private HttpClient HttpClient\n        {\n            get\n            {\n                if (_httpClient is null)\n                {\n                    _httpClient = _context.HttpClientFactory.CreateClient();\n\n                    // Configure the HTTP client with standard headers for Azure Repos API calls\n                    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Http.MimeTypeJson));\n                }\n\n                return _httpClient;\n            }\n        }\n\n        /// <summary>\n        /// Attempt to extract the authority from a Authorization Bearer header.\n        /// </summary>\n        /// <remarks>This method has internal visibility for testing purposes only.</remarks>\n        /// <param name=\"header\">Request header</param>\n        /// <param name=\"authority\">Value of authorization authority, or null if not found.</param>\n        /// <returns>True if an authority was found in the header, false otherwise.</returns>\n        internal static bool TryGetAuthorityFromHeader(AuthenticationHeaderValue header, out string authority)\n        {\n            // We're looking for a \"Bearer\" scheme header\n            if (!(header is null) &&\n                StringComparer.OrdinalIgnoreCase.Equals(header.Scheme, Constants.Http.WwwAuthenticateBearerScheme) &&\n                header.Parameter is string headerValue)\n            {\n                Match match = Regex.Match(headerValue, @\"^authorization_uri=(?'authority'.+)$\", CommonRegexOptions);\n\n                if (match.Success)\n                {\n                    authority = match.Groups[\"authority\"].Value;\n                    return true;\n                }\n            }\n\n            authority = null;\n            return false;\n        }\n\n        /// <summary>\n        /// Parse the input JSON string looking for the first string field with the specified name.\n        /// </summary>\n        /// <remarks>This method has internal visibility for testing purposes only.</remarks>\n        /// <param name=\"json\">JSON string</param>\n        /// <param name=\"fieldName\">Name of field to locate.</param>\n        /// <param name=\"value\">Value of first found field, or null if no such field was found.</param>\n        /// <returns>True if a field and value was found, false otherwise.</returns>\n        internal static bool TryGetFirstJsonStringField(string json, string fieldName, out string value)\n        {\n            if (!string.IsNullOrWhiteSpace(json))\n            {\n                // Find the '\"<field>\" : \"<value>\"' portion of the JSON content\n                string escapedFieldName = Regex.Escape(fieldName);\n                string pattern = $\"\\\"{escapedFieldName}\\\"\\\\s*\\\\:\\\\s*\\\"(?'value'[^\\\"]+)\\\"\";\n                Match match = Regex.Match(json, pattern, CommonRegexOptions);\n                if (match.Success)\n                {\n                    value = match.Groups[\"value\"].Value;\n                    return true;\n                }\n\n            }\n\n            value = null;\n            return false;\n        }\n\n        /// <summary>\n        /// Create the JSON request body content used to request a new personal access token be created\n        /// with the specified scopes.\n        /// </summary>\n        /// <param name=\"organizationUri\">Azure DevOps organization URL to create the token for.</param>\n        /// <param name=\"tokenScopes\">Scopes to request.</param>\n        /// <returns>JSON request content.</returns>\n        private static StringContent CreateAccessTokenRequestJson(Uri organizationUri, IEnumerable<string> tokenScopes)\n        {\n            const string jsonFormat = \"{{ \\\"scope\\\" : \\\"{0}\\\", \\\"displayName\\\" : \\\"Git: {1} on {2}\\\" }}\";\n\n            string orgUrl = organizationUri.AbsoluteUri;\n            string joinedScopes = string.Join(\" \", tokenScopes);\n            string machineName = Environment.MachineName;\n\n            string jsonContent = string.Format(jsonFormat, joinedScopes, orgUrl, machineName);\n            var content = new StringContent(jsonContent, Encoding.UTF8, Constants.Http.MimeTypeJson);\n\n            return content;\n        }\n\n        /// <summary>\n        /// Create an <see cref=\"HttpRequestMessage\"/> with optional content and bearer-token authorization header.\n        /// </summary>\n        /// <param name=\"method\">HTTP request method type.</param>\n        /// <param name=\"uri\">Request URI.</param>\n        /// <param name=\"content\">Optional request content.</param>\n        /// <param name=\"bearerToken\">Optional bearer token for authorization.</param>\n        /// <returns>HTTP request message.</returns>\n        private static HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri uri, HttpContent content = null, string bearerToken = null)\n        {\n            var request = new HttpRequestMessage(method, uri);\n\n            if (!(content is null))\n            {\n                request.Content = content;\n            }\n\n            if (bearerToken != null)\n            {\n                request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken);\n            }\n\n            return request;\n        }\n\n        #endregion\n\n        #region IDisposable\n\n        public void Dispose()\n        {\n            _httpClient?.Dispose();\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/AzureReposBindingManager.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing GitCredentialManager;\n\nnamespace Microsoft.AzureRepos\n{\n    /// <summary>\n    /// Manages bindings of users and organizations for Azure Repos.\n    /// </summary>\n    public interface IAzureReposBindingManager\n    {\n        /// <summary>\n        /// Get the binding for the given Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"orgName\">Organization name.</param>\n        /// <returns>Binding for the organization, or null if no binding exists.</returns>\n        AzureReposBinding GetBinding(string orgName);\n\n        /// <summary>\n        /// Bind a user to the given organization.\n        /// </summary>\n        /// <param name=\"orgName\">Organization to bind the user to.</param>\n        /// <param name=\"userName\">User identifier to bind.</param>\n        /// <param name=\"local\">If true then bind local configuration, otherwise unbind global configuration.</param>\n        /// <remarks>\n        /// To prevent inheritance of a user binding at the global level, you can \"bind\" an organization\n        /// to a special <paramref name=\"userName\"/> value <see cref=\"AzureReposBinding.NoInherit\"/>.\n        /// <para/>\n        /// The special value <see cref=\"AzureReposBinding.NoInherit\"/> can be used as the <paramref name=\"userName\"/>\n        /// only when <paramref name=\"local\"/> is true.\n        /// </remarks>\n        void Bind(string orgName, string userName, bool local);\n\n        /// <summary>\n        /// Unbind the given organization.\n        /// </summary>\n        /// <param name=\"orgName\">Organization to unbind.</param>\n        /// <param name=\"local\">If true then unbind local configuration, otherwise unbind global configuration.</param>\n        void Unbind(string orgName, bool local);\n\n        /// <summary>\n        /// Get all bindings to Azure DevOps organizations.\n        /// </summary>\n        /// <param name=\"orgName\">Optional organization filter.</param>\n        /// <returns>All organization bindings.</returns>\n        IEnumerable<AzureReposBinding> GetBindings(string orgName = null);\n    }\n\n    public class AzureReposBinding\n    {\n        /// <summary>\n        /// Do not inherit any higher-level binding.\n        /// </summary>\n        public const string NoInherit = \"\";\n\n        public AzureReposBinding(string organization, string globalUserName, string localUserName)\n        {\n            Organization = organization;\n            GlobalUserName = globalUserName;\n            LocalUserName = localUserName;\n        }\n\n        public string Organization { get; }\n        public string GlobalUserName { get; }\n        public string LocalUserName { get; }\n    }\n\n    public class AzureReposBindingManager : IAzureReposBindingManager\n    {\n        private readonly ITrace _trace;\n        private readonly IGit _git;\n\n        public AzureReposBindingManager(ICommandContext context) : this(context.Trace, context.Git) { }\n\n        public AzureReposBindingManager(ITrace trace, IGit git)\n        {\n            EnsureArgument.NotNull(trace, nameof(trace));\n            EnsureArgument.NotNull(git, nameof(git));\n\n            _trace = trace;\n            _git = git;\n        }\n\n        public AzureReposBinding GetBinding(string orgName)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            string orgKey = GetOrgUserKey(orgName);\n\n            IGitConfiguration config = _git.GetConfiguration();\n\n            _trace.WriteLine($\"Looking up organization binding for '{orgName}'...\");\n\n            string localUser = null;\n            bool hasLocal = _git.IsInsideRepository() && // Can only check local config if we are inside a repository\n                            config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Raw,\n                                orgKey, out localUser);\n\n            bool hasGlobal = config.TryGet(GitConfigurationLevel.Global, GitConfigurationType.Raw,\n                orgKey, out string globalUser);\n\n            if (hasLocal || hasGlobal)\n            {\n                return new AzureReposBinding(orgName, globalUser, localUser);\n            }\n\n            // No bound user\n            return null;\n        }\n\n        public void Bind(string orgName, string userName, bool local)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            IGitConfiguration config = _git.GetConfiguration();\n\n            string key = GetOrgUserKey(orgName);\n\n            if (local)\n            {\n                _trace.WriteLine(userName == AzureReposBinding.NoInherit\n                    ? $\"Setting binding to 'do not inherit' for organization '{orgName}' in local repository...\"\n                    : $\"Binding user '{userName}' to organization '{orgName}' in local repository...\");\n\n                if (_git.IsInsideRepository())\n                {\n                    config.Set(GitConfigurationLevel.Local, key, userName);\n                }\n                else\n                {\n                    _trace.WriteLine(\"Cannot set local configuration binding - not inside a repository!\");\n                }\n            }\n            else\n            {\n                EnsureArgument.NotNullOrWhiteSpace(userName, nameof(userName));\n\n                _trace.WriteLine($\"Binding user '{userName}' to organization '{orgName}' in global configuration...\");\n                config.Set(GitConfigurationLevel.Global, key, userName);\n            }\n        }\n\n        public void Unbind(string orgName, bool local)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            IGitConfiguration config = _git.GetConfiguration();\n\n            string key = GetOrgUserKey(orgName);\n\n            if (local)\n            {\n                _trace.WriteLine($\"Unbinding organization '{orgName}' in local repository...\");\n                if (_git.IsInsideRepository())\n                {\n                    config.Unset(GitConfigurationLevel.Local, key);\n                }\n                else\n                {\n                    _trace.WriteLine(\"Cannot set local configuration binding - not inside a repository!\");\n                }\n            }\n            else\n            {\n                _trace.WriteLine($\"Unbinding organization '{orgName}' in global configuration...\");\n                config.Unset(GitConfigurationLevel.Global, key);\n            }\n        }\n\n        public IEnumerable<AzureReposBinding> GetBindings(string orgName = null)\n        {\n            var globalUsers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n            var localUsers  = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n\n            IGitConfiguration config = _git.GetConfiguration();\n\n            string orgPrefix = $\"{AzureDevOpsConstants.UrnOrgPrefix}/\";\n\n            bool ExtractUserBinding(GitConfigurationEntry entry, IDictionary<string, string> dict)\n            {\n                if (GitConfigurationKeyComparer.TrySplit(entry.Key, out _, out string scope, out _) &&\n                    Uri.TryCreate(scope, UriKind.Absolute, out Uri uri) &&\n                    uri.Scheme == AzureDevOpsConstants.UrnScheme && uri.AbsolutePath.StartsWith(orgPrefix))\n                {\n                    string entryOrgName = uri.AbsolutePath.Substring(orgPrefix.Length);\n                    if (string.IsNullOrWhiteSpace(orgName) || StringComparer.OrdinalIgnoreCase.Equals(entryOrgName, orgName))\n                    {\n                        dict[entryOrgName] = entry.Value;\n                    }\n                }\n\n                return true;\n            }\n\n            // Only enumerate local configuration if we are inside a repository\n            if (_git.IsInsideRepository())\n            {\n                config.Enumerate(\n                    GitConfigurationLevel.Local,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    Constants.GitConfiguration.Credential.UserName,\n                    entry => ExtractUserBinding(entry, localUsers));\n            }\n\n            config.Enumerate(\n                GitConfigurationLevel.Global,\n                Constants.GitConfiguration.Credential.SectionName,\n                Constants.GitConfiguration.Credential.UserName,\n                entry => ExtractUserBinding(entry, globalUsers));\n\n            foreach (string org in globalUsers.Keys.Union(localUsers.Keys))\n            {\n                // NOT using the short-circuiting OR operator here on purpose - we need both branches to be evaluated\n                if (globalUsers.TryGetValue(org, out string globalUser) | localUsers.TryGetValue(org, out string localUser))\n                {\n                    yield return new AzureReposBinding(org, globalUser, localUser);\n                }\n            }\n        }\n\n        private static string GetOrgUserKey(string orgName)\n        {\n            return string.Format(CultureInfo.InvariantCulture, \"{0}.{1}:{2}/{3}.{4}\",\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.UrnScheme, AzureDevOpsConstants.UrnOrgPrefix, orgName,\n                \"username\"\n            );\n        }\n    }\n\n    public static class AzureReposUserManagerExtensions\n    {\n        /// <summary>\n        /// Get the user that is bound to the specified Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"bindingManager\">Binding manager.</param>\n        /// <param name=\"orgName\">Organization name.</param>\n        /// <returns>User identifier bound to the organization, or null if no such bound user exists.</returns>\n        public static string GetUser(this IAzureReposBindingManager bindingManager, string orgName)\n        {\n            AzureReposBinding binding = bindingManager.GetBinding(orgName);\n            if (binding is null || binding.LocalUserName == AzureReposBinding.NoInherit)\n            {\n                return null;\n            }\n\n            return binding.LocalUserName ?? binding.GlobalUserName;\n        }\n\n        /// <summary>\n        /// Marks a user as 'signed in' to an Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"bindingManager\">Binding manager.</param>\n        /// <param name=\"orgName\">Organization name.</param>\n        /// <param name=\"userName\">User identifier to bind to this organization.</param>\n        public static void SignIn(this IAzureReposBindingManager bindingManager, string orgName, string userName)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n            EnsureArgument.NotNullOrWhiteSpace(userName, nameof(userName));\n\n            //\n            // Try to bind the user to the organization.\n            //\n            //   A = User to sign-in\n            //   B = Another user\n            //   - = No user\n            //\n            //  Global | Local | -> | Global | Local\n            // --------|-------|----|--------|-------\n            //    -    |   -   | -> |   A    |   -\n            //    -    |   A   | -> |   A    |   -\n            //    -    |   B   | -> |   A    |   -\n            //    A    |   -   | -> |   A    |   -\n            //    A    |   A   | -> |   A    |   -\n            //    A    |   B   | -> |   A    |   -\n            //    B    |   -   | -> |   B    |   A\n            //    B    |   A   | -> |   B    |   A\n            //    B    |   B   | -> |   B    |   A\n            //\n            AzureReposBinding existingBinding = bindingManager.GetBinding(orgName);\n            if (existingBinding?.GlobalUserName != null &&\n                !StringComparer.OrdinalIgnoreCase.Equals(existingBinding.GlobalUserName, userName))\n            {\n                bindingManager.Bind(orgName, userName, local: true);\n            }\n            else\n            {\n                bindingManager.Bind(orgName, userName, local: false);\n                bindingManager.Unbind(orgName, local: true);\n            }\n        }\n\n        /// <summary>\n        /// Marks a user as 'signed out' of an Azure DevOps organization.\n        /// </summary>\n        /// <param name=\"bindingManager\">Binding manager.</param>\n        /// <param name=\"orgName\">Organization name.</param>\n        public static void SignOut(this IAzureReposBindingManager bindingManager, string orgName)\n        {\n            EnsureArgument.NotNullOrWhiteSpace(orgName, nameof(orgName));\n\n            //\n            // Unbind the organization so we prompt the user to select a user on the next attempt.\n            //\n            //   U = User\n            //   X = Do not inherit (valid in local only)\n            //   - = No user\n            //\n            //  Global | Local | -> | Global | Local\n            // --------|-------|----|--------|-------\n            //    -    |   -   | -> |   -    |   -\n            //    -    |   U   | -> |   -    |   -\n            //    -    |   X   | -> |   -    |   -\n            //    U    |   -   | -> |   U    |   X\n            //    U    |   X   | -> |   U    |   X\n            //    U    |   U   | -> |   U    |   X\n            //\n            AzureReposBinding existingBinding = bindingManager.GetBinding(orgName);\n            if (existingBinding is null)\n            {\n                // Nothing to do!\n            }\n            else if (existingBinding.GlobalUserName is null)\n            {\n                bindingManager.Unbind(orgName, local: true);\n            }\n            else\n            {\n                bindingManager.Bind(orgName, AzureReposBinding.NoInherit, local: true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Commands;\nusing KnownGitCfg = GitCredentialManager.Constants.GitConfiguration;\n\nnamespace Microsoft.AzureRepos\n{\n    public class AzureReposHostProvider : DisposableObject, IHostProvider, IConfigurableComponent, ICommandProvider\n    {\n        private readonly ICommandContext _context;\n        private readonly IAzureDevOpsRestApi _azDevOps;\n        private readonly IMicrosoftAuthentication _msAuth;\n        private readonly IAzureDevOpsAuthorityCache _authorityCache;\n        private readonly IAzureReposBindingManager _bindingManager;\n\n        public AzureReposHostProvider(ICommandContext context)\n            : this(context, new AzureDevOpsRestApi(context), new MicrosoftAuthentication(context),\n                new AzureDevOpsAuthorityCache(context), new AzureReposBindingManager(context))\n        {\n        }\n\n        public AzureReposHostProvider(ICommandContext context, IAzureDevOpsRestApi azDevOps,\n            IMicrosoftAuthentication msAuth, IAzureDevOpsAuthorityCache authorityCache,\n            IAzureReposBindingManager bindingManager)\n        {\n            EnsureArgument.NotNull(context, nameof(context));\n            EnsureArgument.NotNull(azDevOps, nameof(azDevOps));\n            EnsureArgument.NotNull(msAuth, nameof(msAuth));\n            EnsureArgument.NotNull(authorityCache, nameof(authorityCache));\n            EnsureArgument.NotNull(bindingManager, nameof(bindingManager));\n\n            _context = context;\n            _azDevOps = azDevOps;\n            _msAuth = msAuth;\n            _authorityCache = authorityCache;\n            _bindingManager = bindingManager;\n        }\n\n        #region IHostProvider\n\n        public string Id => \"azure-repos\";\n\n        public string Name => \"Azure Repos\";\n\n        public IEnumerable<string> SupportedAuthorityIds => MicrosoftAuthentication.AuthorityIds;\n\n        public bool IsSupported(InputArguments input)\n        {\n            if (input is null)\n            {\n                return false;\n            }\n\n            // We do not recommend unencrypted HTTP communications to Azure Repos,\n            // but we report `true` here for HTTP so that we can show a helpful\n            // error message for the user in `CreateCredentialAsync`.\n            return input.TryGetHostAndPort(out string hostName, out _)\n                   && (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\") ||\n                       StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"https\")) &&\n                   UriHelpers.IsAzureDevOpsHost(hostName);\n        }\n\n        public bool IsSupported(HttpResponseMessage response)\n        {\n            // Azure DevOps Server (TFS) is handled by the generic provider, which supports basic auth, and WIA detection.\n            return false;\n        }\n\n        public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)\n        {\n            if (UseManagedIdentity(out string mid))\n            {\n                _context.Trace.WriteLine($\"Getting Azure Access Token for managed identity {mid}...\");\n                var azureResult = await _msAuth.GetTokenForManagedIdentityAsync(mid, AzureDevOpsConstants.AzureDevOpsResourceId);\n                return new GetCredentialResult(\n                    new GitCredential(mid, azureResult.AccessToken)\n                );\n            }\n\n            if (UseServicePrincipal(out ServicePrincipalIdentity sp))\n            {\n                _context.Trace.WriteLine($\"Getting Azure Access Token for service principal {sp.TenantId}/{sp.Id}...\");\n                var azureResult = await _msAuth.GetTokenForServicePrincipalAsync(sp, AzureDevOpsConstants.AzureDevOpsDefaultScopes);\n                return new GetCredentialResult(\n                    new GitCredential(sp.Id, azureResult.AccessToken)\n                );\n            }\n\n            if (UsePersonalAccessTokens())\n            {\n                Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true);\n                string service = GetServiceName(remoteWithUserUri);\n                string account = GetAccountNameForCredentialQuery(input);\n\n                _context.Trace.WriteLine($\"Looking for existing credential in store with service={service} account={account}...\");\n\n                ICredential credential = _context.CredentialStore.Get(service, account);\n                if (credential == null)\n                {\n                    _context.Trace.WriteLine(\"No existing credentials found.\");\n\n                    // No existing credential was found, create a new one\n                    _context.Trace.WriteLine(\"Creating new credential...\");\n                    credential = await GeneratePersonalAccessTokenAsync(input);\n                    _context.Trace.WriteLine(\"Credential created.\");\n                }\n                else\n                {\n                    _context.Trace.WriteLine(\"Existing credential found.\");\n                }\n\n                return new GetCredentialResult(credential);\n            }\n            else\n            {\n                // Include the username request here so that we may use it as an override\n                // for user account lookups when getting Azure Access Tokens.\n                var azureResult = await GetAzureAccessTokenAsync(input);\n                var azureCredential = new GitCredential(azureResult.AccountUpn, azureResult.AccessToken);\n                return new GetCredentialResult(azureCredential);\n            }\n        }\n\n        public Task StoreCredentialAsync(InputArguments input)\n        {\n            Uri remoteUri = input.GetRemoteUri();\n\n            if (UseManagedIdentity(out _))\n            {\n                _context.Trace.WriteLine(\"Nothing to store for managed identity authentication.\");\n            }\n            else if (UseServicePrincipal(out _))\n            {\n                _context.Trace.WriteLine(\"Nothing to store for service principal authentication.\");\n            }\n            else if (UsePersonalAccessTokens())\n            {\n                string service = GetServiceName(remoteUri);\n\n                // We always store credentials against the given username argument for\n                // both vs.com and dev.azure.com-style URLs.\n                string account = input.UserName;\n\n                // Add or update the credential in the store.\n                _context.Trace.WriteLine($\"Storing credential with service={service} account={account}...\");\n                _context.CredentialStore.AddOrUpdate(service, account, input.Password);\n                _context.Trace.WriteLine(\"Credential was successfully stored.\");\n            }\n            else\n            {\n                string orgName = UriHelpers.GetOrganizationName(remoteUri);\n                _context.Trace.WriteLine($\"Signing user {input.UserName} in to organization '{orgName}'...\");\n                _bindingManager.SignIn(orgName, input.UserName);\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public Task EraseCredentialAsync(InputArguments input)\n        {\n            Uri remoteUri = input.GetRemoteUri();\n\n            if (UseManagedIdentity(out _))\n            {\n                _context.Trace.WriteLine(\"Nothing to erase for managed identity authentication.\");\n            }\n            else if (UseServicePrincipal(out _))\n            {\n                _context.Trace.WriteLine(\"Nothing to erase for service principal authentication.\");\n            }\n            else if (UsePersonalAccessTokens())\n            {\n                string service = GetServiceName(remoteUri);\n                string account = GetAccountNameForCredentialQuery(input);\n\n                // Try to locate an existing credential\n                _context.Trace.WriteLine(\n                    $\"Erasing stored credential in store with service={service} account={account}...\");\n                if (_context.CredentialStore.Remove(service, account))\n                {\n                    _context.Trace.WriteLine(\"Credential was successfully erased.\");\n                }\n                else\n                {\n                    _context.Trace.WriteLine(\"No credential was erased.\");\n                }\n            }\n            else\n            {\n                string orgName = UriHelpers.GetOrganizationName(remoteUri);\n\n                _context.Trace.WriteLine($\"Signing out of organization '{orgName}'...\");\n                _bindingManager.SignOut(orgName);\n\n                // Clear the authority cache in case this was the reason for failure\n                _authorityCache.EraseAuthority(orgName);\n            }\n\n            return Task.CompletedTask;\n        }\n\n        protected override void ReleaseManagedResources()\n        {\n            _azDevOps.Dispose();\n            base.ReleaseManagedResources();\n        }\n\n        private void ThrowIfUnsafeRemote(InputArguments input)\n        {\n            if (!_context.Settings.AllowUnsafeRemotes &&\n                StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, \"http\"))\n            {\n                throw new Trace2Exception(_context.Trace2,\n                    \"Unencrypted HTTP is not recommended for Azure Repos. \" +\n                    \"Ensure the repository remote URL is using HTTPS \" +\n                    $\"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.\");\n            }\n        }\n\n        private async Task<ICredential> GeneratePersonalAccessTokenAsync(InputArguments input)\n        {\n            ThrowIfDisposed();\n            ThrowIfUnsafeRemote(input);\n\n            Uri remoteUserUri = input.GetRemoteUri(includeUser: true);\n            Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUserUri, out _);\n\n            // Determine the MS authentication authority for this organization\n            _context.Trace.WriteLine(\"Determining Microsoft Authentication Authority...\");\n            string authAuthority = await _azDevOps.GetAuthorityAsync(orgUri);\n            _context.Trace.WriteLine($\"Authority is '{authAuthority}'.\");\n\n            // Get an AAD access token for the Azure DevOps SPS\n            _context.Trace.WriteLine(\"Getting Azure AD access token...\");\n            IMicrosoftAuthenticationResult result = await _msAuth.GetTokenForUserAsync(\n                authAuthority,\n                GetClientId(),\n                GetRedirectUri(),\n                AzureDevOpsConstants.AzureDevOpsDefaultScopes,\n                null,\n                msaPt: true);\n            _context.Trace.WriteLineSecrets(\n                $\"Acquired Azure access token. Account='{result.AccountUpn}' Token='{{0}}'\", new object[] {result.AccessToken});\n\n            // Ask the Azure DevOps instance to create a new PAT\n            var patScopes = new[]\n            {\n                AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite,\n                AzureDevOpsConstants.PersonalAccessTokenScopes.ArtifactsRead\n            };\n            _context.Trace.WriteLine($\"Creating Azure DevOps PAT with scopes '{string.Join(\", \", patScopes)}'...\");\n            string pat = await _azDevOps.CreatePersonalAccessTokenAsync(\n                orgUri,\n                result.AccessToken,\n                patScopes);\n            _context.Trace.WriteLineSecrets(\"PAT created. PAT='{0}'\", new object[] {pat});\n\n            return new GitCredential(result.AccountUpn, pat);\n        }\n\n        private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(InputArguments input)\n        {\n            ThrowIfUnsafeRemote(input);\n\n            Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true);\n            string userName = input.UserName;\n\n            Uri orgUri = UriHelpers.CreateOrganizationUri(remoteWithUserUri, out string orgName);\n\n            _context.Trace.WriteLine($\"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'...\");\n            if (TryGetAuthorityFromHeaders(input.WwwAuth, out string authAuthority))\n            {\n                _context.Trace.WriteLine(\"Authority was found in WWW-Authenticate headers from Git input.\");\n            }\n            else\n            {\n                // Try to get the authority from the cache\n                authAuthority = _authorityCache.GetAuthority(orgName);\n                if (authAuthority is null)\n                {\n                    // If there is no cached value we must query for it and cache it for future use\n                    _context.Trace.WriteLine($\"No cached authority value - querying {orgUri} for authority...\");\n                    authAuthority = await _azDevOps.GetAuthorityAsync(orgUri);\n                    _authorityCache.UpdateAuthority(orgName, authAuthority);\n                }\n                else\n                {\n                    _context.Trace.WriteLine(\"Authority was found in cache.\");\n                }\n            }\n\n            _context.Trace.WriteLine($\"Authority is '{authAuthority}'.\");\n\n            //\n            // If the remote URI is a classic \"*.visualstudio.com\" host name and we have a user specified from the\n            // remote then take that as the current AAD/MSA user in the first instance.\n            //\n            // For \"dev.azure.com\" host names we only use the user info part of the remote when this doesn't\n            // match the Azure DevOps organization name. Our friends in Azure DevOps decided \"borrow\" the username\n            // part of the remote URL to include the organization name (not an actual username).\n            //\n            // If we have no specified user from the remote (or this is org@dev.azure.com/org/..) then query the\n            // user manager for a bound user for this organization, if one exists...\n            //\n            var icmp = StringComparer.OrdinalIgnoreCase;\n            if (!string.IsNullOrWhiteSpace(userName) &&\n                (UriHelpers.IsVisualStudioComHost(remoteWithUserUri.Host) ||\n                 (UriHelpers.IsAzureDevOpsHost(remoteWithUserUri.Host) && !icmp.Equals(orgName, userName))))\n            {\n                _context.Trace.WriteLine(\"Using username as specified in remote.\");\n            }\n            else\n            {\n                _context.Trace.WriteLine($\"Looking up user for organization '{orgName}'...\");\n                userName = _bindingManager.GetUser(orgName);\n            }\n\n            _context.Trace.WriteLine(string.IsNullOrWhiteSpace(userName) ? \"No user found.\" : $\"User is '{userName}'.\");\n\n            // Get an AAD access token for the Azure DevOps SPS\n            _context.Trace.WriteLine(\"Getting Azure AD access token...\");\n            IMicrosoftAuthenticationResult result = await _msAuth.GetTokenForUserAsync(\n                authAuthority,\n                GetClientId(),\n                GetRedirectUri(),\n                AzureDevOpsConstants.AzureDevOpsDefaultScopes,\n                userName,\n                msaPt: true);\n            _context.Trace.WriteLineSecrets(\n                $\"Acquired Azure access token. Account='{result.AccountUpn}' Token='{{0}}'\", new object[] {result.AccessToken});\n\n            return result;\n        }\n\n        internal /* for testing purposes */ static bool TryGetAuthorityFromHeaders(IEnumerable<string> headers, out string authority)\n        {\n            authority = null;\n\n            if (headers is null)\n            {\n                return false;\n            }\n\n            var regex = new Regex(@\"authorization_uri=\"\"?(?<authority>.+)\"\"?\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n            foreach (string header in headers)\n            {\n                Match match = regex.Match(header);\n                if (match.Success)\n                {\n                    authority = match.Groups[\"authority\"].Value.Trim(new[] { '\"', '\\'' });\n                    return true;\n                }\n            }\n\n            return false;\n        }\n\n        private string GetClientId()\n        {\n            // Check for developer override value\n            if (_context.Settings.TryGetSetting(\n                    AzureDevOpsConstants.EnvironmentVariables.DevAadClientId,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    AzureDevOpsConstants.GitConfiguration.Credential.DevAadClientId,\n                    out string clientId))\n            {\n                return clientId;\n            }\n\n            return AzureDevOpsConstants.AadClientId;\n        }\n\n        private Uri GetRedirectUri()\n        {\n            // Check for developer override value\n            if (_context.Settings.TryGetSetting(\n                    AzureDevOpsConstants.EnvironmentVariables.DevAadRedirectUri,\n                    Constants.GitConfiguration.Credential.SectionName, AzureDevOpsConstants.GitConfiguration.Credential.DevAadRedirectUri,\n                    out string redirectUriStr) &&\n                Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))\n            {\n                return redirectUri;\n            }\n\n            return AzureDevOpsConstants.AadRedirectUri;\n        }\n\n        /// <remarks>\n        /// For dev.azure.com-style URLs we use the path arg to get the Azure DevOps organization name.\n        /// We ensure the presence of the path arg by setting credential.useHttpPath = true at install time.\n        ///\n        /// The result of this workaround is that we are now unable to determine if the user wanted to store\n        /// credentials with the full path or not for dev.azure.com-style URLs.\n        ///\n        /// Rather than always assume we're storing credentials against the full path, and therefore resulting\n        /// in an personal access token being created per remote URL/repository, we never store against\n        /// the full path and always store with the organization URL \"dev.azure.com/org\".\n        ///\n        /// For visualstudio.com-style URLs we know the AzDevOps organization name from the host arg, and\n        /// don't set the useHttpPath option. This means if we get the full path for a vs.com-style URL\n        /// we can store against the full remote path (the intended design).\n        ///\n        /// Users that need to clone a repository from Azure Repos against the full path therefore must\n        /// use the vs.com-style remote URL and not the dev.azure.com one.\n        /// </remarks>\n        private static string GetServiceName(Uri remoteUri)\n        {\n            // dev.azure.com\n            if (UriHelpers.IsDevAzureComHost(remoteUri.Host))\n            {\n                // We can never store the new dev.azure.com-style URLs against the full path because\n                // we have forced the useHttpPath option to true to in order to retrieve the AzDevOps\n                // organization name from Git.\n                return UriHelpers.CreateOrganizationUri(remoteUri, out _).AbsoluteUri.TrimEnd('/');\n            }\n\n            // *.visualstudio.com\n            if (UriHelpers.IsVisualStudioComHost(remoteUri.Host))\n            {\n                // If we're given the full path for an older *.visualstudio.com-style URL then we should\n                // respect that in the service name.\n                return remoteUri.WithoutUserInfo().AbsoluteUri.TrimEnd('/');\n            }\n\n            throw new InvalidOperationException(\"Host is not Azure DevOps.\");\n        }\n\n        private static string GetAccountNameForCredentialQuery(InputArguments input)\n        {\n            if (!input.TryGetHostAndPort(out string hostName, out _))\n            {\n                throw new InvalidOperationException(\"Failed to parse host name and/or port\");\n            }\n\n            // dev.azure.com\n            if (UriHelpers.IsDevAzureComHost(hostName))\n            {\n                // We ignore the given username for dev.azure.com-style URLs because AzDevOps recommends\n                // adding the organization name as the user in the remote URL (resulting in URLs like\n                // https://org@dev.azure.com/org/foo/_git/bar) and we don't know if the given username\n                // is an actual username, or the org name.\n                // Use `null` as the account name so we match all possible credentials (regardless of\n                // the account).\n                return null;\n            }\n\n            // *.visualstudio.com\n            if (UriHelpers.IsVisualStudioComHost(hostName))\n            {\n                // If we're given a username for the vs.com-style URLs we can and should respect any\n                // specified username in the remote URL/input arguments.\n                return input.UserName;\n            }\n\n            throw new InvalidOperationException(\"Host is not Azure DevOps.\");\n        }\n\n        /// <summary>\n        /// Check if Azure DevOps Personal Access Tokens should be used or not.\n        /// </summary>\n        /// <returns>True if Personal Access Tokens should be used, false otherwise.</returns>\n        private bool UsePersonalAccessTokens()\n        {\n            // Default to using PATs except on DevBox where we prefer OAuth tokens\n            bool defaultValue = !PlatformUtils.IsDevBox();\n\n            if (_context.Settings.TryGetSetting(\n                AzureDevOpsConstants.EnvironmentVariables.CredentialType,\n                KnownGitCfg.Credential.SectionName,\n                AzureDevOpsConstants.GitConfiguration.Credential.CredentialType,\n                out string valueStr))\n            {\n                _context.Trace.WriteLine($\"Azure Repos credential type override set to '{valueStr}'\");\n\n                switch (valueStr.ToLowerInvariant())\n                {\n                    case AzureDevOpsConstants.PatCredentialType:\n                        return true;\n\n                    case AzureDevOpsConstants.OAuthCredentialType:\n                        return false;\n\n                    default:\n                        _context.Streams.Error.WriteLine(\n                            $\"warning: unknown Azure Repos credential type '{valueStr}' - using PATs\");\n                        return defaultValue;\n                }\n            }\n\n            return defaultValue;\n        }\n\n        private bool UseServicePrincipal(out ServicePrincipalIdentity sp)\n        {\n            if (!_context.Settings.TryGetSetting(\n                    AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalId,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipal,\n                    out string spStr) || string.IsNullOrWhiteSpace(spStr))\n            {\n                sp = null;\n                return false;\n            }\n\n            string[] split = spStr.Split(new[] { '/' }, count: 2);\n\n            if (split.Length < 1 || string.IsNullOrWhiteSpace(split[0]))\n            {\n                _context.Streams.Error.WriteLine(\"error: unable to use configured service principal - missing tenant ID in configuration\");\n                sp = null;\n                return false;\n            }\n\n            if (split.Length < 2 || string.IsNullOrWhiteSpace(split[1]))\n            {\n                _context.Streams.Error.WriteLine(\"error: unable to use configured service principal - missing client ID in configuration\");\n                sp = null;\n                return false;\n            }\n\n            string tenantId = split[0];\n            string clientId = split[1];\n\n            sp = new ServicePrincipalIdentity\n            {\n                Id = clientId,\n                TenantId = tenantId,\n            };\n\n            bool hasClientSecret = _context.Settings.TryGetSetting(\n                AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalSecret,\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipalSecret,\n                out string clientSecret);\n\n            bool hasCertThumbprint = _context.Settings.TryGetSetting(\n                AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalCertificateThumbprint,\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipalCertificateThumbprint,\n                out string certThumbprint);\n\n            if (hasCertThumbprint && hasClientSecret)\n            {\n                _context.Streams.Error.WriteLine(\"warning: both service principal client secret and certificate thumbprint are configured - using certificate\");\n            }\n\n            if (hasCertThumbprint)\n            {\n                sp.SendX5C = _context.Settings.TryGetSetting(\n                    AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalCertificateSendX5C,\n                    Constants.GitConfiguration.Credential.SectionName,\n                    AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipalCertificateSendX5C,\n                    out string certHasX5CStr) && certHasX5CStr.ToBooleanyOrDefault(false);\n\n                X509Certificate2 cert = X509Utils.GetCertificateByThumbprint(certThumbprint);\n                if (cert is null)\n                {\n                    _context.Streams.Error.WriteLine($\"error: unable to find certificate with thumbprint '{certThumbprint}' for service principal\");\n                    return false;\n                }\n\n                sp.Certificate = cert;\n            }\n            else if (hasClientSecret)\n            {\n                sp.ClientSecret = clientSecret;\n            }\n\n            return true;\n        }\n\n        private bool UseManagedIdentity(out string mid)\n        {\n            return _context.Settings.TryGetSetting(\n                       AzureDevOpsConstants.EnvironmentVariables.ManagedIdentity,\n                       KnownGitCfg.Credential.SectionName,\n                       AzureDevOpsConstants.GitConfiguration.Credential.ManagedIdentity,\n                       out mid) &&\n                   !string.IsNullOrWhiteSpace(mid);\n        }\n\n        #endregion\n\n        #region IConfigurationComponent\n\n        string IConfigurableComponent.Name => \"Azure Repos provider\";\n\n        public Task ConfigureAsync(ConfigurationTarget target)\n        {\n            string useHttpPathKey = $\"{KnownGitCfg.Credential.SectionName}.https://dev.azure.com.{KnownGitCfg.Credential.UseHttpPath}\";\n\n            GitConfigurationLevel configurationLevel = target == ConfigurationTarget.System\n                ? GitConfigurationLevel.System\n                : GitConfigurationLevel.Global;\n\n            IGitConfiguration targetConfig = _context.Git.GetConfiguration();\n\n            if (targetConfig.TryGet(useHttpPathKey, false, out string currentValue) && currentValue.IsTruthy())\n            {\n                _context.Trace.WriteLine(\"Git configuration 'credential.useHttpPath' is already set to 'true' for https://dev.azure.com.\");\n            }\n            else\n            {\n                _context.Trace.WriteLine(\"Setting Git configuration 'credential.useHttpPath' to 'true' for https://dev.azure.com...\");\n                targetConfig.Set(configurationLevel, useHttpPathKey, \"true\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public Task UnconfigureAsync(ConfigurationTarget target)\n        {\n            string helperKey = $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\n            string useHttpPathKey = $\"{KnownGitCfg.Credential.SectionName}.https://dev.azure.com.{KnownGitCfg.Credential.UseHttpPath}\";\n\n            _context.Trace.WriteLine(\"Clearing Git configuration 'credential.useHttpPath' for https://dev.azure.com...\");\n\n            GitConfigurationLevel configurationLevel = target == ConfigurationTarget.System\n                ? GitConfigurationLevel.System\n                : GitConfigurationLevel.Global;\n\n            IGitConfiguration targetConfig = _context.Git.GetConfiguration();\n\n            // On Windows, if there is a \"manager\" or \"manager-core\" entry remaining in the system config then we must\n            // not clear the useHttpPath option otherwise this would break the bundled version of GCM in Git for Windows.\n            if (!PlatformUtils.IsWindows() || target != ConfigurationTarget.System ||\n                targetConfig.GetAll(helperKey).All(x => !string.Equals(x, \"manager\") && !string.Equals(x, \"manager-core\")))\n            {\n                targetConfig.Unset(configurationLevel, useHttpPathKey);\n            }\n\n            return Task.CompletedTask;\n        }\n\n        #endregion\n\n        #region ICommandProvider\n\n        ProviderCommand ICommandProvider.CreateCommand()\n        {\n            //\n            // clear-cache\n            //\n            var clearCacheCmd = new Command(\"clear-cache\", \"Clear the Azure authority cache\");\n            clearCacheCmd.SetHandler(ClearCacheCmd);\n\n            //\n            // list <organization> [--show-remotes] [--verbose]\n            //\n            var listCmd = new Command(\"list\", \"List all user account bindings\");\n            var orgFilterArg = new Argument<string>(\"organization\", \"(optional) Filter results by Azure DevOps organization name\")\n            {\n                Arity = ArgumentArity.ZeroOrOne\n            };\n            var remoteOpt = new Option<bool>(\"--show-remotes\")\n            {\n                Description = \"Also show Azure DevOps remote user bindings for the current repository\"\n            };\n            var verboseOpt = new Option<bool>(new[] { \"--verbose\", \"-v\" }, \"Verbose output - show remote URLs\");\n            listCmd.AddArgument(orgFilterArg);\n            listCmd.AddOption(remoteOpt);\n            listCmd.AddOption(verboseOpt);\n            listCmd.SetHandler(ListCmd, orgFilterArg, remoteOpt, verboseOpt);\n\n            //\n            // bind <organization> <username> [--local]\n            //\n            var bindCmd = new Command(\"bind\", \"Bind a user account to an Azure DevOps organization\");\n            var orgArg = new Argument<string>(\"organization\", \"Azure DevOps organization name\")\n            {\n                Arity = ArgumentArity.ExactlyOne\n            };\n            var userNameArg = new Argument<string>(\"username\", \"Username or email (e.g.: alice@example.com)\")\n            {\n                Arity = ArgumentArity.ExactlyOne\n            };\n            var localOpt = new Option<bool>(\"--local\", \"Target the local repository Git configuration\");\n            bindCmd.AddArgument(orgArg);\n            bindCmd.AddArgument(userNameArg);\n            bindCmd.AddOption(localOpt);\n            bindCmd.SetHandler(BindCmd, orgArg, userNameArg, localOpt);\n\n            //\n            // unbind <organization> [--local]\n            //\n            var unbindCmd = new Command(\"unbind\")\n            {\n                Description = \"Remove user account binding for an Azure DevOps organization\",\n            };\n            unbindCmd.AddArgument(orgArg);\n            unbindCmd.AddOption(localOpt);\n            unbindCmd.SetHandler(UnbindCmd, orgArg, localOpt);\n\n            var rootCmd = new ProviderCommand(this);\n            rootCmd.AddCommand(listCmd);\n            rootCmd.AddCommand(bindCmd);\n            rootCmd.AddCommand(unbindCmd);\n            rootCmd.AddCommand(clearCacheCmd);\n            return rootCmd;\n        }\n\n        private void ClearCacheCmd()\n        {\n            _authorityCache.Clear();\n            _context.Streams.Out.WriteLine(\"Authority cache cleared\");\n        }\n\n        private class RemoteBinding\n        {\n            public string Remote { get; set; }\n            public bool IsPush { get; set; }\n            public Uri Uri { get; set; }\n        }\n\n        private void ListCmd(string organization, bool showRemotes, bool verbose)\n        {\n            // Get all organization bindings from the user manager\n            IList<AzureReposBinding> bindings = _bindingManager.GetBindings(organization).ToList();\n            IDictionary<string, IEnumerable<AzureReposBinding>> orgBindingMap =\n                bindings.GroupBy(x => x.Organization).ToDictionary();\n\n            // If we are asked to also show remotes we build the remote binding map\n            var orgRemotesMap = new Dictionary<string, ICollection<RemoteBinding>>();\n            if (showRemotes)\n            {\n                if (!_context.Git.IsInsideRepository())\n                {\n                    _context.Streams.Error.WriteLine(\"warning: not inside a git repository (--show-remotes has no effect)\");\n                }\n\n                static bool IsAzureDevOpsHttpRemote(string url, out Uri uri)\n                {\n                    return Uri.TryCreate(url, UriKind.Absolute, out uri) &&\n                           (StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttp, uri.Scheme) ||\n                            StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttps, uri.Scheme)) &&\n                           UriHelpers.IsAzureDevOpsHost(uri.Host);\n                }\n\n                foreach (GitRemote remote in _context.Git.GetRemotes())\n                {\n                    if (IsAzureDevOpsHttpRemote(remote.FetchUrl, out Uri fetchUri))\n                    {\n                        string fetchOrg = UriHelpers.GetOrganizationName(fetchUri);\n                        var binding = new RemoteBinding {IsPush = false, Remote = remote.Name, Uri = fetchUri};\n                        orgRemotesMap.Append(fetchOrg, binding);\n                    }\n\n                    if (IsAzureDevOpsHttpRemote(remote.PushUrl, out Uri pushUri))\n                    {\n                        string pushOrg = UriHelpers.GetOrganizationName(pushUri);\n                        var binding = new RemoteBinding {IsPush = true, Remote = remote.Name, Uri = pushUri};\n                        orgRemotesMap.Append(pushOrg, binding);\n                    }\n                }\n            }\n\n            bool isFiltered = !string.IsNullOrWhiteSpace(organization);\n            string indent = isFiltered ? string.Empty : \"  \";\n\n            // Get the set of all organization names (organization names are not case sensitive)\n            ISet<string> orgNames = new HashSet<string>(orgBindingMap.Keys, StringComparer.OrdinalIgnoreCase);\n            orgNames.UnionWith(orgRemotesMap.Keys);\n\n            var icmp = StringComparer.OrdinalIgnoreCase;\n\n            foreach (string orgName in orgNames)\n            {\n                if (!isFiltered)\n                {\n                    _context.Streams.Out.WriteLine($\"{orgName}:\");\n                }\n\n                // Print organization bindings\n                foreach (AzureReposBinding binding in orgBindingMap.GetValues(orgName))\n                {\n                    if (binding.GlobalUserName != null)\n                    {\n                        _context.Streams.Out.WriteLine($\"{indent}(global) -> {binding.GlobalUserName}\");\n                    }\n\n                    if (binding.LocalUserName != null)\n                    {\n                        string value = string.IsNullOrEmpty(binding.LocalUserName)\n                            ? \"(no inherit)\"\n                            : binding.LocalUserName;\n                        _context.Streams.Out.WriteLine($\"{indent}(local)  -> {value}\");\n                    }\n                }\n\n                // Print remote bindings\n                IEnumerable<IGrouping<string, RemoteBinding>> remoteBindingMap =\n                    orgRemotesMap.GetValues(orgName).GroupBy(x => x.Remote);\n\n                foreach (var remoteBinding in remoteBindingMap)\n                {\n                    _context.Streams.Out.WriteLine($\"{indent}{remoteBinding.Key}:\");\n                    foreach (RemoteBinding binding in remoteBinding)\n                    {\n                        // User names in dev.azure.com URLs cannot always be used as *actual user names*\n                        // because of the unfortunate decision to use this field to get the Azure DevOps\n                        // organization name to be sent by Git to credential helpers.\n                        //\n                        // We show dev.azure.com URLs as \"inherit\", if there is a username that matches\n                        // the organization name.\n                        if (!binding.Uri.TryGetUserInfo(out string userName, out _) ||\n                            UriHelpers.IsDevAzureComHost(binding.Uri.Host) && icmp.Equals(userName, orgName))\n                        {\n                            userName = \"(inherit)\";\n                        }\n\n                        string url = null;\n                        if (verbose)\n                        {\n                            url = $\"{binding.Uri.WithoutUserInfo()} \";\n                        }\n\n                        _context.Streams.Out.WriteLine(binding.IsPush\n                            ? $\"{indent}  {url}(push)  -> {userName}\"\n                            : $\"{indent}  {url}(fetch) -> {userName}\");\n                    }\n                }\n            }\n        }\n\n        private Task<int> BindCmd(string organization, string userName, bool local)\n        {\n            if (local && !_context.Git.IsInsideRepository())\n            {\n                _context.Streams.Error.WriteLine(\"error: not inside a git repository (cannot use --local)\");\n                return Task.FromResult(-1);\n            }\n\n            _bindingManager.Bind(organization, userName, local);\n            return Task.FromResult(0);\n        }\n\n        private Task<int> UnbindCmd(string organization, bool local)\n        {\n            if (local && !_context.Git.IsInsideRepository())\n            {\n                _context.Streams.Error.WriteLine(\"error: not inside a git repository (cannot use --local)\");\n                return Task.FromResult(-1);\n            }\n\n            _bindingManager.Unbind(organization, local);\n            return Task.FromResult(0);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/InternalsVisibleTo.cs",
    "content": "using System.Runtime.CompilerServices;\n\n[assembly:InternalsVisibleTo(\"Microsoft.AzureRepos.Tests\")]\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/Microsoft.AzureRepos.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net8.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"'$(OSPlatform)'=='windows'\">net8.0;net472</TargetFrameworks>\n    <AssemblyName>Microsoft.AzureRepos</AssemblyName>\n    <RootNamespace>Microsoft.AzureRepos</RootNamespace>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' == 'net472'\">\n    <Reference Include=\"System.Net.Http\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos/UriHelpers.cs",
    "content": "using System;\nusing System.Linq;\nusing GitCredentialManager;\n\nnamespace Microsoft.AzureRepos\n{\n    internal static class UriHelpers\n    {\n        /// <summary>\n        /// Combine two parts of a URI path element with the '/' character.\n        /// </summary>\n        /// <param name=\"basePath\">Left/base URI path element.</param>\n        /// <param name=\"path\">Right/appending URI path element.</param>\n        /// <returns>Concatenated URI path.</returns>\n        public static string CombinePath(string basePath, string path)\n        {\n            if (basePath.Length > 0 && path.Length > 0)\n            {\n                char lastBasePath = basePath.Last();\n                char firstPath = path.First();\n\n                if (lastBasePath == '/' && firstPath == '/')\n                {\n                    return basePath + path.Substring(1);\n                }\n                if (lastBasePath != '/' && firstPath != '/')\n                {\n                    return basePath + '/' + path;\n                }\n            }\n\n            return basePath + path;\n        }\n\n        /// <summary>\n        /// Check if the hostname is the legacy Azure DevOps hostname (*.visualstudio.com).\n        /// </summary>\n        /// <param name=\"input\">Git query arguments.</param>\n        /// <returns>True if the hostname is the legacy Azure DevOps host, false otherwise.</returns>\n        public static bool IsVisualStudioComHost(InputArguments input)\n        {\n            EnsureArgument.NotNull(input, nameof(input));\n\n            if (!input.TryGetHostAndPort(out string hostName, out _))\n            {\n                throw new InvalidOperationException(\"Host name and/or port is invalid.\");\n            }\n\n            return IsVisualStudioComHost(hostName);\n        }\n\n        /// <summary>\n        /// Check if the hostname is the legacy Azure DevOps hostname (*.visualstudio.com).\n        /// </summary>\n        /// <param name=\"host\">Hostname to check.</param>\n        /// <returns>True if the hostname is the legacy Azure DevOps host, false otherwise.</returns>\n        public static bool IsVisualStudioComHost(string host)\n        {\n            return host != null &&\n                   host.EndsWith(AzureDevOpsConstants.VstsHostSuffix, StringComparison.OrdinalIgnoreCase);\n        }\n\n        /// <summary>\n        /// Check if the hostname is the new Azure DevOps hostname (dev.azure.com).\n        /// </summary>\n        /// <param name=\"input\">Git query arguments.</param>\n        /// <returns>True if the hostname is the new Azure DevOps host, false otherwise.</returns>\n        public static bool IsDevAzureComHost(InputArguments input)\n        {\n            EnsureArgument.NotNull(input, nameof(input));\n\n            if (!input.TryGetHostAndPort(out string hostName, out _))\n            {\n                throw new InvalidOperationException(\"Host name and/or port is invalid.\");\n            }\n\n            return IsDevAzureComHost(hostName);\n        }\n\n        /// <summary>\n        /// Check if the hostname is the new Azure DevOps hostname (dev.azure.com).\n        /// </summary>\n        /// <param name=\"host\">Hostname to check.</param>\n        /// <returns>True if the hostname is the new Azure DevOps host, false otherwise.</returns>\n        public static bool IsDevAzureComHost(string host)\n        {\n            return host != null &&\n                   StringComparer.OrdinalIgnoreCase.Equals(host, AzureDevOpsConstants.AzureDevOpsHost);\n        }\n\n        /// <summary>\n        /// Check if the hostname is a valid Azure DevOps hostname (dev.azure.com or *.visualstudio.com).\n        /// </summary>\n        /// <param name=\"host\">Hostname to check</param>\n        /// <returns>True if the hostname is Azure DevOps, false otherwise.</returns>\n        public static bool IsAzureDevOpsHost(string host)\n        {\n            return IsVisualStudioComHost(host) || IsDevAzureComHost(host);\n        }\n\n        public static string GetOrganizationName(Uri remoteUri)\n        {\n            CreateOrganizationUri(remoteUri, out string orgName);\n            return orgName;\n        }\n\n        /// <summary>\n        /// Create a URI for the Azure DevOps organization from the Git remote URI.\n        /// </summary>\n        /// <param name=\"remoteUri\">Git remote URI arguments.</param>\n        /// <param name=\"orgName\">Azure DevOps organization name.</param>\n        /// <returns>Azure DevOps organization URI</returns>\n        /// <exception cref=\"InvalidOperationException\">\n        /// Thrown if <see cref=\"InputArguments.Protocol\"/> is null or white space.\n        /// <para/>\n        /// Thrown if <see cref=\"InputArguments.Host\"/> is null or white space.\n        /// <para/>\n        /// Thrown if <see cref=\"InputArguments.Host\"/> is not an Azure DevOps hostname.\n        /// <para/>\n        /// Thrown if both of <see cref=\"InputArguments.UserName\"/> or <see cref=\"InputArguments.Path\"/>\n        /// are null or white space when <see cref=\"InputArguments.Host\"/> is an Azure-style URL\n        /// ('dev.azure.com' rather than '*.visualstudio.com').\n        /// </exception>\n        public static Uri CreateOrganizationUri(Uri remoteUri, out string orgName)\n        {\n            EnsureArgument.AbsoluteUri(remoteUri, nameof(remoteUri));\n\n            orgName = null;\n\n            if (!IsAzureDevOpsHost(remoteUri.Host))\n            {\n                throw new InvalidOperationException(\"Host is not Azure DevOps.\");\n            }\n\n            var ub = new UriBuilder\n            {\n                Scheme = remoteUri.Scheme,\n                Host = remoteUri.Host,\n            };\n\n            if (!remoteUri.IsDefaultPort)\n            {\n                ub.Port = remoteUri.Port;\n            }\n\n            // Extract the organization name for Azure ('dev.azure.com') style URLs.\n            // The older *.visualstudio.com URLs contained the organization name in the host already.\n            if (IsDevAzureComHost(remoteUri.Host))\n            {\n                string firstPathComponent = GetFirstPathComponent(remoteUri.AbsolutePath);\n                string remoteUriUserName = remoteUri.GetUserName();\n\n                // Prefer getting the org name from the path: dev.azure.com/{org}\n                if (!string.IsNullOrWhiteSpace(firstPathComponent))\n                {\n                    orgName = firstPathComponent;\n                }\n                // Failing that try using the username: {org}@dev.azure.com\n                else if (!string.IsNullOrWhiteSpace(remoteUriUserName))\n                {\n                    orgName = remoteUriUserName;\n                }\n                else\n                {\n                    throw new InvalidOperationException(\n                        \"Cannot determine the organization name for this 'dev.azure.com' remote URL. \" +\n                        \"Ensure the `credential.useHttpPath` configuration value is set, or set the organization \" +\n                        \"name as the user in the remote URL '{org}@dev.azure.com'.\"\n                    );\n                }\n\n                ub.Path = orgName;\n            }\n            else if (IsVisualStudioComHost(remoteUri.Host))\n            {\n                // {org}.visualstudio.com\n                int orgNameLength = remoteUri.Host.Length - AzureDevOpsConstants.VstsHostSuffix.Length;\n                orgName = remoteUri.Host.Substring(0, orgNameLength);\n            }\n\n            return ub.Uri;\n        }\n\n        public static string GetFirstPathComponent(string path)\n        {\n            string[] parts = path?.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);\n            if (parts?.Length > 0)\n            {\n                return parts[0];\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Microsoft.AzureRepos.Tests\n{\n    public class AzureDevOpsApiTests\n    {\n        private const string ExpectedLocationServicePath = \"_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381?api-version=1.0\";\n        private const string ExpectedIdentityServicePath = \"_apis/token/sessiontokens?api-version=1.0&tokentype=compact\";\n        private const string CommonAuthority = \"https://login.microsoftonline.com/common\";\n        private const string OrganizationsAuthority = \"https://login.microsoftonline.com/organizations\";\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_NullUri_ThrowsException()\n        {\n            var api = new AzureDevOpsRestApi(new TestCommandContext());\n\n            await Assert.ThrowsAsync<ArgumentNullException>(() => api.GetAuthorityAsync(null));\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_NoNetwork_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n\n            var httpHandler = new TestHttpMessageHandler {SimulateNoNetwork = true};\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            await Assert.ThrowsAsync<HttpRequestException>(() => api.GetAuthorityAsync(uri));\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_NoHeaders_ReturnsCommonAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n\n            const string expectedAuthority = CommonAuthority;\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_WwwAuthenticateBearer_ReturnsAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n\n            const string expectedAuthority = \"https://login.microsoftonline.com/test-authority\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            httpResponse.Headers.WwwAuthenticate.ParseAdd($\"Bearer authorization_uri={expectedAuthority}\");\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_WwwAuthenticateMultiple_ReturnsBearerAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n\n            const string expectedAuthority = \"https://login.microsoftonline.com/test-authority\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            httpResponse.Headers.WwwAuthenticate.ParseAdd(\"Bearer\");\n            httpResponse.Headers.WwwAuthenticate.ParseAdd($\"Bearer authorization_uri={expectedAuthority}\");\n            httpResponse.Headers.WwwAuthenticate.ParseAdd(\"NTLM [test-challenge-string]\");\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_VssResourceTenantAad_ReturnsAadAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n            var aadTenantId = Guid.NewGuid();\n\n            string expectedAuthority = $\"https://login.microsoftonline.com/{aadTenantId:D}\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized)\n            {\n                Headers = {{AzureDevOpsConstants.VssResourceTenantHeader, aadTenantId.ToString(\"D\")}}\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_VssResourceTenantMultiple_ReturnsFirstAadAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n            var aadTenantId1 = Guid.NewGuid();\n            var msaTenantId  = Guid.Empty;\n            var aadTenantId2 = Guid.NewGuid();\n\n            string expectedAuthority = $\"https://login.microsoftonline.com/{aadTenantId1:D}\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized)\n            {\n                Headers =\n                {\n                    {AzureDevOpsConstants.VssResourceTenantHeader, aadTenantId1.ToString(\"D\")},\n                    {AzureDevOpsConstants.VssResourceTenantHeader, msaTenantId.ToString(\"D\")},\n                    {AzureDevOpsConstants.VssResourceTenantHeader, aadTenantId2.ToString(\"D\")},\n                }\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_VssResourceTenantMsa_ReturnsOrganizationsAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n            var msaTenantId = Guid.Empty;\n\n            // This is only the case because we're using MSA pass-through.. in the future, if and when we\n            // move away from MSA pass-through, this should be the common authority.\n            const string expectedAuthority = OrganizationsAuthority;\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized)\n            {\n                Headers = {{AzureDevOpsConstants.VssResourceTenantHeader, msaTenantId.ToString(\"D\")}}\n            };\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_GetAuthorityAsync_BothWwwAuthAndVssResourceHeaders_ReturnsWwwAuthAuthority()\n        {\n            var context = new TestCommandContext();\n            var uri = new Uri(\"https://example.com\");\n            var aadTenantIdWwwAuth = Guid.NewGuid();\n            var aadTenantIdVssRes = Guid.NewGuid();\n\n            string expectedAuthority = $\"https://login.microsoftonline.com/{aadTenantIdWwwAuth:D}\";\n\n            var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);\n            httpResponse.Headers.Add(AzureDevOpsConstants.VssResourceTenantHeader, aadTenantIdVssRes.ToString(\"D\"));\n            httpResponse.Headers.WwwAuthenticate.ParseAdd($\"Bearer authorization_uri={expectedAuthority}\");\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Head, uri, httpResponse);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualAuthority = await api.GetAuthorityAsync(uri);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_ReturnsPAT()\n        {\n            var context = new TestCommandContext();\n            var orgUri = new Uri(\"https://dev.azure.com/org/\");\n\n            const string expectedPat = \"PERSONAL-ACCESS-TOKEN\";\n            string accessToken = \"ACCESS-TOKEN\";\n            IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};\n\n            var identityServiceUri = new Uri(\"https://identity.example.com/\");\n\n            var locSvcRequestUri = new Uri(orgUri, ExpectedLocationServicePath);\n            var locSvcResponse = CreateLocationServiceResponse(identityServiceUri);\n\n            var identSvcRequestUri = new Uri(identityServiceUri, ExpectedIdentityServicePath);\n            var identSvcResponse = CreateIdentityServiceResponse(expectedPat);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Get, locSvcRequestUri, x =>\n            {\n                AssertAcceptJson(x);\n                AssertBearerToken(x, accessToken);\n                return locSvcResponse;\n            });\n            httpHandler.Setup(HttpMethod.Post, identSvcRequestUri, x =>\n            {\n                AssertAcceptJson(x);\n                AssertBearerToken(x, accessToken);\n                return identSvcResponse;\n            });\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            string actualPat = await api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes);\n\n            Assert.Equal(expectedPat, actualPat);\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_LocSvcReturnsHttp500_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var orgUri = new Uri(\"https://dev.azure.com/org/\");\n\n            string accessToken = \"ACCESS-TOKEN\";\n            IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};\n\n            var locSvcRequestUri = new Uri(orgUri, ExpectedLocationServicePath);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Get, locSvcRequestUri, HttpStatusCode.InternalServerError);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            await Assert.ThrowsAsync<Trace2Exception>(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes));\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcReturnsHttp500_ThrowsException()\n        {\n            var context = new TestCommandContext();\n            var orgUri = new Uri(\"https://dev.azure.com/org/\");\n\n            string accessToken = \"ACCESS-TOKEN\";\n            IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};\n\n            var identityServiceUri = new Uri(\"https://identity.example.com/\");\n\n            var locSvcRequestUri = new Uri(orgUri, ExpectedLocationServicePath);\n            var locSvcResponse = CreateLocationServiceResponse(identityServiceUri);\n\n            var identSvcRequestUri = new Uri(identityServiceUri, ExpectedIdentityServicePath);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Get,  locSvcRequestUri,   x =>\n            {\n                AssertAcceptJson(x);\n                AssertBearerToken(x, accessToken);\n                return locSvcResponse;\n            });\n            httpHandler.Setup(HttpMethod.Post, identSvcRequestUri, HttpStatusCode.InternalServerError);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            await Assert.ThrowsAsync<Trace2Exception>(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes));\n        }\n\n        [Fact]\n        public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcReturnsHttp500WithError_ThrowsExceptionWithErrorMessage()\n        {\n            const string serverErrorMessage = \"ERROR123: This is a test error.\";\n\n            var context = new TestCommandContext();\n            var orgUri = new Uri(\"https://dev.azure.com/org/\");\n\n            string accessToken = \"ACCESS-TOKEN\";\n            IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};\n\n            var identityServiceUri = new Uri(\"https://identity.example.com/\");\n\n            var locSvcRequestUri = new Uri(orgUri, ExpectedLocationServicePath);\n            var locSvcResponse = CreateLocationServiceResponse(identityServiceUri);\n\n            var identSvcRequestUri = new Uri(identityServiceUri, ExpectedIdentityServicePath);\n            var identSvcError = CreateIdentityServiceErrorResponse(serverErrorMessage);\n\n            var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};\n            httpHandler.Setup(HttpMethod.Get, locSvcRequestUri, x =>\n            {\n                AssertAcceptJson(x);\n                AssertBearerToken(x, accessToken);\n                return locSvcResponse;\n            });\n            httpHandler.Setup(HttpMethod.Post, identSvcRequestUri, identSvcError);\n\n            context.HttpClientFactory.MessageHandler = httpHandler;\n            var api = new AzureDevOpsRestApi(context);\n\n            Exception exception = await Assert.ThrowsAsync<Trace2Exception>(\n                () => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes));\n\n            Assert.Contains(serverErrorMessage, exception.Message, StringComparison.Ordinal);\n        }\n\n        [Theory]\n        [InlineData(null, false, null)]\n        [InlineData(\"NotBearer\", false, null)]\n        [InlineData(\"Bearer\", false, null)]\n        [InlineData(\"Bearer foobar\", false, null)]\n        [InlineData(\"Bearer authorization_uri=https://example.com\", true, \"https://example.com\")]\n        public void AzureDevOpsRestApi_TryGetAuthorityFromHeader(string headerValue, bool expectedResult, string expectedAuthority)\n        {\n            var header = headerValue is null ? null : AuthenticationHeaderValue.Parse(headerValue);\n            bool actualResult = AzureDevOpsRestApi.TryGetAuthorityFromHeader(header, out string actualAuthority);\n\n            Assert.Equal(expectedResult, actualResult);\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Theory]\n        [InlineData(null, \"target\", false, null)]\n        [InlineData(\"{}\", \"target\", false, null)]\n        [InlineData(\"{\\\"foo\\\": \\\"123\\\"}\",\n                    \"target\", false, null)]\n        [InlineData(\"{\\\"target\\\": \\\"42\\\"}\",\n                    \"target\", true, \"42\")]\n        [InlineData(\"{\\\"TARGET\\\": \\\"42\\\"}\",\n                    \"target\", true, \"42\")]\n        [InlineData(\"{\\\"target\\\": \\\"42\\\", \\\"other\\\": { \\\"target\\\": \\\"123\\\" } }\",\n                    \"target\", true, \"42\")]\n        [InlineData(\"{\\\"foo\\\": { \\\"bar\\\": {\\\"target\\\":\\\"42\\\"} } }\",\n                    \"target\", true, \"42\")]\n        public void AzureDevOpsRestApi_TryGetFirstJsonStringField(\n            string json, string fieldName, bool expectedResult, string expectedValue)\n        {\n            bool actualResult = AzureDevOpsRestApi.TryGetFirstJsonStringField(json, fieldName, out string actualValue);\n\n            Assert.Equal(expectedResult, actualResult);\n            Assert.Equal(expectedValue, actualValue);\n        }\n\n        #region Helpers\n\n        private static void AssertHeader(HttpRequestMessage request, KeyValuePair<string, IEnumerable<string>> header)\n        {\n            AssertHeader(request, header.Key, header.Value);\n        }\n\n        private static void AssertHeader(HttpRequestMessage request, string headerName, IEnumerable<string> headerValues)\n        {\n            Assert.True(request.Headers.Contains(headerName));\n            Assert.Equal(headerValues, request.Headers.GetValues(headerName));\n        }\n\n        private static void AssertAcceptJson(HttpRequestMessage request)\n        {\n            IEnumerable<string> acceptMimeTypes = request.Headers.Accept.Select(x => x.MediaType);\n            Assert.Contains(Constants.Http.MimeTypeJson, acceptMimeTypes);\n        }\n\n        private static void AssertBearerToken(HttpRequestMessage request, string bearerToken)\n        {\n            AuthenticationHeaderValue authHeader = request.Headers.Authorization;\n            Assert.NotNull(authHeader);\n            Assert.Equal(\"Bearer\", authHeader.Scheme);\n            Assert.Equal(bearerToken, authHeader.Parameter);\n        }\n\n        private static HttpResponseMessage CreateLocationServiceResponse(Uri identityServiceUri)\n        {\n            var json = JsonSerializer.Serialize(new Dictionary<string, object>\n            {\n                [\"location\"] = identityServiceUri.AbsoluteUri\n            });\n\n            return new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new StringContent(json)\n            };\n        }\n\n        private static HttpResponseMessage CreateIdentityServiceResponse(string pat)\n        {\n            var json = JsonSerializer.Serialize(\n                new Dictionary<string, object> {[\"token\"] = pat}\n            );\n\n            return new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new StringContent(json)\n            };\n        }\n\n        private static HttpResponseMessage CreateIdentityServiceErrorResponse(string errorMessage)\n        {\n            var json = JsonSerializer.Serialize(\n                new Dictionary<string, object> {[\"message\"] = errorMessage}\n            );\n\n            return new HttpResponseMessage(HttpStatusCode.InternalServerError)\n            {\n                Content = new StringContent(json)\n            };\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos.Tests/AzureReposAuthorityCacheTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Microsoft.AzureRepos.Tests\n{\n    public class AzureReposAuthorityCacheTests\n    {\n        [Fact]\n        public void AzureReposAuthorityCache_GetAuthority_Null_ThrowException()\n        {\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => cache.GetAuthority(null));\n        }\n\n        [Fact]\n        public void AzureReposAuthorityCache_GetAuthority_NoCachedAuthority_ReturnsNull()\n        {\n            string key = CreateKey(\"contoso\");\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            string authority = cache.GetAuthority(key);\n\n            Assert.Null(authority);\n        }\n\n        [Fact]\n        public void AzureReposAuthorityCache_GetAuthority_CachedAuthority_ReturnsAuthority()\n        {\n            const string orgName = \"contoso\";\n            string key = CreateKey(orgName);\n            const string expectedAuthority = \"https://login.contoso.com\";\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Global =\n                    {\n                        [key] = new[] {expectedAuthority}\n                    }\n                }\n            };\n\n            var trace = new NullTrace();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            string actualAuthority = cache.GetAuthority(orgName);\n\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public void AzureReposAuthorityCache_UpdateAuthority_NoCachedAuthority_SetsAuthority()\n        {\n            const string orgName = \"contoso\";\n            string key = CreateKey(orgName);\n            const string expectedAuthority = \"https://login.contoso.com\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            cache.UpdateAuthority(orgName, expectedAuthority);\n\n            Assert.True(git.Configuration.Global.TryGetValue(key, out IList<string> values));\n            Assert.Single(values);\n            string actualAuthority = values[0];\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public void AzureReposAuthorityCache_UpdateAuthority_CachedAuthority_UpdatesAuthority()\n        {\n            const string orgName = \"contoso\";\n            string key = CreateKey(orgName);\n            const string oldAuthority = \"https://old-login.contoso.com\";\n            const string expectedAuthority = \"https://login.contoso.com\";\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Global =\n                    {\n                        [key] = new[] {oldAuthority}\n                    }\n                }\n            };\n\n            var trace = new NullTrace();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            cache.UpdateAuthority(orgName, expectedAuthority);\n\n            Assert.True(git.Configuration.Global.TryGetValue(key, out IList<string> values));\n            Assert.Single(values);\n            string actualAuthority = values[0];\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        [Fact]\n        public void AzureReposAuthorityCache_EraseAuthority_NoCachedAuthority_DoesNothing()\n        {\n            const string orgName = \"contoso\";\n            string key = CreateKey(orgName);\n            string otherKey = CreateKey(\"org.fabrikam.authority\");\n            const string otherAuthority = \"https://fabrikam.com/login\";\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Global =\n                    {\n                        [otherKey] = new[] {otherAuthority}\n                    }\n                }\n            };\n\n            var trace = new NullTrace();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            cache.EraseAuthority(orgName);\n\n            // Other entries should remain\n            Assert.False(git.Configuration.Global.ContainsKey(key));\n            Assert.Single(git.Configuration.Global);\n            Assert.True(git.Configuration.Global.TryGetValue(otherKey, out IList<string> values));\n            Assert.Single(values);\n            string actualOtherAuthority = values[0];\n            Assert.Equal(otherAuthority, actualOtherAuthority);\n        }\n\n        [Fact]\n        public void AzureReposAuthorityCache_EraseAuthority_CachedAuthority_RemovesAuthority()\n        {\n            const string orgName = \"contoso\";\n            string key = CreateKey(orgName);\n            const string authority = \"https://login.contoso.com\";\n            string otherKey = CreateKey(\"fabrikam\");\n            const string otherAuthority = \"https://fabrikam.com/login\";\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Global =\n                    {\n                        [key] = new[] {authority},\n                        [otherKey] = new[] {otherAuthority}\n                    }\n                }\n            };\n\n            var trace = new NullTrace();\n            var cache = new AzureDevOpsAuthorityCache(trace, git);\n\n            cache.EraseAuthority(orgName);\n\n            // Only the other entries should remain\n            Assert.False(git.Configuration.Global.ContainsKey(key));\n            Assert.Single(git.Configuration.Global);\n            Assert.True(git.Configuration.Global.TryGetValue(otherKey, out IList<string> values));\n            Assert.Single(values);\n            string actualOtherAuthority = values[0];\n            Assert.Equal(otherAuthority, actualOtherAuthority);\n        }\n\n        private static string CreateKey(string orgName)\n        {\n            return string.Format(CultureInfo.InvariantCulture, \"{0}.{1}:{2}/{3}.{4}\",\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.UrnScheme, AzureDevOpsConstants.UrnOrgPrefix, orgName,\n                AzureDevOpsConstants.GitConfiguration.Credential.AzureAuthority);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos.Tests/AzureReposBindingManagerTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing GitCredentialManager;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace Microsoft.AzureRepos.Tests\n{\n    public class AzureReposBindingManagerTests\n    {\n        #region Bind\n\n        [Fact]\n        public void AzureReposBindingManager_Bind_NullOrganization_ThrowException()\n        {\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => manager.Bind(null, \"user\", false));\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_Bind_NoUser_SetsOrgKey()\n        {\n            const string expectedUser = \"user1\";\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            manager.Bind(orgName, expectedUser, false);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var users));\n            Assert.Single(users);\n            string actualUser = users[0];\n            Assert.Equal(expectedUser, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_BindLocal_NoUser_SetsOrgKey()\n        {\n            const string expectedUser = \"user1\";\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            manager.Bind(orgName, expectedUser, true);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var users));\n            Assert.Single(users);\n            string actualUser = users[0];\n            Assert.Equal(expectedUser, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_Bind_ExistingUser_SetsOrgKey()\n        {\n            const string expectedUser = \"user1\";\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] {\"org-user\"};\n\n            manager.Bind(orgName, expectedUser, false);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var users));\n            Assert.Single(users);\n            string actualUser = users[0];\n            Assert.Equal(expectedUser, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_BindLocal_ExistingUser_SetsOrgKey()\n        {\n            const string expectedUser = \"user1\";\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new[] {\"org-user\"};\n\n            manager.Bind(orgName, expectedUser, true);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var users));\n            Assert.Single(users);\n            string actualUser = users[0];\n            Assert.Equal(expectedUser, actualUser);\n        }\n\n        #endregion\n\n        #region Unbind\n\n        [Fact]\n        public void AzureReposBindingManager_Unbind_NullOrganization_ThrowException()\n        {\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => manager.Unbind(null, false));\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_Unbind_NoUser_DoesNothing()\n        {\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            manager.Unbind(orgName, false);\n\n            Assert.Empty(git.Configuration.Global);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_UnbindLocal_NoUser_DoesNothing()\n        {\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            manager.Unbind(orgName, true);\n\n            Assert.Empty(git.Configuration.Local);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_Unbind_ExistingUser_RemovesKey()\n        {\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] {\"org-user\"};\n\n            manager.Unbind(orgName, false);\n\n            Assert.Empty(git.Configuration.Global);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_UnbindLocal_ExistingUser_RemovesKey()\n        {\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new[] {\"org-user\"};\n\n            manager.Unbind(orgName, true);\n\n            Assert.Empty(git.Configuration.Local);\n        }\n\n        #endregion\n\n        #region GetBinding\n\n        [Fact]\n        public void AzureReposBindingManager_GetBinding_Null_ThrowException()\n        {\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => manager.GetBinding(null));\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetBinding_NoUser_ReturnsNull()\n        {\n            const string orgName = \"org\";\n\n            var trace = new NullTrace();\n            var git = new TestGit();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            AzureReposBinding binding = manager.GetBinding(orgName);\n\n            Assert.Null(binding);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetBinding_GlobalUser_ReturnsBinding()\n        {\n            const string orgUser = \"john.doe\";\n            const string orgName = \"org\";\n            string orgKey = CreateKey(orgName);\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Global =\n                    {\n                        [orgKey] = new[] {orgUser}\n                    }\n                }\n            };\n\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            AzureReposBinding binding = manager.GetBinding(orgName);\n\n            Assert.Equal(orgName, binding.Organization);\n            Assert.Equal(orgUser, binding.GlobalUserName);\n            Assert.Null(binding.LocalUserName);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetBinding_LocalUser_ReturnsBinding()\n        {\n            const string orgUser = \"john.doe\";\n            const string orgName = \"org\";\n            string orgKey = CreateKey(orgName);\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Local =\n                    {\n                        [orgKey] = new[] {orgUser}\n                    }\n                }\n            };\n\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            AzureReposBinding binding = manager.GetBinding(orgName);\n\n            Assert.Equal(orgName, binding.Organization);\n            Assert.Null(binding.GlobalUserName);\n            Assert.Equal(orgUser, binding.LocalUserName);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetBinding_LocalAndGlobalUsers_ReturnsBinding()\n        {\n            const string orgUserLocal = \"john.doe\";\n            const string orgUserGlobal = \"jane.doe\";\n            const string orgName = \"org\";\n            string orgKey = CreateKey(orgName);\n\n            var git = new TestGit\n            {\n                Configuration =\n                {\n                    Global = { [orgKey] = new[] {orgUserGlobal} },\n                    Local  = { [orgKey] = new[] {orgUserLocal} }\n                }\n            };\n\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            AzureReposBinding binding = manager.GetBinding(orgName);\n\n            Assert.Equal(orgName, binding.Organization);\n            Assert.Equal(orgUserGlobal, binding.GlobalUserName);\n            Assert.Equal(orgUserLocal, binding.LocalUserName);\n        }\n\n        #endregion\n\n        #region GetBindings\n\n        [Fact]\n        public void AzureReposBindingManager_GetBindings_NoUsers_ReturnsEmpty()\n        {\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            IList<AzureReposBinding> actual = manager.GetBindings().ToList();\n\n            Assert.Empty(actual);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetBindings_Users_ReturnsUsers()\n        {\n            const string org1 = \"org1\";\n            const string org2 = \"org2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(org1)] = new[] {\"user1\"};\n            git.Configuration.Global[CreateKey(org2)] = new[] {\"user2\"};\n\n            AzureReposBinding[] bindings = manager.GetBindings().ToArray();\n\n            static void AssertBinding(\n                string expectedOrg, string expectedGlobalUser, string expectedLocalUser, AzureReposBinding binding)\n            {\n                Assert.Equal(expectedOrg, binding.Organization);\n                Assert.Equal(expectedGlobalUser, binding.GlobalUserName);\n                Assert.Equal(expectedLocalUser, binding.LocalUserName);\n            }\n\n            Assert.Equal(2, bindings.Length);\n            AssertBinding(org1, \"user1\", null, bindings[0]);\n            AssertBinding(org2, \"user2\", null, bindings[1]);\n        }\n\n        #endregion\n\n        #region GetUser\n\n        [Fact]\n        public void AzureReposBindingManager_GetUser_NullOrg_ThrowsException()\n        {\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => manager.GetUser(null));\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetUser_NoUser_ReturnsNull()\n        {\n            const string orgName = \"org\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            string actualUser = manager.GetUser(orgName);\n\n            Assert.Null(actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetUser_GlobalUser_ReturnsGlobalUser()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] {user1};\n\n            string actualUser = manager.GetUser(orgName);\n\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetUser_LocalUser_ReturnsLocalUser()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new[] {user1};\n\n            string actualUser = manager.GetUser(orgName);\n\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_GetUser_GlobalAndLocalUsers_ReturnsLocalUser()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] {user1};\n            git.Configuration.Local[CreateKey(orgName)] = new[] {user2};\n\n            string actualUser = manager.GetUser(orgName);\n\n            Assert.Equal(user2, actualUser);\n        }\n\n        #endregion\n\n        #region SignIn\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_NullOrg_ThrowsException()\n        {\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => manager.SignIn(null, \"user1\"));\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_NullUser_ThrowsException()\n        {\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            Assert.Throws<ArgumentNullException>(() => manager.SignIn(\"org\", null));\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_NoGlobalNoLocal_BindsGlobal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            manager.SignIn(orgName, user1);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualUser = globalUsers[0];\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_NoGlobalSameLocal_BindsGlobalUnbindLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new []{user1};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualUser = globalUsers[0];\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_NoGlobalOtherLocal_BindsGlobalUnbindLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new []{user2};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualUser = globalUsers[0];\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_SameGlobalNoLocal_DoesNothing()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new []{user1};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualUser = globalUsers[0];\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_SameGlobalSameLocal_UnbindLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new []{user1};\n            git.Configuration.Local[CreateKey(orgName)] = new []{user1};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualUser = globalUsers[0];\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_SameGlobalOtherLocal_UnbindLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new []{user1};\n            git.Configuration.Local[CreateKey(orgName)] = new []{user2};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualUser = globalUsers[0];\n            Assert.Equal(user1, actualUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_OtherGlobalNoLocal_BindsLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new []{user2};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var localUsers));\n            string actualLocalUser = localUsers[0];\n            Assert.Equal(user1, actualLocalUser);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualGlobalUser = globalUsers[0];\n            Assert.Equal(user2, actualGlobalUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_OtherGlobalSameLocal_DoesNothing()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new []{user2};\n            git.Configuration.Local[CreateKey(orgName)] = new []{user1};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var localUsers));\n            string actualLocalUser = localUsers[0];\n            Assert.Equal(user1, actualLocalUser);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualGlobalUser = globalUsers[0];\n            Assert.Equal(user2, actualGlobalUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignIn_OtherGlobalOtherLocal_BindsLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new []{user2};\n            git.Configuration.Local[CreateKey(orgName)] = new []{user2};\n\n            manager.SignIn(orgName, user1);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var localUsers));\n            string actualLocalUser = localUsers[0];\n            Assert.Equal(user1, actualLocalUser);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            string actualGlobalUser = globalUsers[0];\n            Assert.Equal(user2, actualGlobalUser);\n        }\n\n        #endregion\n\n        #region SignOut\n\n        [Fact]\n        public void AzureReposBindingManager_SignOut_NoGlobalNoLocal_DoesNothing()\n        {\n            const string orgName = \"org\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            manager.SignOut(orgName);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.Empty(git.Configuration.Global);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignOut_NoGlobalUserLocal_UnbindsLocal()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new[] { user1 };\n\n            manager.SignOut(orgName);\n\n            Assert.Empty(git.Configuration.Local);\n            Assert.Empty(git.Configuration.Global);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignOut_NoGlobalNoInheritLocal_UnbindsLocal()\n        {\n            const string orgName = \"org\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Local[CreateKey(orgName)] = new[] { AzureReposBinding.NoInherit };\n\n            manager.SignOut(orgName);\n\n            Assert.Empty(git.Configuration.Global);\n            Assert.Empty(git.Configuration.Local);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignOut_UserGlobalNoLocal_BindLocalNoInherit()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] { user1 };\n\n            manager.SignOut(orgName);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var localUsers));\n            Assert.Single(localUsers);\n            string actualLocalUser = localUsers[0];\n            Assert.Equal(AzureReposBinding.NoInherit, actualLocalUser);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            Assert.Single(globalUsers);\n            string actualGlobalUser = globalUsers[0];\n            Assert.Equal(user1, actualGlobalUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignOut_UserGlobalNoInheritLocal_DoesNothing()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] { user1 };\n            git.Configuration.Local[CreateKey(orgName)] = new[] { AzureReposBinding.NoInherit };\n\n            manager.SignOut(orgName);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var localUsers));\n            Assert.Single(localUsers);\n            string actualLocalUser = localUsers[0];\n            Assert.Equal(AzureReposBinding.NoInherit, actualLocalUser);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            Assert.Single(globalUsers);\n            string actualGlobalUser = globalUsers[0];\n            Assert.Equal(user1, actualGlobalUser);\n        }\n\n        [Fact]\n        public void AzureReposBindingManager_SignOut_UserGlobalUserLocal_BindLocalNoInherit()\n        {\n            const string orgName = \"org\";\n            const string user1 = \"user1\";\n            const string user2 = \"user2\";\n\n            var git = new TestGit();\n            var trace = new NullTrace();\n            var manager = new AzureReposBindingManager(trace, git);\n\n            git.Configuration.Global[CreateKey(orgName)] = new[] { user1 };\n            git.Configuration.Local[CreateKey(orgName)] = new[] { user2 };\n\n            manager.SignOut(orgName);\n\n            Assert.True(git.Configuration.Local.TryGetValue(CreateKey(orgName), out var localUsers));\n            Assert.Single(localUsers);\n            string actualLocalUser = localUsers[0];\n            Assert.Equal(AzureReposBinding.NoInherit, actualLocalUser);\n\n            Assert.True(git.Configuration.Global.TryGetValue(CreateKey(orgName), out var globalUsers));\n            Assert.Single(globalUsers);\n            string actualGlobalUser = globalUsers[0];\n            Assert.Equal(user1, actualGlobalUser);\n        }\n\n\n        #endregion\n\n        #region Helpers\n\n        private static string CreateKey(string orgName)\n        {\n            return string.Format(CultureInfo.InvariantCulture, \"{0}.{1}:{2}/{3}.{4}\",\n                Constants.GitConfiguration.Credential.SectionName,\n                AzureDevOpsConstants.UrnScheme, AzureDevOpsConstants.UrnOrgPrefix, orgName,\n                Constants.GitConfiguration.Credential.UserName);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing GitCredentialManager;\nusing GitCredentialManager.Authentication;\nusing GitCredentialManager.Tests;\nusing GitCredentialManager.Tests.Objects;\nusing Moq;\nusing Xunit;\n\nnamespace Microsoft.AzureRepos.Tests\n{\n    public class AzureReposHostProviderTests\n    {\n        private static readonly string HelperKey =\n            $\"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}\";\n        private static readonly string AzDevUseHttpPathKey =\n            $\"{Constants.GitConfiguration.Credential.SectionName}.https://dev.azure.com.{Constants.GitConfiguration.Credential.UseHttpPath}\";\n        private static readonly string OrgName = \"org\";\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_AzureHost_UnencryptedHttp_ReturnsTrue()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n\n            // We report that we support unencrypted HTTP here so that we can fail and\n            // show a helpful error message in the call to `CreateCredentialAsync` instead.\n            Assert.True(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_VisualStudioHost_UnencryptedHttp_ReturnsTrue()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"] = \"org.visualstudio.com\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n\n            // We report that we support unencrypted HTTP here so that we can fail and\n            // show a helpful error message in the call to `CreateCredentialAsync` instead.\n            Assert.True(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_AzureHost_WithPath_ReturnsTrue()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n            Assert.True(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_AzureHost_MissingPath_ReturnsTrue()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n            Assert.True(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_VisualStudioHost_ReturnsTrue()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"org.visualstudio.com\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n            Assert.True(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_VisualStudioHost_MissingOrgInHost_ReturnsFalse()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"visualstudio.com\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n            Assert.False(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public void AzureReposProvider_IsSupported_NonAzureRepos_ReturnsFalse()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"example.com\",\n                [\"path\"] = \"org/proj/_git/repo\",\n            });\n\n            var provider = new AzureReposHostProvider(new TestCommandContext());\n            Assert.False(provider.IsSupported(input));\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_UnencryptedHttp_ThrowsException()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"http\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            var context = new TestCommandContext();\n            var azDevOps = Mock.Of<IAzureDevOpsRestApi>();\n            var msAuth = Mock.Of<IMicrosoftAuthentication>();\n            var authorityCache = Mock.Of<IAzureDevOpsAuthorityCache>();\n            var userMgr = Mock.Of<IAzureReposBindingManager>();\n\n            var provider = new AzureReposHostProvider(context, azDevOps, msAuth, authorityCache, userMgr);\n\n            await Assert.ThrowsAsync<Trace2Exception>(() => provider.GetCredentialAsync(input));\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_VsComUrlUser_ReturnsCredential()\n        {\n            var urlAccount = \"jane.doe\";\n\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"org.visualstudio.com\",\n                [\"username\"] = urlAccount\n            });\n\n            var expectedOrgUri = new Uri(\"https://org.visualstudio.com\");\n            var remoteUri = new Uri(\"https://org.visualstudio.com/\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var authResult = CreateAuthResult(urlAccount, accessToken);\n\n            var context = new TestCommandContext();\n\n            // Use OAuth Access Tokens\n            context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.CredentialType] =\n                AzureDevOpsConstants.OAuthCredentialType;\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n            azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, urlAccount, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n            authorityCacheMock.Setup(x => x.GetAuthority(OrgName)).Returns(authorityUrl);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(urlAccount, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_DevAzureUrlUser_ReturnsCredential()\n        {\n            var urlAccount = \"jane.doe\";\n\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/project/_git/repo\",\n                [\"username\"] = urlAccount\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var remoteUri = new Uri(\"https://dev.azure.com/org/project/_git/repo\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var authResult = CreateAuthResult(urlAccount, accessToken);\n\n            var context = new TestCommandContext();\n\n            // Use OAuth Access Tokens\n            context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.CredentialType] =\n                AzureDevOpsConstants.OAuthCredentialType;\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n            azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, urlAccount, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n            authorityCacheMock.Setup(x => x.GetAuthority(OrgName)).Returns(authorityUrl);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(urlAccount, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_DevAzureUrlOrgName_ReturnsCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"username\"] = \"org\"\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var account = \"jane.doe\";\n            var authResult = CreateAuthResult(account, accessToken);\n\n            var context = new TestCommandContext();\n\n            // Use OAuth Access Tokens\n            context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.CredentialType] =\n                AzureDevOpsConstants.OAuthCredentialType;\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n            azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n            authorityCacheMock.Setup(x => x.GetAuthority(OrgName)).Returns(authorityUrl);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n            userMgrMock.Setup(x => x.GetBinding(OrgName)).Returns((AzureReposBinding)null);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_NoUser_ReturnsCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var remoteUri = new Uri(\"https://dev.azure.com/org/proj/_git/repo\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var account = \"john.doe\";\n            var authResult = CreateAuthResult(account, accessToken);\n\n            var context = new TestCommandContext();\n\n            // Use OAuth Access Tokens\n            context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.CredentialType] =\n                AzureDevOpsConstants.OAuthCredentialType;\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n            authorityCacheMock.Setup(x => x.GetAuthority(OrgName)).Returns(authorityUrl);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n            userMgrMock.Setup(x => x.GetBinding(OrgName)).Returns((AzureReposBinding)null);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_BoundUser_ReturnsCredential()\n        {\n\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var remoteUri = new Uri(\"https://dev.azure.com/org/proj/_git/repo\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var account = \"john.doe\";\n            var authResult = CreateAuthResult(account, accessToken);\n\n            var context = new TestCommandContext();\n\n            // Use OAuth Access Tokens\n            context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.CredentialType] =\n                AzureDevOpsConstants.OAuthCredentialType;\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, account, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n            authorityCacheMock.Setup(x => x.GetAuthority(OrgName)).Returns(authorityUrl);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n            userMgrMock.Setup(x => x.GetBinding(OrgName))\n                .Returns(new AzureReposBinding(OrgName, account, null));\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_JwtMode_NoCachedAuthority_NoUser_ReturnsCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var remoteUri = new Uri(\"https://dev.azure.com/org/proj/_git/repo\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var account = \"john.doe\";\n            var authResult = CreateAuthResult(account, accessToken);\n\n            var context = new TestCommandContext();\n\n            // Use OAuth Access Tokens\n            context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.CredentialType] =\n                AzureDevOpsConstants.OAuthCredentialType;\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n            azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n            authorityCacheMock.Setup(x => x.GetAuthority(It.IsAny<string>())).Returns((string)null);\n            authorityCacheMock.Setup(x => x.UpdateAuthority(OrgName, authorityUrl));\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n            userMgrMock.Setup(x => x.GetBinding(OrgName)).Returns((AzureReposBinding)null);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_PatMode_OrgInUserName_NoExistingPat_GeneratesCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"username\"] = \"org\"\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var personalAccessToken = \"PERSONAL-ACCESS-TOKEN\";\n            var account = \"john.doe\";\n            var authResult = CreateAuthResult(account, accessToken);\n\n            var context = new TestCommandContext();\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n            azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);\n            azDevOpsMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedOrgUri, accessToken, It.IsAny<IEnumerable<string>>()))\n                        .ReturnsAsync(personalAccessToken);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(personalAccessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_PatMode_NoExistingPat_GeneratesCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            var expectedOrgUri = new Uri(\"https://dev.azure.com/org\");\n            var remoteUri = new Uri(\"https://dev.azure.com/org/proj/_git/repo\");\n            var authorityUrl = \"https://login.microsoftonline.com/common\";\n            var expectedClientId = AzureDevOpsConstants.AadClientId;\n            var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;\n            var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;\n            var accessToken = \"ACCESS-TOKEN\";\n            var personalAccessToken = \"PERSONAL-ACCESS-TOKEN\";\n            var account = \"john.doe\";\n            var authResult = CreateAuthResult(account, accessToken);\n\n            var context = new TestCommandContext();\n\n            var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);\n            azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);\n            azDevOpsMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedOrgUri, accessToken, It.IsAny<IEnumerable<string>>()))\n                        .ReturnsAsync(personalAccessToken);\n\n            var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);\n            msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true))\n                      .ReturnsAsync(authResult);\n\n            var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);\n\n            var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);\n\n            var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(personalAccessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_PatMode_ExistingPat_ReturnsExistingCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            var remoteUri = new Uri(\"https://dev.azure.com/org/proj/_git/repo\");\n            var personalAccessToken = \"PERSONAL-ACCESS-TOKEN\";\n            const string service = \"https://dev.azure.com/org\";\n            const string account = \"john.doe\";\n\n            var context = new TestCommandContext();\n\n            context.CredentialStore.Add(service, account, personalAccessToken);\n\n            var azDevOps = Mock.Of<IAzureDevOpsRestApi>();\n            var msAuth = Mock.Of<IMicrosoftAuthentication>();\n            var authorityCache = Mock.Of<IAzureDevOpsAuthorityCache>();\n            var userMgr = Mock.Of<IAzureReposBindingManager>();\n\n            var provider = new AzureReposHostProvider(context, azDevOps, msAuth, authorityCache, userMgr);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(account, credential.Account);\n            Assert.Equal(personalAccessToken, credential.Password);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_ManagedIdentity_ReturnsManagedIdCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            const string accessToken = \"MANAGED-IDENTITY-TOKEN\";\n            const string managedIdentity = \"MANAGED-IDENTITY\";\n\n            var context = new TestCommandContext\n            {\n                Environment =\n                {\n                    Variables =\n                    {\n                        [AzureDevOpsConstants.EnvironmentVariables.ManagedIdentity] = managedIdentity\n                    }\n                }\n            };\n\n            var azDevOps = Mock.Of<IAzureDevOpsRestApi>();\n            var authorityCache = Mock.Of<IAzureDevOpsAuthorityCache>();\n            var userMgr = Mock.Of<IAzureReposBindingManager>();\n            var msAuthMock = new Mock<IMicrosoftAuthentication>();\n\n            msAuthMock.Setup(x => x.GetTokenForManagedIdentityAsync(It.IsAny<string>(), It.IsAny<string>()))\n                .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken });\n\n            var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(managedIdentity, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n\n            msAuthMock.Verify(\n                x => x.GetTokenForManagedIdentityAsync(managedIdentity,\n                    AzureDevOpsConstants.AzureDevOpsResourceId), Times.Once);\n        }\n\n        [Fact]\n        public async Task AzureReposProvider_GetCredentialAsync_ServicePrincipal_ReturnsSPCredential()\n        {\n            var input = new InputArguments(new Dictionary<string, string>\n            {\n                [\"protocol\"] = \"https\",\n                [\"host\"] = \"dev.azure.com\",\n                [\"path\"] = \"org/proj/_git/repo\"\n            });\n\n            const string accessToken = \"SP-TOKEN\";\n            const string tenantId = \"78B1822F-107D-40A3-A29C-AB68D8066074\";\n            const string clientId = \"49B4DC1A-58A8-4EEE-A81B-616A40D0BA64\";\n            const string servicePrincipal = $\"{tenantId}/{clientId}\";\n            const string servicePrincipalSecret = \"CLIENT-SECRET\";\n\n            var context = new TestCommandContext\n            {\n                Environment =\n                {\n                    Variables =\n                    {\n                        [AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalId] = servicePrincipal,\n                        [AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalSecret] = servicePrincipalSecret\n                    }\n                }\n            };\n\n            var azDevOps = Mock.Of<IAzureDevOpsRestApi>();\n            var authorityCache = Mock.Of<IAzureDevOpsAuthorityCache>();\n            var userMgr = Mock.Of<IAzureReposBindingManager>();\n            var msAuthMock = new Mock<IMicrosoftAuthentication>();\n\n            msAuthMock.Setup(x =>\n                    x.GetTokenForServicePrincipalAsync(It.IsAny<ServicePrincipalIdentity>(), It.IsAny<string[]>()))\n                .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken });\n\n            var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr);\n\n            var result = await provider.GetCredentialAsync(input);\n            ICredential credential = result.Credential;\n\n            Assert.NotNull(credential);\n            Assert.Equal(clientId, credential.Account);\n            Assert.Equal(accessToken, credential.Password);\n\n            msAuthMock.Verify(x => x.GetTokenForServicePrincipalAsync(\n                It.Is<ServicePrincipalIdentity>(sp => sp.TenantId == tenantId && sp.Id == clientId),\n                It.Is<string[]>(scopes => scopes.Length == 1 && scopes[0] == AzureDevOpsConstants.AzureDevOpsDefaultScopes[0])),\n                Times.Once);\n        }\n\n        [Fact]\n        public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathSetTrue_DoesNothing()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.Global[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.ConfigureAsync(ConfigurationTarget.User);\n\n            Assert.Single(context.Git.Configuration.Global);\n            Assert.True(context.Git.Configuration.Global.TryGetValue(AzDevUseHttpPathKey, out IList<string> actualValues));\n            Assert.Single(actualValues);\n            Assert.Equal(\"true\", actualValues[0]);\n        }\n\n        [Fact]\n        public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathSetFalse_SetsUseHttpPathTrue()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.Global[AzDevUseHttpPathKey] = new List<string> {\"false\"};\n\n            await provider.ConfigureAsync(ConfigurationTarget.User);\n\n            Assert.Single(context.Git.Configuration.Global);\n            Assert.True(context.Git.Configuration.Global.TryGetValue(AzDevUseHttpPathKey, out IList<string> actualValues));\n            Assert.Single(actualValues);\n            Assert.Equal(\"true\", actualValues[0]);\n        }\n\n        [Fact]\n        public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathUnset_SetsUseHttpPathTrue()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            await provider.ConfigureAsync(ConfigurationTarget.User);\n\n            Assert.Single(context.Git.Configuration.Global);\n            Assert.True(context.Git.Configuration.Global.TryGetValue(AzDevUseHttpPathKey, out IList<string> actualValues));\n            Assert.Single(actualValues);\n            Assert.Equal(\"true\", actualValues[0]);\n        }\n\n        [Fact]\n        public async Task AzureReposHostProvider_UnconfigureAsync_UseHttpPathSet_RemovesEntry()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.Global[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.UnconfigureAsync(ConfigurationTarget.User);\n\n            Assert.Empty(context.Git.Configuration.Global);\n        }\n\n        [WindowsFact]\n        public async Task AzureReposHostProvider_UnconfigureAsync_System_Windows_UseHttpPathSetAndManagerHelper_DoesNotRemoveEntry()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.System[HelperKey] = new List<string> {\"manager\"};\n            context.Git.Configuration.System[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.UnconfigureAsync(ConfigurationTarget.System);\n\n            Assert.True(context.Git.Configuration.System.TryGetValue(AzDevUseHttpPathKey, out IList<string> actualValues));\n            Assert.Single(actualValues);\n            Assert.Equal(\"true\", actualValues[0]);\n        }\n\n        [WindowsFact]\n        public async Task AzureReposHostProvider_UnconfigureAsync_System_Windows_UseHttpPathSetAndManagerCoreHelper_DoesNotRemoveEntry()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.System[HelperKey] = new List<string> {\"manager-core\"};\n            context.Git.Configuration.System[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.UnconfigureAsync(ConfigurationTarget.System);\n\n            Assert.True(context.Git.Configuration.System.TryGetValue(AzDevUseHttpPathKey, out IList<string> actualValues));\n            Assert.Single(actualValues);\n            Assert.Equal(\"true\", actualValues[0]);\n        }\n\n        [WindowsFact]\n        public async Task AzureReposHostProvider_UnconfigureAsync_System_Windows_UseHttpPathSetNoManagerCoreHelper_RemovesEntry()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.System[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.UnconfigureAsync(ConfigurationTarget.System);\n\n            Assert.Empty(context.Git.Configuration.System);\n        }\n\n        [WindowsFact]\n        public async Task AzureReposHostProvider_UnconfigureAsync_User_Windows_UseHttpPathSetAndManagerHelper_RemovesEntry()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.Global[HelperKey] = new List<string> {\"manager\"};\n            context.Git.Configuration.Global[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.UnconfigureAsync(ConfigurationTarget.User);\n\n            Assert.False(context.Git.Configuration.Global.TryGetValue(AzDevUseHttpPathKey, out _));\n        }\n\n        [WindowsFact]\n        public async Task AzureReposHostProvider_UnconfigureAsync_User_Windows_UseHttpPathSetAndManagerCoreHelper_RemovesEntry()\n        {\n            var context = new TestCommandContext();\n            var provider = new AzureReposHostProvider(context);\n\n            context.Git.Configuration.Global[HelperKey] = new List<string> {\"manager-core\"};\n            context.Git.Configuration.Global[AzDevUseHttpPathKey] = new List<string> {\"true\"};\n\n            await provider.UnconfigureAsync(ConfigurationTarget.User);\n\n            Assert.False(context.Git.Configuration.Global.TryGetValue(AzDevUseHttpPathKey, out _));\n        }\n\n        [Theory]\n        [InlineData(false, null, \"\")]\n        [InlineData(false, null, \"   \")]\n        [InlineData(false, null, null)]\n        [InlineData(false, null, \"Basic realm=\\\"test\\\"\")]\n        [InlineData(false, null, \"Basic realm=\\\"https://tfsprodwcus0.app.visualstudio.com/\\\"\")]\n        [InlineData(false, null, \"TFS-Federated\")]\n        [InlineData(true, \"https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\")]\n        [InlineData(true, \"https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"bEArEr auThORizAtIoN_uRi=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\")]\n        [InlineData(true, \"https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"\\\"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\\\"\")]\n        [InlineData(true, \"https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"'Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3'\")]\n        [InlineData(true, \"https://login.microsoftonline.com/tenant1\",\n            \"Bearer authorization_uri=https://login.microsoftonline.com/tenant1\",\n            \"Bearer authorization_uri=https://login.microsoftonline.com/tenant2\",\n            \"Bearer authorization_uri=https://login.microsoftonline.com/tenant3\")]\n        [InlineData(true, \"https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"Basic realm=\\\"https://tfsprodwcus0.app.visualstudio.com/\\\"\",\n            \"TFS-Federated\")]\n        [InlineData(true, \"https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\",\n            \"TFS-Federated\",\n            \"Basic realm=\\\"https://tfsprodwcus0.app.visualstudio.com/\\\"\",\n            \"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\")]\n        public void AzureReposHostProvider_TryGetAuthorityFromHeaders(\n            bool expectedResult, string expectedAuthority, params string[] headers)\n        {\n            bool actualResult = AzureReposHostProvider.TryGetAuthorityFromHeaders(headers, out string actualAuthority);\n\n            Assert.Equal(expectedResult, actualResult);\n            Assert.Equal(expectedAuthority, actualAuthority);\n        }\n\n        private static IMicrosoftAuthenticationResult CreateAuthResult(string upn, string token)\n        {\n            return new MockMsAuthResult\n            {\n                AccountUpn = upn,\n                AccessToken = token,\n            };\n        }\n\n        private class MockMsAuthResult : IMicrosoftAuthenticationResult\n        {\n            public string AccessToken { get; set; }\n            public string AccountUpn { get; set; }\n            public string TokenSource { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos.Tests/Microsoft.AzureRepos.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.2\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.11.1\" />\n    <PackageReference Include=\"ReportGenerator\" Version=\"5.3.10\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <DotNetCliToolReference Include=\"dotnet-xunit\" Version=\"2.3.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Microsoft.AzureRepos\\Microsoft.AzureRepos.csproj\" />\n    <ProjectReference Include=\"..\\TestInfrastructure\\TestInfrastructure.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/Microsoft.AzureRepos.Tests/UriHelpersTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing GitCredentialManager;\nusing Xunit;\n\nnamespace Microsoft.AzureRepos.Tests\n{\n    public class UriHelpersTests\n    {\n        [Theory]\n        [InlineData(\"a/b\",  \"c/d\",  \"a/b/c/d\")]\n        [InlineData(\"a/b/\", \"c/d\",  \"a/b/c/d\")]\n        [InlineData(\"a/b\",  \"/c/d\", \"a/b/c/d\")]\n        [InlineData(\"a/b/\", \"/c/d\", \"a/b/c/d\")]\n        public void UriHelpers_CombinePath(string basePath, string path, string expected)\n        {\n            Assert.Equal(expected, UriHelpers.CombinePath(basePath, path));\n        }\n\n        [Theory]\n        [InlineData(\"dev.azure.com\", true)]\n        [InlineData(\"myorg.visualstudio.com\", true)]\n        [InlineData(\"vs-ssh.myorg.visualstudio.com\", true)]\n        [InlineData(\"DEV.AZURE.COM\", true)]\n        [InlineData(\"MYORG.VISUALSTUDIO.COM\", true)]\n        [InlineData(null, false)]\n        [InlineData(\"\", false)]\n        [InlineData(\"    \", false)]\n        [InlineData(\"testdev.azure.com\", false)]\n        [InlineData(\"test.dev.azure.com\", false)]\n        [InlineData(\"visualstudio.com\", false)]\n        [InlineData(\"testvisualstudio.com\", false)]\n        public void UriHelpers_IsAzureDevOpsHost(string host, bool expected)\n        {\n            Assert.Equal(expected, UriHelpers.IsAzureDevOpsHost(host));\n        }\n\n        [Theory]\n        [InlineData(\"dev.azure.com\", true)]\n        [InlineData(\"myorg.visualstudio.com\", false)]\n        [InlineData(\"vs-ssh.myorg.visualstudio.com\", false)]\n        [InlineData(\"DEV.AZURE.COM\", true)]\n        [InlineData(\"MYORG.VISUALSTUDIO.COM\", false)]\n        [InlineData(null, false)]\n        [InlineData(\"\", false)]\n        [InlineData(\"    \", false)]\n        [InlineData(\"testdev.azure.com\", false)]\n        [InlineData(\"test.dev.azure.com\", false)]\n        [InlineData(\"visualstudio.com\", false)]\n        [InlineData(\"testvisualstudio.com\", false)]\n        public void UriHelpers_IsDevAzureComHost(string host, bool expected)\n        {\n            Assert.Equal(expected, UriHelpers.IsDevAzureComHost(host));\n        }\n\n        [Theory]\n        [InlineData(\"dev.azure.com\", false)]\n        [InlineData(\"myorg.visualstudio.com\", true)]\n        [InlineData(\"vs-ssh.myorg.visualstudio.com\", true)]\n        [InlineData(\"DEV.AZURE.COM\", false)]\n        [InlineData(\"MYORG.VISUALSTUDIO.COM\", true)]\n        [InlineData(null, false)]\n        [InlineData(\"\", false)]\n        [InlineData(\"    \", false)]\n        [InlineData(\"testdev.azure.com\", false)]\n        [InlineData(\"test.dev.azure.com\", false)]\n        [InlineData(\"visualstudio.com\", false)]\n        [InlineData(\"testvisualstudio.com\", false)]\n        public void UriHelpers_IsVisualStudioComHost(string host, bool expected)\n        {\n            Assert.Equal(expected, UriHelpers.IsVisualStudioComHost(host));\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_Null_ThrowsException()\n        {\n            Assert.Throws<ArgumentNullException>(() => UriHelpers.CreateOrganizationUri(null, out _));\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_AzureHost_ReturnsCorrectUri()\n        {\n            var expected = new Uri(\"https://dev.azure.com/myorg\");\n            var input =  new Uri(\"https://dev.azure.com/myorg/myproject/_git/myrepo\");\n            const string expectedOrg = \"myorg\";\n\n            Uri actual = UriHelpers.CreateOrganizationUri(input, out string actualOrg);\n\n            Assert.Equal(expected, actual);\n            Assert.Equal(expectedOrg, actualOrg);\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_AzureHost_WithPort_ReturnsCorrectUri()\n        {\n            var expected = new Uri(\"https://dev.azure.com:456/myorg\");\n            var input = new Uri(\"https://dev.azure.com:456/myorg/myproject/_git/myrepo\");\n            const string expectedOrg = \"myorg\";\n\n            Uri actual = UriHelpers.CreateOrganizationUri(input, out string actualOrg);\n\n            Assert.Equal(expected, actual);\n            Assert.Equal(expectedOrg, actualOrg);\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_AzureHost_OrgAlsoInUser_PrefersPathOrg()\n        {\n            var expected = new Uri(\"https://dev.azure.com/myorg-path\");\n            var input = new Uri(\"https://myorg-user@dev.azure.com/myorg-path\");\n            const string expectedOrg = \"myorg-path\";\n\n            Uri actual = UriHelpers.CreateOrganizationUri(input, out string actualOrg);\n\n            Assert.Equal(expected, actual);\n            Assert.Equal(expectedOrg, actualOrg);\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_AzureHost_InputArgsMissingPath_HasUser_UsesUserOrg()\n        {\n            var expected = new Uri(\"https://dev.azure.com/myorg-user\");\n            var input = new Uri(\"https://myorg-user@dev.azure.com\");\n            const string expectedOrg = \"myorg-user\";\n\n            Uri actual = UriHelpers.CreateOrganizationUri(input, out string actualOrg);\n\n            Assert.Equal(expected, actual);\n            Assert.Equal(expectedOrg, actualOrg);\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_AzureHost_InputArgsMissingPathAndUser_ThrowsException()\n        {\n            var input = new Uri(\"https://dev.azure.com\");\n\n            Assert.Throws<InvalidOperationException>(() => UriHelpers.CreateOrganizationUri(input, out _));\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_VisualStudioHost_ReturnsCorrectUri()\n        {\n            var expected = new Uri(\"https://myorg.visualstudio.com\");\n            var input = new Uri(\"https://myorg.visualstudio.com\");\n            const string expectedOrg = \"myorg\";\n\n            Uri actual = UriHelpers.CreateOrganizationUri(input, out string actualOrg);\n\n            Assert.Equal(expected, actual);\n            Assert.Equal(expectedOrg, actualOrg);\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_VisualStudioHost_MissingOrgInHost_ThrowsException()\n        {\n            var input = new Uri(\"https://visualstudio.com\");\n\n            Assert.Throws<InvalidOperationException>(() => UriHelpers.CreateOrganizationUri(input, out _));\n        }\n\n        [Fact]\n        public void UriHelpers_CreateOrganizationUri_NonAzureDevOpsHost_ThrowsException()\n        {\n            var input = new Uri(\"https://example.com\");\n\n            Assert.Throws<InvalidOperationException>(() => UriHelpers.CreateOrganizationUri(input, out _));\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/AssertEx.cs",
    "content": "using Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public static class AssertEx\n    {\n        /// <summary>\n        /// Requires the fact or theory be marked with the <see cref=\"SkippableFactAttribute\"/>\n        /// or <see cref=\"SkippableTheoryAttribute\"/>.\n        /// </summary>\n        /// <param name=\"reason\">Reason the test has been skipped.</param>\n        public static void Skip(string reason)\n        {\n            Xunit.Skip.If(true, reason);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/GitTestUtilities.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing GitCredentialManager.Tests.Objects;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public static class GitTestUtilities\n    {\n        public static string GetGitPath()\n        {\n            ProcessStartInfo psi;\n            if (PlatformUtils.IsWindows())\n            {\n                psi = new ProcessStartInfo(\n                    Path.Combine(\n                        Environment.GetFolderPath(Environment.SpecialFolder.System),\n                        \"where.exe\"),\n                    \"git.exe\"\n                );\n            }\n            else\n            {\n                psi = new ProcessStartInfo(\"/usr/bin/which\", \"git\");\n            }\n\n            psi.RedirectStandardOutput = true;\n\n            using (var which = new ChildProcess(new NullTrace2(), psi))\n            {\n                which.Start(Trace2ProcessClass.None);\n                which.WaitForExit();\n\n                if (which.ExitCode != 0)\n                {\n                    throw new Exception(\"Failed to locate Git\");\n                }\n\n                string data = which.StandardOutput.ReadLine();\n\n                if (string.IsNullOrWhiteSpace(data))\n                {\n                    throw new Exception(\"Failed to locate Git on the PATH\");\n                }\n\n                return data;\n            }\n        }\n\n        public static string CreateRepository() => CreateRepository(out _);\n\n        public static string CreateRepository(out string workDirPath)\n        {\n            string tempDirectory = Path.GetTempPath();\n            string repoName = $\"repo-{Guid.NewGuid().ToString(\"N\").Substring(0, 8)}\";\n            workDirPath = Path.Combine(tempDirectory, repoName);\n            string gitDirPath = Path.Combine(workDirPath, \".git\");\n\n            if (Directory.Exists(workDirPath))\n            {\n                Directory.Delete(workDirPath);\n            }\n\n            Directory.CreateDirectory(workDirPath);\n\n            ExecGit(gitDirPath, workDirPath, \"init\").AssertSuccess();\n\n            return gitDirPath;\n        }\n\n        public static GitResult ExecGit(string repositoryPath, string workingDirectory, string command)\n        {\n            var procInfo = new ProcessStartInfo(\"git\", command)\n            {\n                RedirectStandardOutput = true,\n                RedirectStandardError = true,\n                WorkingDirectory = workingDirectory\n            };\n\n            procInfo.Environment[\"GIT_DIR\"] = repositoryPath;\n\n            var proc = ChildProcess.Start(new NullTrace2(), procInfo, Trace2ProcessClass.None);\n            if (proc is null)\n            {\n                throw new Exception(\"Failed to start Git process\");\n            }\n\n            proc.WaitForExit();\n\n            var result = new GitResult\n            {\n                ExitCode = proc.ExitCode,\n                StandardOutput = proc.StandardOutput.ReadToEnd(),\n                StandardError = proc.StandardError.ReadToEnd()\n            };\n\n            return result;\n        }\n\n        public struct GitResult\n        {\n            public int ExitCode;\n            public string StandardOutput;\n            public string StandardError;\n\n            public void AssertSuccess()\n            {\n                Assert.Equal(0, ExitCode);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/NullTrace.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class NullTrace : ITrace\n    {\n        #region ITrace\n\n        bool ITrace.HasListeners\n        {\n            get => false;\n        }\n\n        bool ITrace.IsSecretTracingEnabled\n        {\n            get => false;\n            set {}\n        }\n\n        void ITrace.AddListener(TextWriter listener) { }\n\n        void ITrace.Flush() { }\n\n        void ITrace.WriteException(Exception exception, string filePath, int lineNumber, string memberName) { }\n\n        void ITrace.WriteDictionary<TKey, TValue>(\n            IDictionary<TKey, TValue> dictionary, string filePath, int lineNumber, string memberName) { }\n\n        public void WriteDictionarySecrets<TKey, TValue>(IDictionary<TKey, TValue> dictionary, TKey[] secretKeys,\n            IEqualityComparer<TKey> keyComparer = null, string filePath = \"\", int lineNumber = 0,\n            string memberName = \"\") { }\n\n        void ITrace.WriteLine(string message, string filePath, int lineNumber, string memberName) { }\n\n        void ITrace.WriteLineSecrets(\n            string format, object[] secrets, string filePath, int lineNumber, string memberName) { }\n\n        #endregion\n\n        #region IDisposable\n\n        void IDisposable.Dispose() { }\n\n        #endregion\n    }\n\n    public class NullTrace2 : ITrace2\n    {\n        #region ITrace2\n\n        public void Initialize(DateTimeOffset startTime) { }\n\n        public void Start(string appPath,\n            string[] args,\n            string filePath = \"\",\n            int lineNumber = 0) { }\n\n        public void Stop(int exitCode,\n            string fileName,\n            int lineNumber) { }\n\n        public void WriteChildStart(DateTimeOffset startTime,\n            Trace2ProcessClass processClass,\n            bool useShell,\n            string appName,\n            string argv,\n            string filePath = \"\",\n            int lineNumber = 0) { }\n\n        public void WriteChildExit(\n            double relativeTime,\n            int pid,\n            int code,\n            string filePath = \"\",\n            int lineNumber = 0) { }\n\n        public void WriteError(\n            string errorMessage,\n            string parameterizedMessage = null,\n            string filePath = \"\",\n            int lineNumber = 0) { }\n\n        public Region CreateRegion(\n            string category,\n            string label,\n            string message = \"\",\n            string filePath = \"\",\n            int lineNumber = 0)\n        {\n            return new Region(this, category, label, filePath, lineNumber, message);\n        }\n\n        public void WriteRegionEnter(\n            string category,\n            string label,\n            string message = \"\",\n            string filePath = \"\",\n            int lineNumber = 0) { }\n\n        public void WriteRegionLeave(\n            double relativeTime,\n            string category,\n            string label,\n            string message = \"\",\n            string filePath = \"\",\n            int lineNumber = 0) { }\n\n        #endregion\n\n        #region IDisposable\n\n        void IDisposable.Dispose() { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestCommandContext.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestCommandContext : ICommandContext\n    {\n        public TestCommandContext()\n        {\n            AppPath = PlatformUtils.IsWindows()\n                ? @\"C:\\Program Files\\Git Credential Manager Core\\git-credential-manager.exe\"\n                : \"/usr/local/bin/git-credential-manager\";\n\n            InstallDir = Path.GetDirectoryName(AppPath);\n\n            Streams = new TestStandardStreams();\n            Terminal = new TestTerminal();\n            SessionManager = new TestSessionManager();\n            Trace = new NullTrace();\n            Trace2 = new NullTrace2();\n            FileSystem = new TestFileSystem();\n            CredentialStore = new TestCredentialStore();\n            HttpClientFactory = new TestHttpClientFactory();\n            Git = new TestGit();\n            Environment = new TestEnvironment(FileSystem);\n\n            Settings = new TestSettings {Environment = Environment, GitConfiguration = Git.Configuration};\n        }\n\n        public string AppPath { get; set; }\n        public string InstallDir { get; set; }\n        public TestSettings Settings { get; set; }\n        public TestStandardStreams Streams { get; set; }\n        public TestTerminal Terminal { get; set; }\n        public TestSessionManager SessionManager { get; set; }\n        public ITrace Trace { get; set; }\n        public ITrace2 Trace2 { get; set; }\n        public TestFileSystem FileSystem { get; set; }\n        public TestCredentialStore CredentialStore { get; set; }\n        public TestHttpClientFactory HttpClientFactory { get; set; }\n        public TestGit Git { get; set; }\n        public TestEnvironment Environment { get; set; }\n\n        public IProcessManager ProcessManager { get; set; }\n\n        #region ICommandContext\n\n        string ICommandContext.ApplicationPath\n        {\n            get => AppPath;\n            set => AppPath = value;\n        }\n\n        string ICommandContext.InstallationDirectory => InstallDir;\n\n        IStandardStreams ICommandContext.Streams => Streams;\n\n        ISettings ICommandContext.Settings => Settings;\n\n        ITerminal ICommandContext.Terminal => Terminal;\n\n        ISessionManager ICommandContext.SessionManager => SessionManager;\n\n        ITrace ICommandContext.Trace => Trace;\n\n        IFileSystem ICommandContext.FileSystem => FileSystem;\n\n        ICredentialStore ICommandContext.CredentialStore => CredentialStore;\n\n        IHttpClientFactory ICommandContext.HttpClientFactory => HttpClientFactory;\n\n        IGit ICommandContext.Git => Git;\n\n        IEnvironment ICommandContext.Environment => Environment;\n\n        #endregion\n\n        #region IDisposable\n\n        void IDisposable.Dispose() { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestCredentialStore.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestCredentialStore : ICredentialStore\n    {\n        private readonly IDictionary<(string service, string account), TestCredential> _store;\n\n        public TestCredentialStore()\n        {\n            _store = new Dictionary<(string,string), TestCredential>();\n        }\n\n        #region ICredentialStore\n\n        public IList<string> GetAccounts(string service)\n        {\n            return Query(service, null).Select(x => x.Account).Distinct().ToList();\n        }\n\n        ICredential ICredentialStore.Get(string service, string account)\n        {\n            return TryGet(service, account, out TestCredential credential) ? credential : null;\n        }\n\n        void ICredentialStore.AddOrUpdate(string service, string account, string secret)\n        {\n            Add(service, account, secret);\n        }\n\n        bool ICredentialStore.Remove(string service, string account)\n        {\n            foreach (var key in _store.Keys)\n            {\n                if ((service == null || key.service == service) &&\n                    (account == null || key.account == account))\n                {\n                    _store.Remove(key);\n                    return true;\n                }\n            }\n\n            return false;\n        }\n\n        #endregion\n\n        public int Count => _store.Count;\n\n        public bool TryGet(string service, string account, out TestCredential credential)\n        {\n            credential = Query(service, account).FirstOrDefault();\n            return credential != null;\n        }\n\n        public void Add(string service, TestCredential credential)\n        {\n            _store[(service, credential.Account)] = credential;\n        }\n\n        public TestCredential Add(string service, string account, string secret)\n        {\n            var credential = new TestCredential(service, account, secret);\n            _store[(service, account)] = credential;\n            return credential;\n        }\n\n        public bool Contains(string service, string account)\n        {\n            return TryGet(service, account, out _);\n        }\n\n        private IEnumerable<TestCredential> Query(string service, string account)\n        {\n            if (string.IsNullOrWhiteSpace(account))\n            {\n                // Find the all credentials matching service\n                foreach (var kvp in _store)\n                {\n                    if (kvp.Key.service == service)\n                    {\n                        yield return kvp.Value;\n                    }\n                }\n            }\n\n            // Find the specific credential matching both service and credential\n            if (_store.TryGetValue((service, account), out var credential))\n            {\n                yield return credential;\n            }\n        }\n    }\n\n    public class TestCredential : ICredential\n    {\n        public TestCredential(string service, string account, string password)\n        {\n            Service = service;\n            Account = account;\n            Password = password;\n        }\n\n        public string Service { get; }\n\n        public string Account { get; }\n\n        public string Password { get; }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestEnvironment.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Linq;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestEnvironment : IEnvironment\n    {\n        private readonly IFileSystem _fileSystem;\n        private readonly IEqualityComparer<string> _pathComparer;\n        private readonly IEqualityComparer<string> _envarComparer;\n        private readonly string _envPathSeparator;\n\n        public TestEnvironment(IFileSystem fileSystem = null, string envPathSeparator = null, IEqualityComparer<string> pathComparer = null, IEqualityComparer<string> envarComparer = null)\n        {\n            _fileSystem = fileSystem ?? new TestFileSystem();\n\n            // Use the current platform separators and comparison types by default\n            _envPathSeparator = envPathSeparator ?? (PlatformUtils.IsWindows() ? \";\" : \":\");\n\n            _envarComparer = envarComparer ??\n                             (PlatformUtils.IsWindows()\n                                 ? StringComparer.OrdinalIgnoreCase\n                                 : StringComparer.Ordinal);\n\n            _pathComparer = pathComparer ??\n                            (PlatformUtils.IsLinux()\n                                ? StringComparer.Ordinal\n                                : StringComparer.OrdinalIgnoreCase);\n\n            Variables = new Dictionary<string, string>(_envarComparer);\n            Symlinks = new Dictionary<string, string>(_pathComparer);\n        }\n\n        public IDictionary<string, string> Variables { get; set; }\n\n        public IDictionary<string, string> Symlinks { get; set; }\n\n        public IList<string> Path\n        {\n            get\n            {\n                if (Variables.TryGetValue(\"PATH\", out string value))\n                {\n                    return value.Split(new[] {_envPathSeparator}, StringSplitOptions.RemoveEmptyEntries);\n                }\n\n                return new string[0];\n            }\n\n            set => Variables[\"PATH\"] = string.Join(_envPathSeparator, value);\n        }\n\n        #region IEnvironment\n\n        IReadOnlyDictionary<string, string> IEnvironment.Variables => new ReadOnlyDictionary<string, string>(Variables);\n\n        bool IEnvironment.IsDirectoryOnPath(string directoryPath)\n        {\n            return Path.Any(x => _pathComparer.Equals(x, directoryPath));\n        }\n\n        public void AddDirectoryToPath(string directoryPath, EnvironmentVariableTarget target)\n        {\n            Path.Add(directoryPath);\n\n            // Update envar\n            Variables[\"PATH\"] = string.Join(_envPathSeparator, Path);\n        }\n\n        public void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target)\n        {\n            Path.Remove(directoryPath);\n\n            // Update envar\n            Variables[\"PATH\"] = string.Join(_envPathSeparator, Path);\n        }\n\n        public bool TryLocateExecutable(string program, out string path)\n        {\n            if (Variables.TryGetValue(\"PATH\", out string pathValue))\n            {\n                string[] paths = pathValue.Split(new[]{_envPathSeparator}, StringSplitOptions.None);\n                foreach (var basePath in paths)\n                {\n                    string candidatePath = System.IO.Path.Combine(basePath, program);\n                    if (_fileSystem.FileExists(candidatePath))\n                    {\n                        path = candidatePath;\n                        return true;\n                    }\n                }\n            }\n\n            path = null;\n            return false;\n        }\n\n        public void SetEnvironmentVariable(string variable, string value,\n            EnvironmentVariableTarget target = EnvironmentVariableTarget.Process)\n        {\n            if (Variables.Keys.Contains(variable)) return;\n            Environment.SetEnvironmentVariable(variable, value, target);\n            Variables.Add(variable, value);\n        }\n\n        public void Refresh()\n        {\n            // Nothing to do!\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestFileSystem.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestFileSystem : IFileSystem\n    {\n        public string UserHomePath { get; set; }\n        public string UserDataDirectoryPath { get; set; }\n        public IDictionary<string, byte[]> Files { get; set; } = new Dictionary<string, byte[]>();\n        public ISet<string> Directories { get; set; } = new HashSet<string>();\n        public string CurrentDirectory { get; set; } = Path.GetTempPath();\n        public bool IsCaseSensitive { get; set; } = false;\n\n        public TestFileSystem()\n        {\n            var gcmTestRoot = Path.Combine(Path.GetTempPath(), $\"gcmtest-{Guid.NewGuid():N}\");\n            UserHomePath = Path.Combine(gcmTestRoot, \"HOME\");\n            UserDataDirectoryPath = Path.Combine(UserHomePath, \".gcm\");\n        }\n\n        #region IFileSystem\n\n        bool IFileSystem.IsSamePath(string a, string b)\n        {\n            return IsCaseSensitive\n                ? StringComparer.Ordinal.Equals(a, b)\n                : StringComparer.OrdinalIgnoreCase.Equals(a, b);\n        }\n\n        bool IFileSystem.FileExists(string path)\n        {\n            return Files.ContainsKey(path);\n        }\n\n        bool IFileSystem.DirectoryExists(string path)\n        {\n            return Directories.Contains(TrimSlash(path));\n        }\n\n        string IFileSystem.GetCurrentDirectory()\n        {\n            return CurrentDirectory;\n        }\n\n        Stream IFileSystem.OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare)\n        {\n            bool writable = fileAccess != FileAccess.Read;\n\n            if (fileMode == FileMode.Create)\n            {\n                return new TestFileStream(this, path);\n            }\n\n            return new MemoryStream(Files[path], writable);\n        }\n\n        void IFileSystem.CreateDirectory(string path)\n        {\n            Directories.Add(TrimSlash(path));\n        }\n\n        void IFileSystem.DeleteFile(string path)\n        {\n            Files.Remove(path);\n        }\n\n        IEnumerable<string> IFileSystem.EnumerateFiles(string path, string searchPattern)\n        {\n            bool IsPatternMatch(string s, string p)\n            {\n                var options = IsCaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase;\n                string regex = p\n                    .Replace(\".\", \"\\\\.\")\n                    .Replace(\"*\", \".*\");\n\n                return Regex.IsMatch(s, regex, options);\n            }\n\n            StringComparison comparer = IsCaseSensitive\n                ? StringComparison.Ordinal\n                : StringComparison.OrdinalIgnoreCase;\n\n            foreach (var filePath in Files.Keys)\n            {\n                if (filePath.StartsWith(path, comparer) && IsPatternMatch(filePath, searchPattern))\n                {\n                    yield return filePath;\n                }\n            }\n        }\n\n        IEnumerable<string> IFileSystem.EnumerateDirectories(string path)\n        {\n            StringComparison comparer = IsCaseSensitive\n                ? StringComparison.Ordinal\n                : StringComparison.OrdinalIgnoreCase;\n\n            foreach (var dirPath in Directories)\n            {\n                if (dirPath.StartsWith(path, comparer))\n                {\n                    yield return dirPath;\n                }\n            }\n        }\n\n        string IFileSystem.ReadAllText(string path)\n        {\n            if (Files.TryGetValue(path, out byte[] data))\n            {\n                return Encoding.UTF8.GetString(data);\n            }\n\n            throw new IOException(\"File not found\");\n        }\n\n        string[] IFileSystem.ReadAllLines(string path)\n        {\n            if (Files.TryGetValue(path, out byte[] data))\n            {\n                return Encoding.UTF8.GetString(data).Split(new[] { \"\\r\\n\", \"\\n\" }, StringSplitOptions.None);\n            }\n\n            throw new IOException(\"File not found\");\n        }\n\n        #endregion\n\n        /// <summary>\n        /// Trim trailing slashes from a path.\n        /// </summary>\n        public static string TrimSlash(string path)\n        {\n            if (path.Length > 0 && path[path.Length - 1] == Path.DirectorySeparatorChar)\n            {\n                return path.Substring(0, path.Length - 1);\n            }\n\n            return path;\n        }\n    }\n\n    public class TestFileStream : MemoryStream\n    {\n        private readonly TestFileSystem _fs;\n        private readonly string _path;\n\n        public TestFileStream(TestFileSystem fs, string path)\n        {\n            _fs = fs;\n            _path = path;\n        }\n\n        public override void Flush()\n        {\n            base.Flush();\n            _fs.Files[_path] = base.ToArray();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestGit.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestGit : IGit\n    {\n        public GitVersion Version { get; set; } = new GitVersion(\"2.32.0.test.0\");\n\n        public string CurrentRepository { get; set; }\n\n        public IList<GitRemote> Remotes { get; set; } = new List<GitRemote>();\n\n        public readonly TestGitConfiguration Configuration = new TestGitConfiguration();\n\n        public TestGit(bool insideRepo = true)\n        {\n            if (insideRepo)\n            {\n                CurrentRepository = GetFakeRepositoryPath();\n            }\n        }\n\n        #region IGit\n\n        GitVersion IGit.Version => Version;\n\n        public ChildProcess CreateProcess(string args)\n        {\n            throw new NotImplementedException();\n        }\n\n        bool IGit.IsInsideRepository() => !string.IsNullOrWhiteSpace(CurrentRepository);\n\n        string IGit.GetCurrentRepository() => CurrentRepository;\n\n        IEnumerable<GitRemote> IGit.GetRemotes() => Remotes;\n\n        IGitConfiguration IGit.GetConfiguration() => Configuration;\n\n        Task<IDictionary<string, string>> IGit.InvokeHelperAsync(string args, IDictionary<string, string> standardInput)\n        {\n            throw new NotImplementedException();\n        }\n\n        #endregion\n\n        private static IDictionary<string, IList<string>> MergeDictionaries(params IDictionary<string, IList<string>>[] dictionaries)\n        {\n            var result = new Dictionary<string, IList<string>>(GitConfigurationKeyComparer.Instance);\n\n            foreach (IDictionary<string, IList<string>> dict in dictionaries)\n            {\n                foreach (var kvp in dict)\n                {\n                    result[kvp.Key] = kvp.Value;\n                }\n            }\n\n            return result;\n        }\n\n        public static string GetFakeRepositoryPath(string name = null)\n        {\n            name ??= Guid.NewGuid().ToString(\"N\").Substring(8);\n            var basePath = Path.GetTempPath();\n            return Path.Combine(basePath, \"fake-repo\", name);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestGitConfiguration : IGitConfiguration\n    {\n        public const string CanonicalPathPrefix = \"/my/path/prefix\";\n\n        public IDictionary<string, IList<string>> System { get; set; } =\n            new Dictionary<string, IList<string>>(GitConfigurationKeyComparer.Instance);\n        public IDictionary<string, IList<string>> Global { get; set; } =\n            new Dictionary<string, IList<string>>(GitConfigurationKeyComparer.Instance);\n        public IDictionary<string, IList<string>> Local { get; set; } =\n            new Dictionary<string, IList<string>>(GitConfigurationKeyComparer.Instance);\n\n        #region IGitConfiguration\n\n        public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb)\n        {\n            foreach (var (dictLevel, dict) in GetDictionaries(level))\n            {\n                foreach (var kvp in dict)\n                {\n                    foreach (var value in kvp.Value)\n                    {\n                        var entry = new GitConfigurationEntry(kvp.Key, value);\n                        if (!cb(entry))\n                        {\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, string name, out string value)\n        {\n            value = null;\n\n            // Proceed in order from least to most specific level and read the entry value\n            foreach (var (_, dict) in GetDictionaries(level))\n            {\n                if (dict.TryGetValue(name, out var values))\n                {\n                    if (values.Count > 1)\n                    {\n                        throw new Exception(\"Configuration entry is a multivar\");\n                    }\n\n                    if (values.Count == 1)\n                    {\n                        value = values[0];\n\n                        if (type == GitConfigurationType.Path)\n                        {\n                            // Create \"fake\" canonical path\n                            value = value.Replace(\"~\", CanonicalPathPrefix);\n                        }\n                    }\n                }\n            }\n\n            return value != null;\n        }\n\n        public void Set(GitConfigurationLevel level, string name, string value)\n        {\n            IDictionary<string, IList<string>> dict = GetDictionary(level);\n\n            if (!dict.TryGetValue(name, out var values))\n            {\n                values = new List<string>();\n                dict[name] = values;\n            }\n\n            // Simulate git\n            if (values.Count > 1)\n            {\n                throw new Exception(\"Configuration entry is a multivar\");\n            }\n\n            if (values.Count == 1)\n            {\n                values[0] = value;\n            }\n            else if (values.Count == 0)\n            {\n                values.Add(value);\n            }\n        }\n\n        public void Add(GitConfigurationLevel level, string name, string value)\n        {\n            IDictionary<string, IList<string>> dict = GetDictionary(level);\n\n            if (!dict.TryGetValue(name, out IList<string> values))\n            {\n                values = new List<string>();\n                dict[name] = values;\n            }\n\n            values.Add(value);\n        }\n\n        public void Unset(GitConfigurationLevel level, string name)\n        {\n            IDictionary<string, IList<string>> dict = GetDictionary(level);\n\n            // Simulate git\n            if (dict.TryGetValue(name, out var values) && values.Count > 1)\n            {\n                throw new Exception(\"Configuration entry is a multivar\");\n            }\n\n            dict.Remove(name);\n        }\n\n        public IEnumerable<string> GetAll(GitConfigurationLevel level, GitConfigurationType type, string name)\n        {\n            foreach (var (_, dict) in GetDictionaries(level))\n            {\n                if (dict.TryGetValue(name, out IList<string> values))\n                {\n                    foreach (string value in values)\n                    {\n                        yield return value;\n                    }\n                }\n            }\n        }\n\n        public IEnumerable<string> GetRegex(GitConfigurationLevel level, GitConfigurationType type, string nameRegex, string valueRegex)\n        {\n            foreach (var (_, dict) in GetDictionaries(level))\n            {\n                foreach (string key in dict.Keys)\n                {\n                    if (Regex.IsMatch(key, nameRegex))\n                    {\n                        var values = dict[key].Where(x => Regex.IsMatch(x, valueRegex));\n                        foreach (string value in values)\n                        {\n                            yield return value;\n                        }\n                    }\n                }\n            }\n        }\n\n        public void ReplaceAll(GitConfigurationLevel level, string nameRegex, string valueRegex, string value)\n        {\n            IDictionary<string, IList<string>> dict = GetDictionary(level);\n\n            if (!dict.TryGetValue(nameRegex, out IList<string> values))\n            {\n                values = new List<string>();\n                dict[nameRegex] = values;\n            }\n\n            bool updated = false;\n            for (int i = 0; i < values.Count; i++)\n            {\n                // Update matching values\n                if (Regex.IsMatch(values[i], valueRegex))\n                {\n                    values[i] = value;\n                    updated = true;\n                }\n            }\n\n            // If no existing value was found to update, add a new one\n            if (!updated)\n            {\n                values.Add(value);\n            }\n        }\n\n        public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex)\n        {\n            IDictionary<string, IList<string>> dict = GetDictionary(level);\n\n            if (dict.TryGetValue(name, out IList<string> values))\n            {\n                for (int i = 0; i < values.Count;)\n                {\n                    // Remove matching values\n                    if (Regex.IsMatch(values[i], valueRegex))\n                    {\n                        values.RemoveAt(i);\n                    }\n                    else\n                    {\n                        // Move to the next value\n                        i++;\n                    }\n                }\n\n                // If we've removed all values, remove the top-level list from the multivar dictionary\n                if (values.Count == 0)\n                {\n                    dict.Remove(name);\n                }\n            }\n        }\n\n        #endregion\n\n        private IDictionary<string, IList<string>> GetDictionary(GitConfigurationLevel level)\n        {\n            switch (level)\n            {\n                case GitConfigurationLevel.System:\n                    return System;\n                case GitConfigurationLevel.Global:\n                    return Global;\n                case GitConfigurationLevel.Local:\n                    return Local;\n                case GitConfigurationLevel.All:\n                    throw new ArgumentException(\"Must specify a specific level\");\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(level), level, \"Unsupported level\");\n            }\n        }\n\n        private IEnumerable<(GitConfigurationLevel level, IDictionary<string, IList<string>> dict)> GetDictionaries(\n            GitConfigurationLevel level)\n        {\n            switch (level)\n            {\n                case GitConfigurationLevel.System:\n                    yield return (GitConfigurationLevel.System, System);\n                    break;\n                case GitConfigurationLevel.Global:\n                    yield return (GitConfigurationLevel.Global, Global);\n                    break;\n                case GitConfigurationLevel.Local:\n                    yield return (GitConfigurationLevel.Local, Local);\n                    break;\n                case GitConfigurationLevel.All:\n                    yield return (GitConfigurationLevel.System, System);\n                    yield return (GitConfigurationLevel.Global, Global);\n                    yield return (GitConfigurationLevel.Local, Local);\n                    break;\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(level), level, \"Unsupported level\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestGpg.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing GitCredentialManager.Interop.Posix;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestGpg : IGpg\n    {\n        private readonly TestFileSystem _fs;\n        private readonly ISet<string> _keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        public TestGpg(TestFileSystem fs)\n        {\n            _fs = fs;\n        }\n\n        public string DecryptFile(string path)\n        {\n            // No encryption\n            return Encoding.UTF8.GetString(_fs.Files[path]);\n        }\n\n        public void EncryptFile(string path, string gpgId, string contents)\n        {\n            if (!_keys.Contains(gpgId))\n            {\n                throw new Exception($\"No GPG key found for '{gpgId}'.\");\n            }\n\n            // No encryption\n            _fs.Files[path] = Encoding.UTF8.GetBytes(contents);\n        }\n\n        public void GenerateKeys(string userId)\n        {\n            _keys.Add(userId);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestHostProvider.cs",
    "content": "using System;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestHostProvider : HostProvider\n    {\n        public TestHostProvider(ICommandContext context)\n            : base(context) { }\n\n        public Func<InputArguments, bool> IsSupportedFunc { get; set; }\n\n        public string LegacyAuthorityIdValue { get; set; }\n\n        public Func<InputArguments, ICredential> GenerateCredentialFunc { get; set; }\n\n        #region HostProvider\n\n        public override string Id { get; } = \"test-provider\";\n\n        public override string Name { get; } = \"TestHostProvider\";\n\n        public string LegacyAuthorityId => LegacyAuthorityIdValue;\n\n        public override bool IsSupported(InputArguments input) => IsSupportedFunc(input);\n\n        public override Task<ICredential> GenerateCredentialAsync(InputArguments input)\n        {\n            return Task.FromResult(GenerateCredentialFunc(input));\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestHostProviderRegistry.cs",
    "content": "using System.Threading.Tasks;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestHostProviderRegistry : IHostProviderRegistry\n    {\n        public IHostProvider Provider { get; set; }\n\n        #region IHostProviderRegistry\n\n        void IHostProviderRegistry.Register(IHostProvider hostProvider, HostProviderPriority priority)\n        {\n        }\n\n        Task<IHostProvider> IHostProviderRegistry.GetProviderAsync(InputArguments input)\n        {\n            return Task.FromResult(Provider);\n        }\n\n        #endregion\n\n        public void Dispose()\n        {\n            Provider?.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestHttpClientFactory.cs",
    "content": "using System.Net;\nusing System.Net.Http;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestHttpClientFactory : IHttpClientFactory\n    {\n        public HttpMessageHandler MessageHandler { get; set; } = new TestHttpMessageHandler();\n\n        #region IHttpClientFactory\n\n        HttpClient IHttpClientFactory.CreateClient()\n        {\n            return new HttpClient(MessageHandler);\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestHttpMessageHandler.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestHttpMessageHandler : HttpMessageHandler\n    {\n        public delegate HttpResponseMessage RequestHandler(HttpRequestMessage request);\n\n        public delegate Task<HttpResponseMessage> AsyncRequestHandler(HttpRequestMessage request);\n\n        private readonly IDictionary<(HttpMethod method, Uri uri), AsyncRequestHandler> _handlers =\n                      new Dictionary<(HttpMethod, Uri), AsyncRequestHandler>();\n\n        private readonly IDictionary<(HttpMethod method, Uri uri), int> _requestCounts =\n                      new Dictionary<(HttpMethod, Uri), int>();\n\n        public bool ThrowOnUnexpectedRequest { get; set; }\n        public bool SimulateNoNetwork { get; set; }\n\n        public bool SimulatePrimaryUriFailure { get; set; }\n\n        public IDictionary<(HttpMethod method, Uri uri), int> RequestCounts => _requestCounts;\n\n        public void Setup(HttpMethod method, Uri uri, AsyncRequestHandler handler)\n        {\n            _handlers[CreateRequestKey(method, uri)] = handler;\n        }\n\n        public void Setup(HttpMethod method, Uri uri, RequestHandler handler)\n        {\n            Setup(method, uri, x => Task.FromResult(handler(x)));\n        }\n\n        public void Setup(HttpMethod method, Uri uri, HttpResponseMessage responseMessage)\n        {\n            Setup(method, uri, _ => responseMessage);\n        }\n\n        public void Setup(HttpMethod method, Uri uri, HttpStatusCode responseCode, string content)\n        {\n            Setup(method, uri, new HttpResponseMessage(responseCode){Content = new StringContent(content)});\n        }\n\n        public void Setup(HttpMethod method, Uri uri, HttpStatusCode responseCode)\n        {\n            Setup(method, uri, responseCode, string.Empty);\n        }\n\n        public void AssertRequest(HttpMethod method, Uri uri, int expectedNumberOfCalls)\n        {\n            int numCalls;\n            if (!_requestCounts.TryGetValue((method, uri), out numCalls))\n            {\n                numCalls = 0;\n            }\n\n            Assert.Equal(expectedNumberOfCalls, numCalls);\n        }\n\n        public void AssertNoRequests()\n        {\n            Assert.Empty(_requestCounts);\n        }\n\n        #region HttpMessageHandler\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        {\n            // Build the request key to match against registered handlers\n            (HttpMethod method, Uri uri) requestKey = CreateRequestKey(request.Method, request.RequestUri);\n\n            IncrementRequestCount(requestKey);\n\n            if (SimulateNoNetwork)\n            {\n                throw new HttpRequestException(\"Simulated no network\");\n            }\n\n            if (SimulatePrimaryUriFailure && request.RequestUri != null  &&\n                request.RequestUri.ToString().Equals(\"http://example.com/\"))\n            {\n                throw new HttpRequestException(\"Simulated http failure.\");\n            }\n\n            foreach (var kvp in _handlers)\n            {\n                if (kvp.Key == requestKey)\n                {\n                    return await kvp.Value(request);\n                }\n            }\n\n            if (ThrowOnUnexpectedRequest)\n            {\n                throw new Exception($\"No handler configured for the request '{request.Method} {request.RequestUri}'\");\n            }\n            else\n            {\n                return new HttpResponseMessage(HttpStatusCode.OK);\n            }\n        }\n\n        #endregion\n\n        private static (HttpMethod Method, Uri requestUri) CreateRequestKey(HttpMethod method, Uri uri)\n        {\n            // Trim the query and fragment\n            var normalizedUri = new Uri(uri.GetLeftPart(UriPartial.Path));\n\n            return (method, normalizedUri);\n        }\n\n        private void IncrementRequestCount((HttpMethod, Uri) requestKey)\n        {\n            if (!_requestCounts.ContainsKey(requestKey))\n            {\n                _requestCounts[requestKey] = 0;\n            }\n            _requestCounts[requestKey]++;\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestOAuth2Server.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication.OAuth;\nusing GitCredentialManager.Authentication.OAuth.Json;\nusing System.Text.Json;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestOAuth2Server\n    {\n        private readonly IDictionary<string, OAuth2Application> _apps = new Dictionary<string, OAuth2Application>();\n        private readonly Uri _deviceCodeVerificationUri = new Uri(\"https://example.com/devicelogin\");\n\n        public TestOAuth2Server(OAuth2ServerEndpoints endpoints)\n        {\n            Endpoints = endpoints;\n        }\n\n        public OAuth2ServerEndpoints Endpoints { get; }\n\n        public TestOAuth2ServerTokenGenerator TokenGenerator = new TestOAuth2ServerTokenGenerator();\n\n        public event EventHandler<HttpRequestMessage> AuthorizationEndpointInvoked;\n        public event EventHandler<HttpRequestMessage> DeviceAuthorizationEndpointInvoked;\n        public event EventHandler<HttpRequestMessage> TokenEndpointInvoked;\n\n        public void RegisterApplication(OAuth2Application application)\n        {\n            _apps[application.Id] = application;\n        }\n\n        public void Bind(TestHttpMessageHandler httpHandler)\n        {\n            httpHandler.Setup(HttpMethod.Get, Endpoints.AuthorizationEndpoint, OnAuthorizationEndpointAsync);\n            httpHandler.Setup(HttpMethod.Post, Endpoints.DeviceAuthorizationEndpoint, OnDeviceAuthorizationEndpointAsync);\n            httpHandler.Setup(HttpMethod.Post, Endpoints.TokenEndpoint, OnTokenEndpointAsync);\n        }\n\n        public void SignInDeviceWithUserCode(string userCode)\n        {\n            OAuth2Application app = _apps.Values.FirstOrDefault(x => x.OwnsDeviceCodeGrant(userCode));\n            if (app is null)\n            {\n                throw new Exception($\"Unknown user code '{userCode}'\");\n            }\n\n            app.ApproveDeviceCodeGrant(userCode);\n        }\n\n        private Task<HttpResponseMessage> OnAuthorizationEndpointAsync(HttpRequestMessage request)\n        {\n            AuthorizationEndpointInvoked?.Invoke(this, request);\n\n            IDictionary<string, string> reqQuery = request.RequestUri.GetQueryParameters();\n\n            // The only support response type so far is 'code'\n            Assert.True(reqQuery.TryGetValue(OAuth2Constants.AuthorizationEndpoint.ResponseTypeParameter, out string respType));\n            Assert.Equal(OAuth2Constants.AuthorizationEndpoint.AuthorizationCodeResponseType, respType);\n\n            // The client/app ID must be specified and must match a known application\n            if (!reqQuery.TryGetValue(OAuth2Constants.ClientIdParameter, out string clientId) ||\n                !_apps.TryGetValue(clientId, out OAuth2Application app))\n            {\n                throw new Exception($\"Unknown OAuth application '{clientId}'\");\n            }\n\n            // Redirect is optional, but if it is specified it must match a registered URL\n            reqQuery.TryGetValue(OAuth2Constants.RedirectUriParameter, out string redirectUrlStr);\n            Uri redirectUri = app.ValidateRedirect(redirectUrlStr);\n\n            // Scope is optional\n            reqQuery.TryGetValue(OAuth2Constants.ScopeParameter, out string scopeStr);\n            string[] scopes = scopeStr?.Split(' ');\n\n            // Code challenge is optional\n            reqQuery.TryGetValue(OAuth2Constants.AuthorizationEndpoint.PkceChallengeParameter, out string codeChallenge);\n\n            // Code challenge method is optional and defaults to \"plain\"\n            var codeChallengeMethod = OAuth2PkceChallengeMethod.Plain;\n            if (reqQuery.TryGetValue(OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodParameter, out string challengeMethodStr))\n            {\n                if (StringComparer.OrdinalIgnoreCase.Equals(challengeMethodStr,\n                    OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodPlain))\n                {\n                    codeChallengeMethod = OAuth2PkceChallengeMethod.Plain;\n                }\n                else if (StringComparer.OrdinalIgnoreCase.Equals(challengeMethodStr,\n                    OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodS256))\n                {\n                    codeChallengeMethod = OAuth2PkceChallengeMethod.Sha256;\n                }\n                else\n                {\n                    throw new Exception($\"Unsupported code challenge method '{challengeMethodStr}'\");\n                }\n            }\n\n            // Create the auth code grant\n            OAuth2Application.AuthCodeGrant grant = app.CreateAuthorizationCodeGrant(\n                TokenGenerator, scopes, redirectUrlStr, codeChallenge, codeChallengeMethod);\n\n            var respQuery = new Dictionary<string, string>\n            {\n                [OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter] = grant.Code\n            };\n\n            // State is optional but must be included in the reply if specified\n            if (reqQuery.TryGetValue(OAuth2Constants.AuthorizationEndpoint.StateParameter, out string state))\n            {\n                respQuery[OAuth2Constants.AuthorizationGrantResponse.StateParameter] = state;\n            }\n\n            // Build the final redirect URI including the auth code\n            var ub = new UriBuilder(redirectUri)\n            {\n                Query = respQuery.ToQueryString()\n            };\n\n            var response = new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                RequestMessage = request,\n                Headers = {Location = ub.Uri}\n            };\n\n            return Task.FromResult(response);\n        }\n\n        private async Task<HttpResponseMessage> OnDeviceAuthorizationEndpointAsync(HttpRequestMessage request)\n        {\n            DeviceAuthorizationEndpointInvoked?.Invoke(this, request);\n\n            IDictionary<string, string> formData = await request.Content.ReadAsFormContentAsync();\n\n            // The client/app ID must be specified and must match a known application\n            if (!formData.TryGetValue(OAuth2Constants.ClientIdParameter, out string clientId) ||\n                !_apps.TryGetValue(clientId, out OAuth2Application app))\n            {\n                throw new Exception($\"Unknown OAuth application '{clientId}'\");\n            }\n\n            // Scope is optional\n            formData.TryGetValue(OAuth2Constants.ScopeParameter, out string scopeStr);\n            string[] scopes = scopeStr?.Split(' ');\n\n            // Create the device code grant\n            OAuth2Application.DeviceCodeGrant grant = app.CreateDeviceCodeGrant(TokenGenerator, scopes);\n\n            var deviceResp = new DeviceAuthorizationEndpointResponseJson\n            {\n                DeviceCode = grant.DeviceCode,\n                UserCode = grant.UserCode,\n                VerificationUri = _deviceCodeVerificationUri,\n            };\n\n            string responseJson = JsonSerializer.Serialize(deviceResp);\n\n            return new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                RequestMessage = request,\n                Content = new StringContent(responseJson)\n            };\n        }\n\n        private async Task<HttpResponseMessage> OnTokenEndpointAsync(HttpRequestMessage request)\n        {\n            TokenEndpointInvoked?.Invoke(this, request);\n\n            IDictionary<string, string> formData = await request.Content.ReadAsFormContentAsync();\n\n            if (!formData.TryGetValue(OAuth2Constants.TokenEndpoint.GrantTypeParameter, out string grantType))\n            {\n                throw new Exception(\"Missing grant type\");\n            }\n\n            if (!formData.TryGetValue(OAuth2Constants.ClientIdParameter, out string clientId))\n            {\n                throw new Exception(\"Missing client ID in request body\");\n            }\n\n            if (!_apps.TryGetValue(clientId, out OAuth2Application app))\n            {\n                throw new Exception($\"Unknown OAuth application '{clientId}'\");\n            }\n\n            TokenEndpointResponseJson tokenResp;\n            if (StringComparer.OrdinalIgnoreCase.Equals(grantType, OAuth2Constants.TokenEndpoint.AuthorizationCodeGrantType))\n            {\n                if (!formData.TryGetValue(OAuth2Constants.TokenEndpoint.AuthorizationCodeParameter, out string authCode))\n                {\n                    throw new Exception(\"Missing authorization code parameter\");\n                }\n\n                formData.TryGetValue(OAuth2Constants.TokenEndpoint.PkceVerifierParameter, out string codeVerifier);\n                if (formData.TryGetValue(OAuth2Constants.RedirectUriParameter, out string redirectUriStr))\n                {\n                    app.ValidateRedirect(redirectUriStr);\n                }\n\n                tokenResp = app.CreateTokenByAuthorizationGrant(TokenGenerator, authCode, codeVerifier, redirectUriStr);\n            }\n            else if (StringComparer.OrdinalIgnoreCase.Equals(grantType, OAuth2Constants.TokenEndpoint.RefreshTokenGrantType))\n            {\n                if (!formData.TryGetValue(OAuth2Constants.TokenEndpoint.RefreshTokenParameter, out string refreshToken))\n                {\n                    throw new Exception(\"Missing refresh token parameter\");\n                }\n\n                app.ValidateRedirect(formData[OAuth2Constants.RedirectUriParameter]);\n\n                tokenResp = app.CreateTokenByRefreshTokenGrant(TokenGenerator, refreshToken);\n            }\n            else if (StringComparer.OrdinalIgnoreCase.Equals(grantType, OAuth2Constants.DeviceAuthorization.DeviceCodeGrantType))\n            {\n                if (!formData.TryGetValue(OAuth2Constants.DeviceAuthorization.DeviceCodeParameter, out string deviceCode))\n                {\n                    throw new Exception(\"Missing device code parameter\");\n                }\n\n                if (app.IsDeviceCodeGrantApproved(deviceCode))\n                {\n                    tokenResp = app.CreateTokenByDeviceCodeGrant(TokenGenerator, deviceCode);\n                }\n                else\n                {\n                    var errorResp = new ErrorResponseJson\n                    {\n                        Error = OAuth2Constants.DeviceAuthorization.Errors.AuthorizationPending\n                    };\n\n                    var errorJson = JsonSerializer.Serialize(errorResp);\n\n                    return new HttpResponseMessage(HttpStatusCode.BadRequest)\n                    {\n                        RequestMessage = request,\n                        Content = new StringContent(errorJson)\n                    };\n                }\n            }\n            else\n            {\n                throw new Exception($\"Unknown grant type '{grantType}'\");\n            }\n\n            string responseJson = JsonSerializer.Serialize(tokenResp);\n\n            return new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                RequestMessage = request,\n                Content = new StringContent(responseJson)\n            };\n        }\n    }\n\n    public class TestOAuth2ServerTokenGenerator\n    {\n        private int _authCodesIndex = -1;\n        private int _deviceCodesIndex = -1;\n        private int _userCodesIndex = -1;\n        private int _accessTokensIndex = -1;\n        private int _refreshTokensIndex = -1;\n\n        public readonly List<string> AuthCodes = new List<string>();\n        public readonly List<string> DeviceCodes = new List<string>();\n        public readonly List<string> UserCodes = new List<string>();\n        public readonly List<string> AccessTokens = new List<string>();\n        public readonly List<string> RefreshTokens = new List<string>();\n\n        public string CreateAuthorizationCode() => GetNextValueOrRandom(AuthCodes, ref _authCodesIndex);\n        public string CreateDeviceCode() => GetNextValueOrRandom(DeviceCodes, ref _deviceCodesIndex);\n        public string CreateUserCode() => GetNextValueOrRandom(UserCodes, ref _userCodesIndex);\n        public string CreateAccessToken() => GetNextValueOrRandom(AccessTokens, ref _accessTokensIndex);\n        public string CreateRefreshToken() => GetNextValueOrRandom(RefreshTokens, ref _refreshTokensIndex);\n\n        private static string GetNextValueOrRandom(List<string> values, ref int index)\n        {\n            if (index < 0)\n            {\n                index = 0;\n            }\n\n            if (index < values.Count)\n            {\n                return values[index++];\n            }\n\n            return Guid.NewGuid().ToString(\"N\").Substring(8);\n        }\n    }\n\n    public class OAuth2Application\n    {\n        public class AuthCodeGrant\n        {\n            public AuthCodeGrant(string code, string[] scopes, string redirectUri = null,\n                string codeChallenge = null, OAuth2PkceChallengeMethod codeChallengeMethod = OAuth2PkceChallengeMethod.Plain)\n            {\n                Code = code;\n                Scopes = scopes;\n                RedirectUri = redirectUri;\n                CodeChallenge = codeChallenge;\n                CodeChallengeMethod = codeChallengeMethod;\n            }\n            public string Code { get; }\n            public string[] Scopes { get; }\n            public string RedirectUri { get; }\n            public string CodeChallenge { get; }\n            public OAuth2PkceChallengeMethod CodeChallengeMethod { get; }\n        }\n\n        public class DeviceCodeGrant\n        {\n            public DeviceCodeGrant(string userCode, string deviceCode, string[] scopes)\n            {\n                UserCode = userCode;\n                DeviceCode = deviceCode;\n                Scopes = scopes;\n            }\n\n            public bool Approved { get; set; }\n            public string UserCode { get; }\n            public string DeviceCode { get; }\n            public string[] Scopes { get; }\n        }\n\n        public OAuth2Application(string id)\n        {\n            Id = id;\n        }\n\n        public string Id { get; }\n\n        public string Secret { get; set; }\n\n        public Uri[] RedirectUris { get; set; }\n\n        public IList<AuthCodeGrant> AuthGrants { get; } = new List<AuthCodeGrant>();\n\n        public IList<DeviceCodeGrant> DeviceGrants { get; } = new List<DeviceCodeGrant>();\n\n        public IDictionary<string, string> AccessTokens { get; } = new Dictionary<string, string>();\n\n        public IDictionary<string, string[]> RefreshTokens { get; } = new Dictionary<string, string[]>();\n\n        public AuthCodeGrant CreateAuthorizationCodeGrant(TestOAuth2ServerTokenGenerator generator,\n            string[] scopes, string redirectUri, string codeChallenge, OAuth2PkceChallengeMethod codeChallengeMethod)\n        {\n            string code = generator.CreateAuthorizationCode();\n\n            var grant = new AuthCodeGrant(code, scopes, redirectUri, codeChallenge, codeChallengeMethod);\n            AuthGrants.Add(grant);\n\n            return grant;\n        }\n\n        public DeviceCodeGrant CreateDeviceCodeGrant(TestOAuth2ServerTokenGenerator generator, string[] scopes)\n        {\n            string deviceCode = generator.CreateDeviceCode();\n            string userCode = generator.CreateUserCode();\n\n            var grant = new DeviceCodeGrant(userCode, deviceCode, scopes);\n            DeviceGrants.Add(grant);\n\n            return grant;\n        }\n\n        public bool OwnsDeviceCodeGrant(string userCode)\n        {\n            return DeviceGrants.Any(x => x.UserCode == userCode);\n        }\n\n        public void ApproveDeviceCodeGrant(string userCode)\n        {\n            DeviceCodeGrant grant = DeviceGrants.FirstOrDefault(x => x.UserCode == userCode);\n\n            if (grant is null)\n            {\n                throw new Exception($\"Invalid user code '{userCode}'\");\n            }\n\n            grant.Approved = true;\n        }\n\n        public bool IsDeviceCodeGrantApproved(string deviceCode)\n        {\n            DeviceCodeGrant grant = DeviceGrants.FirstOrDefault(x => x.DeviceCode == deviceCode);\n\n            if (grant is null)\n            {\n                throw new Exception($\"Invalid device code '{deviceCode}'\");\n            }\n\n            return grant.Approved;\n        }\n\n        public TokenEndpointResponseJson CreateTokenByAuthorizationGrant(\n            TestOAuth2ServerTokenGenerator generator, string authCode, string codeVerifier, string redirectUri)\n        {\n            var grant = AuthGrants.FirstOrDefault(x => x.Code == authCode);\n            if (grant is null)\n            {\n                throw new Exception($\"Invalid authorization code '{authCode}'\");\n            }\n\n            // Validate the grant's code challenge was constructed from the given code verifier\n            if (!string.IsNullOrWhiteSpace(grant.CodeChallenge))\n            {\n                if (string.IsNullOrWhiteSpace(codeVerifier))\n                {\n                    throw new Exception(\"Missing code verifier\");\n                }\n\n                switch (grant.CodeChallengeMethod)\n                {\n                    case OAuth2PkceChallengeMethod.Sha256:\n                        using (var sha256 = SHA256.Create())\n                        {\n                            string challenge = Base64UrlConvert.Encode(\n                                sha256.ComputeHash(\n                                    Encoding.ASCII.GetBytes(codeVerifier)\n                                ),\n                                false\n                            );\n\n                            if (challenge != grant.CodeChallenge)\n                            {\n                                throw new Exception($\"Invalid code verifier '{codeVerifier}'\");\n                            }\n                        }\n                        break;\n\n                    case OAuth2PkceChallengeMethod.Plain:\n                        // The case matters!\n                        if (!StringComparer.Ordinal.Equals(codeVerifier, grant.CodeChallenge))\n                        {\n                            throw new Exception($\"Invalid code verifier '{codeVerifier}'\");\n                        }\n                        break;\n                }\n            }\n\n            // If an explicit redirect URI was used as part of the authorization request then\n            // the redirect URI used for the token call must match exactly.\n            if (!string.IsNullOrWhiteSpace(grant.RedirectUri) && !StringComparer.Ordinal.Equals(grant.RedirectUri, redirectUri))\n            {\n                throw new Exception(\"Redirect URI must match exactly the one used when requesting the authorization code.\");\n            }\n\n            string accessToken = generator.CreateAccessToken();\n            string refreshToken = generator.CreateRefreshToken();\n\n            // Remove the auth code grant now we've generated an access token (do not allow auth code reuse)\n            AuthGrants.Remove(grant);\n\n            // Store the tokens\n            AccessTokens[accessToken] = refreshToken;\n            RefreshTokens[refreshToken] = grant.Scopes;\n\n            return new TokenEndpointResponseJson\n            {\n                TokenType = Constants.Http.WwwAuthenticateBearerScheme,\n                AccessToken = accessToken,\n                RefreshToken = refreshToken,\n                Scope = string.Join(\" \", grant.Scopes) // Keep the same scopes as before\n            };\n        }\n\n        public TokenEndpointResponseJson CreateTokenByRefreshTokenGrant(TestOAuth2ServerTokenGenerator generator, string refreshToken)\n        {\n            if (!RefreshTokens.TryGetValue(refreshToken, out string[] scopes))\n            {\n                throw new Exception($\"Invalid refresh token '{refreshToken}'\");\n            }\n\n            string newAccessToken = generator.CreateAccessToken();\n            string newRefreshToken = generator.CreateRefreshToken();\n\n            // Store the tokens\n            AccessTokens[newAccessToken] = newRefreshToken;\n            RefreshTokens[newRefreshToken] = scopes;\n\n            return new TokenEndpointResponseJson\n            {\n                TokenType = Constants.Http.WwwAuthenticateBearerScheme,\n                AccessToken = newAccessToken,\n                RefreshToken = newRefreshToken,\n                Scope = string.Join(\" \", scopes) // Keep the same scopes as before\n            };\n        }\n\n        public TokenEndpointResponseJson CreateTokenByDeviceCodeGrant(TestOAuth2ServerTokenGenerator generator, string deviceCode)\n        {\n            DeviceCodeGrant grant = DeviceGrants.FirstOrDefault(x => x.DeviceCode == deviceCode);\n\n            if (grant is null)\n            {\n                throw new Exception($\"Invalid user code '{deviceCode}'\");\n            }\n\n            if (!grant.Approved)\n            {\n                throw new Exception($\"Grant with device code '{deviceCode}' has not been approved'\");\n            }\n\n            string accessToken = generator.CreateAccessToken();\n            string refreshToken = generator.CreateRefreshToken();\n\n            // Remove the device code grant now we've generated an access token (do not allow device code reuse)\n            DeviceGrants.Remove(grant);\n\n            // Store the tokens\n            AccessTokens[accessToken] = refreshToken;\n            RefreshTokens[refreshToken] = grant.Scopes;\n\n            return new TokenEndpointResponseJson\n            {\n                TokenType = Constants.Http.WwwAuthenticateBearerScheme,\n                AccessToken = accessToken,\n                RefreshToken = refreshToken,\n                Scope = string.Join(\" \", grant.Scopes) // Keep the same scopes as before\n            };\n        }\n\n        private bool IsValidRedirect(string url)\n        {\n            foreach (Uri redirectUri in RedirectUris)\n            {\n                // We only accept exact matches, including trailing slashes and case sensitivity\n                if (StringComparer.Ordinal.Equals(redirectUri.OriginalString, url))\n                {\n                    return true;\n                }\n\n                // For loopback URLs _only_ we ignore the port number\n                if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri) && uri.IsLoopback && redirectUri.IsLoopback)\n                {\n                    // *Case-sensitive* comparison of scheme, host and path\n                    var cmp = StringComparer.Ordinal;\n\n                    // Uri::Authority includes port, whereas Uri::Host does not\n                    return cmp.Equals(redirectUri.Scheme, uri.Scheme) &&\n                           cmp.Equals(redirectUri.Host, uri.Host) &&\n                           cmp.Equals(redirectUri.GetComponents(UriComponents.Path, UriFormat.UriEscaped),\n                               uri.GetComponents(UriComponents.Path, UriFormat.UriEscaped));\n                }\n            }\n\n            return false;\n        }\n\n        internal Uri ValidateRedirect(string redirectUrl)\n        {\n            // Use default redirect URI if one has not been specified for this grant\n            if (redirectUrl == null)\n            {\n                return RedirectUris.First();\n            }\n\n            if (!Uri.TryCreate(redirectUrl, UriKind.Absolute, out _))\n            {\n                throw new Exception($\"Redirect '{redirectUrl}' is not a valid URL\");\n            }\n\n            if (!IsValidRedirect(redirectUrl))\n            {\n                // If a redirect URL has been specified, it must match one of those that has been previously registered\n                throw new Exception($\"Redirect URL '{redirectUrl}' does not match any registered values.\");\n            }\n\n            return new Uri(redirectUrl);\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestOAuth2WebBrowser.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing GitCredentialManager.Authentication.OAuth;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestOAuth2WebBrowser : IOAuth2WebBrowser\n    {\n        private readonly HttpClient _httpClient;\n\n        public TestOAuth2WebBrowser(HttpMessageHandler httpHandler)\n        {\n            _httpClient = new HttpClient(httpHandler);\n        }\n\n        public Uri UpdateRedirectUri(Uri uri)\n        {\n            return uri;\n        }\n\n        public async Task<Uri> GetAuthenticationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken ct)\n        {\n            using (var response = await _httpClient.SendAsync(HttpMethod.Get, authorizationUri))\n            {\n                response.EnsureSuccessStatusCode();\n                return response.Headers.Location;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestSessionManager.cs",
    "content": "using System;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestSessionManager : ISessionManager\n    {\n        public bool? IsWebBrowserAvailableOverride { get; set; }\n\n        public bool IsDesktopSession { get; set; }\n\n        public Action<Uri> OpenBrowserFunc { get; set; } = _ => { };\n\n        bool ISessionManager.IsWebBrowserAvailable => IsWebBrowserAvailableOverride ?? IsDesktopSession;\n\n        void ISessionManager.OpenBrowser(Uri uri) => OpenBrowserFunc(uri);\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestSettings.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestSettings : ISettings\n    {\n        public TestEnvironment Environment { get; set; }\n\n        public TestGitConfiguration GitConfiguration { get; set; }\n\n        public bool IsDebuggingEnabled { get; set; }\n\n        public bool IsTerminalPromptsEnabled { get; set; } = true;\n\n        public bool IsGuiPromptsEnabled { get; set; } = true;\n\n        public bool IsInteractionAllowed { get; set; } = true;\n\n        public string Trace { get; set; }\n\n        public bool IsSecretTracingEnabled { get; set; }\n\n        public bool IsMsalTracingEnabled { get; set; }\n\n        public string ProviderOverride { get; set; }\n\n        public string LegacyAuthorityOverride { get; set; }\n\n        public bool IsWindowsIntegratedAuthenticationEnabled { get; set; } = true;\n\n        public bool IsCertificateVerificationEnabled { get; set; } = true;\n\n        public bool AutomaticallyUseClientCertificates { get; set; }\n\n        public ProxyConfiguration ProxyConfiguration { get; set; }\n\n        public string ParentWindowId { get; set; }\n\n        public string CredentialNamespace { get; set; } = \"git-test\";\n\n        public string CredentialBackingStore { get; set; }\n\n        public string CustomCertificateBundlePath { get; set; }\n\n        public string CustomCookieFilePath { get; set; }\n\n        public TlsBackend TlsBackend { get; set; }\n\n        public bool UseCustomCertificateBundleWithSchannel { get; set; }\n\n        public int AutoDetectProviderTimeout { get; set; } = Constants.DefaultAutoDetectProviderTimeoutMs;\n\n        public bool UseMsAuthDefaultAccount { get; set; }\n\n        public bool AllowUnsafeRemotes { get; set; } = false;\n\n        public Trace2Settings GetTrace2Settings()\n        {\n            return new Trace2Settings()\n            {\n                FormatTargetsAndValues = new Dictionary<Trace2FormatTarget, string>()\n                {\n                    { Trace2FormatTarget.Event, \"foo\" }\n                }\n            };\n        }\n\n        #region ISettings\n\n        public bool TryGetSetting(string envarName, string section, string property, out string value)\n        {\n            value = null;\n\n            if (envarName is not null && (Environment?.Variables.TryGetValue(envarName, out value) ?? false))\n            {\n                return true;\n            }\n\n            if (RemoteUri != null)\n            {\n                foreach (string scope in RemoteUri.GetGitConfigurationScopes())\n                {\n                    string key = $\"{section}.{scope}.{property}\";\n                    if (GitConfiguration?.TryGet(key, false, out value) ?? false)\n                    {\n                        return true;\n                    }\n                }\n            }\n\n            if (GitConfiguration?.TryGet($\"{section}.{property}\", false, out value) ?? false)\n            {\n                return true;\n            }\n\n            return false;\n        }\n\n        public bool TryGetPathSetting(string envarName, string section, string property, out string value)\n        {\n            return TryGetSetting(envarName, section, property, out value);\n        }\n\n        public IEnumerable<string> GetSettingValues(string envarName, string section, string property, bool isPath)\n        {\n            string envarValue = null;\n            if (Environment?.Variables.TryGetValue(envarName, out envarValue) ?? false)\n            {\n                yield return envarValue;\n            }\n\n            IEnumerable<string> configValues;\n            if (RemoteUri != null)\n            {\n                foreach (string scope in RemoteUri.GetGitConfigurationScopes())\n                {\n                    string key = $\"{section}.{scope}.{property}\";\n\n                    configValues = GitConfiguration.GetAll(key);\n                    foreach (string value in configValues)\n                    {\n                        yield return value;\n                    }\n                }\n            }\n\n            configValues = GitConfiguration.GetAll($\"{section}.{property}\");\n            foreach (string value in configValues)\n            {\n                yield return value;\n            }\n        }\n\n        public string RepositoryPath { get; set; }\n\n        public Uri RemoteUri { get; set; }\n\n        bool ISettings.IsDebuggingEnabled => IsDebuggingEnabled;\n\n        bool ISettings.IsTerminalPromptsEnabled => IsTerminalPromptsEnabled;\n\n        bool ISettings.IsGuiPromptsEnabled\n        {\n            get => IsGuiPromptsEnabled;\n            set => IsGuiPromptsEnabled = value;\n        }\n\n        bool ISettings.IsInteractionAllowed => IsInteractionAllowed;\n\n        bool ISettings.GetTracingEnabled(out string value)\n        {\n            value = Trace;\n            return Trace != null;\n        }\n\n        bool ISettings.IsSecretTracingEnabled => IsSecretTracingEnabled;\n\n        bool ISettings.IsMsalTracingEnabled => IsMsalTracingEnabled;\n\n        string ISettings.ProviderOverride => ProviderOverride;\n\n        string ISettings.LegacyAuthorityOverride => LegacyAuthorityOverride;\n\n        bool ISettings.IsWindowsIntegratedAuthenticationEnabled => IsWindowsIntegratedAuthenticationEnabled;\n\n        bool ISettings.IsCertificateVerificationEnabled => IsCertificateVerificationEnabled;\n\n        ProxyConfiguration ISettings.GetProxyConfiguration()\n        {\n            return ProxyConfiguration;\n        }\n\n        string ISettings.ParentWindowId => ParentWindowId;\n\n        string ISettings.CredentialNamespace => CredentialNamespace;\n\n        string ISettings.CredentialBackingStore => CredentialBackingStore;\n\n        string ISettings.CustomCertificateBundlePath => CustomCertificateBundlePath;\n\n        string ISettings.CustomCookieFilePath => CustomCookieFilePath;\n\n        TlsBackend ISettings.TlsBackend => TlsBackend;\n\n        bool ISettings.UseCustomCertificateBundleWithSchannel => UseCustomCertificateBundleWithSchannel;\n\n        int ISettings.AutoDetectProviderTimeout => AutoDetectProviderTimeout;\n\n        bool ISettings.UseMsAuthDefaultAccount => UseMsAuthDefaultAccount;\n\n        bool ISettings.UseSoftwareRendering => false;\n\n        bool ISettings.AllowUnsafeRemotes => AllowUnsafeRemotes;\n\n        #endregion\n\n        #region IDisposable\n\n        void IDisposable.Dispose() { }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestStandardStreams.cs",
    "content": "using System.IO;\nusing System.Text;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestStandardStreams : IStandardStreams\n    {\n        public string NewLine { get; set; } = \"\\n\";\n        public string In { get; set; } = string.Empty;\n        public StringBuilder Out { get; set; } = new StringBuilder();\n        public StringBuilder Error { get; set; } = new StringBuilder();\n\n        #region IStandardStreams\n\n        TextReader IStandardStreams.In => new StringReader(In);\n\n        TextWriter IStandardStreams.Out => new StringWriter(Out){NewLine = NewLine};\n\n        TextWriter IStandardStreams.Error => new StringWriter(Error){NewLine = NewLine};\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/Objects/TestTerminal.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace GitCredentialManager.Tests.Objects\n{\n    public class TestTerminal : ITerminal\n    {\n        public IDictionary<string, string> Prompts = new Dictionary<string, string>();\n        public IDictionary<string, string> SecretPrompts = new Dictionary<string, string>();\n        public IList<(string, object[])> Messages = new List<(string, object[])>();\n\n        #region ITerminal\n\n        public void WriteLine(string format, params object[] args)\n        {\n            Messages.Add((format, args));\n        }\n\n        string ITerminal.Prompt(string prompt)\n        {\n            if (!Prompts.TryGetValue(prompt, out string result))\n            {\n                throw new Exception($\"No result has been configured for prompt text '{prompt}'\");\n            }\n\n            return result;\n        }\n\n        string ITerminal.PromptSecret(string prompt)\n        {\n            if (!SecretPrompts.TryGetValue(prompt, out string result))\n            {\n                throw new Exception($\"No result has been configured for secret prompt text '{prompt}'\");\n            }\n\n            return result;\n        }\n\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/PlatformAttributes.cs",
    "content": "using System.Runtime.InteropServices;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public class WindowsFactAttribute : FactAttribute\n    {\n        public WindowsFactAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class MacOSFactAttribute : FactAttribute\n    {\n        public MacOSFactAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class LinuxFactAttribute : FactAttribute\n    {\n        public LinuxFactAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class PosixFactAttribute : FactAttribute\n    {\n        public PosixFactAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) &&\n                !RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class WindowsTheoryAttribute : TheoryAttribute\n    {\n        public WindowsTheoryAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class MacOSTheoryAttribute : TheoryAttribute\n    {\n        public MacOSTheoryAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class LinuxTheoryAttribute : TheoryAttribute\n    {\n        public LinuxTheoryAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n\n    public class PosixTheoryAttribute : TheoryAttribute\n    {\n        public PosixTheoryAttribute()\n        {\n            if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) &&\n                !RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n            {\n                Skip = \"Test not supported on this platform.\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared/TestInfrastructure/RestTestUtilities.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing Xunit;\n\nnamespace GitCredentialManager.Tests\n{\n    public static class RestTestUtilities\n    {\n        public static void AssertBasicAuth(HttpRequestMessage request, string userName, string password)\n        {\n            string expectedBasicValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($\"{userName}:{password}\"));\n\n            AuthenticationHeaderValue authHeader = request.Headers.Authorization;\n            Assert.NotNull(authHeader);\n            Assert.Equal(\"Basic\", authHeader.Scheme);\n            Assert.Equal(expectedBasicValue, authHeader.Parameter);\n        }\n\n        public static void AssertBearerAuth(HttpRequestMessage request, string token)\n        {\n            AuthenticationHeaderValue authHeader = request.Headers.Authorization;\n            Assert.NotNull(authHeader);\n            Assert.Equal(\"Bearer\", authHeader.Scheme);\n            Assert.Equal(token, authHeader.Parameter);\n        }\n    }\n}"
  },
  {
    "path": "src/shared/TestInfrastructure/TestInfrastructure.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <RootNamespace>GitCredentialManager.Tests</RootNamespace>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>false</IsTestProject>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Moq\" Version=\"4.20.72\" />\n    <PackageReference Include=\"ReportGenerator\" Version=\"5.3.10\" />\n    <PackageReference Include=\"xunit\" Version=\"2.9.2\" />\n    <PackageReference Include=\"Xunit.SkippableFact\" Version=\"1.4.13\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/shared/TestInfrastructure/TestUtils.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\n\nnamespace GitCredentialManager.Tests\n{\n    public static class TestUtils\n    {\n        public static IDisposable ChangeDirectory(string path)\n        {\n            var cookie = new ChangeDirectoryCookie(Environment.CurrentDirectory);\n            Environment.CurrentDirectory = path;\n            return cookie;\n        }\n\n        private class ChangeDirectoryCookie : IDisposable\n        {\n            private readonly string _oldPath;\n            public ChangeDirectoryCookie(string oldPath) => _oldPath = oldPath;\n            public void Dispose() => Environment.CurrentDirectory = _oldPath;\n        }\n\n        private static string JoinPaths(string basePath, params string[] names)\n        {\n            string path = basePath;\n            foreach (string name in names)\n            {\n                path = Path.Combine(path, name);\n            }\n            return path;\n        }\n\n        public static string CreateFileSymlink(string baseDir, string linkName, string targetPath)\n        {\n            string linkPath = Path.Combine(baseDir, linkName);\n            FileSystemInfo fsi = File.CreateSymbolicLink(linkPath, targetPath);\n            return fsi.FullName;\n        }\n\n        public static string CreateDirectorySymlink(string baseDir, string linkName, string targetPath)\n        {\n            string linkPath = Path.Combine(baseDir, linkName);\n            FileSystemInfo fsi = Directory.CreateSymbolicLink(linkPath, targetPath);\n            return fsi.FullName;\n        }\n\n        public static string CreateFile(string baseDir, params string[] names)\n        {\n            string path = JoinPaths(baseDir, names);\n            string parentDir = Path.GetDirectoryName(path);\n            if (parentDir != null) Directory.CreateDirectory(parentDir);\n            File.Create(path);\n            return path;\n        }\n\n        public static string CreateDirectory(string baseDir, params string[] names)\n        {\n            string path = JoinPaths(baseDir, names);\n            Directory.CreateDirectory(path);\n            return path;\n        }\n\n        public static string GetTempDirectory(bool create = true)\n        {\n            // Note that on macOS, Path.GetTempPath() returns a path\n            // under /var that is actually a directory symlink to /private/var.\n            // We should return a manually resolved path instead to ensure\n            // callers can do path comparisons and computations that may resolve\n            // symlinks.\n            string tempDir = PlatformUtils.IsMacOS()\n                ? \"/private/tmp\"\n                : Path.GetTempPath();\n\n            string unique = GetUuid(8);\n            string path = Path.Combine(tempDir, unique);\n            if (create) Directory.CreateDirectory(path);\n            return path;\n        }\n\n        public static string GetUuid(int length = -1)\n        {\n            string uuid = Guid.NewGuid().ToString(\"N\");\n\n            if (length <= 0 || length > uuid.Length)\n            {\n                return uuid;\n            }\n\n            return uuid.Substring(0, length);\n        }\n\n        public static async Task<string> RunCommandAsync(string filePath, string arguments, string workingDirectory = null)\n        {\n            using var process = new Process\n            {\n                StartInfo = new ProcessStartInfo\n                {\n                    FileName = filePath,\n                    Arguments = arguments,\n                    RedirectStandardOutput = true,\n                    RedirectStandardError = true,\n                    UseShellExecute = false,\n                    CreateNoWindow = true,\n                    WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory\n                }\n            };\n\n            process.Start();\n\n            string output = await process.StandardOutput.ReadToEndAsync();\n            string error = await process.StandardError.ReadToEndAsync();\n\n            await process.WaitForExitAsync();\n\n            if (process.ExitCode != 0)\n            {\n                throw new InvalidOperationException(\n                    $\"Command `{filePath} {arguments}` failed with exit code {process.ExitCode}.\" +\n                    Environment.NewLine +\n                    $\"Output: {output}\" +\n                    Environment.NewLine +\n                    $\"Error: {error}\");\n            }\n\n            return output;\n        }\n    }\n}\n"
  },
  {
    "path": "src/windows/Directory.Build.props",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <!-- Import parent Directory.Build.props file -->\n  <Import Project=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))\" />\n\n  <!-- Set binary and intermediate output directories -->\n  <PropertyGroup>\n    <PlatformOutPath>$(RepoOutPath)windows\\</PlatformOutPath>\n    <ProjectOutPath>$(PlatformOutPath)$(MSBuildProjectName)\\</ProjectOutPath>\n    <BaseOutputPath>$(ProjectOutPath)bin\\</BaseOutputPath>\n    <BaseIntermediateOutputPath>$(ProjectOutPath)obj\\</BaseIntermediateOutputPath>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/windows/Installer.Windows/Installer.Windows.csproj",
    "content": "<Project>\n  <!-- Implicit SDK props import -->\n  <Import Project=\"Sdk.props\" Sdk=\"Microsoft.NET.Sdk\" />\n\n  <!-- Default RuntimeIdentifier to the host architecture if not specified -->\n  <PropertyGroup Condition=\"'$(RuntimeIdentifier)' == ''\">\n    <RuntimeIdentifier Condition=\"'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'\">win-x64</RuntimeIdentifier>\n    <RuntimeIdentifier Condition=\"'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X86'\">win-x86</RuntimeIdentifier>\n    <RuntimeIdentifier Condition=\"'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'\">win-arm64</RuntimeIdentifier>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <TargetFramework>net472</TargetFramework>\n    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>\n    <EnableDefaultItems>false</EnableDefaultItems>\n    <PayloadPath>$(PlatformOutPath)Installer.Windows\\bin\\$(Configuration)\\net472\\$(RuntimeIdentifier)\\</PayloadPath>\n    <InnoSetupVersion>6.3.1</InnoSetupVersion>\n    <!-- We already append the RID to our intermediate PayloadPath and also to the\n         final installer filenames so there's no need to append to the output path. -->\n    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../../shared/Git-Credential-Manager/Git-Credential-Manager.csproj\" ReferenceOutputAssembly=\"false\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Include=\"Setup.iss\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Tools.InnoSetup\" Version=\"$(InnoSetupVersion)\" />\n  </ItemGroup>\n\n  <!-- Implicit SDK targets import (so we can override the default targets below) -->\n  <Import Project=\"Sdk.targets\" Sdk=\"Microsoft.NET.Sdk\" />\n\n  <Target Name=\"CoreCompile\" Condition=\"'$(OSPlatform)'=='windows'\">\n    <PropertyGroup>\n      <InnoSetupCommandSystem>\"$(NuGetPackageRoot)Tools.InnoSetup\\$(InnoSetupVersion)\\tools\\ISCC.exe\" /DPayloadDir=\"$(PayloadPath)\" /DInstallTarget=system /DGcmRuntimeIdentifier=\"$(RuntimeIdentifier)\" \"$(RepoSrcPath)\\windows\\Installer.Windows\\Setup.iss\" /O\"$(OutputPath)\"</InnoSetupCommandSystem>\n      <InnoSetupCommandUser>\"$(NuGetPackageRoot)Tools.InnoSetup\\$(InnoSetupVersion)\\tools\\ISCC.exe\" /DPayloadDir=\"$(PayloadPath)\" /DInstallTarget=user /DGcmRuntimeIdentifier=\"$(RuntimeIdentifier)\" \"$(RepoSrcPath)\\windows\\Installer.Windows\\Setup.iss\" /O\"$(OutputPath)\"</InnoSetupCommandUser>\n    </PropertyGroup>\n\n    <Message Text=\"Lay Out\" Importance=\"High\" />\n    <Exec Condition=\"'$(NoLayout)'!='true'\"\n          ConsoleToMSBuild=\"true\"\n          Command=\"powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -Command &quot;&amp; {&amp;'$(MSBuildProjectDirectory)\\layout.ps1' -Configuration '$(Configuration)' -Output '$(PayloadPath)' -RuntimeIdentifier '$(RuntimeIdentifier)'; if ($?) { exit 0 } else { exit 1 }}&quot;\"\n          IgnoreExitCode=\"true\">\n      <!-- If we want to display the console output if the exit code is not 0, we need to capture it and then output it using the <Error /> below -->\n      <Output TaskParameter=\"ExitCode\" PropertyName=\"ExitCodeOfExec\" />\n      <Output TaskParameter=\"ConsoleOutput\" PropertyName=\"OutputOfExec\" />\n    </Exec>\n    <Error Condition=\"'$(NoLayout)'!='true' AND '$(ExitCodeOfExec)' != '0'\" Text=\"Layout script failed with exit code $(ExitCodeOfExec) and message $(OutputOfExec)\" />\n    <Message Text=\"$(InnoSetupCommandSystem)\" Importance=\"High\" />\n    <Exec Command=\"$(InnoSetupCommandSystem)\" />\n    <Message Text=\"$(InnoSetupCommandUser)\" Importance=\"High\" />\n    <Exec Command=\"$(InnoSetupCommandUser)\" />\n  </Target>\n\n  <!-- We don't produce or copy any dependent files for this project -->\n  <Target Name=\"CopyFilesToOutputDirectory\" />\n  <Target Name=\"CoreClean\">\n    <RemoveDir Directories=\"$(ProjectOutPath)\" />\n  </Target>\n\n</Project>\n"
  },
  {
    "path": "src/windows/Installer.Windows/Setup.iss",
    "content": "; This script requires Inno Setup Compiler 6.0.0 or later to compile\n; The Inno Setup Compiler (and IDE) can be found at http://www.jrsoftware.org/isinfo.php\n; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php\n\n; Ensure minimum Inno Setup tooling version\n#if VER < EncodeVer(6,0,0)\n  #error Update your Inno Setup version (6.0.0 or newer)\n#endif\n\n#ifndef PayloadDir\n  #error Payload directory path property 'PayloadDir' must be specified\n#endif\n\n#ifndef InstallTarget\n  #error Installer target property 'InstallTarget' must be specifed\n#endif\n\n#ifndef GcmRuntimeIdentifier\n  #error GCM Runtime Identifier 'GcmRuntimeIdentifier' must be specifed (e.g. win-x64)\n#endif\n\n#if InstallTarget == \"user\"\n  #define GcmAppId \"{{aa76d31d-432c-42ee-844c-bc0bc801cef3}}\"\n  #define GcmLongName \"Git Credential Manager (User)\"\n  #define GcmSetupExe \"gcmuser\"\n  #define GcmConfigureCmdArgs \"\"\n#elif InstallTarget == \"system\"\n  #define GcmAppId \"{{fdfae50a-1bc1-4ead-9228-1e1c275e8d12}}\"\n  #define GcmLongName \"Git Credential Manager\"\n  #define GcmSetupExe \"gcm\"\n  #define GcmConfigureCmdArgs \"--system\"\n#else\n  #error Installer target property 'InstallTarget' must be 'user' or 'system'\n#endif\n\n; Define core properties\n#define GcmShortName \"Git Credential Manager\"\n#define GcmPublisher \"GitHub\"\n#define GcmVersionInfoDescription \"Secure, cross-platform Git credential manager.\"\n#define GcmPublisherUrl \"https://www.github.com\"\n#define GcmCopyright \"Copyright (c) GitHub, Inc. and contributors\"\n#define GcmUrl \"https://aka.ms/gcm\"\n#define GcmReadme \"https://github.com/git-ecosystem/git-credential-manager/blob/main/README.md\"\n#define GcmRepoRoot \"..\\..\\..\"\n#define GcmAssets GcmRepoRoot + \"\\assets\"\n#define GcmExe \"git-credential-manager.exe\"\n\n#ifnexist PayloadDir + \"\\\" + GcmExe\n  #error Payload files are missing\n#endif\n\n; Generate the GCM version version from the CLI executable\n#define VerMajor\n#define VerMinor\n#define VerBuild\n#define VerRevision\n#expr GetVersionComponents(PayloadDir + \"\\\" + GcmExe, VerMajor, VerMinor, VerBuild, VerRevision)\n#define GcmVersionSimple str(VerMajor) + \".\" + str(VerMinor) + \".\" + str(VerBuild)\n#define GcmVersion str(GcmVersionSimple) + \".\" + str(VerRevision)\n\n[Setup]\nAppId={#GcmAppId}\nAppName={#GcmLongName}\nAppVersion={#GcmVersion}\nAppVerName={#GcmLongName} {#GcmVersion}\nAppPublisher={#GcmPublisher}\nAppPublisherURL={#GcmPublisherUrl}\nAppSupportURL={#GcmUrl}\nAppUpdatesURL={#GcmUrl}\nAppContact={#GcmUrl}\nAppCopyright={#GcmCopyright}\nAppReadmeFile={#GcmReadme}\n; Windows ARM64 supports installing and running x64 binaries, but not vice versa.\n#if GcmRuntimeIdentifier==\"win-x64\"\nArchitecturesAllowed=x64compatible\nArchitecturesInstallIn64BitMode=x64compatible\n#elif GcmRuntimeIdentifier==\"win-arm64\"\nArchitecturesAllowed=arm64\nArchitecturesInstallIn64BitMode=arm64\n#endif\nVersionInfoVersion={#GcmVersion}\nLicenseFile={#GcmRepoRoot}\\LICENSE\nOutputBaseFilename={#GcmSetupExe}-{#GcmRuntimeIdentifier}-{#GcmVersionSimple}\nDefaultDirName={autopf}\\{#GcmShortName}\nCompression=lzma2\nSolidCompression=yes\nMinVersion=6.1sp1\nDisableDirPage=yes\nUninstallDisplayIcon={app}\\{#GcmExe}\nSetupIconFile={#GcmAssets}\\gcmicon.ico\nWizardImageFile={#GcmAssets}\\gcmicon128.bmp\nWizardSmallImageFile={#GcmAssets}\\gcmicon64.bmp\nWizardStyle=modern\nWizardImageStretch=no\nWindowResizable=no\nChangesEnvironment=yes\n#if InstallTarget == \"user\"\n  PrivilegesRequired=lowest\n#endif\n\n[Languages]\nName: english; MessagesFile: \"compiler:Default.isl\";\n\n[Types]\nName: full; Description: \"Full installation\"; Flags: iscustom;\n\n[Components]\n; No individual components\n\n[Run]\nFilename: \"{app}\\{#GcmExe}\"; Parameters: \"configure {#GcmConfigureCmdArgs}\"; Flags: runhidden\n\n[UninstallRun]\nFilename: \"{app}\\{#GcmExe}\"; Parameters: \"unconfigure {#GcmConfigureCmdArgs}\"; Flags: runhidden; RunOnceId: \"unconfigure\"\n\n[Files]\nSource: \"{#PayloadDir}\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs\n\n[Code]\n// Don't allow installing conflicting architectures\nfunction InitializeSetup(): Boolean;\nbegin\n  Result := True;\n\n  #if InstallTarget == \"user\"\n    if not WizardSilent() and IsAdmin() then begin\n      if MsgBox('This User Installer is not meant to be run as an Administrator. If you would like to install Git Credential Manager for all users in this system, download the System Installer instead from https://aka.ms/gcm/latest. Are you sure you want to continue?', mbError, MB_OKCANCEL) = IDCANCEL then begin\n        Result := False;\n      end;\n    end;\n  #endif\nend;\n"
  },
  {
    "path": "src/windows/Installer.Windows/layout.ps1",
    "content": "# Inputs\nparam ([Parameter(Mandatory)] $Configuration, [Parameter(Mandatory)] $Output, $RuntimeIdentifier, $SymbolOutput)\n\n# Trim trailing slashes from output paths\n$Output = $Output.TrimEnd('\\','/')\n$SymbolOutput = $SymbolOutput.TrimEnd('\\','/')\n\nWrite-Output \"Output: $Output\"\n\n# Determine a runtime if one was not provided\nif (-not $RuntimeIdentifier) {\n    $arch = $env:PROCESSOR_ARCHITECTURE\n    switch ($arch) {\n        \"AMD64\" { $RuntimeIdentifier = \"win-x64\" }\n        \"x86\"   { $RuntimeIdentifier = \"win-x86\" }\n        \"ARM64\" { $RuntimeIdentifier = \"win-arm64\" }\n        default {\n            Write-Host \"Unknown architecture: $arch\"\n            exit 1\n        }\n    }\n}\n\nWrite-Output \"Building for runtime '$RuntimeIdentifier'\"\n\nif ($RuntimeIdentifier -ne 'win-x86' -and $RuntimeIdentifier -ne 'win-x64' -and $RuntimeIdentifier -ne 'win-arm64') {\n    Write-Host \"Unsupported RuntimeIdentifier: $RuntimeIdentifier\"\n    exit 1\n}\n\n# Directories\n$THISDIR = $PSScriptRoot\n$ROOT = (Get-Item $THISDIR).Parent.Parent.Parent.FullName\n$SRC = \"$ROOT\\src\"\n$GCM_SRC = \"$SRC\\shared\\Git-Credential-Manager\"\n\n# Perform pre-execution checks\n$PAYLOAD = \"$Output\"\nif ($SymbolOutput)\n{\n    $SYMBOLS = \"$SymbolOutput\"\n}\nelse\n{\n    $SYMBOLS = \"$PAYLOAD.sym\"\n}\n\n# Clean up any old payload and symbols directories\nif (Test-Path -Path $PAYLOAD)\n{\n    Write-Output \"Cleaning old payload directory '$PAYLOAD'...\"\n    Remove-Item -Recurse \"$PAYLOAD\" -Force\n}\n\nif (Test-Path -Path $SYMBOLS)\n{\n    Write-Output \"Cleaning old symbols directory '$SYMBOLS'...\"\n    Remove-Item -Recurse \"$SYMBOLS\" -Force\n}\n\n# Ensure payload and symbol directories exist\nmkdir -p \"$PAYLOAD\",\"$SYMBOLS\" | Out-Null\n\n# Publish core application executables\nWrite-Output \"Publishing core application...\"\ndotnet publish \"$GCM_SRC\" `\n\t--framework net472 `\n\t--configuration \"$Configuration\" `\n\t--runtime $RuntimeIdentifier `\n\t--output \"$PAYLOAD\"\n\n# Delete libraries that are not needed for Windows but find their way\n# into the publish output.\nRemove-Item -Path \"$PAYLOAD/*.dylib\" -Force -ErrorAction Ignore\n\n# Delete extraneous files that get included for other runtimes\nRemove-Item -Path \"$PAYLOAD/musl-x64/\" -Recurse -Force -ErrorAction Ignore\n\nswitch ($RuntimeIdentifier) {\n    \"win-x86\" {\n        Remove-Item -Path \"$PAYLOAD/arm/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/arm64/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x64/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-arm64/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-x64/\" -Recurse -Force -ErrorAction Ignore\n        # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly\n        Remove-Item -Path \"$PAYLOAD/x86/libSkiaSharp.dll\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x86/libHarfBuzzSharp.dll\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-x86/native/msalruntime_x86.dll\" -Force -ErrorAction Ignore\n    }\n    \"win-x64\" {\n        Remove-Item -Path \"$PAYLOAD/arm/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/arm64/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x86/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-arm64/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-x86/\" -Recurse -Force -ErrorAction Ignore\n        # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly\n        Remove-Item -Path \"$PAYLOAD/x64/libSkiaSharp.dll\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x64/libHarfBuzzSharp.dll\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x64/libSkiaSharp.so\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x64/libHarfBuzzSharp.so\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-x64/native/msalruntime.dll\" -Force -ErrorAction Ignore\n    }\n    \"win-arm64\" {\n        Remove-Item -Path \"$PAYLOAD/arm/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x86/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/x64/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-x86/\" -Recurse -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-x64/\" -Recurse -Force -ErrorAction Ignore\n        # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly\n        Remove-Item -Path \"$PAYLOAD/arm64/libSkiaSharp.dll\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/arm64/libHarfBuzzSharp.dll\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/arm64/libSkiaSharp.so\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/arm64/libHarfBuzzSharp.so\" -Force -ErrorAction Ignore\n        Remove-Item -Path \"$PAYLOAD/runtimes/win-arm64/native/msalruntime_arm64.dll\" -Force -ErrorAction Ignore\n    }\n}\n\n# Delete localized resource assemblies - we don't localize the core GCM assembly anyway\nGet-ChildItem \"$PAYLOAD\" -Recurse -Include \"*.resources.dll\" | Remove-Item -Force -ErrorAction Ignore\n\n# Delete any empty directories\nGet-ChildItem \"$PAYLOAD\" -Recurse -Directory `\n\t| Sort-Object -Property FullName -Descending `\n\t| Where-Object { ! (Get-ChildItem $_.FullName -File -Recurse).Count } `\n\t| Remove-Item -Force\n\n# Collect symbols\nWrite-Output \"Collecting managed symbols...\"\nMove-Item -Path \"$PAYLOAD/*.pdb\" -Destination \"$SYMBOLS\"\n\nWrite-Output \"Layout complete.\"\n"
  }
]