diff --git a/.github/ISSUE_TEMPLATE/auth-issue.yml b/.github/ISSUE_TEMPLATE/auth-issue.yml
index 19cc53066..6f9128eb1 100644
--- a/.github/ISSUE_TEMPLATE/auth-issue.yml
+++ b/.github/ISSUE_TEMPLATE/auth-issue.yml
@@ -15,7 +15,7 @@ body:
description: |
What version of Git Credential Manager are you using?
- Run `git credential-manager-core --version` from a terminal to see the current version.
+ Run `git credential-manager --version` from a terminal to see the current version.
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.
placeholder: |
@@ -120,6 +120,6 @@ body:
WSLENV=$WSLENV:GCM_TRACE:GIT_TRACE GCM_TRACE=1 GIT_TRACE=1 git fetch
```
- If you are using GCM version 2.0.567 onwards you can also run `git credential-manager-core diagnose` to collect useful diagnostic information that can be attached here.
+ 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.
:warning: **Please review and redact any private information before attaching logs and files!**
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 329fc29bc..06a331d98 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -22,10 +22,7 @@ jobs:
language: [ 'csharp' ]
steps:
- - name: Checkout repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # patch around Nerdbank.GitVersioning failure
+ - uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 9a1df1459..277234c27 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -14,12 +14,10 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-18.04, ubuntu-20.04, windows-2019, macos-latest]
+ os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ - uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3.0.3
diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml
index 864ae2f73..3ecff5b9d 100644
--- a/.github/workflows/lint-docs.yml
+++ b/.github/workflows/lint-docs.yml
@@ -18,9 +18,9 @@ jobs:
name: Lint markdown files
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- - uses: DavidAnson/markdownlint-cli2-action@5b7c9f74fec47e6b15667b2cc23c63dff11e449e
+ - uses: DavidAnson/markdownlint-cli2-action@bb4bb94c73936643d73d345b48fead3e96f90a5e
with:
globs: |
"**/*.md"
@@ -30,12 +30,12 @@ jobs:
name: Check for broken links
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- name: Run link checker
# For any troubleshooting, see:
# https://github.com/lycheeverse/lychee/blob/master/docs/TROUBLESHOOTING.md
- uses: lycheeverse/lychee-action@4dcb8bee2a0a4531cba1a1f392c54e8375d6dd81
+ uses: lycheeverse/lychee-action@97189f2c0a3c8b0cb0e704fd4e878af6e5e2b2c5
with:
# user-agent: if a user agent is not specified, some websites (e.g.
diff --git a/.github/workflows/release-homebrew.yaml b/.github/workflows/release-homebrew.yaml
index f45431b03..7e0be9d00 100644
--- a/.github/workflows/release-homebrew.yaml
+++ b/.github/workflows/release-homebrew.yaml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update Homebrew tap
- uses: mjcheetham/update-homebrew@v1.2
+ uses: mjcheetham/update-homebrew@v1.3
with:
token: ${{ secrets.HOMEBREW_TOKEN }}
tap: microsoft/git
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 83516cd7d..521fea366 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,17 +12,15 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
- runtime: [ osx-x64, osx-arm64 ]
+ runtime: [ osx-x64, osx-arm64 ]
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ - uses: actions/checkout@v3
- name: Set up dotnet
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: 6.0.201
-
+
- name: Install dependencies
run: dotnet restore
@@ -31,7 +29,7 @@ jobs:
dotnet build src/osx/Installer.Mac/*.csproj \
--configuration=MacRelease --no-self-contained \
--runtime=${{ matrix.runtime }}
-
+
- name: Run macOS unit tests
run: |
dotnet test --configuration=MacRelease
@@ -53,7 +51,7 @@ jobs:
echo $CERT_BASE64 | base64 -D > $RUNNER_TEMP/cert.p12
security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/buildagent.keychain -P $CERT_PASSPHRASE -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $RUNNER_TEMP/buildagent.keychain
-
+
- name: Developer sign
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -67,7 +65,7 @@ jobs:
path: |
payload
symbols
-
+
osx-payload-sign:
name: Sign macOS payload
# ESRP service requires signing to run on Windows
@@ -77,25 +75,24 @@ jobs:
runtime: [ osx-x64, osx-arm64 ]
needs: osx-build
steps:
- - name: Check out repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- name: Download payload
uses: actions/download-artifact@v3
with:
name: tmp.${{ matrix.runtime }}-build
-
+
- name: Zip unsigned payload
shell: pwsh
run: |
Compress-Archive -Path payload payload/payload.zip
cd payload
Get-ChildItem -Exclude payload.zip | Remove-Item -Recurse -Force
-
+
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
-
+
- name: Set up ESRP client
shell: pwsh
env:
@@ -104,7 +101,7 @@ jobs:
REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
run: |
.github\set_up_esrp.ps1
-
+
- name: Run ESRP client
shell: pwsh
env:
@@ -115,7 +112,7 @@ jobs:
python .github\run_esrp_signing.py payload `
$env:APPLE_KEY_CODE $env:APPLE_SIGNING_OP_CODE `
--params 'Hardening' '--options=runtime'
-
+
- name: Unzip signed payload
shell: pwsh
run: |
@@ -128,83 +125,77 @@ jobs:
name: ${{ matrix.runtime }}-payload-sign
path: |
signed
-
+
osx-pack:
name: Package macOS payload
runs-on: macos-latest
strategy:
matrix:
- runtime: [ osx-x64, osx-arm64 ]
+ runtime: [ osx-x64, osx-arm64 ]
needs: osx-payload-sign
steps:
- - name: Check out repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ - uses: actions/checkout@v3
+
+ - name: Set version environment variable
+ run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV
- name: Set up dotnet
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: 6.0.201
-
- # Install Nerdbank.GitVersioning
- - uses: dotnet/nbgv@master
- with:
- setCommonVars: true
- name: Download signed payload
uses: actions/download-artifact@v3
with:
name: ${{ matrix.runtime }}-payload-sign
-
+
- name: Create component package
run: |
src/osx/Installer.Mac/pack.sh --payload=payload \
- --version=$GitBuildVersionSimple \
+ --version=$VERSION \
--output=components/com.microsoft.gitcredentialmanager.component.pkg
-
+
- name: Create product archive
run: |
src/osx/Installer.Mac/dist.sh --package-path=components \
- --version=$GitBuildVersionSimple --runtime=${{ matrix.runtime }} \
- --output=pkg/gcm-${{ matrix.runtime }}-$GitBuildVersionSimple.pkg || exit 1
-
+ --version=$VERSION --runtime=${{ matrix.runtime }} \
+ --output=pkg/gcm-${{ matrix.runtime }}-$VERSION.pkg || exit 1
+
- name: Upload package
uses: actions/upload-artifact@v3
with:
name: tmp.${{ matrix.runtime }}-pack
path: |
pkg
-
+
osx-sign:
name: Sign and notarize macOS package
# ESRP service requires signing to run on Windows
runs-on: windows-latest
strategy:
matrix:
- runtime: [ osx-x64, osx-arm64 ]
+ runtime: [ osx-x64, osx-arm64 ]
needs: osx-pack
steps:
- - name: Check out repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- name: Download unsigned package
uses: actions/download-artifact@v3
with:
name: tmp.${{ matrix.runtime }}-pack
path: pkg
-
+
- name: Zip unsigned package
shell: pwsh
run: |
Compress-Archive -Path pkg/*.pkg pkg/gcm-pkg.zip
cd pkg
Get-ChildItem -Exclude gcm-pkg.zip | Remove-Item -Recurse -Force
-
+
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
-
+
- name: Set up ESRP client
shell: pwsh
env:
@@ -213,7 +204,7 @@ jobs:
REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
run: |
.github\set_up_esrp.ps1
-
+
- name: Sign package
shell: pwsh
env:
@@ -222,14 +213,14 @@ jobs:
APPLE_SIGNING_OP_CODE: ${{ secrets.APPLE_SIGNING_OPERATION_CODE }}
run: |
python .github\run_esrp_signing.py pkg $env:APPLE_KEY_CODE $env:APPLE_SIGNING_OP_CODE
-
+
- name: Unzip signed package
shell: pwsh
run: |
mkdir unsigned
Expand-Archive -LiteralPath signed\gcm-pkg.zip -DestinationPath .\unsigned -Force
Remove-Item signed\gcm-pkg.zip -Force
-
+
- name: Notarize signed package
shell: pwsh
env:
@@ -238,7 +229,7 @@ jobs:
APPLE_NOTARIZATION_OP_CODE: ${{ secrets.APPLE_NOTARIZATION_OPERATION_CODE }}
run: |
python .github\run_esrp_signing.py unsigned $env:APPLE_KEY_CODE $env:APPLE_NOTARIZATION_OP_CODE --params 'BundleId' 'com.microsoft.gitcredentialmanager'
-
+
- name: Publish signed package
uses: actions/upload-artifact@v3
with:
@@ -252,20 +243,13 @@ jobs:
name: Build and Sign Windows
runs-on: windows-latest
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ - uses: actions/checkout@v3
- name: Set up dotnet
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: 6.0.201
- # Install Nerdbank.GitVersioning
- - uses: dotnet/nbgv@master
- with:
- setCommonVars: true
-
- name: Install dependencies
run: dotnet restore
@@ -288,7 +272,7 @@ jobs:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
-
+
- name: Set up ESRP client
shell: pwsh
env:
@@ -297,7 +281,7 @@ jobs:
REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
run: |
.github\set_up_esrp.ps1
-
+
- name: Run ESRP client for unsigned payload
shell: pwsh
env:
@@ -312,7 +296,7 @@ jobs:
'OpusInfo' 'http://www.microsoft.com' `
'FileDigest' '/fd "SHA256"' 'PageHash' '/NPH' `
'TimeStamp' '/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256'
-
+
- name: Lay out signed payload
shell: pwsh
run: |
@@ -327,7 +311,7 @@ jobs:
shell: pwsh
run: |
dotnet build src/windows/Installer.Windows /p:PayloadPath=$env:GITHUB_WORKSPACE/signed-payload /p:NoLayout=true --configuration=WindowsRelease
-
+
- name: Run ESRP client for installers
shell: pwsh
env:
@@ -343,7 +327,7 @@ jobs:
'OpusInfo' 'http://www.microsoft.com' `
'FileDigest' '/fd "SHA256"' 'PageHash' '/NPH' `
'TimeStamp' '/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256'
-
+
- name: Publish final artifacts
uses: actions/upload-artifact@v3
with:
@@ -360,9 +344,7 @@ jobs:
name: Build Linux
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ - uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3.0.3
@@ -394,13 +376,13 @@ jobs:
# ESRP service requires signing to run on Windows
runs-on: windows-latest
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: linux-build
-
+
- name: Remove symbols
run: |
rm tar/*symbols*
@@ -408,7 +390,7 @@ jobs:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
-
+
- name: Set up ESRP client
shell: pwsh
env:
@@ -417,7 +399,7 @@ jobs:
REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
run: |
.github\set_up_esrp.ps1
-
+
- name: Run ESRP client
shell: pwsh
env:
@@ -427,7 +409,7 @@ jobs:
run: |
python .github/run_esrp_signing.py deb $env:LINUX_KEY_CODE $env:LINUX_OP_CODE
python .github/run_esrp_signing.py tar $env:LINUX_KEY_CODE $env:LINUX_OP_CODE
-
+
- name: Re-name tarball signature file
shell: bash
run: |
@@ -440,7 +422,7 @@ jobs:
name: linux-sign
path: |
signed
-
+
# ================================
# .NET Tool
# ================================
@@ -449,18 +431,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
- name: Setup .NET
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: 6.0.201
- - uses: dotnet/nbgv@master
- with:
- setCommonVars: true
-
- name: Build .NET tool
run: |
src/shared/DotnetTool/layout.sh --configuration=Release
@@ -478,25 +454,24 @@ jobs:
runs-on: windows-latest
needs: dotnet-tool-build
steps:
- - name: Check out repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- name: Download payload
uses: actions/download-artifact@v3
with:
name: tmp.dotnet-tool-build
-
+
- name: Zip unsigned payload
shell: pwsh
run: |
Compress-Archive -Path payload payload/payload.zip
cd payload
Get-ChildItem -Exclude payload.zip | Remove-Item -Recurse -Force
-
+
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
-
+
- name: Set up ESRP client
shell: pwsh
env:
@@ -505,7 +480,7 @@ jobs:
REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
run: |
.github\set_up_esrp.ps1
-
+
- name: Run ESRP client
shell: pwsh
env:
@@ -515,7 +490,7 @@ jobs:
run: |
python .github\run_esrp_signing.py payload `
$env:NUGET_KEY_CODE $env:NUGET_OPERATION_CODE
-
+
- name: Lay out signed payload, images, and symbols
shell: bash
run: |
@@ -530,15 +505,18 @@ jobs:
name: dotnet-tool-payload-sign
path: |
dotnet-tool-payload-sign
-
+
dotnet-tool-pack:
name: Package .NET tool
runs-on: ubuntu-latest
needs: dotnet-tool-payload-sign
steps:
- uses: actions/checkout@v3
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+
+ - name: Set version environment variable
+ run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV
+
+ - uses: actions/checkout@v3
- name: Download signed payload
uses: actions/download-artifact@v3
@@ -551,14 +529,10 @@ jobs:
with:
dotnet-version: 6.0.201
- - uses: dotnet/nbgv@master
- with:
- setCommonVars: true
-
- name: Package tool
run: |
src/shared/DotnetTool/pack.sh --configuration=Release \
- --version=$GitBuildVersionSimple --publish-dir=$(pwd)/signed
+ --version=$VERSION --publish-dir=$(pwd)/signed
- name: Upload unsigned package
uses: actions/upload-artifact@v3
@@ -573,26 +547,25 @@ jobs:
runs-on: windows-latest
needs: dotnet-tool-pack
steps:
- - name: Check out repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
+ - uses: actions/checkout@v3
- name: Download unsigned package
uses: actions/download-artifact@v3
with:
name: tmp.dotnet-tool-package-unsigned
path: nupkg
-
+
- name: Zip unsigned package
shell: pwsh
run: |
Compress-Archive -Path nupkg/*.nupkg nupkg/gcm-nupkg.zip
cd nupkg
Get-ChildItem -Exclude gcm-nupkg.zip | Remove-Item -Recurse -Force
-
+
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
-
+
- name: Set up ESRP client
shell: pwsh
env:
@@ -601,7 +574,7 @@ jobs:
REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
run: |
.github\set_up_esrp.ps1
-
+
- name: Sign package
shell: pwsh
env:
@@ -610,13 +583,13 @@ jobs:
NUGET_OPERATION_CODE: ${{ secrets.NUGET_OPERATION_CODE }}
run: |
python .github\run_esrp_signing.py nupkg $env:NUGET_KEY_CODE $env:NUGET_OPERATION_CODE
-
+
- name: Unzip signed package
shell: pwsh
run: |
Expand-Archive -LiteralPath signed\gcm-nupkg.zip -DestinationPath .\signed -Force
Remove-Item signed\gcm-nupkg.zip -Force
-
+
- name: Publish signed package
uses: actions/upload-artifact@v3
with:
@@ -658,13 +631,7 @@ jobs:
runs-on: ${{ matrix.component.os }}
needs: [ osx-sign, win-sign, linux-sign, dotnet-tool-sign ]
steps:
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
-
- - uses: dotnet/nbgv@master
- with:
- setCommonVars: true
+ - uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v3
@@ -687,7 +654,7 @@ jobs:
debpath=$(find ./*.deb)
sudo apt install $debpath
"${{ matrix.component.command }}" configure
-
+
- name: Install Linux (tarball)
if: contains(matrix.component.description, 'tarball')
run: |
@@ -702,7 +669,7 @@ jobs:
# Only validate x64, given arm64 agents are not available
pkgpath=$(find ./*.pkg)
sudo installer -pkg $pkgpath -target /
-
+
- name: Install .NET tool
if: contains(matrix.component.description, 'dotnet-tool')
run: |
@@ -714,7 +681,7 @@ jobs:
shell: bash
run: |
"${{ matrix.component.command }}" --version | sed 's/+.*//' >actual
- echo $GitBuildVersionSimple >expect
+ cat VERSION | sed -E 's/.[0-9]+$//' >expect
cmp expect actual || exit 1
# ================================
@@ -724,22 +691,19 @@ jobs:
name: Publish GitHub draft release
runs-on: ubuntu-latest
needs: [ validate ]
- steps:
- - name: Check out repository
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set version environment variable
+ run: |
+ # Remove the "revision" portion of the version
+ echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV
- name: Set up dotnet
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: 6.0.201
-
- # Install Nerdbank.GitVersioning
- - uses: dotnet/nbgv@master
- with:
- setCommonVars: true
-
+
- name: Download artifacts
uses: actions/download-artifact@v3
@@ -747,24 +711,24 @@ jobs:
run: |
mkdir osx-payload-and-symbols
- tar -C osx-x64-payload-sign -czf osx-payload-and-symbols/gcm-osx-x64-$GitBuildVersionSimple.tar.gz .
- tar -C tmp.osx-x64-build/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$GitBuildVersionSimple-symbols.tar.gz .
+ tar -C osx-x64-payload-sign -czf osx-payload-and-symbols/gcm-osx-x64-$VERSION.tar.gz .
+ tar -C tmp.osx-x64-build/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$VERSION-symbols.tar.gz .
- tar -C osx-arm64-payload-sign -czf osx-payload-and-symbols/gcm-osx-arm64-$GitBuildVersionSimple.tar.gz .
- tar -C tmp.osx-arm64-build/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$GitBuildVersionSimple-symbols.tar.gz .
+ tar -C osx-arm64-payload-sign -czf osx-payload-and-symbols/gcm-osx-arm64-$VERSION.tar.gz .
+ tar -C tmp.osx-arm64-build/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$VERSION-symbols.tar.gz .
- name: Archive Windows payload and symbols
run: |
mkdir win-x86-payload-and-symbols
- zip -jr win-x86-payload-and-symbols/gcm-win-x86-$GitBuildVersionSimple.zip win-sign/signed-payload
- zip -jr win-x86-payload-and-symbols/gcm-win-x86-$GitBuildVersionSimple-symbols.zip win-sign/src/windows/Installer.Windows/symbols
+ zip -jr win-x86-payload-and-symbols/gcm-win-x86-$VERSION.zip win-sign/signed-payload
+ zip -jr win-x86-payload-and-symbols/gcm-win-x86-$VERSION-symbols.zip win-sign/src/windows/Installer.Windows/symbols
- uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const path = require('path');
- const version = process.env.GitBuildVersionSimple
+ const version = process.env.VERSION
var releaseMetadata = {
owner: context.repo.owner,
diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml
index 554bb605a..95ebdb17c 100644
--- a/.github/workflows/validate-install-from-source.yml
+++ b/.github/workflows/validate-install-from-source.yml
@@ -34,10 +34,10 @@ jobs:
zypper -n install tar gzip
elif [[ ${{matrix.vector.image}} == *"centos"* ]]; then
dnf install which -y
- fi
- - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+ fi
+
+ - uses: actions/checkout@v3
+
- run: |
sh "${GITHUB_WORKSPACE}/src/linux/Packaging.Linux/install-from-source.sh" -y
- git-credential-manager --help || exit 1
\ No newline at end of file
+ git-credential-manager --help || exit 1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fdf9b2f15..87314ca39 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -52,10 +52,10 @@ request being accepted:
[code-of-conduct]: CODE_OF_CONDUCT.md
[commits]: https://www.youtube.com/watch?v=4qLtKx9S9a8
[contribute-under-repo-license]: https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license
-[fork]: https://github.com/GitCredentialManager/git-credential-manager/fork
+[fork]: https://github.com/git-ecosystem/git-credential-manager/fork
[github-help]: https://help.github.com
[how-to-contribute]: https://opensource.guide/how-to-contribute/
-[issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose
+[issue]: https://github.com/git-ecosystem/git-credential-manager/issues/new/choose
[license]: LICENSE
-[pr]: https://github.com/GitCredentialManager/git-credential-manager/compare
+[pr]: https://github.com/git-ecosystem/git-credential-manager/compare
[prs]: https://help.github.com/articles/about-pull-requests/
diff --git a/Directory.Build.props b/Directory.Build.props
index 3abe378a3..36038d416 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -27,10 +27,10 @@
-
-
- 3.4.244
- all
+
+
+ 13.0.1
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 3c84f230d..72d4712e7 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -5,6 +5,15 @@
+
+
+
+
+
+
+
+
+
$(IntermediateOutputPath)app.manifest
diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln
index 39248b52c..75e1254b7 100644
--- a/Git-Credential-Manager.sln
+++ b/Git-Credential-Manager.sln
@@ -41,18 +41,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packaging.Linux", "src\linu
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "linux", "linux", "{8F9D7E67-7DD7-4E32-9134-423281AF00E9}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\shared\GitHub.UI\GitHub.UI.csproj", "{B5F00B46-FE93-45F2-B283-52B74B3E13B9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI", "src\shared\Atlassian.Bitbucket.UI\Atlassian.Bitbucket.UI.csproj", "{EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.UI", "src\shared\Core.UI\Core.UI.csproj", "{001846B0-462B-4A27-90CD-2435D4C0F680}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.UI.Avalonia", "src\shared\Core.UI.Avalonia\Core.UI.Avalonia.csproj", "{DE620324-250C-4262-BA13-198FA6FDB82A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Avalonia", "src\shared\GitHub.UI.Avalonia\GitHub.UI.Avalonia.csproj", "{459501A8-31E6-41CB-BE54-D31FFF4B2007}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI.Avalonia", "src\shared\Atlassian.Bitbucket.UI.Avalonia\Atlassian.Bitbucket.UI.Avalonia.csproj", "{714ACBE7-0C69-4D8A-9224-22792CAA8264}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Windows", "src\windows\GitHub.UI.Windows\GitHub.UI.Windows.csproj", "{0A86ED89-1FC5-42AA-925C-4578FA30607A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI.Windows", "src\windows\Atlassian.Bitbucket.UI.Windows\Atlassian.Bitbucket.UI.Windows.csproj", "{3F015046-DAF2-4D2A-96EC-F9782F169E45}"
@@ -61,14 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab", "src\shared\GitLab
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.Tests", "src\shared\GitLab.Tests\GitLab.Tests.csproj", "{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI", "src\shared\GitLab.UI\GitLab.UI.csproj", "{9AFD88E2-7E2C-46DA-9D38-4342086426D3}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI.Avalonia", "src\shared\GitLab.UI.Avalonia\GitLab.UI.Avalonia.csproj", "{47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI.Windows", "src\windows\GitLab.UI.Windows\GitLab.UI.Windows.csproj", "{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Git-Credential-Manager.UI.Avalonia", "src\shared\Git-Credential-Manager.UI.Avalonia\Git-Credential-Manager.UI.Avalonia.csproj", "{35659127-8859-4DB9-8DD6-A08C1952632E}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Git-Credential-Manager.UI.Windows", "src\windows\Git-Credential-Manager.UI.Windows\Git-Credential-Manager.UI.Windows.csproj", "{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}"
EndProject
Global
@@ -283,102 +265,6 @@ Global
{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Release|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Release|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Release|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Release|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Release|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{0A86ED89-1FC5-42AA-925C-4578FA30607A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A86ED89-1FC5-42AA-925C-4578FA30607A}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
{0A86ED89-1FC5-42AA-925C-4578FA30607A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -431,38 +317,6 @@ Global
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacRelease|Any CPU.Build.0 = Release|Any CPU
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Release|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Release|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -473,22 +327,6 @@ Global
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Release|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
@@ -522,20 +360,11 @@ Global
{2B3CD8FF-84A6-4B53-A28B-D7A75B0AB4D7} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{8F9D7E67-7DD7-4E32-9134-423281AF00E9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}
{AD2A935F-3720-4802-8119-6A9B35B254DF} = {8F9D7E67-7DD7-4E32-9134-423281AF00E9}
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {001846B0-462B-4A27-90CD-2435D4C0F680} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {DE620324-250C-4262-BA13-198FA6FDB82A} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {459501A8-31E6-41CB-BE54-D31FFF4B2007} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {714ACBE7-0C69-4D8A-9224-22792CAA8264} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{0A86ED89-1FC5-42AA-925C-4578FA30607A} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{3F015046-DAF2-4D2A-96EC-F9782F169E45} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{570897DC-A85C-4598-B793-9A00CF710119} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
- {35659127-8859-4DB9-8DD6-A08C1952632E} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/README.md b/README.md
index b793f45e7..6a9663e44 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,12 @@ See the [documentation index][docs-index] for links to additional resources.
- [Windows broker (experimental)][gcm-windows-broker]
+## Future features
+
+Curious about what's coming next in the GCM project? Take a look at the [project
+roadmap][roadmap]! You can find more details about the construction of the
+roadmap and how to interpret it [here][roadmap-announcement].
+
## Contributing
This project welcomes contributions and suggestions.
@@ -113,26 +119,28 @@ When using GitHub logos, please be sure to follow the
[azure-devops-ssh]: https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops
[bitbucket]: https://bitbucket.org
[bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html
-[build-status-badge]: https://github.com/GitCredentialManager/git-credential-manager/actions/workflows/continuous-integration.yml/badge.svg
-[docs-index]: https://github.com/GitCredentialManager/git-credential-manager/blob/release/docs/README.md
+[build-status-badge]: https://github.com/git-ecosystem/git-credential-manager/actions/workflows/continuous-integration.yml/badge.svg
+[docs-index]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/README.md
[dotnet]: https://dotnet.microsoft.com
[dotnet-distributions]: https://learn.microsoft.com/en-us/dotnet/core/install/linux
[git-credential-helper]: https://git-scm.com/docs/gitcredentials
-[gcm]: https://github.com/GitCredentialManager/git-credential-manager
+[gcm]: https://github.com/git-ecosystem/git-credential-manager
[gcm-coc]: CODE_OF_CONDUCT.md
[gcm-commit-12294990]: https://github.com/git/git/commit/12294990c90e043862be9eb7eb22c3784b526340
[gcm-contributing]: CONTRIBUTING.md
-[gcm-credstores]: https://github.com/GitCredentialManager/git-credential-manager/blob/release/docs/credstores.md
+[gcm-credstores]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/credstores.md
[gcm-for-mac-and-linux]: https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux
[gcm-for-windows]: https://github.com/microsoft/Git-Credential-Manager-for-Windows
-[gcm-http-proxy]: https://github.com/GitCredentialManager/git-credential-manager/blob/release/docs/netconfig.md#http-proxy
+[gcm-http-proxy]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/netconfig.md#http-proxy
[gcm-license]: LICENSE
-[gcm-usage]: https://github.com/GitCredentialManager/git-credential-manager/blob/release/docs/usage.md
-[gcm-windows-broker]: https://github.com/GitCredentialManager/git-credential-manager/blob/release/docs/windows-broker.md
+[gcm-usage]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/usage.md
+[gcm-windows-broker]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/windows-broker.md
[git-tools-credential-storage]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage
[github]: https://github.com
[github-ssh]: https://help.github.com/en/articles/connecting-to-github-with-ssh
[github-logos]: https://github.com/logos
-[install]: https://github.com/GitCredentialManager/git-credential-manager/blob/release/docs/install.md
+[install]: https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/install.md
[ms-package-repos]: https://packages.microsoft.com/repos/
-[workflow-status]: https://github.com/GitCredentialManager/git-credential-manager/actions/workflows/continuous-integration.yml
+[roadmap]: https://github.com/git-ecosystem/git-credential-manager/milestones?direction=desc&sort=due_date&state=open
+[roadmap-announcement]: https://github.com/git-ecosystem/git-credential-manager/discussions/1203
+[workflow-status]: https://github.com/git-ecosystem/git-credential-manager/actions/workflows/continuous-integration.yml
diff --git a/VERSION b/VERSION
new file mode 100644
index 000000000..9c1c5ffbd
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+2.1.0.0
diff --git a/build/GCM.tasks b/build/GCM.tasks
index fe7031f34..d48d07751 100644
--- a/build/GCM.tasks
+++ b/build/GCM.tasks
@@ -13,4 +13,10 @@
+
+
+
+
+
+
diff --git a/build/GetVersion.cs b/build/GetVersion.cs
new file mode 100644
index 000000000..2b3473641
--- /dev/null
+++ b/build/GetVersion.cs
@@ -0,0 +1,45 @@
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using System.IO;
+
+namespace GitCredentialManager.MSBuild
+{
+ public class GetVersion : Task
+ {
+ [Required]
+ public string VersionFile { get; set; }
+
+ [Output]
+ public string Version { get; set; }
+
+ [Output]
+ public string AssemblyVersion { get; set; }
+
+ [Output]
+ public string FileVersion { get; set; }
+
+ public override bool Execute()
+ {
+ Log.LogMessage(MessageImportance.Normal, "Reading VERSION file...");
+ string textVersion = File.ReadAllText(VersionFile);
+
+ if (!System.Version.TryParse(textVersion, out System.Version fullVersion))
+ {
+ Log.LogError("Invalid version '{0}' specified.", textVersion);
+ return false;
+ }
+
+ // System.Version names its version components as follows:
+ // major.minor[.build[.revision]]
+ // The main version number we use for GCM contains the first three
+ // components.
+ // The assembly and file version numbers contain all components, as
+ // ommitting the revision portion from these properties causes
+ // runtime failures on Windows.
+ Version = $"{fullVersion.Major}.{fullVersion.Minor}.{fullVersion.Build}";
+ AssemblyVersion = FileVersion = fullVersion.ToString();
+
+ return true;
+ }
+ }
+}
diff --git a/docs/architecture.md b/docs/architecture.md
index 2b2da52c0..0aff15287 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -280,7 +280,7 @@ to the trace object in most places of GCM.
[avalonia]: https://avaloniaui.net/
[core-program]: ../src/shared/Git-Credential-Manager/Program.cs
[credential-provider]: configuration.md#credentialprovider
-[issue-113]: https://github.com/GitCredentialManager/git-credential-manager/issues/113
-[issue-136]: https://github.com/GitCredentialManager/git-credential-manager/issues/136
+[issue-113]: https://github.com/git-ecosystem/git-credential-manager/issues/113
+[issue-136]: https://github.com/git-ecosystem/git-credential-manager/issues/136
[gcm-provider]: environment.md#GCM_PROVIDER
[msal]: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet
diff --git a/docs/bitbucket-authentication.md b/docs/bitbucket-authentication.md
new file mode 100644
index 000000000..c8d47e955
--- /dev/null
+++ b/docs/bitbucket-authentication.md
@@ -0,0 +1,61 @@
+# Bitbucket Authentication
+
+When GCM is triggered by Git, it will check the `host` parameter passed
+to it. If this parameter contains `bitbucket.org` it will trigger Bitbucket
+authentication and prompt you for credentials. In this scenario, you have two
+options for authentication: `OAuth` or `Password/Token`.
+
+### OAuth
+
+The dialog GCM presents for authentication contains two tabs. The first tab
+(labeled `Browser`) will trigger OAuth Authentication. Clicking the `Sign in
+with your browser` button opens a browser request to
+`_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
+(and potentially complete 2FA) to authorize GCM to access Bitbucket with the
+specified scopes. GCM will then spawn a temporary local webserver, listening on
+port 34106, to handle the OAuth redirect/callback. Assuming you successfully
+log into Bitbucket and authorize GCM, this callback will include the appropriate
+tokens for GCM to handle authencation. These tokens are then stored in your
+configured [credential store][credstores] and are returned to Git.
+
+### Password/Token
+
+**Note:** Bitbucket Data Center, also known as Bitbucket Server or Bitbucket On
+Premises, only supports Basic Authentication - please follow the below
+instructions if you are using this product.
+
+The dialog GCM presents for authentication contains two tabs. The second tab
+(labeled `Password/Token`) will trigger Basic Authentication. This tab contains
+two fields, one for your username and one for your password or token. If the
+`username` parameter was passed into GCM, that will pre-populate the username
+field, although it can be overridden. Enter your username (if needed) and your
+password or token (i.e. Bitbucket App Password) and click `Sign in`.
+
+:rotating_light: Requirements for App Passwords :rotating_light:
+
+If you are planning to use an [App Password][app-password] for basic
+authentication, it must at a minimum have _Account Read_ permissions (as shown
+below). If your App Password does not have these permissions, you will be
+re-prompted for credentials on every interaction with the server.
+
+![][app-password-example]
+
+When your username and password are submitted, GCM will attempt to retrieve a
+basic authentication token for these credentials via the Bitbucket REST API. If
+this is successful, the credentials, username, and password/token are stored in
+your configured [credential store][credstores] and are returned to Git.
+
+If the API request fails with a 401 return code, the entered username/password
+combination is invalid; nothing is stored and nothing is returned to Git. In
+this scenario, re-attempt authentication, ensuring your credentials are correct.
+
+If the API request fails with a 403 (Forbidden) return code, the username and
+password are valid, but 2FA is enabled on the corresponding Bitbucket Account.
+In this scenario, you will be prompted to complete the OAuth authentication
+process. If this is successful, the credentials, username, and password/token
+are stored in your configured [credential store][credstores] and are returned to
+Git.
+
+[app-password]: https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/
+[app-password-example]: img/app-password.png
+[credstores]: ./credstores.md
diff --git a/docs/bitbucket-development.md b/docs/bitbucket-development.md
index 76605223f..9e6f5abb1 100644
--- a/docs/bitbucket-development.md
+++ b/docs/bitbucket-development.md
@@ -54,60 +54,12 @@ i.e. using a key such as `git:https://mminns@bitbucket.org/` rather than
GCM can support multiple accounts, and usernames, for a single user against
Bitbucket, e.g. a personal account and a work account.
-## Authentication User Experience
-
-When the GCM is triggered by Git, the GCM will check the `host` parameter passed
-to it. If it contains `bitbucket.org` it will trigger the Bitbucket related
-processes.
-
-### Basic Authentication
-
-If the GCM needs to prompt the user for credentials they will always be shown an
-initial dialog where they can enter a username and password. If the `username`
-parameter was passed into the GCM it is used to pre-populate the username field,
-although it can be overridden. When username and password credentials are
-submitted the GCM will use them to attempt to retrieve a token, for Basic
-Authentication this token is in effect the password the user just entered. The
-GCM retrieves this `token` by checking the password can be used to successfully
-retrieve the User profile via the Bitbucket REST API.
-
-If the username and password credentials sent as Basic Authentication
-credentials works, then the password is identified as the token. The
-credentials, the username and the password/token, are then stored and the values
-returned to Git.
-
-If the request for the User profile via the REST API fails with a 401 return
-code it indicates the username/password combination is invalid, nothing is
-stored and nothing is returned to Git.
-
-However if the request fails with a 403 (Forbidden) return code, this indicates
-that the username and password are valid but 2FA is enabled on the Bitbucket
-Account. When this occurs the user it prompted to complete the OAuth
-authentication process.
-
-### OAuth
-
-OAuth authentication prompts the User with a new dialog where they can trigger
-OAuth authentication. This involves opening a browser request to `_https://bitbucket.org/site/oauth2/authorize?response_type=code&client_id={consumerkey}&state=authenticated&scope={scopes}&redirect_uri=http://localhost:34106/_`.
-This will trigger a flow on Bitbucket where the user must login, potentially
-including a 2FA prompt, and authorize the GCM to access Bitbucket with the
-specified scopes. The GCM will spawn a temporary, local webserver, listening on
-port 34106, to handle the OAuth redirect/callback. Assuming the user
-successfully logins into Bitbucket and authorizes the GCM this callback will
-include the Access and Refresh Tokens.
-
-The Access and Refresh Tokens will be stored against the username and the
-username/Access Token credentials returned to Git.
-
## On-Premise Bitbucket
On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC,
has a number of differences compared to the cloud instance of Bitbucket,
[bitbucket.org][bitbucket].
-As far as GCMC is concerned the main difference it doesn't support OAuth so only
-Basic Authentication is available.
-
It is possible to test with Bitbucket Server by running it locally using the
following command from the Atlassian SDK:
@@ -174,13 +126,13 @@ host by specifying the host url, e.g. https://bitbucket.example.com/
Due to the way GCM resolves hosts and determines REST API urls, if the Bitbucket
DC instance is hosted under a relative url (e.g. https://example.com/bitbucket)
it is necessary to configure Git to send the full path to GCM. This is done
-using the [credential.useHttpPath](configuration.md#credential.useHttpPath)
+using the [credential.useHttpPath](configuration.md#credential.useHttpPath)
setting.
❯ git config --global credential.https://example.com/bitbucket.usehttppath true
If a port number is used in the url of the Bitbucket DC instance the Git
-configuration needs to reflect this. However, due to [Issue 608](https://github.com/GitCredentialManager/git-credential-manager/issues/608)
+configuration needs to reflect this. However, due to [Issue 608](https://github.com/git-ecosystem/git-credential-manager/issues/608)
the port is ignored when resolving [credential.bitbucketDataCenterOAuthClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId)
and [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credential.bitbucketDataCenterOAuthClientSecret).
diff --git a/docs/configuration.md b/docs/configuration.md
index d77819731..586ce7b89 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -76,6 +76,86 @@ Defaults to enabled.
---
+### credential.trace
+
+Enables trace logging of all activities.
+Configuring Git and GCM to trace to the same location is often desirable, and
+GCM is compatible and cooperative with `GIT_TRACE`.
+
+#### Example
+
+```shell
+git config --global credential.trace /tmp/git.log
+```
+
+If the value of `credential.trace` is a full path to a file in an existing
+directory, logs are appended to the file.
+
+If the value of `credential.trace` is `true` or `1`, logs are written to
+standard error.
+
+Defaults to disabled.
+
+**Also see: [GCM_TRACE][gcm-trace]**
+
+---
+
+### credential.traceSecrets
+
+Enables tracing of secret and sensitive information, which is by default masked
+in trace output. Requires that `credential.trace` is also enabled.
+
+#### Example
+
+```shell
+git config --global credential.traceSecrets true
+```
+
+If the value of `credential.traceSecrets` is `true` or `1`, trace logs will include
+secret information.
+
+Defaults to disabled.
+
+**Also see: [GCM_TRACE_SECRETS][gcm-trace-secrets]**
+
+---
+
+### credential.traceMsAuth
+
+Enables inclusion of Microsoft Authentication library (MSAL) logs in GCM trace
+output. Requires that `credential.trace` is also enabled.
+
+#### Example
+
+```shell
+git config --global credential.traceMsAuth true
+```
+
+If the value of `credential.traceMsAuth` is `true` or `1`, trace logs will
+include verbose MSAL logs.
+
+Defaults to disabled.
+
+**Also see: [GCM_TRACE_MSAUTH][gcm-trace-msauth]**
+
+---
+
+### credential.debug
+
+Pauses execution of GCM at launch to wait for a debugger to be attached.
+
+#### Example
+
+```shell
+git config --global credential.debug true
+```
+
+Defaults to disabled.
+
+**Also see: [GCM_DEBUG][gcm-debug]**
+
+---
+
### credential.provider
Define the host provider to use when authenticating.
@@ -543,7 +623,10 @@ git config --global credential.msauthFlow devicecode
Use the operating system account manager where available.
-Defaults to `false`. This default is subject to change in the future.
+Defaults to `false`. In certain cloud hosted environments when using a work or
+school account, such as [Microsoft DevBox][devbox], the default is `true`.
+
+These defaults are subject to change in the future.
_**Note:** before you enable this option on Windows, please review the
[Windows Broker][wam] details for what this means to your local Windows user
@@ -564,6 +647,30 @@ git config --global credential.msauthUseBroker true
---
+### credential.msauthUseDefaultAccount _(experimental)_
+
+Use the current operating system account by default when the broker is enabled.
+
+Defaults to `false`. In certain cloud hosted environments when using a work or
+school account, such as [Microsoft DevBox][devbox], the default is `true`.
+
+These defaults are subject to change in the future.
+
+Value|Description
+-|-
+`true`|Use the current operating system account by default.
+`false` _(default)_|Do not assume any account to use by default.
+
+#### Example
+
+```shell
+git config --global credential.msauthUseDefaultAccount true
+```
+
+**Also see: [GCM_MSAUTH_USEDEFAULTACCOUNT][gcm-msauth-usedefaultaccount]**
+
+---
+
### credential.useHttpPath
Tells Git to pass the entire repository URL, rather than just the hostname, when
@@ -661,6 +768,75 @@ git config --global credential.azreposCredentialType oauth
**Also see: [GCM_AZREPOS_CREDENTIALTYPE][gcm-azrepos-credentialtype]**
+---
+
+### trace2.normalTarget
+
+Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format
+documentation][trace2-normal-docs] for more details.
+
+#### Example
+
+```shell
+git config --global trace2.normalTarget true
+```
+
+If the value of `trace2.normalTarget` is a full path to a file in an existing
+directory, logs are appended to the file.
+
+If the value of `trace2.normalTarget` is `true` or `1`, logs are written to
+standard error.
+
+Defaults to disabled.
+
+**Also see: [GIT_TRACE2][trace2-normal-env]**
+
+---
+
+### trace2.eventTarget
+
+Turns on Trace2 Event Format tracing - see [Git's Trace2 Event Format
+documentation][trace2-event-docs] for more details.
+
+#### Example
+
+```shell
+git config --global trace2.eventTarget true
+```
+
+If the value of `trace2.eventTarget` is a full path to a file in an existing
+directory, logs are appended to the file.
+
+If the value of `trace2.eventTarget` is `true` or `1`, logs are written to
+standard error.
+
+Defaults to disabled.
+
+**Also see: [GIT_TRACE2_EVENT][trace2-event-env]**
+
+---
+
+### trace2.perfTarget
+
+Turns on Trace2 Performance Format tracing - see [Git's Trace2 Performance
+Format documentation][trace2-performance-docs] for more details.
+
+#### Example
+
+```shell
+git config --global trace2.perfTarget true
+```
+
+If the value of `trace2.perfTarget` is a full path to a file in an existing
+directory, logs are appended to the file.
+
+If the value of `trace2.perfTarget` is `true` or `1`, logs are written to
+standard error.
+
+Defaults to disabled.
+
+**Also see: [GIT_TRACE2_PERF][trace2-performance-env]**
+
[auto-detection]: autodetect.md
[azure-tokens]: azrepos-users-and-tokens.md
[use-http-path]: https://git-scm.com/docs/gitcredentials/#Documentation/gitcredentials.txt-useHttpPath
@@ -671,6 +847,7 @@ git config --global credential.azreposCredentialType oauth
[credential-plaintextstorepath]: #credentialplaintextstorepath
[credential-cache]: https://git-scm.com/docs/git-credential-cache
[cred-stores]: credstores.md
+[devbox]: https://azure.microsoft.com/en-us/products/dev-box
[enterprise-config]: enterprise-config.md
[envars]: environment.md
[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/
@@ -682,6 +859,7 @@ git config --global credential.azreposCredentialType oauth
[gcm-bitbucket-authmodes]: environment.md#GCM_BITBUCKET_AUTHMODES
[gcm-credential-cache-options]: environment.md#GCM_CREDENTIAL_CACHE_OPTIONS
[gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE
+[gcm-debug]: environment.md#GCM_DEBUG
[gcm-dpapi-store-path]: environment.md#GCM_DPAPI_STORE_PATH
[gcm-github-authmodes]: environment.md#GCM_GITHUB_AUTHMODES
[gcm-gitlab-authmodes]:environment.md#GCM_GITLAB_AUTHMODES
@@ -690,9 +868,13 @@ git config --global credential.azreposCredentialType oauth
[gcm-interactive]: environment.md#GCM_INTERACTIVE
[gcm-msauth-flow]: environment.md#GCM_MSAUTH_FLOW
[gcm-msauth-usebroker]: environment.md#GCM_MSAUTH_USEBROKER-experimental
+[gcm-msauth-usedefaultaccount]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental
[gcm-namespace]: environment.md#GCM_NAMESPACE
[gcm-plaintext-store-path]: environment.md#GCM_PLAINTEXT_STORE_PATH
[gcm-provider]: environment.md#GCM_PROVIDER
+[gcm-trace]: environment.md#GCM_TRACE
+[gcm-trace-secrets]: environment.md#GCM_TRACE_SECRETS
+[gcm-trace-msauth]: environment.md#GCM_TRACE_MSAUTH
[usage]: usage.md
[git-config-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy
[http-proxy]: netconfig.md#http-proxy
@@ -701,4 +883,10 @@ git config --global credential.azreposCredentialType oauth
[provider-migrate]: migration.md#gcm_authority
[cache-options]: https://git-scm.com/docs/git-credential-cache#_options
[pass]: https://www.passwordstore.org/
+[trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target
+[trace2-normal-env]: environment.md#GIT_TRACE2
+[trace2-event-docs]: https://git-scm.com/docs/api-trace2#_the_event_format_target
+[trace2-event-env]: environment.md#GIT_TRACE2_EVENT
+[trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target
+[trace2-performance-env]: environment.md#GIT_TRACE2_PERF
[wam]: windows-broker.md
diff --git a/docs/development.md b/docs/development.md
index 2b8fcfd41..4637f4574 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -3,7 +3,7 @@
Start by cloning this repository:
```shell
-git clone https://github.com/GitCredentialManager/git-credential-manager
+git clone https://github.com/git-ecosystem/git-credential-manager
```
You also need the latest version of the .NET SDK which can be downloaded and
@@ -98,6 +98,12 @@ Git to be the one launching us.
### Collect trace output
+GCM has two tracing systems - one that is distinctly GCM's and one that
+implements certain features of [Git's Trace2 API][trace2]. Below are
+instructions for how to use each.
+
+#### `GCM_TRACE`
+
If you want to debug a release build or installation of GCM, you can set the
`GCM_TRACE` environment variable to `1` to print trace information to standard
error, or to an absolute file path to write trace information to a file.
@@ -110,6 +116,80 @@ $ GCM_TRACE=1 git-credential-manager version
> Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0)
```
+#### Git's Trace2 API
+
+This API can also be used to print debug, performance, and telemetry information
+to stderr or a file in various formats.
+
+##### Supported format targets
+
+1. The Normal Format Target: Similar to `GCM_TRACE`, this target writes
+human-readable output and is best suited for debugging. It can be enabled via
+environment variable or config, for example:
+
+ ```shell
+ export GIT_TRACE2=1
+ ```
+
+ or
+
+ ```shell
+ git config --global trace2.normalTarget ~/log.normal
+ ```
+
+0. The Performance Format Target: This format is column-based and geared toward
+analyzing performance during development and testing. It can be enabled via
+environment variable or config, for example:
+
+ ```shell
+ export GIT_TRACE2_PERF=1
+ ```
+
+ or
+
+ ```shell
+ git config --global trace2.perfTarget ~/log.perf
+ ```
+
+0. The Event Format Target: This format is json-based and is geared toward
+collection of large quantities of data for advanced analysis. It can be enabled
+via environment variable or config, for example:
+
+ ```shell
+ export GIT_TRACE2_EVENT=1
+ ```
+
+ or
+
+ ```shell
+ git config --global trace2.eventTarget ~/log.event
+ ```
+
+You can read more about each of these format targets in the [corresponding
+section][trace2-targets] of Git's Trace2 API documentation.
+
+##### Supported events
+
+The below describes, at a high level, the Trace2 API events that are currently
+supported in GCM and the information they provide:
+
+1. `version`: contains the version of the current executable (e.g. GCM or a
+helper exe)
+0. `start`: contains the complete argv received by current executable's `Main()`
+method
+0. `exit`: contains current executable's exit code
+0. `child_start`: describes a child process that is about to be spawned
+0. `child_exit`: describes a child process at exit
+0. `region_enter`: describes a region (e.g. a timer for a section of code that
+is interesting) on entry
+0. `region_leave`: describes a region on leaving
+
+You can read more about each of these format targets in the [corresponding
+section][trace2-events] of Git's Trace2 API documentation.
+
+Want to see more events? Consider contributing! We'd :love: to see your
+awesome work in support of building out this API.
+
### Code coverage metrics
If you want code coverage metrics these can be generated either from the command
@@ -169,4 +249,7 @@ Some URLs are ignored by lychee, per the [lycheeignore][lycheeignore].
[lycheeignore]: ../.lycheeignore
[markdownlint]: https://github.com/DavidAnson/markdownlint-cli2
[markdownlint-config]: ../.markdownlint.jsonc
+[trace2]: https://git-scm.com/docs/api-trace2
+[trace2-events]: https://git-scm.com/docs/api-trace2#_event_specific_keyvalue_pairs
+[trace2-targets]: https://git-scm.com/docs/api-trace2#_trace2_targets
[vscode-markdownlint]: https://github.com/DavidAnson/vscode-markdownlint
diff --git a/docs/environment.md b/docs/environment.md
index 7c73965a7..fcdb7db65 100644
--- a/docs/environment.md
+++ b/docs/environment.md
@@ -39,9 +39,9 @@ logs are appended to the file.
If the value of `GCM_TRACE` is `true` or `1`, logs are written to standard error.
-Defaults to tracing disabled.
+Defaults to disabled.
-_No configuration equivalent._
+**Also see: [credential.trace][credential-trace]**
---
@@ -71,14 +71,14 @@ secret information.
Defaults to disabled.
-_No configuration equivalent._
+**Also see: [credential.traceSecrets][credential-trace-secrets]**
---
### GCM_TRACE_MSAUTH
-Enables inclusion of Microsoft Authentication libraries (ADAL, MSAL) logs in GCM
-trace output. Requires that `GCM_TRACE` is also enabled.
+Enables inclusion of Microsoft Authentication library (MSAL) logs in GCM trace
+output. Requires that `GCM_TRACE` is also enabled.
#### Example
@@ -97,11 +97,11 @@ export GCM_TRACE_MSAUTH=1
```
If the value of `GCM_TRACE_MSAUTH` is `true` or `1`, trace logs will include
-verbose ADAL/MSAL logs.
+verbose MSAL logs.
Defaults to disabled.
-_No configuration equivalent._
+**Also see: [credential.traceMsAuth][credential-trace-msauth]**
---
@@ -125,7 +125,7 @@ export GCM_DEBUG=1
Defaults to disabled.
-_No configuration equivalent._
+**Also see: [credential.debug][credential-debug]**
---
@@ -776,7 +776,10 @@ export GCM_MSAUTH_FLOW="devicecode"
Use the operating system account manager where available.
-Defaults to `false`. This default is subject to change in the future.
+Defaults to `false`. In certain cloud hosted environments when using a work or
+school account, such as [Microsoft DevBox][devbox], the default is `true`.
+
+These defaults are subject to change in the future.
_**Note:** before you enable this option on Windows, please
[review the details][windows-broker] about what this means to your local Windows
@@ -803,6 +806,36 @@ export GCM_MSAUTH_USEBROKER="false"
---
+### GCM_MSAUTH_USEDEFAULTACCOUNT _(experimental)_
+
+Use the current operating system account by default when the broker is enabled.
+
+Defaults to `false`. In certain cloud hosted environments when using a work or
+school account, such as [Microsoft DevBox][devbox], the default is `true`.
+
+These defaults are subject to change in the future.
+
+Value|Description
+-|-
+`true`|Use the current operating system account by default.
+`false` _(default)_|Do not assume any account to use by default.
+
+#### Windows
+
+```batch
+SET GCM_MSAUTH_USEDEFAULTACCOUNT="true"
+```
+
+#### macOS/Linux
+
+```bash
+export GCM_MSAUTH_USEDEFAULTACCOUNT="false"
+```
+
+**Also see: [credential.msauthUseDefaultAccount][credential-msauth-usedefaultaccount]**
+
+---
+
### GCM_AZREPOS_CREDENTIALTYPE
Specify the type of credential the Azure Repos host provider should return.
@@ -830,6 +863,93 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth"
**Also see: [credential.azreposCredentialType][credential-azrepos-credential-type]**
+---
+
+### GIT_TRACE2
+
+Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format
+documentation][trace2-normal-docs] for more details.
+
+#### Windows
+
+```batch
+SET GIT_TRACE2=%UserProfile%\log.normal
+```
+
+#### macOS/Linux
+
+```bash
+export GIT_TRACE2=~/log.normal
+```
+
+If the value of `GIT_TRACE2` is a full path to a file in an existing directory,
+logs are appended to the file.
+
+If the value of `GIT_TRACE2` is `true` or `1`, logs are written to standard
+error.
+
+Defaults to disabled.
+
+**Also see: [trace2.normalFormat][trace2-normal-config]**
+
+---
+
+### GIT_TRACE2_EVENT
+
+Turns on Trace2 Event Format tracing - see [Git's Trace2 Event Format
+documentation][trace2-event-docs] for more details.
+
+#### Windows
+
+```batch
+SET GIT_TRACE2_EVENT=%UserProfile%\log.event
+```
+
+#### macOS/Linux
+
+```bash
+export GIT_TRACE2_EVENT=~/log.event
+```
+
+If the value of `GIT_TRACE2_EVENT` is a full path to a file in an existing
+directory, logs are appended to the file.
+
+If the value of `GIT_TRACE2_EVENT` is `true` or `1`, logs are written to
+standard error.
+
+Defaults to disabled.
+
+**Also see: [trace2.eventFormat][trace2-event-config]**
+
+---
+
+### GIT_TRACE2_PERF
+
+Turns on Trace2 Performance Format tracing - see [Git's Trace2 Performance
+Format documentation][trace2-performance-docs] for more details.
+
+#### Windows
+
+```batch
+SET GIT_TRACE2_PERF=%UserProfile%\log.perf
+```
+
+#### macOS/Linux
+
+```bash
+export GIT_TRACE2_PERF=~/log.perf
+```
+
+If the value of `GIT_TRACE2_PERF` is a full path to a file in an existing
+directory, logs are appended to the file.
+
+If the value of `GIT_TRACE2_PERF` is `true` or `1`, logs are written to
+standard error.
+
+Defaults to disabled.
+
+**Also see: [trace2.perfFormat][trace2-performance-config]**
+
[autodetect]: autodetect.md
[azure-access-tokens]: azrepos-users-and-tokens.md
[configuration]: configuration.md
@@ -840,6 +960,7 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth"
[credential-bitbucketauthmodes]: configuration.md#credentialbitbucketAuthModes
[credential-cacheoptions]: configuration.md#credentialcacheoptions
[credential-credentialstore]: configuration.md#credentialcredentialstore
+[credential-debug]: configuration.md#credentialdebug
[credential-dpapi-store-path]: configuration.md#credentialdpapistorepath
[credential-githubauthmodes]: configuration.md#credentialgitHubAuthModes
[credential-gitlabauthmodes]: configuration.md#credentialgitLabAuthModes
@@ -849,10 +970,15 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth"
[credential-namespace]: configuration.md#credentialnamespace
[credential-msauth-flow]: configuration.md#credentialmsauthflow
[credential-msauth-usebroker]: configuration.md#credentialmsauthusebroker-experimental
+[credential-msauth-usedefaultaccount]: configuration.md#credentialmsauthusedefaultaccount-experimental
[credential-plain-text-store]: configuration.md#credentialplaintextstorepath
[credential-provider]: configuration.md#credentialprovider
[credential-stores]: credstores.md
+[credential-trace]: configuration.md#credentialtrace
+[credential-trace-secrets]: configuration.md#credentialtracesecrets
+[credential-trace-msauth]: configuration.md#credentialtracemsauth
[default-values]: enterprise-config.md
+[devbox]: https://azure.microsoft.com/en-us/products/dev-box
[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/
[gcm]: usage.md
[gcm-interactive]: #gcm_interactive
@@ -867,4 +993,10 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth"
[libsecret]: https://wiki.gnome.org/Projects/Libsecret
[migration-guide]: migration.md#gcm_authority
[passwordstore]: https://www.passwordstore.org/
+[trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target
+[trace2-normal-config]: configuration.md#trace2normalTarget
+[trace2-event-docs]: https://git-scm.com/docs/api-trace2#_the_event_format_target
+[trace2-event-config]: configuration.md#trace2eventTarget
+[trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target
+[trace2-performance-config]: configuration.md#trace2perfTarget
[windows-broker]: windows-broker.md
diff --git a/docs/faq.md b/docs/faq.md
index 7008b793b..cf761b1e8 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -235,7 +235,7 @@ initiate this flow for you next time access is requested).
[bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html
[config-gui-prompt]: configuration.md#credentialguiprompt
[config-interactive]: configuration.md#credentialinteractive
-[create-issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/create
+[create-issue]: https://github.com/git-ecosystem/git-credential-manager/issues/create
[credstores]: credstores.md
[download-and-install]: ../README.md#download-and-install
[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
diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md
index f9201db0a..6a54a7a40 100644
--- a/docs/github-apideprecation.md
+++ b/docs/github-apideprecation.md
@@ -128,7 +128,7 @@ the new token-based authentication requirements **DO NOT** apply to GHES:
[gcm]: https://aka.ms/gcm
[gcm-install]: ../README.md#download-and-install
[gcm-latest]: https://aka.ms/gcm/latest
-[gcm-new-issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose
+[gcm-new-issue]: https://github.com/git-ecosystem/git-credential-manager/issues/new/choose
[gcm-windows]: https://github.com/microsoft/Git-Credential-Manager-for-Windows
[git-windows]: https://git-scm.com/download/win
[github-display-pat-image]: img/github-display-pat.png
diff --git a/docs/gitlab.md b/docs/gitlab.md
index 04b122e1c..4c1135e6b 100644
--- a/docs/gitlab.md
+++ b/docs/gitlab.md
@@ -58,7 +58,7 @@ git config --global credential.https://code.videolan.org.gitLabDevClientId f35c3
git config --global credential.https://code.videolan.org.gitLabDevClientSecret 631558ec973c5ef65b78db9f41103f8247dc68d979c86f051c0fe4389e1995e8
```
-See also [issue #677](https://github.com/GitCredentialManager/git-credential-manager/issues/677).
+See also [issue #677](https://github.com/git-ecosystem/git-credential-manager/issues/677).
## Preferences
diff --git a/docs/hostprovider.md b/docs/hostprovider.md
index 9a6e16412..25aaf3d85 100644
--- a/docs/hostprovider.md
+++ b/docs/hostprovider.md
@@ -343,7 +343,7 @@ take, but implementors SHOULD attempt to follow existing practices and styles.
1. [`System.CommandLine` API][github-dotnet-cli]
-[gcm]: https://github.com/GitCredentialManager/git-credential-manager
+[gcm]: https://github.com/git-ecosystem/git-credential-manager
[github-dotnet-cli]: https://github.com/dotnet/command-line-api
[hostprovider-base-class]: #26-hostprovider-base-class
[references]: #references
diff --git a/docs/img/app-password.png b/docs/img/app-password.png
new file mode 100644
index 000000000..da6b4e119
Binary files /dev/null and b/docs/img/app-password.png differ
diff --git a/docs/install.md b/docs/install.md
index f434ceaa7..08b3c358c 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -203,7 +203,7 @@ tool][dotnet-tool]. This is
the preferred install method for Linux because you can use it to install on any
[.NET-supported
distribution][dotnet-supported-distributions]. You
-can also use this method on macOS or Windows if you so choose.
+can also use this method on macOS if you so choose.
**Note:** Make sure you have installed [version 6.0 of the .NET
SDK][dotnet-install] before attempting to run the following `dotnet tool`
@@ -237,6 +237,6 @@ dotnet tool uninstall -g git-credential-manager
[gcm-wsl]: wsl.md
[git-for-windows]: https://gitforwindows.org/
[git-for-windows-screenshot]: https://user-images.githubusercontent.com/5658207/140082529-1ac133c1-0922-4a24-af03-067e27b3988b.png
-[latest-release]: https://github.com/GitCredentialManager/git-credential-manager/releases/latest
+[latest-release]: https://github.com/git-ecosystem/git-credential-manager/releases/latest
[linux-uninstall]: linux-fromsrc-uninstall.md
[ms-wsl]: https://aka.ms/wsl#
diff --git a/docs/rename.md b/docs/rename.md
index b69e1c2f1..4d090cc69 100644
--- a/docs/rename.md
+++ b/docs/rename.md
@@ -156,10 +156,10 @@ or `manager` respectively.
> helper that is configured, and overrides any helpers configured at the system/
> machine-wide level.
-[rename-pr]: https://github.com/GitCredentialManager/git-credential-manager/pull/541
+[rename-pr]: https://github.com/git-ecosystem/git-credential-manager/pull/541
[rename-blog]: https://github.blog/2022-04-07-git-credential-manager-authentication-for-everyone/#universal-git-authentication
-[gcm-org]: https://github.com/GitCredentialManager
-[rename-ver]: https://github.com/GitCredentialManager/git-credential-manager/releases
+[gcm-org]: https://github.com/git-ecosystem
+[rename-ver]: https://github.com/git-ecosystem/git-credential-manager/releases
[git-windows]: https://git-scm.com/download/win
[gcm-latest]: https://aka.ms/gcm/latest
[warnings]: #rename-transition
diff --git a/docs/windows-broker.md b/docs/windows-broker.md
index c767c172d..24794ebb1 100644
--- a/docs/windows-broker.md
+++ b/docs/windows-broker.md
@@ -34,6 +34,23 @@ fewer multi-factor authentication prompts, and the ability to use additional
authentication technologies like smart cards and Windows Hello. These
convenience and security features make a good case for enabling WAM.
+## Using the current OS account by default
+
+Enabling WAM does not currently automatically use the current Windows account
+for authentication. In order to opt-in to this behavior you can set the
+[`GCM_MSAUTH_USEDEFAULTACCOUNT`][GCM_MSAUTH_USEDEFAULTACCOUNT] environment
+variable or set the
+[`credential.msauthUseDefaultAccount`][credential.msauthUseDefaultAccount] Git
+configuration value to `true`.
+
+In certain cloud hosted environments when using a work or school account, such
+as [Microsoft Dev Box][devbox], this setting is **_automatically enabled_**.
+
+To disable this behavior, set the environment variable
+[`GCM_MSAUTH_USEDEFAULTACCOUNT`][GCM_MSAUTH_USEDEFAULTACCOUNT] or the
+[`credential.msauthUseDefaultAccount`][credential.msauthUseDefaultAccount] Git
+configuration value explicitly to `false`.
+
## Surprising behaviors
The WAM and Windows identity systems are complex, addressing a very broad range
@@ -174,8 +191,10 @@ In order to fix the problem, there are a few options:
[azure-refresh-token-terms]: https://docs.microsoft.com/azure/active-directory/devices/concept-primary-refresh-token#key-terminology-and-components
[azure-conditional-access]: https://docs.microsoft.com/azure/active-directory/conditional-access/overview
[azure-devops]: https://dev.azure.com
-[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER
-[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker
+[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER-experimental
+[GCM_MSAUTH_USEDEFAULTACCOUNT]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental
+[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker-experimental
+[credential.msauthUseDefaultAccount]: configuration.md#credentialmsauthusedefaultaccount-experimental
[aad-questions]: img/aad-questions.png
[aad-questions-21h1]: img/aad-questions-21H1.png
[aad-bitlocker]: img/aad-bitlocker.png
@@ -186,3 +205,4 @@ In order to fix the problem, there are a few options:
[apps-must-ask]: img/apps-must-ask.png
[ms-com]: https://docs.microsoft.com/en-us/windows/win32/com/the-component-object-model
[msal-dotnet]: https://aka.ms/msal-net
+[devbox]: https://azure.microsoft.com/en-us/products/dev-box
diff --git a/docs/wsl.md b/docs/wsl.md
index f37ff614c..8007fc74b 100644
--- a/docs/wsl.md
+++ b/docs/wsl.md
@@ -25,6 +25,9 @@ credential helper:
git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"
```
+> **Note:** the location of git-credential-manager.exe may be different in your
+installation of Git for Windows.
+
If you intend to use Azure DevOps you must _also_ set the following Git
configuration _inside of your WSL installation_.
diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj
index e14eef0a4..8254da948 100644
--- a/src/linux/Packaging.Linux/Packaging.Linux.csproj
+++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj
@@ -17,18 +17,14 @@
-
-
-
-
-
-
-
+
+
+
diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh
index 903e6ede8..6cc767013 100755
--- a/src/linux/Packaging.Linux/install-from-source.sh
+++ b/src/linux/Packaging.Linux/install-from-source.sh
@@ -34,7 +34,7 @@ Git Credential Manager is licensed under the MIT License: https://aka.ms/gcm/lic
[Nn]*)
exit
;;
- *)
+ *)
echo "Please answer yes or no."
;;
esac
@@ -198,7 +198,7 @@ script_path="$(cd "$(dirname "$0")" && pwd)"
toplevel_path="${script_path%/src/linux/Packaging.Linux}"
if [ "z$script_path" = "z$toplevel_path" ] || [ ! -f "$toplevel_path/Git-Credential-Manager.sln" ]; then
toplevel_path="$PWD/git-credential-manager"
- test -d "$toplevel_path" || git clone https://github.com/GitCredentialManager/git-credential-manager
+ test -d "$toplevel_path" || git clone https://github.com/git-ecosystem/git-credential-manager
fi
if [ -z "$DOTNET_ROOT" ]; then
diff --git a/src/linux/Packaging.Linux/layout.sh b/src/linux/Packaging.Linux/layout.sh
index 0f2c8ab6a..74b76a313 100755
--- a/src/linux/Packaging.Linux/layout.sh
+++ b/src/linux/Packaging.Linux/layout.sh
@@ -35,10 +35,6 @@ ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )"
SRC="$ROOT/src"
OUT="$ROOT/out"
GCM_SRC="$SRC/shared/Git-Credential-Manager"
-GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia"
-BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia"
-GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia"
-GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia"
PROJ_OUT="$OUT/linux/Packaging.Linux"
# Build parameters
@@ -81,42 +77,6 @@ $DOTNET_ROOT/dotnet publish "$GCM_SRC" \
-p:PublishSingleFile=true \
--output="$(make_absolute "$PAYLOAD")" || exit 1
-echo "Publishing core UI helper..."
-$DOTNET_ROOT/dotnet publish "$GCM_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing Bitbucket UI helper..."
-$DOTNET_ROOT/dotnet publish "$BITBUCKET_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitHub UI helper..."
-$DOTNET_ROOT/dotnet publish "$GITHUB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitLab UI helper..."
-$DOTNET_ROOT/dotnet publish "$GITLAB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained=true \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
# Collect symbols
echo "Collecting managed symbols..."
mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1
diff --git a/src/osx/Installer.Mac/Installer.Mac.csproj b/src/osx/Installer.Mac/Installer.Mac.csproj
index dd0f33ea8..4f9d44390 100644
--- a/src/osx/Installer.Mac/Installer.Mac.csproj
+++ b/src/osx/Installer.Mac/Installer.Mac.csproj
@@ -13,18 +13,14 @@
-
-
-
-
-
-
-
+
+
+
diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh
index 0dc664338..a95eb1f0d 100755
--- a/src/osx/Installer.Mac/layout.sh
+++ b/src/osx/Installer.Mac/layout.sh
@@ -22,9 +22,6 @@ OUT="$ROOT/out"
INSTALLER_SRC="$SRC/osx/Installer.Mac"
GCM_SRC="$SRC/shared/Git-Credential-Manager"
GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia"
-BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia"
-GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia"
-GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia"
# Build parameters
FRAMEWORK=net6.0
@@ -103,38 +100,6 @@ dotnet publish "$GCM_SRC" \
--self-contained \
--output="$(make_absolute "$PAYLOAD")" || exit 1
-echo "Publishing core UI helper..."
-dotnet publish "$GCM_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing Bitbucket UI helper..."
-dotnet publish "$BITBUCKET_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitHub UI helper..."
-dotnet publish "$GITHUB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitLab UI helper..."
-dotnet publish "$GITLAB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
# Collect symbols
echo "Collecting managed symbols..."
mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1
diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
index e92cb9061..a4a5e4afb 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
+++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
@@ -51,6 +51,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_ShowsMenu_OAut
{
var context = new TestCommandContext();
context.SessionManager.IsDesktopSession = true; // Allow OAuth mode
+ context.Settings.IsGuiPromptsEnabled = false; // Force text prompts
context.Terminal.Prompts["option (enter for default)"] = "1";
Uri targetUri = null;
@@ -71,6 +72,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_ShowsMenu_Basi
var context = new TestCommandContext();
context.SessionManager.IsDesktopSession = true; // Allow OAuth mode
+ context.Settings.IsGuiPromptsEnabled = false; // Force text prompts
context.Terminal.Prompts["option (enter for default)"] = "2";
context.Terminal.Prompts["Username"] = username;
context.Terminal.SecretPrompts["Password"] = password;
diff --git a/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs
index a1afb8f62..1a6866fb6 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs
+++ b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs
@@ -7,6 +7,7 @@
using Atlassian.Bitbucket.Cloud;
using GitCredentialManager;
using GitCredentialManager.Authentication.OAuth;
+using GitCredentialManager.Tests.Objects;
using Moq;
using Xunit;
@@ -16,7 +17,6 @@ public class BitbucketOAuth2ClientTest
{
private Mock httpClient = new Mock(MockBehavior.Strict);
private Mock settings = new Mock(MockBehavior.Loose);
- private Mock trace = new Mock(MockBehavior.Loose);
private Mock browser = new Mock(MockBehavior.Strict);
private Mock codeGenerator = new Mock(MockBehavior.Strict);
private IEnumerable scopes = new List();
@@ -55,7 +55,7 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient
Uri finalCallbackUri = MockFinalCallbackUri();
Bitbucket.Cloud.BitbucketOAuth2Client client = GetBitbucketOAuth2Client();
-
+
MockGetAuthenticationCodeAsync(finalCallbackUri, clientId, client.Scopes);
MockCodeGenerator();
@@ -68,8 +68,9 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient
[Fact]
public async Task BitbucketOAuth2Client_GetDeviceCodeAsync()
{
- var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object);
- await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct));
+ var trace2 = new NullTrace2();
+ var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);
+ await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct));
}
[Theory]
@@ -79,7 +80,8 @@ public async Task BitbucketOAuth2Client_GetDeviceCodeAsync()
[InlineData("https", "example.com/", "john", "https://example.com/refresh_token")]
public void BitbucketOAuth2Client_GetRefreshTokenServiceName(string protocol, string host, string username, string expectedResult)
{
- var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object);
+ var trace2 = new NullTrace2();
+ var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);
var input = new InputArguments(new Dictionary
{
["protocol"] = protocol,
@@ -100,7 +102,8 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result)
private Bitbucket.Cloud.BitbucketOAuth2Client GetBitbucketOAuth2Client()
{
- var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object);
+ var trace2 = new NullTrace2();
+ var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);
client.CodeGenerator = codeGenerator.Object;
return client;
}
diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs
index 97d194764..e2e7225db 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs
+++ b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs
@@ -7,6 +7,7 @@
using Atlassian.Bitbucket.DataCenter;
using GitCredentialManager;
using GitCredentialManager.Authentication.OAuth;
+using GitCredentialManager.Tests.Objects;
using Moq;
using Xunit;
@@ -16,7 +17,6 @@ public class BitbucketOAuth2ClientTest
{
private Mock httpClient = new Mock(MockBehavior.Strict);
private Mock settings = new Mock(MockBehavior.Loose);
- private Mock trace = new Mock(MockBehavior.Loose);
private Mock browser = new Mock(MockBehavior.Strict);
private Mock codeGenerator = new Mock(MockBehavior.Strict);
private CancellationToken ct = new CancellationToken();
@@ -77,7 +77,8 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result,
private Bitbucket.DataCenter.BitbucketOAuth2Client GetBitbucketOAuth2Client()
{
- var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object);
+ var trace2 = new NullTrace2();
+ var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2);
client.CodeGenerator = codeGenerator.Object;
return client;
}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj
deleted file mode 100644
index a9185278a..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- WinExe
- net6.0
- osx-x64;linux-x64;osx-arm64
- Atlassian.Bitbucket.UI
- Atlassian.Bitbucket.UI
-
-
-
-
-
-
-
-
-
- TesterWindow.axaml
- Code
-
-
-
-
-
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/CredentialsCommandImpl.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/CredentialsCommandImpl.cs
deleted file mode 100644
index e9be595ad..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/CredentialsCommandImpl.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Atlassian.Bitbucket.UI.ViewModels;
-using Atlassian.Bitbucket.UI.Views;
-using GitCredentialManager;
-using GitCredentialManager.UI;
-
-namespace Atlassian.Bitbucket.UI.Commands
-{
- public class CredentialsCommandImpl : CredentialsCommand
- {
- public CredentialsCommandImpl(ICommandContext context) : base(context) { }
-
- protected override Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct)
- {
- return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct);
- }
- }
-}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml
deleted file mode 100644
index fca8daf48..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
deleted file mode 100644
index 25ae2cef8..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System;
-using Atlassian.Bitbucket.UI.ViewModels;
-using Atlassian.Bitbucket.UI.Views;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-using GitCredentialManager;
-using GitCredentialManager.Interop.Linux;
-using GitCredentialManager.Interop.MacOS;
-using GitCredentialManager.Interop.Posix;
-using GitCredentialManager.Interop.Windows;
-using GitCredentialManager.UI.Controls;
-
-namespace Atlassian.Bitbucket.UI.Controls
-{
- public class TesterWindow : Window
- {
- private readonly IEnvironment _environment;
-
- public TesterWindow()
- {
- InitializeComponent();
-#if DEBUG
- this.AttachDevTools();
-#endif
-
- if (PlatformUtils.IsWindows())
- {
- _environment = new WindowsEnvironment(new WindowsFileSystem());
- }
- else
- {
- IFileSystem fs;
- if (PlatformUtils.IsMacOS())
- {
- fs = new MacOSFileSystem();
- }
- else
- {
- fs = new LinuxFileSystem();
- }
-
- _environment = new PosixEnvironment(fs);
- }
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- private void ShowCredentials(object sender, RoutedEventArgs e)
- {
- var vm = new CredentialsViewModel(_environment)
- {
- ShowOAuth = this.FindControl("showOAuth").IsChecked ?? false,
- ShowBasic = this.FindControl("showBasic").IsChecked ?? false,
- UserName = this.FindControl("username").Text
- };
-
- if (Uri.TryCreate(this.FindControl("url").Text, UriKind.Absolute, out Uri uri))
- {
- vm.Url = uri;
- }
-
- var view = new CredentialsView();
- var window = new DialogWindow(view) {DataContext = vm};
- window.ShowDialog(this);
- }
- }
-}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs
deleted file mode 100644
index be0ba8044..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-using Atlassian.Bitbucket.UI.Commands;
-using Atlassian.Bitbucket.UI.Controls;
-using Avalonia;
-using GitCredentialManager;
-using GitCredentialManager.UI;
-
-namespace Atlassian.Bitbucket.UI
-{
- public static class Program
- {
- public static void Main(string[] args)
- {
- // If we have no arguments then just start the app with the test window.
- if (args.Length == 0)
- {
- BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
- return;
- }
-
- // Create the dispatcher on the main thread. This is required
- // for some platform UI services such as macOS that mandates
- // all controls are created/accessed on the initial thread
- // created by the process (the process entry thread).
- Dispatcher.Initialize();
-
- // Run AppMain in a new thread and keep the main thread free
- // to process the dispatcher's job queue.
- var appMain = new Thread(AppMain) {Name = nameof(AppMain)};
- appMain.Start(args);
-
- // Process the dispatcher job queue (aka: message pump, run-loop, etc...)
- // We must ensure to run this on the same thread that it was created on
- // (the main thread) so we cannot use any async/await calls between
- // Dispatcher.Create and Run.
- Dispatcher.MainThread.Run();
-
- // Execution should never reach here as AppMain terminates the process on completion.
- throw new InvalidOperationException("Main dispatcher job queue shutdown unexpectedly");
- }
-
- private static void AppMain(object o)
- {
- string[] args = (string[]) o;
-
- using (var context = new CommandContext(args))
- using (var app = new HelperApplication(context))
- {
- app.RegisterCommand(new CredentialsCommandImpl(context));
-
- int exitCode = app.RunAsync(args)
- .ConfigureAwait(false)
- .GetAwaiter()
- .GetResult();
-
- context.Trace2.Stop(exitCode);
- Environment.Exit(exitCode);
- }
- }
-
- public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure(() => new AvaloniaApp(() => new TesterWindow()))
- .UsePlatformDetect()
- .LogToTrace();
- }
-}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj b/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj
deleted file mode 100644
index 3a7610643..000000000
--- a/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- net6.0
- net6.0;net472
- Atlassian.Bitbucket.UI
- Atlassian.Bitbucket.UI.Shared
-
-
-
-
-
-
-
-
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
index 1cacd769e..6ebee08b8 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
@@ -4,10 +4,12 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Atlassian.Bitbucket.Cloud;
+using Atlassian.Bitbucket.UI.ViewModels;
+using Atlassian.Bitbucket.UI.Views;
using GitCredentialManager;
using GitCredentialManager.Authentication;
using GitCredentialManager.Authentication.OAuth;
+using GitCredentialManager.UI;
namespace Atlassian.Bitbucket
{
@@ -70,8 +72,6 @@ public async Task GetCredentialsAsync(Uri targetUri, st
{
ThrowIfUserInteractionDisabled();
- string password;
-
// If we don't have a desktop session/GUI then we cannot offer OAuth since the only
// supported grant is authcode (i.e, using a web browser; device code is not supported).
if (!Context.SessionManager.IsDesktopSession)
@@ -92,106 +92,152 @@ public async Task GetCredentialsAsync(Uri targetUri, st
}
// Shell out to the UI helper and show the Bitbucket u/p prompt
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string helperCommand, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- var promptArgs = new StringBuilder(args);
- promptArgs.Append("prompt");
- if (!BitbucketHelper.IsBitbucketOrg(targetUri))
+ if (TryFindHelperCommand(out string helperCommand, out string args))
{
- promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString()));
+ return await GetCredentialsViaHelperAsync(targetUri, userName, modes, helperCommand, args);
}
- if (!string.IsNullOrWhiteSpace(userName))
- {
- promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
- }
+ return await GetCredentialsViaUiAsync(targetUri, userName, modes);
+ }
- if ((modes & AuthenticationModes.Basic) != 0)
- {
- promptArgs.Append(" --show-basic");
- }
+ return GetCredentialsViaTty(targetUri, userName, modes);
+ }
- if ((modes & AuthenticationModes.OAuth) != 0)
- {
- promptArgs.Append(" --show-oauth");
- }
+ private async Task GetCredentialsViaUiAsync(
+ Uri targetUri, string userName, AuthenticationModes modes)
+ {
+ var viewModel = new CredentialsViewModel(Context.Environment)
+ {
+ Url = targetUri,
+ UserName = userName,
+ ShowOAuth = (modes & AuthenticationModes.OAuth) != 0,
+ ShowBasic = (modes & AuthenticationModes.Basic) != 0
+ };
- IDictionary output = await InvokeHelperAsync(helperCommand, promptArgs.ToString());
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
- if (output.TryGetValue("mode", out string mode) &&
- StringComparer.OrdinalIgnoreCase.Equals(mode, "oauth"))
- {
+ ThrowIfWindowCancelled(viewModel);
+
+ switch (viewModel.SelectedMode)
+ {
+ case AuthenticationModes.OAuth:
return new CredentialsPromptResult(AuthenticationModes.OAuth);
- }
- else
- {
- if (!output.TryGetValue("username", out userName))
+
+ case AuthenticationModes.Basic:
+ return new CredentialsPromptResult(
+ AuthenticationModes.Basic,
+ new GitCredential(viewModel.UserName, viewModel.Password)
+ );
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(AuthenticationModes),
+ "Unknown authentication mode", viewModel.SelectedMode.ToString());
+ }
+ }
+
+ private CredentialsPromptResult GetCredentialsViaTty(Uri targetUri, string userName, AuthenticationModes modes)
+ {
+ ThrowIfTerminalPromptsDisabled();
+
+ switch (modes)
+ {
+ case AuthenticationModes.Basic:
+ Context.Terminal.WriteLine("Enter Bitbucket credentials for '{0}'...", targetUri);
+
+ if (!string.IsNullOrWhiteSpace(userName))
{
- throw new Exception("Missing username in response");
+ // Don't need to prompt for the username if it has been specified already
+ Context.Terminal.WriteLine("Username: {0}", userName);
}
-
- if (!output.TryGetValue("password", out password))
+ else
{
- throw new Exception("Missing password in response");
+ // Prompt for username
+ userName = Context.Terminal.Prompt("Username");
}
+ // Prompt for password
+ string password = Context.Terminal.PromptSecret("Password");
+
return new CredentialsPromptResult(
AuthenticationModes.Basic,
new GitCredential(userName, password));
- }
- }
- else
- {
- ThrowIfTerminalPromptsDisabled();
- switch (modes)
- {
- case AuthenticationModes.Basic:
- Context.Terminal.WriteLine("Enter Bitbucket credentials for '{0}'...", targetUri);
+ case AuthenticationModes.OAuth:
+ return new CredentialsPromptResult(AuthenticationModes.OAuth);
- if (!string.IsNullOrWhiteSpace(userName))
- {
- // Don't need to prompt for the username if it has been specified already
- Context.Terminal.WriteLine("Username: {0}", userName);
- }
- else
- {
- // Prompt for username
- userName = Context.Terminal.Prompt("Username");
- }
+ case AuthenticationModes.None:
+ throw new ArgumentOutOfRangeException(nameof(modes),
+ @$"At least one {nameof(AuthenticationModes)} must be supplied");
- // Prompt for password
- password = Context.Terminal.PromptSecret("Password");
+ default:
+ var menuTitle = $"Select an authentication method for '{targetUri}'";
+ var menu = new TerminalMenu(Context.Terminal, menuTitle);
- return new CredentialsPromptResult(
- AuthenticationModes.Basic,
- new GitCredential(userName, password));
+ TerminalMenuItem oauthItem = null;
+ TerminalMenuItem basicItem = null;
- case AuthenticationModes.OAuth:
- return new CredentialsPromptResult(AuthenticationModes.OAuth);
+ if ((modes & AuthenticationModes.OAuth) != 0) oauthItem = menu.Add("OAuth");
+ if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add("Username/password");
- case AuthenticationModes.None:
- throw new ArgumentOutOfRangeException(nameof(modes), @$"At least one {nameof(AuthenticationModes)} must be supplied");
+ // Default to the 'first' choice in the menu
+ TerminalMenuItem choice = menu.Show(0);
- default:
- var menuTitle = $"Select an authentication method for '{targetUri}'";
- var menu = new TerminalMenu(Context.Terminal, menuTitle);
+ if (choice == oauthItem) goto case AuthenticationModes.OAuth;
+ if (choice == basicItem) goto case AuthenticationModes.Basic;
- TerminalMenuItem oauthItem = null;
- TerminalMenuItem basicItem = null;
+ throw new Exception();
+ }
+ }
- if ((modes & AuthenticationModes.OAuth) != 0) oauthItem = menu.Add("OAuth");
- if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add("Username/password");
+ private async Task GetCredentialsViaHelperAsync(
+ Uri targetUri, string userName, AuthenticationModes modes, string helperCommand, string args)
+ {
+ var promptArgs = new StringBuilder(args);
+ promptArgs.Append("prompt");
+ if (!BitbucketHelper.IsBitbucketOrg(targetUri))
+ {
+ promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString()));
+ }
- // Default to the 'first' choice in the menu
- TerminalMenuItem choice = menu.Show(0);
+ if (!string.IsNullOrWhiteSpace(userName))
+ {
+ promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
+ }
- if (choice == oauthItem) goto case AuthenticationModes.OAuth;
- if (choice == basicItem) goto case AuthenticationModes.Basic;
+ if ((modes & AuthenticationModes.Basic) != 0)
+ {
+ promptArgs.Append(" --show-basic");
+ }
- throw new Exception();
+ if ((modes & AuthenticationModes.OAuth) != 0)
+ {
+ promptArgs.Append(" --show-oauth");
+ }
+
+ IDictionary output = await InvokeHelperAsync(helperCommand, promptArgs.ToString());
+
+ if (output.TryGetValue("mode", out string mode) &&
+ StringComparer.OrdinalIgnoreCase.Equals(mode, "oauth"))
+ {
+ return new CredentialsPromptResult(AuthenticationModes.OAuth);
+ }
+ else
+ {
+ if (!output.TryGetValue("username", out userName))
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing username in response");
}
+
+ if (!output.TryGetValue("password", out string password))
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing password in response");
+ }
+
+ return new CredentialsPromptResult(
+ AuthenticationModes.Basic,
+ new GitCredential(userName, password));
}
}
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
index f1950aca1..f3f653f01 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Atlassian.Bitbucket.Cloud;
@@ -79,7 +78,8 @@ public async Task GetCredentialAsync(InputArguments input)
if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")
&& BitbucketHelper.IsBitbucketOrg(input))
{
- throw new Exception("Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS.");
+ throw new Trace2Exception(_context.Trace2,
+ "Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS.");
}
var authModes = await GetSupportedAuthenticationModesAsync(input);
@@ -145,8 +145,9 @@ private async Task GetRefreshedCredentials(InputArguments input, Au
var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, input.UserName, authModes);
if (result is null || result.AuthenticationMode == AuthenticationModes.None)
{
- _context.Trace.WriteLine("User cancelled credential prompt");
- throw new Exception("User cancelled credential prompt.");
+ var message = "User cancelled credential prompt";
+ _context.Trace.WriteLine(message);
+ throw new Trace2Exception(_context.Trace2, message);
}
switch (result.AuthenticationMode)
@@ -176,8 +177,10 @@ private async Task GetRefreshedCredentials(InputArguments input, Au
}
catch (OAuth2Exception ex)
{
- _context.Trace.WriteLine("Failed to refresh existing OAuth credential using refresh token");
+ var message = "Failed to refresh existing OAuth credential using refresh token";
+ _context.Trace.WriteLine(message);
_context.Trace.WriteException(ex);
+ _context.Trace2.WriteError(message);
// We failed to refresh the AT using the RT; log the refresh failure and fall through to restart
// the OAuth authentication flow
@@ -279,7 +282,7 @@ public async Task GetSupportedAuthenticationModesAsync(Inpu
try
{
var authenticationMethods = await _restApiRegistry.Get(input).GetAuthenticationMethodsAsync();
-
+
var modes = AuthenticationModes.None;
if (authenticationMethods.Contains(AuthenticationMethod.BasicAuth))
@@ -298,10 +301,14 @@ public async Task GetSupportedAuthenticationModesAsync(Inpu
}
catch (Exception ex)
{
- _context.Trace.WriteLine($"Failed to query '{input.GetRemoteUri()}' for supported authentication schemes.");
+ var format = "Failed to query '{0}' for supported authentication schemes.";
+ var message = string.Format(format, input.GetRemoteUri());
+
+ _context.Trace.WriteLine(message);
_context.Trace.WriteException(ex);
+ _context.Trace2.WriteError(message, format);
- _context.Terminal.WriteLine($"warning: failed to query '{input.GetRemoteUri()}' for supported authentication schemes.");
+ _context.Terminal.WriteLine($"warning: {message}");
// Fall-back to offering all modes so the user is never blocked from authenticating by at least one mode
return AuthenticationModes.All;
@@ -356,7 +363,8 @@ private async Task ResolveOAuthUserNameAsync(InputArguments input, strin
return result.Response.UserName;
}
- throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}");
+ throw new Trace2Exception(_context.Trace2,
+ $"Failed to resolve username. HTTP: {result.StatusCode}");
}
private async Task ResolveBasicAuthUserNameAsync(InputArguments input, string username, string password)
@@ -367,7 +375,8 @@ private async Task ResolveBasicAuthUserNameAsync(InputArguments input, s
return result.Response.UserName;
}
- throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}");
+ throw new Trace2Exception(_context.Trace2,
+ $"Failed to resolve username. HTTP: {result.StatusCode}");
}
private async Task ValidateCredentialsWork(InputArguments input, ICredential credentials, AuthenticationModes authModes)
@@ -404,8 +413,10 @@ private async Task ValidateCredentialsWork(InputArguments input, ICredenti
}
catch (Exception ex)
{
- _context.Trace.WriteLine($"Failed to validate existing credentials using OAuth");
+ var message = "Failed to validate existing credentials using OAuth";
+ _context.Trace.WriteLine(message);
_context.Trace.WriteException(ex);
+ _context.Trace2.WriteError(message);
}
}
@@ -419,8 +430,10 @@ private async Task ValidateCredentialsWork(InputArguments input, ICredenti
}
catch (Exception ex)
{
- _context.Trace.WriteLine($"Failed to validate existing credentials using Basic Auth");
+ var message = "Failed to validate existing credentials using Basic Auth";
+ _context.Trace.WriteLine(message);
_context.Trace.WriteException(ex);
+ _context.Trace2.WriteError(message);
return false;
}
}
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs
index 9d4f0043e..1ca23d0f5 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs
@@ -10,7 +10,12 @@ namespace Atlassian.Bitbucket
{
public abstract class BitbucketOAuth2Client : OAuth2Client
{
- public BitbucketOAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri, string clientSecret, ITrace trace) : base(httpClient, endpoints, clientId, redirectUri, clientSecret, trace, false)
+ public BitbucketOAuth2Client(HttpClient httpClient,
+ OAuth2ServerEndpoints endpoints,
+ string clientId,
+ Uri redirectUri,
+ string clientSecret,
+ ITrace2 trace2) : base(httpClient, endpoints, clientId, trace2, redirectUri, clientSecret, false)
{
}
@@ -27,9 +32,9 @@ public string GetRefreshTokenServiceName(InputArguments input)
return uri.AbsoluteUri.TrimEnd('/');
}
- public Task GetAuthorizationCodeAsync(IOAuth2WebBrowser browser, CancellationToken ct)
+ public Task GetAuthorizationCodeAsync(IOAuth2WebBrowser browser, CancellationToken ct)
{
- return GetAuthorizationCodeAsync(Scopes, browser, ct);
+ return this.GetAuthorizationCodeAsync(Scopes, browser, ct);
}
protected override bool TryCreateTokenEndpointResult(string json, out OAuth2TokenResult result)
diff --git a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs
index 9e49c10c6..4b5edbbf7 100644
--- a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs
+++ b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs
@@ -10,9 +10,9 @@ namespace Atlassian.Bitbucket.Cloud
{
public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client
{
- public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace trace)
+ public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2)
: base(httpClient, GetEndpoints(),
- GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace)
+ GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2)
{
}
@@ -62,7 +62,7 @@ private static string GetClientSecret(ISettings settings)
return CloudConstants.OAuth2ClientSecret;
}
-
+
private static OAuth2ServerEndpoints GetEndpoints()
{
return new OAuth2ServerEndpoints(
diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs
index 378a30f94..97abd533c 100644
--- a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs
+++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs
@@ -12,9 +12,9 @@ namespace Atlassian.Bitbucket.DataCenter
{
public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client
{
- public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace trace)
+ public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2)
: base(httpClient, GetEndpoints(settings),
- GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace)
+ GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2)
{
}
diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs
index 0688e0323..689eba15f 100644
--- a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs
+++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs
@@ -20,7 +20,7 @@ public BitbucketRestApi(ICommandContext context)
EnsureArgument.NotNull(context, nameof(context));
_context = context;
-
+
}
public async Task> GetUserInformationAsync(string userName, string password, bool isBearerToken)
@@ -35,7 +35,7 @@ public async Task> GetUserInformationAsync(string userN
}
// Bitbucket Server/DC doesn't actually provide a REST API we can use to trade an access_token for the owning username,
- // therefore this is always going to return a placeholder username, however this call does provide a way to validate the
+ // therefore this is always going to return a placeholder username, however this call does provide a way to validate the
// credentials we do have
var requestUri = new Uri(ApiUri, "api/1.0/users");
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri))
@@ -131,9 +131,9 @@ public void Dispose()
private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient();
- private Uri ApiUri
+ private Uri ApiUri
{
- get
+ get
{
var remoteUri = _context.Settings?.RemoteUri;
if (remoteUri == null)
diff --git a/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs b/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs
index 573140eb3..db1a0689c 100644
--- a/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs
+++ b/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs
@@ -8,6 +8,7 @@ public class OAuth2ClientRegistry : IRegistry
private readonly HttpClient http;
private ISettings settings;
private readonly ITrace trace;
+ private readonly ITrace2 trace2;
private Cloud.BitbucketOAuth2Client cloudClient;
private DataCenter.BitbucketOAuth2Client dataCenterClient;
@@ -16,6 +17,7 @@ public OAuth2ClientRegistry(ICommandContext context)
this.http = context.HttpClientFactory.CreateClient();
this.settings = context.Settings;
this.trace = context.Trace;
+ this.trace2 = context.Trace2;
}
public BitbucketOAuth2Client Get(InputArguments input)
@@ -36,7 +38,7 @@ public void Dispose()
dataCenterClient = null;
}
- private Cloud.BitbucketOAuth2Client CloudClient => cloudClient ??= new Cloud.BitbucketOAuth2Client(http, settings, trace);
- private DataCenter.BitbucketOAuth2Client DataCenterClient => dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(http, settings, trace);
+ private Cloud.BitbucketOAuth2Client CloudClient => cloudClient ??= new Cloud.BitbucketOAuth2Client(http, settings, trace2);
+ private DataCenter.BitbucketOAuth2Client DataCenterClient => dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(http, settings, trace2);
}
-}
\ No newline at end of file
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Assets/atlassian-logo.png b/src/shared/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png
similarity index 100%
rename from src/shared/Atlassian.Bitbucket.UI.Avalonia/Assets/atlassian-logo.png
rename to src/shared/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png
diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs b/src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs
similarity index 96%
rename from src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs
rename to src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs
index a17bdf5f3..4ae07c0a5 100644
--- a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs
+++ b/src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs
@@ -48,7 +48,7 @@ private async Task ExecuteAsync(Uri url, string userName, bool showOAuth, b
if (!viewModel.WindowResult || viewModel.SelectedMode == AuthenticationModes.None)
{
- throw new Exception("User cancelled dialog.");
+ throw new Trace2Exception(Context.Trace2, "User cancelled dialog.");
}
switch (viewModel.SelectedMode)
diff --git a/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs
similarity index 100%
rename from src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs
rename to src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml b/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
similarity index 95%
rename from src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
rename to src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
index f42feced3..f74161a5c 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
+++ b/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
@@ -2,8 +2,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:vm="clr-namespace:Atlassian.Bitbucket.UI.ViewModels;assembly=Atlassian.Bitbucket.UI.Shared"
- xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcoreuiavn"
+ xmlns:vm="clr-namespace:Atlassian.Bitbucket.UI.ViewModels;assembly=Atlassian.Bitbucket"
+ xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Atlassian.Bitbucket.UI.Views.CredentialsView">
@@ -22,7 +22,7 @@
-
+
();
var auth = new Mock(MockBehavior.Strict, context);
auth.Setup(x => x.InvokeHelperAsync(
@@ -109,19 +119,29 @@ public async Task BasicAuthentication_GetCredentials_DesktopSession_CallsHelper(
}
[Fact]
- public async Task BasicAuthentication_GetCredentials_DesktopSession_UserName_CallsHelper()
+ public async Task BasicAuthentication_GetCredentials_DesktopSession_UIHelper_UserName_CallsHelper()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
+ const string unixHelperPath = "/usr/local/bin/git-credential-manager-ui";
+ const string windowsHelperPath = @"C:\Program Files\Git Credential Manager\git-credential-manager-ui.exe";
+ string helperPath = PlatformUtils.IsWindows() ? windowsHelperPath : unixHelperPath;
+
var context = new TestCommandContext
{
- SessionManager = {IsDesktopSession = true}
+ SessionManager = { IsDesktopSession = true },
+ Environment =
+ {
+ Variables =
+ {
+ [Constants.EnvironmentVariables.GcmUiHelper] = helperPath
+ }
+ }
};
- context.FileSystem.Files["/usr/local/bin/git-credential-manager-ui"] = new byte[0];
- context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\git-credential-manager-ui.exe"] = new byte[0];
+ context.FileSystem.Files[helperPath] = Array.Empty();
var auth = new Mock(MockBehavior.Strict, context);
auth.Setup(x => x.InvokeHelperAsync(
diff --git a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs
index 8839eda37..9ab5770c8 100644
--- a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs
+++ b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs
@@ -23,7 +23,7 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenA
var msAuth = new MicrosoftAuthentication(context);
- await Assert.ThrowsAsync(
+ await Assert.ThrowsAsync(
() => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName));
}
}
diff --git a/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs b/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs
index bed64f033..3dd87cc93 100644
--- a/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs
+++ b/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -35,13 +36,95 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync()
IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
- OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, CancellationToken.None);
+ OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, null, CancellationToken.None);
Assert.Equal(expectedAuthCode, result.Code);
}
+ [Fact]
+ public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams()
+ {
+ const string expectedAuthCode = "68c39cbd8d";
+
+ var baseUri = new Uri("https://example.com");
+ OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
+
+ var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
+
+ string[] expectedScopes = {"read", "write", "delete"};
+
+ var extraParams = new Dictionary
+ {
+ ["param1"] = "value1",
+ ["param2"] = "value2",
+ ["param3"] = "value3"
+ };
+
+ OAuth2Application app = CreateTestApplication();
+
+ var server = new TestOAuth2Server(endpoints);
+ server.RegisterApplication(app);
+ server.Bind(httpHandler);
+ server.TokenGenerator.AuthCodes.Add(expectedAuthCode);
+
+ server.AuthorizationEndpointInvoked += (_, request) =>
+ {
+ IDictionary actualParams = request.RequestUri.GetQueryParameters();
+ foreach (var expected in extraParams)
+ {
+ Assert.True(actualParams.TryGetValue(expected.Key, out string actualValue));
+ Assert.Equal(expected.Value, actualValue);
+ }
+ };
+
+ IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);
+
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
+
+ OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None);
+
+ Assert.Equal(expectedAuthCode, result.Code);
+ }
+
+ [Fact]
+ public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams_OverrideStandardArgs_ThrowsException()
+ {
+ const string expectedAuthCode = "68c39cbd8d";
+
+ var baseUri = new Uri("https://example.com");
+ OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
+
+ var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
+
+ string[] expectedScopes = {"read", "write", "delete"};
+
+ var extraParams = new Dictionary
+ {
+ ["param1"] = "value1",
+ [OAuth2Constants.ClientIdParameter] = "value2",
+ ["param3"] = "value3"
+ };
+
+ OAuth2Application app = CreateTestApplication();
+
+ var server = new TestOAuth2Server(endpoints);
+ server.RegisterApplication(app);
+ server.Bind(httpHandler);
+ server.TokenGenerator.AuthCodes.Add(expectedAuthCode);
+
+ IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);
+
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
+
+ await Assert.ThrowsAsync(() =>
+ client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None));
+ }
+
[Fact]
public async Task OAuth2Client_GetDeviceCodeAsync()
{
@@ -63,7 +146,8 @@ public async Task OAuth2Client_GetDeviceCodeAsync()
server.TokenGenerator.UserCodes.Add(expectedUserCode);
server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
OAuth2DeviceCodeResult result = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None);
@@ -94,7 +178,8 @@ public async Task OAuth2Client_GetTokenByAuthorizationCodeAsync()
server.TokenGenerator.AccessTokens.Add(expectedAccessToken);
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
var authCodeResult = new OAuth2AuthorizationCodeResult(authCode, TestRedirectUri);
OAuth2TokenResult result = await client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);
@@ -131,7 +216,8 @@ public async Task OAuth2Client_GetTokenByRefreshTokenAsync()
server.TokenGenerator.AccessTokens.Add(expectedAccessToken);
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
OAuth2TokenResult result = await client.GetTokenByRefreshTokenAsync(oldRefreshToken, CancellationToken.None);
@@ -169,7 +255,8 @@ public async Task OAuth2Client_GetTokenByDeviceCodeAsync()
server.TokenGenerator.AccessTokens.Add(expectedAccessToken);
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
var deviceCodeResult = new OAuth2DeviceCodeResult(expectedDeviceCode, expectedUserCode, null, null);
@@ -214,10 +301,11 @@ public async Task OAuth2Client_E2E_InteractiveWebFlowAndRefresh()
IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
OAuth2AuthorizationCodeResult authCodeResult = await client.GetAuthorizationCodeAsync(
- expectedScopes, browser, CancellationToken.None);
+ expectedScopes, browser, null, CancellationToken.None);
OAuth2TokenResult result1 = await client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);
@@ -263,7 +351,8 @@ public async Task OAuth2Client_E2E_DeviceFlowAndRefresh()
server.TokenGenerator.AccessTokens.Add(expectedAccessToken1);
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1);
- OAuth2Client client = CreateClient(httpHandler, endpoints);
+ var trace2 = new NullTrace2();
+ OAuth2Client client = CreateClient(httpHandler, endpoints, trace2);
OAuth2DeviceCodeResult deviceResult = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None);
@@ -296,9 +385,9 @@ public async Task OAuth2Client_E2E_DeviceFlowAndRefresh()
RedirectUris = new[] {TestRedirectUri}
};
- private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, IOAuth2CodeGenerator generator = null)
+ private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, ITrace2 trace2, IOAuth2CodeGenerator generator = null)
{
- return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, TestRedirectUri, TestClientSecret)
+ return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, trace2, TestRedirectUri, TestClientSecret)
{
CodeGenerator = generator
};
diff --git a/src/shared/Core.Tests/GitConfigurationTests.cs b/src/shared/Core.Tests/GitConfigurationTests.cs
index a131bcbcf..498651f73 100644
--- a/src/shared/Core.Tests/GitConfigurationTests.cs
+++ b/src/shared/Core.Tests/GitConfigurationTests.cs
@@ -47,8 +47,9 @@ public void GitProcess_GetConfiguration_ReturnsConfiguration()
{
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath);
var config = git.GetConfiguration();
Assert.NotNull(config);
}
@@ -70,8 +71,9 @@ public void GitConfiguration_Enumerate_CallbackReturnsTrue_InvokesCallbackForEac
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
var actualVisitedEntries = new List<(string name, string value)>();
@@ -108,8 +110,9 @@ public void GitConfiguration_Enumerate_CallbackReturnsFalse_InvokesCallbackForEa
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
var actualVisitedEntries = new List<(string name, string value)>();
@@ -135,11 +138,12 @@ public void GitConfiguration_TryGet_Name_Exists_ReturnsTrueOutString()
{
string repoPath = CreateRepository(out string workDirPath);
ExecGit(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess();
-
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
bool result = config.TryGet("user.name", false, out string value);
@@ -155,8 +159,10 @@ public void GitConfiguration_TryGet_Name_DoesNotExists_ReturnsFalse()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}";
@@ -174,8 +180,9 @@ public void GitConfiguration_TryGet_IsPath_True_ReturnsCanonicalPath()
string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
bool result = config.TryGet("example.path", true, out string value);
@@ -192,8 +199,9 @@ public void GitConfiguration_TryGet_IsPath_False_ReturnsRawConfig()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
bool result = config.TryGet("example.path", false, out string value);
@@ -210,8 +218,9 @@ public void GitConfiguration_TryGet_BoolType_ReturnsCanonicalBool()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Bool,
@@ -229,8 +238,9 @@ public void GitConfiguration_TryGet_BoolWithoutType_ReturnsRawConfig()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Raw,
@@ -248,8 +258,10 @@ public void GitConfiguration_Get_Name_Exists_ReturnsString()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
string value = config.Get("user.name");
@@ -264,8 +276,9 @@ public void GitConfiguration_Get_Name_DoesNotExists_ThrowsException()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}";
@@ -279,15 +292,16 @@ public void GitConfiguration_Set_Local_SetsLocalConfig()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);;
IGitConfiguration config = git.GetConfiguration();
config.Set(GitConfigurationLevel.Local, "core.foobar", "foo123");
GitResult localResult = ExecGit(repoPath, workDirPath, "config --local core.foobar");
- Assert.Equal("foo123", localResult.StandardOutput.Trim());
+ Assert.Equal("foo123", localResult.StandardOutput.Trim());
}
[Fact]
@@ -297,11 +311,13 @@ public void GitConfiguration_Set_All_ThrowsException()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
- Assert.Throws(() => config.Set(GitConfigurationLevel.All, "core.foobar", "test123"));
+ Assert.Throws(() =>
+ config.Set(GitConfigurationLevel.All, "core.foobar", "test123"));
}
[Fact]
@@ -310,14 +326,14 @@ public void GitConfiguration_Unset_Global_UnsetsGlobalConfig()
string repoPath = CreateRepository(out string workDirPath);
try
{
-
ExecGit(repoPath, workDirPath, "config --global core.foobar alice").AssertSuccess();
ExecGit(repoPath, workDirPath, "config --local core.foobar bob").AssertSuccess();
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
config.Unset(GitConfigurationLevel.Global, "core.foobar");
@@ -347,8 +363,9 @@ public void GitConfiguration_Unset_Local_UnsetsLocalConfig()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
config.Unset(GitConfigurationLevel.Local, "core.foobar");
@@ -373,8 +390,9 @@ public void GitConfiguration_Unset_All_ThrowsException()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
Assert.Throws(() => config.Unset(GitConfigurationLevel.All, "core.foobar"));
@@ -390,8 +408,9 @@ public void GitConfiguration_UnsetAll_UnsetsAllConfig()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
config.UnsetAll(GitConfigurationLevel.Local, "core.foobar", "foo*");
@@ -408,11 +427,14 @@ public void GitConfiguration_UnsetAll_All_ThrowsException()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, repoPath);
+
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);
IGitConfiguration config = git.GetConfiguration();
- Assert.Throws(() => config.UnsetAll(GitConfigurationLevel.All, "core.foobar", Constants.RegexPatterns.Any));
+ Assert.Throws(() =>
+ config.UnsetAll(GitConfigurationLevel.All, "core.foobar", Constants.RegexPatterns.Any));
}
}
}
diff --git a/src/shared/Core.Tests/GitTests.cs b/src/shared/Core.Tests/GitTests.cs
index 63224cde9..a6905bb8f 100644
--- a/src/shared/Core.Tests/GitTests.cs
+++ b/src/shared/Core.Tests/GitTests.cs
@@ -13,8 +13,9 @@ public void Git_GetCurrentRepository_NoLocalRepo_ReturnsNull()
{
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, Path.GetTempPath());
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath());
string actual = git.GetCurrentRepository();
@@ -28,8 +29,9 @@ public void Git_GetCurrentRepository_LocalRepo_ReturnsNotNull()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
string actual = git.GetCurrentRepository();
@@ -41,8 +43,9 @@ public void Git_GetRemotes_NoLocalRepo_ReturnsEmpty()
{
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, Path.GetTempPath());
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath());
GitRemote[] remotes = git.GetRemotes().ToArray();
@@ -56,8 +59,9 @@ public void Git_GetRemotes_NoRemotes_ReturnsEmpty()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
GitRemote[] remotes = git.GetRemotes().ToArray();
@@ -74,9 +78,10 @@ public void Git_GetRemotes_OneRemote_ReturnsRemote()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
GitRemote[] remotes = git.GetRemotes().ToArray();
Assert.Single(remotes);
@@ -95,9 +100,10 @@ public void Git_GetRemotes_OneRemoteFetchAndPull_ReturnsRemote()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
GitRemote[] remotes = git.GetRemotes().ToArray();
Assert.Single(remotes);
@@ -118,9 +124,10 @@ public void Git_GetRemotes_NonHttpRemote_ReturnsRemote(string url)
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
GitRemote[] remotes = git.GetRemotes().ToArray();
Assert.Single(remotes);
@@ -143,9 +150,10 @@ public void Git_GetRemotes_MultipleRemotes_ReturnsAllRemotes()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
GitRemote[] remotes = git.GetRemotes().ToArray();
Assert.Equal(3, remotes.Length);
@@ -167,9 +175,10 @@ public void Git_GetRemotes_RemoteNoFetchOnlyPull_ReturnsRemote()
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
- var git = new GitProcess(trace, env, gitPath, workDirPath);
+ var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath);
GitRemote[] remotes = git.GetRemotes().ToArray();
Assert.Single(remotes);
@@ -182,12 +191,14 @@ public void Git_Version_ReturnsVersion()
{
string gitPath = GetGitPath();
var trace = new NullTrace();
- var env = new TestEnvironment();
+ var trace2 = new NullTrace2();
+ var processManager = new TestProcessManager();
- var git = new GitProcess(trace, env, gitPath, Path.GetTempPath());
+ var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath());
GitVersion version = git.Version;
Assert.NotEqual(new GitVersion(), version);
+
}
#region Test Helpers
diff --git a/src/shared/Core.Tests/HttpClientFactoryTests.cs b/src/shared/Core.Tests/HttpClientFactoryTests.cs
index 1b1f4e91e..23a12aeb2 100644
--- a/src/shared/Core.Tests/HttpClientFactoryTests.cs
+++ b/src/shared/Core.Tests/HttpClientFactoryTests.cs
@@ -13,19 +13,19 @@ public class HttpClientFactoryTests
[Fact]
public void HttpClientFactory_GetClient_SetsDefaultHeaders()
{
- var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), new TestStandardStreams());
+ var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), new TestStandardStreams());
HttpClient client = factory.CreateClient();
Assert.NotNull(client);
- Assert.Equal(Constants.GetHttpUserAgent(), client.DefaultRequestHeaders.UserAgent.ToString());
+ Assert.Equal(Constants.GetHttpUserAgent(Mock.Of()), client.DefaultRequestHeaders.UserAgent.ToString());
Assert.True(client.DefaultRequestHeaders.CacheControl.NoCache);
}
[Fact]
public void HttpClientFactory_GetClient_MultipleCalls_ReturnsNewInstance()
{
- var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), new TestStandardStreams());
+ var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), new TestStandardStreams());
HttpClient client1 = factory.CreateClient();
HttpClient client2 = factory.CreateClient();
@@ -45,7 +45,7 @@ public void HttpClientFactory_TryCreateProxy_NoProxy_ReturnsFalseOutNull()
RemoteUri = repoRemoteUri,
RepositoryPath = repoPath
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -69,7 +69,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyNoCredentials_ReturnsTrueOutPr
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -102,7 +102,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithBypass_ReturnsTrueOutProxy
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -138,7 +138,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithWildcardBypass_ReturnsFals
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -167,7 +167,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithCredentials_ReturnsTrueOut
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -203,7 +203,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithNonEmptyUserAndEmptyPass_R
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -239,7 +239,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithEmptyUserAndNonEmptyPass_R
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -274,7 +274,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyEmptyUserAndEmptyPass_ReturnsT
RepositoryPath = repoPath,
ProxyConfiguration = proxyConfig
};
- var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, Mock.Of());
+ var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, Mock.Of());
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
@@ -304,7 +304,7 @@ public void HttpClientFactory_GetClient_ChecksCertBundleOnlyIfEnabled(string cus
UseCustomCertificateBundleWithSchannel = useCustomCertBundleWithSchannel
};
- var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of(), settings, new TestStandardStreams());
+ var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of(), Mock.Of(), settings, new TestStandardStreams());
HttpClient client = factory.CreateClient();
diff --git a/src/shared/Core.Tests/IniFileTests.cs b/src/shared/Core.Tests/IniFileTests.cs
new file mode 100644
index 000000000..a661547d4
--- /dev/null
+++ b/src/shared/Core.Tests/IniFileTests.cs
@@ -0,0 +1,135 @@
+using System.Collections.Generic;
+using System.Text;
+using GitCredentialManager.Tests.Objects;
+using Xunit;
+
+namespace GitCredentialManager.Tests
+{
+ public class IniFileTests
+ {
+ [Fact]
+ public void IniSectionName_Equality()
+ {
+ var a1 = new IniSectionName("foo");
+ var b1 = new IniSectionName("foo");
+ Assert.Equal(a1,b1);
+ Assert.Equal(a1.GetHashCode(),b1.GetHashCode());
+
+ var a2 = new IniSectionName("foo");
+ var b2 = new IniSectionName("FOO");
+ Assert.Equal(a2,b2);
+ Assert.Equal(a2.GetHashCode(),b2.GetHashCode());
+
+ var a3 = new IniSectionName("foo", "bar");
+ var b3 = new IniSectionName("foo", "BAR");
+ Assert.NotEqual(a3,b3);
+ Assert.NotEqual(a3.GetHashCode(),b3.GetHashCode());
+
+ var a4 = new IniSectionName("foo", "bar");
+ var b4 = new IniSectionName("FOO", "bar");
+ Assert.Equal(a4,b4);
+ Assert.Equal(a4.GetHashCode(),b4.GetHashCode());
+ }
+
+ [Fact]
+ public void IniSerializer_Deserialize()
+ {
+ const string path = "/tmp/test.ini";
+ string iniText = @"
+[one]
+ foo = 123
+ [two]
+ foo = abc
+# comment
+[two ""subsection name""] # comment [section]
+ foo = this is different # comment prop = val
+
+#[notasection]
+
+ [
+[bad #section]
+recovery tests]
+[]
+ ]
+
+ [three]
+ bar = a
+ bar = b
+ # comment
+ bar = c
+ empty =
+[TWO]
+ foo = hello
+ widget = ""Hello, World!""
+[four]
+[five]
+ prop1 = ""this hash # is inside quotes""
+ prop2 = ""this hash # is inside quotes"" # this line has two hashes
+ prop3 = "" this dquoted string has three spaces around ""
+ #prop4 = this property has been commented-out
+";
+
+ var fs = new TestFileSystem
+ {
+ Files = { [path] = Encoding.UTF8.GetBytes(iniText) }
+ };
+
+ IniFile ini = IniSerializer.Deserialize(fs, path);
+
+ Assert.Equal(6, ini.Sections.Count);
+
+ AssertSection(ini, "one", out IniSection one);
+ Assert.Equal(1, one.Properties.Count);
+ AssertProperty(one, "foo", "123");
+
+ AssertSection(ini, "two", out IniSection twoA);
+ Assert.Equal(3, twoA.Properties.Count);
+ AssertProperty(twoA, "foo", "hello");
+ AssertProperty(twoA, "widget", "Hello, World!");
+
+ AssertSection(ini, "two", "subsection name", out IniSection twoB);
+ Assert.Equal(1, twoB.Properties.Count);
+ AssertProperty(twoB, "foo", "this is different");
+
+ AssertSection(ini, "three", out IniSection three);
+ Assert.Equal(4, three.Properties.Count);
+ AssertMultiProperty(three, "bar", "a", "b", "c");
+ AssertProperty(three, "empty", "");
+
+ AssertSection(ini, "four", out IniSection four);
+ Assert.Equal(0, four.Properties.Count);
+
+ AssertSection(ini, "five", out IniSection five);
+ Assert.Equal(3, five.Properties.Count);
+ AssertProperty(five, "prop1", "this hash # is inside quotes");
+ AssertProperty(five, "prop2", "this hash # is inside quotes");
+ AssertProperty(five, "prop3", " this dquoted string has three spaces around ");
+ }
+
+ private static void AssertSection(IniFile file, string name, out IniSection section)
+ {
+ Assert.True(file.TryGetSection(name, out section));
+ Assert.Equal(name, section.Name.Name);
+ Assert.Null(section.Name.SubName);
+ }
+
+ private static void AssertSection(IniFile file, string name, string subName, out IniSection section)
+ {
+ Assert.True(file.TryGetSection(name, subName, out section));
+ Assert.Equal(name, section.Name.Name);
+ Assert.Equal(subName, section.Name.SubName);
+ }
+
+ private static void AssertProperty(IniSection section, string name, string value)
+ {
+ Assert.True(section.TryGetProperty(name, out var actualValue));
+ Assert.Equal(value, actualValue);
+ }
+
+ private static void AssertMultiProperty(IniSection section, string name, params string[] values)
+ {
+ Assert.True(section.TryGetMultiProperty(name, out IEnumerable actualValues));
+ Assert.Equal(values, actualValues);
+ }
+ }
+}
diff --git a/src/shared/Core.Tests/InputArgumentsTests.cs b/src/shared/Core.Tests/InputArgumentsTests.cs
index 00270abe4..37fe4c5f3 100644
--- a/src/shared/Core.Tests/InputArgumentsTests.cs
+++ b/src/shared/Core.Tests/InputArgumentsTests.cs
@@ -220,6 +220,29 @@ public void InputArguments_GetRemoteUri_AuthorityPathUserInfo_ReturnsUriWithAuth
Assert.Equal(expectedUri, actualUri);
}
+ [Theory]
+ [InlineData("foo?query=true")]
+ [InlineData("foo#fragment")]
+ [InlineData("foo?query=true#fragment")]
+ public void InputArguments_GetRemoteUri_PathQueryFragment_ReturnsCorrectUri(string path)
+ {
+ var expectedUri = new Uri($"https://example.com/{path}");
+
+ var dict = new Dictionary
+ {
+ ["protocol"] = "https",
+ ["host"] = "example.com",
+ ["path"] = path
+ };
+
+ var inputArgs = new InputArguments(dict);
+
+ Uri actualUri = inputArgs.GetRemoteUri();
+
+ Assert.NotNull(actualUri);
+ Assert.Equal(expectedUri, actualUri);
+ }
+
[Fact]
public void InputArguments_GetRemoteUri_IncludeUser_AuthorityPathUserInfo_ReturnsUriWithAll()
{
diff --git a/src/shared/Core.Tests/ProcessManagerTests.cs b/src/shared/Core.Tests/ProcessManagerTests.cs
new file mode 100644
index 000000000..8e96c41f0
--- /dev/null
+++ b/src/shared/Core.Tests/ProcessManagerTests.cs
@@ -0,0 +1,33 @@
+using GitCredentialManager;
+using Xunit;
+
+namespace Core.Tests;
+
+public class ProcessManagerTests
+{
+ [Theory]
+ [InlineData("", 0)]
+ [InlineData("foo", 0)]
+ [InlineData("foo/bar", 1)]
+ [InlineData("foo/bar/baz", 2)]
+ public void CreateSid_Envar_Returns_Expected_Sid(string input, int expected)
+ {
+ ProcessManager.Sid = input;
+ var actual = ProcessManager.GetProcessDepth();
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("", 0)]
+ [InlineData("foo", 0)]
+ [InlineData("foo/bar", 1)]
+ [InlineData("foo/bar/baz", 2)]
+ public void TryGetProcessDepth_Returns_Expected_Depth(string input, int expected)
+ {
+ ProcessManager.Sid = input;
+ var actual = ProcessManager.GetProcessDepth();
+
+ Assert.Equal(expected, actual);
+ }
+}
\ No newline at end of file
diff --git a/src/shared/Core.Tests/SettingsTests.cs b/src/shared/Core.Tests/SettingsTests.cs
index c93685e75..24a7438b7 100644
--- a/src/shared/Core.Tests/SettingsTests.cs
+++ b/src/shared/Core.Tests/SettingsTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Net;
using GitCredentialManager.Tests.Objects;
diff --git a/src/shared/Core.Tests/StringExtensionsTests.cs b/src/shared/Core.Tests/StringExtensionsTests.cs
index 24fb99b21..b17bc1272 100644
--- a/src/shared/Core.Tests/StringExtensionsTests.cs
+++ b/src/shared/Core.Tests/StringExtensionsTests.cs
@@ -293,5 +293,20 @@ public void StringExtensions_TrimMiddle_String_ComparisonType(string input, stri
string actual = StringExtensions.TrimMiddle(input, trim, comparisonType);
Assert.Equal(expected, actual);
}
+
+ [Theory]
+ [InlineData("FooBar", "foo_bar")]
+ [InlineData("fooBar", "foo_bar")]
+ [InlineData("FBBaz", "fb_baz")]
+ [InlineData("Foo", "foo")]
+ [InlineData("Fo", "fo")]
+ [InlineData("fO", "f_o")]
+ [InlineData("OO", "oo")]
+ [InlineData("F", "f")]
+ [InlineData("", "")]
+ public void StringExtensions_ToSnakeCase_Converts_Correctly(string input, string expected)
+ {
+ Assert.Equal(expected, input.ToSnakeCase());
+ }
}
}
diff --git a/src/shared/Core.Tests/TestProcessManager.cs b/src/shared/Core.Tests/TestProcessManager.cs
new file mode 100644
index 000000000..df54b1bb4
--- /dev/null
+++ b/src/shared/Core.Tests/TestProcessManager.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using GitCredentialManager.Tests.Objects;
+using Moq;
+
+namespace GitCredentialManager.Tests;
+
+public class TestProcessManager : IProcessManager
+{
+ public ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
+ {
+ var psi = new ProcessStartInfo(path, args)
+ {
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true, // Ok to redirect stderr for testing
+ UseShellExecute = useShellExecute,
+ WorkingDirectory = workingDirectory ?? string.Empty
+ };
+
+ return CreateProcess(psi);
+ }
+
+ public ChildProcess CreateProcess(ProcessStartInfo psi)
+ {
+ return new ChildProcess(new NullTrace2(), psi);
+ }
+}
diff --git a/src/shared/Core.Tests/Trace2MessageTests.cs b/src/shared/Core.Tests/Trace2MessageTests.cs
new file mode 100644
index 000000000..fd46e430a
--- /dev/null
+++ b/src/shared/Core.Tests/Trace2MessageTests.cs
@@ -0,0 +1,38 @@
+using GitCredentialManager;
+using Xunit;
+
+namespace Core.Tests;
+
+public class Trace2MessageTests
+{
+ [Theory]
+ [InlineData(0.013772, " 0.013772 ")]
+ [InlineData(26.316083, " 26.316083 ")]
+ [InlineData(100.316083, "100.316083 ")]
+ [InlineData(1000.316083, "1000.316083")]
+ public void BuildTimeSpan_Match_Returns_Expected_String(double input, string expected)
+ {
+ var actual = Trace2Message.BuildTimeSpan(input);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void BuildRepoSpan_Match_Returns_Expected_String()
+ {
+ var input = 1;
+ var expected = " r1 ";
+ var actual = Trace2Message.BuildRepoSpan(input);
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("foo", " foo ")]
+ [InlineData("foobar", " foobar ")]
+ [InlineData("foo_bar_baz", " foo_bar_baz ")]
+ [InlineData("foobarbazfoo", " foobarbazfo ")]
+ public void BuildCategorySpan_Match_Returns_Expected_String(string input, string expected)
+ {
+ var actual = Trace2Message.BuildCategorySpan(input);
+ Assert.Equal(expected, actual);
+ }
+}
diff --git a/src/shared/Core.Tests/Trace2Tests.cs b/src/shared/Core.Tests/Trace2Tests.cs
index da3d6d95a..60d89ac2f 100644
--- a/src/shared/Core.Tests/Trace2Tests.cs
+++ b/src/shared/Core.Tests/Trace2Tests.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Text.RegularExpressions;
-using GitCredentialManager.Tests.Objects;
using Xunit;
namespace GitCredentialManager.Tests;
@@ -11,13 +8,9 @@ public class Trace2Tests
[InlineData("af_unix:foo", "foo")]
[InlineData("af_unix:stream:foo-bar", "foo-bar")]
[InlineData("af_unix:dgram:foo-bar-baz", "foo-bar-baz")]
- public void TryParseEventTarget_Posix_Returns_Expected_Value(string input, string expected)
+ public void TryGetPipeName_Posix_Returns_Expected_Value(string input, string expected)
{
- var environment = new TestEnvironment();
- var settings = new TestSettings();
-
- var trace2 = new Trace2(environment, settings.GetTrace2Settings(), new []{""}, DateTimeOffset.UtcNow);
- var isSuccessful = trace2.TryGetPipeName(input, out var actual);
+ var isSuccessful = Trace2.TryGetPipeName(input, out var actual);
Assert.True(isSuccessful);
Assert.Matches(actual, expected);
@@ -27,32 +20,11 @@ public void TryParseEventTarget_Posix_Returns_Expected_Value(string input, strin
[InlineData("\\\\.\\pipe\\git-foo", "git-foo")]
[InlineData("\\\\.\\pipe\\git-foo-bar", "git-foo-bar")]
[InlineData("\\\\.\\pipe\\foo\\git-bar", "git-bar")]
- public void TryParseEventTarget_Windows_Returns_Expected_Value(string input, string expected)
+ public void TryGetPipeName_Windows_Returns_Expected_Value(string input, string expected)
{
- var environment = new TestEnvironment();
- var settings = new TestSettings();
-
- var trace2 = new Trace2(environment, settings.GetTrace2Settings(), new []{""}, DateTimeOffset.UtcNow);
- var isSuccessful = trace2.TryGetPipeName(input, out var actual);
+ var isSuccessful = Trace2.TryGetPipeName(input, out var actual);
Assert.True(isSuccessful);
Assert.Matches(actual, expected);
}
-
- [Theory]
- [InlineData("20190408T191610.507018Z-H9b68c35f-P000059a8")]
- [InlineData("")]
- public void SetSid_Envar_Returns_Expected_Value(string parentSid)
- {
- Regex rx = new Regex(@$"{parentSid}\/[\d\w-]*");
-
- var environment = new TestEnvironment();
- environment.Variables.Add("GIT_TRACE2_PARENT_SID", parentSid);
-
- var settings = new TestSettings();
- var trace2 = new Trace2(environment, settings.GetTrace2Settings(), new []{""}, DateTimeOffset.UtcNow);
- var sid = trace2.SetSid();
-
- Assert.Matches(rx, sid);
- }
}
diff --git a/src/shared/Core.Tests/UriExtensionsTests.cs b/src/shared/Core.Tests/UriExtensionsTests.cs
index 2adb955b3..c1511216d 100644
--- a/src/shared/Core.Tests/UriExtensionsTests.cs
+++ b/src/shared/Core.Tests/UriExtensionsTests.cs
@@ -30,12 +30,11 @@ public void UriExtensions_GetQueryParameters()
[InlineData("http://hostname", "http://hostname")]
[InlineData("http://example.com",
"http://example.com")]
+ [InlineData("http://hostname:7990", "http://hostname:7990")]
[InlineData("http://foo.example.com",
"http://foo.example.com", "http://example.com")]
[InlineData("http://example.com/foo",
"http://example.com/foo", "http://example.com")]
- [InlineData("http://example.com/foo/",
- "http://example.com/foo", "http://example.com")]
[InlineData("http://example.com/foo?query=true#fragment",
"http://example.com/foo", "http://example.com")]
[InlineData("http://buzz.foo.example.com/bar/baz",
diff --git a/src/shared/Core.Tests/WslUtilsTests.cs b/src/shared/Core.Tests/WslUtilsTests.cs
index ca6ad603d..722a8654d 100644
--- a/src/shared/Core.Tests/WslUtilsTests.cs
+++ b/src/shared/Core.Tests/WslUtilsTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using Moq;
using Xunit;
namespace GitCredentialManager.Tests
@@ -100,14 +101,14 @@ public void WslUtils_CreateWslProcess()
string expectedFileName = WslUtils.GetWslPath();
string expectedArgs = $"--distribution {distribution} --exec {command}";
- Process process = WslUtils.CreateWslProcess(distribution, command);
+ ChildProcess process = WslUtils.CreateWslProcess(distribution, command, Mock.Of());
Assert.NotNull(process);
Assert.Equal(expectedArgs, process.StartInfo.Arguments);
Assert.Equal(expectedFileName, process.StartInfo.FileName);
Assert.True(process.StartInfo.RedirectStandardInput);
Assert.True(process.StartInfo.RedirectStandardOutput);
- Assert.True(process.StartInfo.RedirectStandardError);
+ Assert.False(process.StartInfo.RedirectStandardError);
Assert.False(process.StartInfo.UseShellExecute);
}
@@ -121,14 +122,14 @@ public void WslUtils_CreateWslProcess_WorkingDirectory()
string expectedFileName = WslUtils.GetWslPath();
string expectedArgs = $"--distribution {distribution} --exec {command}";
- Process process = WslUtils.CreateWslProcess(distribution, command, expectedWorkingDirectory);
+ ChildProcess process = WslUtils.CreateWslProcess(distribution, command, Mock.Of(), expectedWorkingDirectory);
Assert.NotNull(process);
Assert.Equal(expectedArgs, process.StartInfo.Arguments);
Assert.Equal(expectedFileName, process.StartInfo.FileName);
Assert.True(process.StartInfo.RedirectStandardInput);
Assert.True(process.StartInfo.RedirectStandardOutput);
- Assert.True(process.StartInfo.RedirectStandardError);
+ Assert.False(process.StartInfo.RedirectStandardError);
Assert.False(process.StartInfo.UseShellExecute);
Assert.Equal(expectedWorkingDirectory, process.StartInfo.WorkingDirectory);
}
diff --git a/src/shared/Core.UI.Avalonia/Assets/DarkBase.axaml b/src/shared/Core.UI.Avalonia/Assets/DarkBase.axaml
deleted file mode 100644
index 13365a69c..000000000
--- a/src/shared/Core.UI.Avalonia/Assets/DarkBase.axaml
+++ /dev/null
@@ -1,5 +0,0 @@
-
- #22272D
-
-
diff --git a/src/shared/Core.UI.Avalonia/Assets/Images.axaml b/src/shared/Core.UI.Avalonia/Assets/Images.axaml
deleted file mode 100644
index 624266403..000000000
--- a/src/shared/Core.UI.Avalonia/Assets/Images.axaml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/shared/Core.UI.Avalonia/Assets/LightBase.axaml b/src/shared/Core.UI.Avalonia/Assets/LightBase.axaml
deleted file mode 100644
index 646fd826f..000000000
--- a/src/shared/Core.UI.Avalonia/Assets/LightBase.axaml
+++ /dev/null
@@ -1,5 +0,0 @@
-
- #FBFBFB
-
-
diff --git a/src/shared/Core.UI.Avalonia/Converters/BoolConvertersEx.cs b/src/shared/Core.UI.Avalonia/Converters/BoolConvertersEx.cs
deleted file mode 100644
index d130473bc..000000000
--- a/src/shared/Core.UI.Avalonia/Converters/BoolConvertersEx.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Linq;
-using Avalonia.Data.Converters;
-
-namespace GitCredentialManager.UI.Converters
-{
- public static class BoolConvertersEx
- {
- public static readonly IMultiValueConverter Or =
- new FuncMultiValueConverter(x => x.Aggregate(false, (a, b) => a || b));
-
- public static readonly IMultiValueConverter And =
- new FuncMultiValueConverter(x => x.Aggregate(true, (a, b) => a && b));
- }
-}
diff --git a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj b/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj
deleted file mode 100644
index 961f67649..000000000
--- a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- net6.0
- gcmcoreuiavn
- GitCredentialManager.UI
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AvaloniaApp.axaml
- Code
-
-
-
-
diff --git a/src/shared/Core.UI/Core.UI.csproj b/src/shared/Core.UI/Core.UI.csproj
deleted file mode 100644
index fefd97dac..000000000
--- a/src/shared/Core.UI/Core.UI.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net6.0
- net6.0;net472
- gcmcoreui
- GitCredentialManager.UI
-
-
-
-
-
-
-
diff --git a/src/shared/Core/Application.cs b/src/shared/Core/Application.cs
index 931f75f1e..6a6776196 100644
--- a/src/shared/Core/Application.cs
+++ b/src/shared/Core/Application.cs
@@ -91,7 +91,7 @@ protected override async Task RunInternalAsync(string[] args)
}
// Trace the current version, OS, runtime, and program arguments
- PlatformInformation info = PlatformUtils.GetPlatformInformation();
+ PlatformInformation info = PlatformUtils.GetPlatformInformation(Context.Trace2);
Context.Trace.WriteLine($"Version: {Constants.GcmVersion}");
Context.Trace.WriteLine($"Runtime: {info.ClrVersion}");
Context.Trace.WriteLine($"Platform: {info.OperatingSystemType} ({info.CpuArchitecture})");
diff --git a/src/shared/Core/ApplicationBase.cs b/src/shared/Core/ApplicationBase.cs
index f5e2e25db..42f1390e9 100644
--- a/src/shared/Core/ApplicationBase.cs
+++ b/src/shared/Core/ApplicationBase.cs
@@ -1,9 +1,6 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
-using System.Reflection;
-using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -77,9 +74,6 @@ public Task RunAsync(string[] args)
Context.Trace.WriteLine("Tracing of secrets is enabled. Trace output may contain sensitive information.");
}
- // Enable TRACE2 tracing
- Context.Trace2.Start(Context.Streams.Error, Context.FileSystem, Context.ApplicationPath);
-
return RunInternalAsync(args);
}
diff --git a/src/shared/Core/Authentication/AuthenticationBase.cs b/src/shared/Core/Authentication/AuthenticationBase.cs
index 65d38e002..594f2be33 100644
--- a/src/shared/Core/Authentication/AuthenticationBase.cs
+++ b/src/shared/Core/Authentication/AuthenticationBase.cs
@@ -5,6 +5,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using GitCredentialManager.UI.ViewModels;
namespace GitCredentialManager.Authentication
{
@@ -43,10 +44,13 @@ protected internal virtual async Task> InvokeHelperA
// authentication helper's messages.
Context.Trace.Flush();
- var process = Process.Start(procStartInfo);
+ var process = ChildProcess.Start(Context.Trace2, procStartInfo, Trace2ProcessClass.UIHelper);
if (process is null)
{
- throw new Exception($"Failed to start helper process: {path} {args}");
+ var format = "Failed to start helper process: {0} {1}";
+ var message = string.Format(format, path, args);
+
+ throw new Trace2Exception(Context.Trace2, message, format);
}
// Kill the process upon a cancellation request
@@ -69,7 +73,7 @@ protected internal virtual async Task> InvokeHelperA
errorMessage = "Unknown";
}
- throw new Exception($"helper error ({exitCode}): {errorMessage}");
+ throw new Trace2Exception(Context.Trace2, $"helper error ({exitCode}): {errorMessage}");
}
return resultDict;
@@ -85,8 +89,7 @@ protected void ThrowIfUserInteractionDisabled()
Constants.GitConfiguration.Credential.Interactive);
Context.Trace.WriteLine($"{envName} / {cfgName} is false/never; user interactivity has been disabled.");
-
- throw new InvalidOperationException("Cannot prompt because user interactivity has been disabled.");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because user interactivity has been disabled.");
}
}
@@ -95,8 +98,7 @@ protected void ThrowIfGuiPromptsDisabled()
if (!Context.Settings.IsGuiPromptsEnabled)
{
Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; GUI prompts have been disabled.");
-
- throw new InvalidOperationException("Cannot show prompt because GUI prompts have been disabled.");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Cannot show prompt because GUI prompts have been disabled.");
}
}
@@ -105,9 +107,26 @@ protected void ThrowIfTerminalPromptsDisabled()
if (!Context.Settings.IsTerminalPromptsEnabled)
{
Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled.");
-
- throw new InvalidOperationException("Cannot prompt because terminal prompts have been disabled.");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because terminal prompts have been disabled.");
+ }
+ }
+
+ protected void ThrowIfWindowCancelled(WindowViewModel viewModel)
+ {
+ if (!viewModel.WindowResult)
+ {
+ throw new Exception("User cancelled dialog.");
+ }
+ }
+
+ protected IntPtr GetParentWindowHandle()
+ {
+ if (int.TryParse(Context.Settings.ParentWindowId, out int id))
+ {
+ return new IntPtr(id);
}
+
+ return IntPtr.Zero;
}
protected bool TryFindHelperCommand(string envar, string configName, string defaultValue, out string command, out string args)
@@ -147,8 +166,22 @@ protected bool TryFindHelperCommand(string envar, string configName, string defa
}
else
{
- Context.Trace.WriteLine($"Using default UI helper: '{defaultValue}'.");
- helperName = defaultValue;
+ // Whilst we evaluate using the Avalonia/in-proc GUIs on Windows we include
+ // a 'fallback' flag that lets us continue to use the WPF out-of-proc helpers.
+ if (PlatformUtils.IsWindows() &&
+ Context.Settings.TryGetSetting(
+ Constants.EnvironmentVariables.GcmDevUseLegacyUiHelpers,
+ Constants.GitConfiguration.Credential.SectionName,
+ Constants.GitConfiguration.Credential.DevUseLegacyUiHelpers,
+ out string str) && str.IsTruthy())
+ {
+ Context.Trace.WriteLine($"Using default legacy UI helper: '{defaultValue}'.");
+ helperName = defaultValue;
+ }
+ else
+ {
+ return false;
+ }
}
//
diff --git a/src/shared/Core/Authentication/BasicAuthentication.cs b/src/shared/Core/Authentication/BasicAuthentication.cs
index 9d485105e..7715c27a9 100644
--- a/src/shared/Core/Authentication/BasicAuthentication.cs
+++ b/src/shared/Core/Authentication/BasicAuthentication.cs
@@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
+using GitCredentialManager.UI;
+using GitCredentialManager.UI.ViewModels;
+using GitCredentialManager.UI.Views;
namespace GitCredentialManager.Authentication
{
@@ -34,18 +38,39 @@ public async Task GetCredentialsAsync(string resource, string userN
ThrowIfUserInteractionDisabled();
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string command, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- return await GetCredentialsByUiAsync(command, args, resource, userName);
+ if (TryFindHelperCommand(out string command, out string args))
+ {
+ return await GetCredentialsViaHelperAsync(command, args, resource, userName);
+ }
+
+ return await GetCredentialsViaUiAsync(resource, userName);
}
ThrowIfTerminalPromptsDisabled();
- return GetCredentialsByTty(resource, userName);
+ return GetCredentialsViaTty(resource, userName);
+ }
+
+ private async Task GetCredentialsViaUiAsync(string resource, string userName)
+ {
+ var viewModel = new CredentialsViewModel
+ {
+ Description = !string.IsNullOrWhiteSpace(resource)
+ ? $"Enter your credentials for '{resource}'"
+ : "Enter your credentials",
+ UserName = userName,
+ };
+
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
+
+ ThrowIfWindowCancelled(viewModel);
+
+ return new GitCredential(viewModel.UserName, viewModel.Password);
}
- private ICredential GetCredentialsByTty(string resource, string userName)
+ private ICredential GetCredentialsViaTty(string resource, string userName)
{
Context.Terminal.WriteLine("Enter basic credentials for '{0}':", resource);
@@ -66,7 +91,7 @@ private ICredential GetCredentialsByTty(string resource, string userName)
return new GitCredential(userName, password);
}
- private async Task GetCredentialsByUiAsync(string command, string args, string resource, string userName)
+ private async Task GetCredentialsViaHelperAsync(string command, string args, string resource, string userName)
{
var promptArgs = new StringBuilder(args);
promptArgs.Append("basic");
@@ -85,12 +110,12 @@ private async Task GetCredentialsByUiAsync(string command, string a
if (!resultDict.TryGetValue("username", out userName))
{
- throw new Exception("Missing 'username' in response");
+ throw new Trace2Exception(Context.Trace2, "Missing 'username' in response");
}
if (!resultDict.TryGetValue("password", out string password))
{
- throw new Exception("Missing 'password' in response");
+ throw new Trace2Exception(Context.Trace2, "Missing 'password' in response");
}
return new GitCredential(userName, password);
diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs
index e38e244bd..900cb8d65 100644
--- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs
+++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs
@@ -3,11 +3,19 @@
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
+using GitCredentialManager.Interop.Windows.Native;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
+using System.Text;
+using System.Threading;
+using GitCredentialManager.UI;
+using GitCredentialManager.UI.ViewModels;
+using GitCredentialManager.UI.Views;
#if NETFRAMEWORK
-using Microsoft.Identity.Client.Desktop;
+using System.Drawing;
+using System.Windows.Forms;
+using Microsoft.Identity.Client.Broker;
#endif
namespace GitCredentialManager.Authentication
@@ -41,54 +49,9 @@ public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthenticat
"live", "liveconnect", "liveid",
};
- #region Broker Initialization
-
- public static bool IsBrokerInitialized { get; private set; }
-
- public static void InitializeBroker()
- {
- if (IsBrokerInitialized)
- {
- return;
- }
-
- IsBrokerInitialized = true;
-
- // Broker is only supported on Windows 10 and later
- if (!PlatformUtils.IsWindowsBrokerSupported())
- {
- return;
- }
-
- // Nothing to do when not an elevated user
- if (!PlatformUtils.IsElevatedUser())
- {
- return;
- }
-
- // Lower COM security so that MSAL can make the calls to WAM
- int result = Interop.Windows.Native.Ole32.CoInitializeSecurity(
- IntPtr.Zero,
- -1,
- IntPtr.Zero,
- IntPtr.Zero,
- Interop.Windows.Native.Ole32.RpcAuthnLevel.None,
- Interop.Windows.Native.Ole32.RpcImpLevel.Impersonate,
- IntPtr.Zero,
- Interop.Windows.Native.Ole32.EoAuthnCap.None,
- IntPtr.Zero
- );
-
- if (result != 0)
- {
- throw new Exception(
- $"Failed to set COM process security to allow Windows broker from an elevated process (0x{result:x})." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmWamComSecurity} for more information.");
- }
- }
-
- #endregion
+#if NETFRAMEWORK
+ private DummyWindow _dummyWindow;
+#endif
public MicrosoftAuthentication(ICommandContext context)
: base(context) { }
@@ -99,116 +62,191 @@ public async Task GetTokenAsync(
string authority, string clientId, Uri redirectUri, string[] scopes, string userName)
{
// Check if we can and should use OS broker authentication
- bool useBroker = false;
- if (CanUseBroker(Context))
+ bool useBroker = CanUseBroker();
+ Context.Trace.WriteLine(useBroker
+ ? "OS broker is available and enabled."
+ : "OS broker is not available or enabled.");
+
+ try
{
- // Can only use the broker if it has been initialized
- useBroker = IsBrokerInitialized;
+ // Create the public client application for authentication
+ IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker);
- if (IsBrokerInitialized)
- Context.Trace.WriteLine("OS broker is available and enabled.");
- else
- Context.Trace.WriteLine("OS broker has not been initialized and cannot not be used.");
- }
-
- // Create the public client application for authentication
- IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker);
-
- AuthenticationResult result = null;
-
- // Try silent authentication first if we know about an existing user
- if (!string.IsNullOrWhiteSpace(userName))
- {
- result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
- }
-
- //
- // If we failed to acquire an AT silently (either because we don't have an existing user, or the user's RT has expired)
- // we need to prompt the user for credentials.
- //
- // If the user has expressed a preference in how the want to perform the interactive authentication flows then we respect that.
- // Otherwise, depending on the current platform and session type we try to show the most appropriate authentication interface:
- //
- // On Windows 10 & .NET Framework, MSAL supports the Web Account Manager (WAM) broker - we try to use that if possible
- // in the first instance.
- //
- // On .NET Framework MSAL supports the WinForms based 'embedded' webview UI. For Windows + .NET Framework this is the
- // best and natural experience.
- //
- // On other runtimes (e.g., .NET Core) MSAL only supports the system webview flow (launch the user's browser),
- // and the device-code flows.
- //
- // Note: .NET Core 3 allows using WinForms when run on Windows but MSAL does not yet support this.
- //
- // The system webview flow requires that the redirect URI is a loopback address, and that we are in an interactive session.
- //
- // The device code flow has no limitations other than a way to communicate to the user the code required to authenticate.
- //
- if (result is null)
- {
- // If the user has disabled interaction all we can do is fail at this point
- ThrowIfUserInteractionDisabled();
-
- // If we're using the OS broker then delegate everything to that
- if (useBroker)
+ AuthenticationResult result = null;
+
+ // Try silent authentication first if we know about an existing user
+ bool hasExistingUser = !string.IsNullOrWhiteSpace(userName);
+ if (hasExistingUser)
{
- Context.Trace.WriteLine("Performing interactive auth with broker...");
- result = await app.AcquireTokenInteractive(scopes)
- .WithPrompt(Prompt.SelectAccount)
- // We must configure the system webview as a fallback
- .WithSystemWebViewOptions(GetSystemWebViewOptions())
- .ExecuteAsync();
+ result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
}
- else
- {
- // Check for a user flow preference if they've specified one
- MicrosoftAuthenticationFlowType flowType = GetFlowType();
- switch (flowType)
- {
- case MicrosoftAuthenticationFlowType.Auto:
- if (CanUseEmbeddedWebView())
- goto case MicrosoftAuthenticationFlowType.EmbeddedWebView;
-
- if (CanUseSystemWebView(app, redirectUri))
- goto case MicrosoftAuthenticationFlowType.SystemWebView;
- // Fall back to device code flow
- goto case MicrosoftAuthenticationFlowType.DeviceCode;
-
- case MicrosoftAuthenticationFlowType.EmbeddedWebView:
- Context.Trace.WriteLine("Performing interactive auth with embedded web view...");
- EnsureCanUseEmbeddedWebView();
- result = await app.AcquireTokenInteractive(scopes)
- .WithPrompt(Prompt.SelectAccount)
- .WithUseEmbeddedWebView(true)
- .WithEmbeddedWebViewOptions(GetEmbeddedWebViewOptions())
- .ExecuteAsync();
- break;
+ //
+ // If we failed to acquire an AT silently (either because we don't have an existing user, or the user's
+ // RT has expired) we need to prompt the user for credentials.
+ //
+ // If the user has expressed a preference in how they want to perform the interactive authentication
+ // flows then we respect that. Otherwise, depending on the current platform and session type we try to
+ // show the most appropriate authentication interface:
+ //
+ // On Windows 10+ & .NET Framework, MSAL supports the Web Account Manager (WAM) broker - we try to use
+ // that if possible in the first instance.
+ //
+ // On .NET Framework MSAL supports the WinForms based 'embedded' webview UI. This experience is less
+ // jarring that the system webview flow so try that option next.
+ //
+ // On other runtimes (e.g., .NET 6+) MSAL only supports the system webview flow (launch the user's
+ // browser), and the device-code flows. The system webview flow requires that the redirect URI is a
+ // loopback address, and that we are in an interactive session.
+ //
+ // The device code flow has no limitations other than a way to communicate to the user the code required
+ // to authenticate.
+ //
+ if (result is null)
+ {
+ // If the user has disabled interaction all we can do is fail at this point
+ ThrowIfUserInteractionDisabled();
- case MicrosoftAuthenticationFlowType.SystemWebView:
- Context.Trace.WriteLine("Performing interactive auth with system web view...");
- EnsureCanUseSystemWebView(app, redirectUri);
+ // If we're using the OS broker then delegate everything to that
+ if (useBroker)
+ {
+ // If the user has enabled the default account feature then we can try to acquire an access
+ // token 'silently' without knowing the user's UPN. Whilst this could be done truly silently,
+ // we still prompt the user to confirm this action because if the OS account is the incorrect
+ // account then the user may become stuck in a loop of authentication failures.
+ if (!hasExistingUser && Context.Settings.UseMsAuthDefaultAccount)
+ {
+ result = await GetAccessTokenSilentlyAsync(app, scopes, null);
+
+ if (result is null || !await UseDefaultAccountAsync(result.Account.Username))
+ {
+ result = null;
+ }
+ }
+
+ if (result is null)
+ {
+ Context.Trace.WriteLine("Performing interactive auth with broker...");
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
+ // We must configure the system webview as a fallback
.WithSystemWebViewOptions(GetSystemWebViewOptions())
.ExecuteAsync();
- break;
-
- case MicrosoftAuthenticationFlowType.DeviceCode:
- Context.Trace.WriteLine("Performing interactive auth with device code...");
- // We don't have a way to display a device code without a terminal at the moment
- // TODO: introduce a small GUI window to show a code if no TTY exists
- ThrowIfTerminalPromptsDisabled();
- result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();
- break;
-
- default:
- goto case MicrosoftAuthenticationFlowType.Auto;
+ }
+ }
+ else
+ {
+ // Check for a user flow preference if they've specified one
+ MicrosoftAuthenticationFlowType flowType = GetFlowType();
+ switch (flowType)
+ {
+ case MicrosoftAuthenticationFlowType.Auto:
+ if (CanUseEmbeddedWebView())
+ goto case MicrosoftAuthenticationFlowType.EmbeddedWebView;
+
+ if (CanUseSystemWebView(app, redirectUri))
+ goto case MicrosoftAuthenticationFlowType.SystemWebView;
+
+ // Fall back to device code flow
+ goto case MicrosoftAuthenticationFlowType.DeviceCode;
+
+ case MicrosoftAuthenticationFlowType.EmbeddedWebView:
+ Context.Trace.WriteLine("Performing interactive auth with embedded web view...");
+ EnsureCanUseEmbeddedWebView();
+ result = await app.AcquireTokenInteractive(scopes)
+ .WithPrompt(Prompt.SelectAccount)
+ .WithUseEmbeddedWebView(true)
+ .WithEmbeddedWebViewOptions(GetEmbeddedWebViewOptions())
+ .ExecuteAsync();
+ break;
+
+ case MicrosoftAuthenticationFlowType.SystemWebView:
+ Context.Trace.WriteLine("Performing interactive auth with system web view...");
+ EnsureCanUseSystemWebView(app, redirectUri);
+ result = await app.AcquireTokenInteractive(scopes)
+ .WithPrompt(Prompt.SelectAccount)
+ .WithSystemWebViewOptions(GetSystemWebViewOptions())
+ .ExecuteAsync();
+ break;
+
+ case MicrosoftAuthenticationFlowType.DeviceCode:
+ Context.Trace.WriteLine("Performing interactive auth with device code...");
+ // We don't have a way to display a device code without a terminal at the moment
+ // TODO: introduce a small GUI window to show a code if no TTY exists
+ ThrowIfTerminalPromptsDisabled();
+ result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();
+ break;
+
+ default:
+ goto case MicrosoftAuthenticationFlowType.Auto;
+ }
+ }
+ }
+
+ return new MsalResult(result);
+ }
+ finally
+ {
+#if NETFRAMEWORK
+ // If we created a dummy window during authentication we should dispose of it now that we're done
+ _dummyWindow?.Dispose();
+#endif
+ }
+ }
+
+ private async Task UseDefaultAccountAsync(string userName)
+ {
+ ThrowIfUserInteractionDisabled();
+
+ if (Context.SessionManager.IsDesktopSession && Context.Settings.IsGuiPromptsEnabled)
+ {
+ if (TryFindHelperCommand(out string command, out string args))
+ {
+ var sb = new StringBuilder(args);
+ sb.Append("default-account");
+ sb.AppendFormat(" --username {0}", QuoteCmdArg(userName));
+
+ IDictionary result = await InvokeHelperAsync(command, sb.ToString());
+
+ if (result.TryGetValue("use_default_account", out string str) && !string.IsNullOrWhiteSpace(str))
+ {
+ return str.ToBooleanyOrDefault(false);
+ }
+ else
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing use_default_account in response");
}
}
+
+ var viewModel = new DefaultAccountViewModel(Context.Environment)
+ {
+ UserName = userName
+ };
+
+ await AvaloniaUi.ShowViewAsync(
+ viewModel, GetParentWindowHandle(), CancellationToken.None);
+
+ ThrowIfWindowCancelled(viewModel);
+
+ return viewModel.UseDefaultAccount;
}
+ else
+ {
+ string question = $"Continue with current account ({userName})?";
+
+ var menu = new TerminalMenu(Context.Terminal, question);
+ TerminalMenuItem yesItem = menu.Add("Yes");
+ TerminalMenuItem noItem = menu.Add("No, use another account");
+ TerminalMenuItem choice = menu.Show();
+
+ if (choice == yesItem)
+ return true;
- return new MsalResult(result);
+ if (choice == noItem)
+ return false;
+
+ throw new Exception();
+ }
}
internal MicrosoftAuthenticationFlowType GetFlowType()
@@ -247,11 +285,20 @@ private async Task GetAccessTokenSilentlyAsync(IPublicClie
{
try
{
- Context.Trace.WriteLine($"Attempting to acquire token silently for user '{userName}'...");
+ if (userName is null)
+ {
+ Context.Trace.WriteLine("Attempting to acquire token silently for current operating system account...");
+
+ return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount).ExecuteAsync();
+ }
+ else
+ {
+ Context.Trace.WriteLine($"Attempting to acquire token silently for user '{userName}'...");
- // We can either call `app.GetAccountsAsync` and filter through the IAccount objects for the instance with the correct user name,
- // or we can just pass the user name string we have as the `loginHint` and let MSAL do exactly that for us instead!
- return await app.AcquireTokenSilent(scopes, loginHint: userName).ExecuteAsync();
+ // We can either call `app.GetAccountsAsync` and filter through the IAccount objects for the instance with the correct user name,
+ // or we can just pass the user name string we have as the `loginHint` and let MSAL do exactly that for us instead!
+ return await app.AcquireTokenSilent(scopes, loginHint: userName).ExecuteAsync();
+ }
}
catch (MsalUiRequiredException)
{
@@ -279,27 +326,60 @@ private async Task CreatePublicClientApplicationAsync(
appBuilder.WithLogging(OnMsalLogMessage, LogLevel.Verbose, enablePiiLogging, false);
}
- // If we have a parent window ID we should tell MSAL about it so it can parent any authentication dialogs
- // correctly. We only support this on Windows right now as MSAL only supports embedded/dialogs on Windows.
- if (PlatformUtils.IsWindows() && !string.IsNullOrWhiteSpace(Context.Settings.ParentWindowId) &&
- int.TryParse(Context.Settings.ParentWindowId, out int hWndInt) && hWndInt > 0)
+ // On Windows we should set the parent window handle for the authentication dialogs
+ // so that they are displayed as a child of the correct window.
+ if (PlatformUtils.IsWindows())
{
- appBuilder.WithParentActivityOrWindow(() => new IntPtr(hWndInt));
+ // If we have a parent window ID then use that, otherwise use the hosting terminal window.
+ if (!string.IsNullOrWhiteSpace(Context.Settings.ParentWindowId) &&
+ int.TryParse(Context.Settings.ParentWindowId, out int hWndInt) && hWndInt > 0)
+ {
+ Context.Trace.WriteLine($"Using provided parent window ID '{hWndInt}' for MSAL authentication dialogs.");
+ appBuilder.WithParentActivityOrWindow(() => new IntPtr(hWndInt));
+ }
+ else
+ {
+ IntPtr consoleHandle = Kernel32.GetConsoleWindow();
+ IntPtr parentHandle = User32.GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);
+
+ // If we don't have a console window then create a dummy top-level window (for .NET Framework)
+ // that we can use as a parent. When not on .NET Framework just use the Desktop window.
+ if (parentHandle != IntPtr.Zero)
+ {
+ Context.Trace.WriteLine($"Using console parent window ID '{parentHandle}' for MSAL authentication dialogs.");
+ appBuilder.WithParentActivityOrWindow(() => parentHandle);
+ }
+ else if (enableBroker) // Only actually need to set a parent window when using the Windows broker
+ {
+#if NETFRAMEWORK
+ Context.Trace.WriteLine($"Using dummy parent window for MSAL authentication dialogs.");
+ _dummyWindow = new DummyWindow();
+ appBuilder.WithParentActivityOrWindow(_dummyWindow.ShowAndGetHandle);
+#endif
+ }
+ }
}
- // On Windows 10+ & .NET Framework try and use the WAM broker
- if (enableBroker && PlatformUtils.IsWindowsBrokerSupported())
+ // Configure the broker if enabled
+ // Currently only supported on Windows so only included in the .NET Framework builds
+ // to save on the distribution size of the .NET builds (no need for MSALRuntime bits).
+ if (enableBroker)
{
#if NETFRAMEWORK
- appBuilder.WithExperimentalFeatures();
- appBuilder.WithWindowsBroker();
+ appBuilder.WithBroker(
+ new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
+ {
+ Title = "Git Credential Manager",
+ MsaPassthrough = true,
+ }
+ );
#endif
}
IPublicClientApplication app = appBuilder.Build();
// Register the application token cache
- await RegisterTokenCacheAsync(app);
+ await RegisterTokenCacheAsync(app, Context.Trace2);
return app;
}
@@ -308,14 +388,14 @@ private async Task CreatePublicClientApplicationAsync(
#region Helpers
- private async Task RegisterTokenCacheAsync(IPublicClientApplication app)
+ private async Task RegisterTokenCacheAsync(IPublicClientApplication app, ITrace2 trace2)
{
Context.Trace.WriteLine(
"Configuring Microsoft Authentication token cache to instance shared with Microsoft developer tools...");
if (!PlatformUtils.IsWindows() && !PlatformUtils.IsPosix())
{
- string osType = PlatformUtils.GetPlatformInformation().OperatingSystemType;
+ string osType = PlatformUtils.GetPlatformInformation(trace2).OperatingSystemType;
Context.Trace.WriteLine($"Token cache integration is not supported on {osType}.");
return;
}
@@ -333,9 +413,11 @@ private async Task RegisterTokenCacheAsync(IPublicClientApplication app)
}
catch (MsalCachePersistenceException ex)
{
+ var message = "Cannot persist Microsoft Authentication data securely!";
Context.Streams.Error.WriteLine("warning: cannot persist Microsoft authentication token cache securely!");
- Context.Trace.WriteLine("Cannot persist Microsoft Authentication data securely!");
+ Context.Trace.WriteLine(message);
Context.Trace.WriteException(ex);
+ Context.Trace2.WriteError(message);
if (PlatformUtils.IsMacOS())
{
@@ -432,6 +514,16 @@ private void OnMsalLogMessage(LogLevel level, string message, bool containspii)
Context.Trace.WriteLine($"[{level.ToString()}] {message}", memberName: "MSAL");
}
+ private bool TryFindHelperCommand(out string command, out string args)
+ {
+ return TryFindHelperCommand(
+ Constants.EnvironmentVariables.GcmUiHelper,
+ Constants.GitConfiguration.Credential.UiHelper,
+ Constants.DefaultUiHelper,
+ out command,
+ out args);
+ }
+
private class MsalHttpClientFactoryAdaptor : IMsalHttpClientFactory
{
private readonly IHttpClientFactory _factory;
@@ -456,19 +548,19 @@ public HttpClient GetHttpClient()
#region Auth flow capability detection
- public static bool CanUseBroker(ICommandContext context)
+ public bool CanUseBroker()
{
#if NETFRAMEWORK
// We only support the broker on Windows 10+ and in an interactive session
- if (!context.SessionManager.IsDesktopSession || !PlatformUtils.IsWindowsBrokerSupported())
+ if (!Context.SessionManager.IsDesktopSession || !PlatformUtils.IsWindowsBrokerSupported())
{
return false;
}
- // Default to not using the OS broker
- const bool defaultValue = false;
+ // Default to using the OS broker only on DevBox for the time being
+ bool defaultValue = PlatformUtils.IsDevBox();
- if (context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,
+ if (Context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,
Constants.GitConfiguration.Credential.SectionName,
Constants.GitConfiguration.Credential.MsAuthUseBroker,
out string valueStr))
@@ -498,34 +590,39 @@ private void EnsureCanUseEmbeddedWebView()
#if NETFRAMEWORK
if (!Context.SessionManager.IsDesktopSession)
{
- throw new InvalidOperationException("Embedded web view is not available without a desktop session.");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "Embedded web view is not available without a desktop session.");
}
#else
- throw new InvalidOperationException("Embedded web view is not available on .NET Core.");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "Embedded web view is not available on .NET Core.");
#endif
}
private bool CanUseSystemWebView(IPublicClientApplication app, Uri redirectUri)
{
// MSAL requires the application redirect URI is a loopback address to use the System WebView
- return Context.SessionManager.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback;
+ return Context.SessionManager.IsWebBrowserAvailable && app.IsSystemWebViewAvailable && redirectUri.IsLoopback;
}
private void EnsureCanUseSystemWebView(IPublicClientApplication app, Uri redirectUri)
{
- if (!Context.SessionManager.IsDesktopSession)
+ if (!Context.SessionManager.IsWebBrowserAvailable)
{
- throw new InvalidOperationException("System web view is not available without a desktop session.");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "System web view is not available without a way to start a browser.");
}
if (!app.IsSystemWebViewAvailable)
{
- throw new InvalidOperationException("System web view is not available on this platform.");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "System web view is not available on this platform.");
}
if (!redirectUri.IsLoopback)
{
- throw new InvalidOperationException("System web view is not available for this service configuration.");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "System web view is not available for this service configuration.");
}
}
@@ -543,5 +640,73 @@ public MsalResult(AuthenticationResult msalResult)
public string AccessToken => _msalResult.AccessToken;
public string AccountUpn => _msalResult.Account.Username;
}
+
+#if NETFRAMEWORK
+ private class DummyWindow : IDisposable
+ {
+ private readonly Thread _staThread;
+ private readonly ManualResetEventSlim _readyEvent;
+ private Form _window;
+ private IntPtr _handle;
+
+ public DummyWindow()
+ {
+ _staThread = new Thread(ThreadProc);
+ _staThread.SetApartmentState(ApartmentState.STA);
+ _readyEvent = new ManualResetEventSlim();
+ }
+
+ public IntPtr ShowAndGetHandle()
+ {
+ _staThread.Start();
+ _readyEvent.Wait();
+ return _handle;
+ }
+
+ public void Dispose()
+ {
+ _window?.Invoke(() => _window.Close());
+
+ if (_staThread.IsAlive)
+ {
+ _staThread.Join();
+ }
+ }
+
+ private void ThreadProc()
+ {
+ System.Windows.Forms.Application.EnableVisualStyles();
+ _window = new Form
+ {
+ TopMost = true,
+ ControlBox = false,
+ MaximizeBox = false,
+ MinimizeBox = false,
+ ClientSize = new Size(182, 46),
+ FormBorderStyle = FormBorderStyle.None,
+ StartPosition = FormStartPosition.CenterScreen,
+ };
+
+ var progress = new ProgressBar
+ {
+ Style = ProgressBarStyle.Marquee,
+ Location = new Point(12, 12),
+ Size = new Size(158, 23),
+ MarqueeAnimationSpeed = 30,
+ };
+
+ _window.Controls.Add(progress);
+ _window.Shown += (s, e) =>
+ {
+ _handle = _window.Handle;
+ _readyEvent.Set();
+ };
+
+ _window.ShowDialog();
+ _window.Dispose();
+ _window = null;
+ }
+ }
+#endif
}
}
diff --git a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs
index 9558a1f66..8818581be 100644
--- a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs
+++ b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs
@@ -20,9 +20,15 @@ public interface IOAuth2Client
///
/// Scopes to request.
/// User agent to use to start the authorization code grant flow.
+ /// Extra parameters to add to the URL query component.
/// Token to cancel the operation.
/// Authorization code.
- Task GetAuthorizationCodeAsync(IEnumerable scopes, IOAuth2WebBrowser browser, CancellationToken ct);
+ Task GetAuthorizationCodeAsync(
+ IEnumerable scopes,
+ IOAuth2WebBrowser browser,
+ IDictionary extraQueryParams,
+ CancellationToken ct
+ );
///
/// Retrieve a device code grant.
@@ -64,20 +70,26 @@ public class OAuth2Client : IOAuth2Client
private readonly OAuth2ServerEndpoints _endpoints;
private readonly Uri _redirectUri;
private readonly string _clientId;
+ private readonly ITrace2 _trace2;
private readonly string _clientSecret;
- private readonly ITrace _trace;
private readonly bool _addAuthHeader;
private IOAuth2CodeGenerator _codeGenerator;
- public OAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri = null, string clientSecret = null, ITrace trace = null, bool addAuthHeader = true)
+ public OAuth2Client(HttpClient httpClient,
+ OAuth2ServerEndpoints endpoints,
+ string clientId,
+ ITrace2 trace2,
+ Uri redirectUri = null,
+ string clientSecret = null,
+ bool addAuthHeader = true)
{
_httpClient = httpClient;
_endpoints = endpoints;
_clientId = clientId;
+ _trace2 = trace2;
_redirectUri = redirectUri;
_clientSecret = clientSecret;
- _trace = trace;
_addAuthHeader = addAuthHeader;
}
@@ -87,21 +99,10 @@ public IOAuth2CodeGenerator CodeGenerator
set => _codeGenerator = value;
}
- protected string ClientId => _clientId;
-
- protected string ClientSecret => _clientSecret;
-
- protected ITrace Trace => _trace;
-
- protected OAuth2ServerEndpoints Endpoints => _endpoints;
-
- protected HttpClient HttpClient => _httpClient;
-
- protected Uri RedirectUri => _redirectUri;
-
#region IOAuth2Client
- public async Task GetAuthorizationCodeAsync(IEnumerable scopes, IOAuth2WebBrowser browser, CancellationToken ct)
+ public async Task GetAuthorizationCodeAsync(IEnumerable scopes,
+ IOAuth2WebBrowser browser, IDictionary extraQueryParams, CancellationToken ct)
{
string state = CodeGenerator.CreateNonce();
string codeVerifier = CodeGenerator.CreatePkceCodeVerifier();
@@ -118,6 +119,21 @@ public async Task GetAuthorizationCodeAsync(IEnum
[OAuth2Constants.AuthorizationEndpoint.PkceChallengeParameter] = codeChallenge
};
+ if (extraQueryParams?.Count > 0)
+ {
+ foreach (var kvp in extraQueryParams)
+ {
+ if (queryParams.ContainsKey(kvp.Key))
+ {
+ throw new ArgumentException(
+ $"Extra query parameter '{kvp.Key}' would override required standard OAuth parameters.",
+ nameof(extraQueryParams));
+ }
+
+ queryParams[kvp.Key] = kvp.Value;
+ }
+ }
+
Uri redirectUri = null;
if (_redirectUri != null)
{
@@ -147,17 +163,19 @@ public async Task GetAuthorizationCodeAsync(IEnum
IDictionary redirectQueryParams = finalUri.GetQueryParameters();
if (!redirectQueryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState))
{
- throw new OAuth2Exception($"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.");
+ throw new Trace2OAuth2Exception(_trace2, $"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.");
}
if (!StringComparer.Ordinal.Equals(state, replyState))
{
- throw new OAuth2Exception($"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response. Does not match initial request.");
+ throw new Trace2OAuth2Exception(_trace2,
+ $"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.");
}
// We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason)
if (!redirectQueryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode))
{
- throw new OAuth2Exception($"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response.");
+ throw new Trace2OAuth2Exception(_trace2,
+ $"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response.");
}
return new OAuth2AuthorizationCodeResult(authCode, redirectUri, codeVerifier);
@@ -165,9 +183,13 @@ public async Task GetAuthorizationCodeAsync(IEnum
public async Task GetDeviceCodeAsync(IEnumerable scopes, CancellationToken ct)
{
+ var label = "get device code";
+ using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);
+
if (_endpoints.DeviceAuthorizationEndpoint is null)
{
- throw new InvalidOperationException("No device authorization endpoint has been configured for this client.");
+ throw new Trace2InvalidOperationException(_trace2,
+ "No device authorization endpoint has been configured for this client.");
}
string scopesStr = string.Join(" ", scopes);
@@ -199,6 +221,9 @@ public async Task GetDeviceCodeAsync(IEnumerable
public async Task GetTokenByAuthorizationCodeAsync(OAuth2AuthorizationCodeResult authorizationCodeResult, CancellationToken ct)
{
+ var label = "get token by auth code";
+ using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);
+
var formData = new Dictionary
{
[OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.AuthorizationCodeGrantType,
@@ -235,6 +260,9 @@ public async Task GetTokenByAuthorizationCodeAsync(OAuth2Auth
public async Task GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct)
{
+ var label = "get token by refresh token";
+ using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);
+
var formData = new Dictionary
{
[OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.RefreshTokenGrantType,
@@ -367,10 +395,13 @@ protected Exception CreateExceptionFromResponse(string json)
{
if (TryCreateExceptionFromResponse(json, out OAuth2Exception exception))
{
+ _trace2.WriteError(exception.Message);
return exception;
}
- return new OAuth2Exception($"Unknown OAuth error: {json}");
+ var format = "Unknown OAuth error: {0}";
+ var message = string.Format(format, json);
+ return new Trace2OAuth2Exception(_trace2, message, format);
}
protected static bool TryDeserializeJson(string json, out T obj)
@@ -389,4 +420,13 @@ protected static bool TryDeserializeJson(string json, out T obj)
#endregion
}
+
+ public static class OAuth2ClientExtensions
+ {
+ public static Task GetAuthorizationCodeAsync(
+ this IOAuth2Client client, IEnumerable scopes, IOAuth2WebBrowser browser, CancellationToken ct)
+ {
+ return client.GetAuthorizationCodeAsync(scopes, browser, null, ct);
+ }
+ }
}
diff --git a/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs b/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs
index d630d0282..0b96a6047 100644
--- a/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs
+++ b/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs
@@ -7,6 +7,7 @@ public static class OAuth2Constants
public const string ClientSecretParameter = "client_secret";
public const string RedirectUriParameter = "redirect_uri";
public const string ScopeParameter = "scope";
+ public const string Trace2Category = "oauth2";
public static class AuthorizationEndpoint
{
diff --git a/src/shared/Core/Authentication/OAuthAuthentication.cs b/src/shared/Core/Authentication/OAuthAuthentication.cs
index 8da18faad..641baeb08 100644
--- a/src/shared/Core/Authentication/OAuthAuthentication.cs
+++ b/src/shared/Core/Authentication/OAuthAuthentication.cs
@@ -4,6 +4,9 @@
using System.Threading;
using System.Threading.Tasks;
using GitCredentialManager.Authentication.OAuth;
+using GitCredentialManager.UI;
+using GitCredentialManager.UI.ViewModels;
+using GitCredentialManager.UI.Views;
namespace GitCredentialManager.Authentication
{
@@ -57,76 +60,118 @@ public async Task GetAuthenticationModeAsync(
return modes;
}
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string command, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- var promptArgs = new StringBuilder(args);
- promptArgs.Append("oauth");
-
- if (!string.IsNullOrWhiteSpace(resource))
+ if (TryFindHelperCommand(out string command, out string args))
{
- promptArgs.AppendFormat(" --resource {0}", QuoteCmdArg(resource));
+ return await GetAuthenticationModeViaHelperAsync(resource, modes, args, command);
}
- if ((modes & OAuthAuthenticationModes.Browser) != 0)
- {
- promptArgs.Append(" --browser");
- }
+ return await GetAuthenticationModeViaUiAsync(resource, modes);
+ }
- if ((modes & OAuthAuthenticationModes.DeviceCode) != 0)
- {
- promptArgs.Append(" --device-code");
- }
+ return GetAuthenticationModeViaTty(resource, modes);
+ }
- IDictionary resultDict = await InvokeHelperAsync(command, promptArgs.ToString());
+ private async Task GetAuthenticationModeViaUiAsync(string resource, OAuthAuthenticationModes modes)
+ {
+ var viewModel = new OAuthViewModel
+ {
+ Description = !string.IsNullOrWhiteSpace(resource)
+ ? $"Sign in to '{resource}'"
+ : "Select a sign-in option",
+ ShowBrowserLogin = (modes & OAuthAuthenticationModes.Browser) != 0,
+ ShowDeviceCodeLogin = (modes & OAuthAuthenticationModes.DeviceCode) != 0,
+ };
- if (!resultDict.TryGetValue("mode", out string responseMode))
- {
- throw new Exception("Missing 'mode' in response");
- }
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
- switch (responseMode.ToLowerInvariant())
- {
- case "browser":
- return OAuthAuthenticationModes.Browser;
+ ThrowIfWindowCancelled(viewModel);
- case "devicecode":
- return OAuthAuthenticationModes.DeviceCode;
+ switch (viewModel.SelectedMode)
+ {
+ case OAuthAuthenticationModes.Browser:
+ return OAuthAuthenticationModes.Browser;
- default:
- throw new Exception($"Unknown mode value in response '{responseMode}'");
- }
+ case OAuthAuthenticationModes.DeviceCode:
+ return OAuthAuthenticationModes.DeviceCode;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
- else
+ }
+
+ private OAuthAuthenticationModes GetAuthenticationModeViaTty(string resource, OAuthAuthenticationModes modes)
+ {
+ ThrowIfTerminalPromptsDisabled();
+
+ switch (modes)
{
- ThrowIfTerminalPromptsDisabled();
+ case OAuthAuthenticationModes.Browser:
+ return OAuthAuthenticationModes.Browser;
- switch (modes)
- {
- case OAuthAuthenticationModes.Browser:
- return OAuthAuthenticationModes.Browser;
+ case OAuthAuthenticationModes.DeviceCode:
+ return OAuthAuthenticationModes.DeviceCode;
+
+ default:
+ var menuTitle = $"Select an authentication method for '{resource}'";
+ var menu = new TerminalMenu(Context.Terminal, menuTitle);
- case OAuthAuthenticationModes.DeviceCode:
- return OAuthAuthenticationModes.DeviceCode;
+ TerminalMenuItem browserItem = null;
+ TerminalMenuItem deviceItem = null;
- default:
- var menuTitle = $"Select an authentication method for '{resource}'";
- var menu = new TerminalMenu(Context.Terminal, menuTitle);
+ if ((modes & OAuthAuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser");
+ if ((modes & OAuthAuthenticationModes.DeviceCode) != 0) deviceItem = menu.Add("Device code");
- TerminalMenuItem browserItem = null;
- TerminalMenuItem deviceItem = null;
+ // Default to the 'first' choice in the menu
+ TerminalMenuItem choice = menu.Show(0);
- if ((modes & OAuthAuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser");
- if ((modes & OAuthAuthenticationModes.DeviceCode) != 0) deviceItem = menu.Add("Device code");
+ if (choice == browserItem) goto case OAuthAuthenticationModes.Browser;
+ if (choice == deviceItem) goto case OAuthAuthenticationModes.DeviceCode;
- // Default to the 'first' choice in the menu
- TerminalMenuItem choice = menu.Show(0);
+ throw new Exception();
+ }
+ }
- if (choice == browserItem) goto case OAuthAuthenticationModes.Browser;
- if (choice == deviceItem) goto case OAuthAuthenticationModes.DeviceCode;
+ private async Task GetAuthenticationModeViaHelperAsync(
+ string resource, OAuthAuthenticationModes modes, string args, string command)
+ {
+ var promptArgs = new StringBuilder(args);
+ promptArgs.Append("oauth");
- throw new Exception();
- }
+ if (!string.IsNullOrWhiteSpace(resource))
+ {
+ promptArgs.AppendFormat(" --resource {0}", QuoteCmdArg(resource));
+ }
+
+ if ((modes & OAuthAuthenticationModes.Browser) != 0)
+ {
+ promptArgs.Append(" --browser");
+ }
+
+ if ((modes & OAuthAuthenticationModes.DeviceCode) != 0)
+ {
+ promptArgs.Append(" --device-code");
+ }
+
+ IDictionary resultDict = await InvokeHelperAsync(command, promptArgs.ToString());
+
+ if (!resultDict.TryGetValue("mode", out string responseMode))
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response");
+ }
+
+ switch (responseMode.ToLowerInvariant())
+ {
+ case "browser":
+ return OAuthAuthenticationModes.Browser;
+
+ case "devicecode":
+ return OAuthAuthenticationModes.DeviceCode;
+
+ default:
+ throw new Trace2Exception(Context.Trace2,
+ $"Unknown mode value in response '{responseMode}'");
}
}
@@ -137,7 +182,8 @@ public async Task GetTokenByBrowserAsync(OAuth2Client client,
// We require a desktop session to launch the user's default web browser
if (!Context.SessionManager.IsDesktopSession)
{
- throw new InvalidOperationException("Browser authentication requires a desktop session");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "Browser authentication requires a desktop session");
}
var browserOptions = new OAuth2WebBrowserOptions();
@@ -153,19 +199,15 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie
OAuth2DeviceCodeResult dcr = await client.GetDeviceCodeAsync(scopes, CancellationToken.None);
// If we have a desktop session show the device code in a dialog
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string command, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- var promptArgs = new StringBuilder(args);
- promptArgs.Append("device");
- promptArgs.AppendFormat(" --code {0} ", QuoteCmdArg(dcr.UserCode));
- promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(dcr.VerificationUri.ToString()));
-
var promptCts = new CancellationTokenSource();
var tokenCts = new CancellationTokenSource();
// Show the dialog with the device code but don't await its closure
- Task promptTask = InvokeHelperAsync(command, promptArgs.ToString(), null, promptCts.Token);
+ Task promptTask = TryFindHelperCommand(out string command, out string args)
+ ? ShowDeviceCodeViaHelperAsync(dcr, command, args, promptCts.Token)
+ : ShowDeviceCodeViaUiAsync(dcr, promptCts.Token);
// Start the request for an OAuth token but don't wait
Task tokenTask = client.GetTokenByDeviceCodeAsync(dcr, tokenCts.Token);
@@ -185,7 +227,7 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie
}
catch (OperationCanceledException)
{
- throw new Exception("User canceled device code authentication");
+ throw new Trace2Exception(Context.Trace2, "User canceled device code authentication");
}
// Close the dialog
@@ -193,17 +235,43 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie
return tokenResult;
}
- else
+
+ return await GetTokenByDeviceCodeViaTtyAsync(client, dcr);
+ }
+
+ private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationToken ct)
+ {
+ var viewModel = new DeviceCodeViewModel(Context.Environment)
{
- ThrowIfTerminalPromptsDisabled();
+ UserCode = dcr.UserCode,
+ VerificationUrl = dcr.VerificationUri.ToString(),
+ };
- string deviceMessage = $"To complete authentication please visit {dcr.VerificationUri} and enter the following code:" +
- Environment.NewLine +
- dcr.UserCode;
- Context.Terminal.WriteLine(deviceMessage);
+ return AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
+ }
- return await client.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None);
- }
+ private Task ShowDeviceCodeViaHelperAsync(
+ OAuth2DeviceCodeResult dcr, string command, string args, CancellationToken ct)
+ {
+ var promptArgs = new StringBuilder(args);
+ promptArgs.Append("device");
+ promptArgs.AppendFormat(" --code {0} ", QuoteCmdArg(dcr.UserCode));
+ promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(dcr.VerificationUri.ToString()));
+
+ return InvokeHelperAsync(command, promptArgs.ToString(), null, ct);
+ }
+
+ private async Task GetTokenByDeviceCodeViaTtyAsync(OAuth2Client client, OAuth2DeviceCodeResult dcr)
+ {
+ ThrowIfTerminalPromptsDisabled();
+
+ string deviceMessage =
+ $"To complete authentication please visit {dcr.VerificationUri} and enter the following code:" +
+ Environment.NewLine +
+ dcr.UserCode;
+ Context.Terminal.WriteLine(deviceMessage);
+
+ return await client.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None);
}
private bool TryFindHelperCommand(out string command, out string args)
diff --git a/src/shared/Core/BrowserUtils.cs b/src/shared/Core/BrowserUtils.cs
index 0df5dec51..6e908d1fc 100644
--- a/src/shared/Core/BrowserUtils.cs
+++ b/src/shared/Core/BrowserUtils.cs
@@ -25,9 +25,10 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
string url = uri.ToString();
- ProcessStartInfo psi = null;
+ ProcessStartInfo psi;
if (PlatformUtils.IsLinux())
{
+ //
// On Linux, 'shell execute' utilities like xdg-open launch a process without
// detaching from the standard in/out descriptors. Some applications (like
// Chromium) write messages to stdout, which is currently hooked up and being
@@ -40,28 +41,17 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
// We try and use the same 'shell execute' utilities as the Framework does,
// searching for them in the same order until we find one.
//
- // One additional 'shell execute' utility we also attempt to use is `wslview`
- // that is commonly found on WSL (Windows Subsystem for Linux) distributions that
- // opens the browser on the Windows host.
- foreach (string shellExec in new[] {"xdg-open", "gnome-open", "kfmclient", "wslview"})
+ if (!TryGetLinuxShellExecuteHandler(environment, out string shellExecPath))
{
- if (environment.TryLocateExecutable(shellExec, out string shellExecPath))
- {
- psi = new ProcessStartInfo(shellExecPath, url)
- {
- RedirectStandardOutput = true,
- RedirectStandardError = true
- };
-
- // We found a way to open the URI; stop searching!
- break;
- }
+ throw new Exception("Failed to locate a utility to launch the default web browser.");
}
- if (psi is null)
+ psi = new ProcessStartInfo(shellExecPath, url)
{
- throw new Exception("Failed to locate a utility to launch the default web browser.");
- }
+ RedirectStandardOutput = true,
+ // Ok to redirect stderr for non-git-related processes
+ RedirectStandardError = true
+ };
}
else
{
@@ -70,7 +60,29 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
psi = new ProcessStartInfo(url) {UseShellExecute = true};
}
+ // We purposefully do not use a ChildProcess here, as the purpose of that
+ // class is to allow us to collect child process information using TRACE2.
+ // Since we will not be collecting TRACE2 data from the browser, there
+ // is no need to add the extra overhead associated with ChildProcess here.
Process.Start(psi);
}
+
+ public static bool TryGetLinuxShellExecuteHandler(IEnvironment env, out string shellExecPath)
+ {
+ // One additional 'shell execute' utility we also attempt to use over the Framework
+ // is `wslview` that is commonly found on WSL (Windows Subsystem for Linux) distributions
+ // that opens the browser on the Windows host.
+ string[] shellHandlers = { "xdg-open", "gnome-open", "kfmclient", WslUtils.WslViewShellHandlerName };
+ foreach (string shellExec in shellHandlers)
+ {
+ if (env.TryLocateExecutable(shellExec, out shellExecPath))
+ {
+ return true;
+ }
+ }
+
+ shellExecPath = null;
+ return false;
+ }
}
}
diff --git a/src/shared/Core/ChildProcess.cs b/src/shared/Core/ChildProcess.cs
new file mode 100644
index 000000000..9e86cc53f
--- /dev/null
+++ b/src/shared/Core/ChildProcess.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace GitCredentialManager;
+
+public class ChildProcess : DisposableObject
+{
+ private readonly ITrace2 _trace2;
+
+ private DateTimeOffset _startTime;
+ private DateTimeOffset _exitTime => Process.ExitTime;
+ private ProcessStartInfo _startInfo => Process.StartInfo;
+
+ private int _id => Process.Id;
+
+ public ProcessStartInfo StartInfo => Process.StartInfo;
+ public Process Process { get; }
+ public StreamWriter StandardInput => Process.StandardInput;
+ public StreamReader StandardOutput => Process.StandardOutput;
+ public StreamReader StandardError => Process.StandardError;
+ public int ExitCode => Process.ExitCode;
+
+ public static ChildProcess Start(ITrace2 trace2, ProcessStartInfo startInfo, Trace2ProcessClass processClass)
+ {
+ var childProc = new ChildProcess(trace2, startInfo);
+ childProc.Start(processClass);
+ return childProc;
+ }
+
+ public ChildProcess(ITrace2 trace2, ProcessStartInfo startInfo)
+ {
+ _trace2 = trace2;
+ Process = new Process() { StartInfo = startInfo };
+ Process.Exited += ProcessOnExited;
+ }
+
+ public bool Start(Trace2ProcessClass processClass)
+ {
+ ThrowIfDisposed();
+ // Record the time just before the process starts, since:
+ // (1) There is no event related to Start as there is with Exit.
+ // (2) Using Process.StartTime causes a race condition that leads
+ // to an exception if the process finishes executing before the
+ // variable is passed to Trace2.
+ _startTime = DateTimeOffset.UtcNow;
+ _trace2.WriteChildStart(
+ _startTime,
+ processClass,
+ _startInfo.UseShellExecute,
+ _startInfo.FileName,
+ _startInfo.Arguments);
+ return Process.Start();
+ }
+
+ public void WaitForExit() => Process.WaitForExit();
+
+ public void Kill() => Process.Kill();
+
+ protected override void ReleaseManagedResources()
+ {
+ Process.Exited -= ProcessOnExited;
+ Process.Dispose();
+ base.ReleaseUnmanagedResources();
+ }
+
+ private void ProcessOnExited(object sender, EventArgs e)
+ {
+ if (sender is Process)
+ {
+ double elapsedTime = (_exitTime - _startTime).TotalSeconds;
+ _trace2.WriteChildExit(
+ elapsedTime,
+ _id,
+ Process.ExitCode);
+ }
+ }
+}
diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs
index d8a45fcb6..712db32e1 100644
--- a/src/shared/Core/CommandContext.cs
+++ b/src/shared/Core/CommandContext.cs
@@ -78,6 +78,11 @@ public interface ICommandContext : IDisposable
/// The current process environment.
///
IEnvironment Environment { get; }
+
+ ///
+ /// Process manager.
+ ///
+ IProcessManager ProcessManager { get; }
}
///
@@ -85,25 +90,27 @@ public interface ICommandContext : IDisposable
///
public class CommandContext : DisposableObject, ICommandContext
{
- public CommandContext(string[] argv)
+ public CommandContext()
{
- var applicationStartTime = DateTimeOffset.UtcNow;
ApplicationPath = GetEntryApplicationPath();
InstallationDirectory = GetInstallationDirectory();
Streams = new StandardStreams();
Trace = new Trace();
+ Trace2 = new Trace2(this);
if (PlatformUtils.IsWindows())
{
FileSystem = new WindowsFileSystem();
- SessionManager = new WindowsSessionManager();
Environment = new WindowsEnvironment(FileSystem);
- Terminal = new WindowsTerminal(Trace);
+ SessionManager = new WindowsSessionManager(Environment, FileSystem);
+ ProcessManager = new WindowsProcessManager(Trace2);
+ Terminal = new WindowsTerminal(Trace, Trace2);
string gitPath = GetGitPath(Environment, FileSystem, Trace);
Git = new GitProcess(
Trace,
- Environment,
+ Trace2,
+ ProcessManager,
gitPath,
FileSystem.GetCurrentDirectory()
);
@@ -112,13 +119,15 @@ public CommandContext(string[] argv)
else if (PlatformUtils.IsMacOS())
{
FileSystem = new MacOSFileSystem();
- SessionManager = new MacOSSessionManager();
Environment = new MacOSEnvironment(FileSystem);
- Terminal = new MacOSTerminal(Trace);
+ SessionManager = new MacOSSessionManager(Environment, FileSystem);
+ ProcessManager = new ProcessManager(Trace2);
+ Terminal = new MacOSTerminal(Trace, Trace2);
string gitPath = GetGitPath(Environment, FileSystem, Trace);
Git = new GitProcess(
Trace,
- Environment,
+ Trace2,
+ ProcessManager,
gitPath,
FileSystem.GetCurrentDirectory()
);
@@ -127,14 +136,15 @@ public CommandContext(string[] argv)
else if (PlatformUtils.IsLinux())
{
FileSystem = new LinuxFileSystem();
- // TODO: support more than just 'Posix' or X11
- SessionManager = new PosixSessionManager();
Environment = new PosixEnvironment(FileSystem);
- Terminal = new LinuxTerminal(Trace);
+ SessionManager = new LinuxSessionManager(Environment, FileSystem);
+ ProcessManager = new ProcessManager(Trace2);
+ Terminal = new LinuxTerminal(Trace, Trace2);
string gitPath = GetGitPath(Environment, FileSystem, Trace);
Git = new GitProcess(
Trace,
- Environment,
+ Trace2,
+ ProcessManager,
gitPath,
FileSystem.GetCurrentDirectory()
);
@@ -145,8 +155,7 @@ public CommandContext(string[] argv)
throw new PlatformNotSupportedException();
}
- Trace2 = new Trace2(Environment, Settings.GetTrace2Settings(), argv, applicationStartTime);
- HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Settings, Streams);
+ HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Trace2, Settings, Streams);
CredentialStore = new CredentialStore(this);
}
@@ -210,6 +219,8 @@ private static string GetGitPath(IEnvironment environment, IFileSystem fileSyste
public IEnvironment Environment { get; }
+ public IProcessManager ProcessManager { get; }
+
#endregion
#region IDisposable
diff --git a/src/shared/Core/Commands/DiagnoseCommand.cs b/src/shared/Core/Commands/DiagnoseCommand.cs
index b8b4aaa56..11b15d9f5 100644
--- a/src/shared/Core/Commands/DiagnoseCommand.cs
+++ b/src/shared/Core/Commands/DiagnoseCommand.cs
@@ -26,11 +26,11 @@ public DiagnoseCommand(ICommandContext context)
_diagnostics = new List
{
// Add standard diagnostics
- new EnvironmentDiagnostic(context.Environment),
- new FileSystemDiagnostic(context.FileSystem),
- new NetworkingDiagnostic(context.HttpClientFactory),
- new GitDiagnostic(context.Git),
- new CredentialStoreDiagnostic(context.CredentialStore),
+ new EnvironmentDiagnostic(context),
+ new FileSystemDiagnostic(context),
+ new NetworkingDiagnostic(context),
+ new GitDiagnostic(context),
+ new CredentialStoreDiagnostic(context),
new MicrosoftAuthenticationDiagnostic(context)
};
diff --git a/src/shared/Core/Commands/GitCommandBase.cs b/src/shared/Core/Commands/GitCommandBase.cs
index 2bb6d0fe5..4db6974e4 100644
--- a/src/shared/Core/Commands/GitCommandBase.cs
+++ b/src/shared/Core/Commands/GitCommandBase.cs
@@ -58,22 +58,24 @@ protected virtual void EnsureMinimumInputArguments(InputArguments input)
{
if (input.Protocol is null)
{
- throw new InvalidOperationException("Missing 'protocol' input argument");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'protocol' input argument");
}
if (string.IsNullOrWhiteSpace(input.Protocol))
{
- throw new InvalidOperationException("Invalid 'protocol' input argument (cannot be empty)");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "Invalid 'protocol' input argument (cannot be empty)");
}
if (input.Host is null)
{
- throw new InvalidOperationException("Missing 'host' input argument");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'host' input argument");
}
if (string.IsNullOrWhiteSpace(input.Host))
{
- throw new InvalidOperationException("Invalid 'host' input argument (cannot be empty)");
+ throw new Trace2InvalidOperationException(Context.Trace2,
+ "Invalid 'host' input argument (cannot be empty)");
}
}
diff --git a/src/shared/Core/Commands/StoreCommand.cs b/src/shared/Core/Commands/StoreCommand.cs
index 7a4f078d5..8085e87ed 100644
--- a/src/shared/Core/Commands/StoreCommand.cs
+++ b/src/shared/Core/Commands/StoreCommand.cs
@@ -23,12 +23,12 @@ protected override void EnsureMinimumInputArguments(InputArguments input)
// An empty string username/password are valid inputs, so only check for `null` (not provided)
if (input.UserName is null)
{
- throw new InvalidOperationException("Missing 'username' input argument");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'username' input argument");
}
if (input.Password is null)
{
- throw new InvalidOperationException("Missing 'password' input argument");
+ throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'password' input argument");
}
}
}
diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs
index 6fe006716..84ddc2133 100644
--- a/src/shared/Core/Constants.cs
+++ b/src/shared/Core/Constants.cs
@@ -5,6 +5,7 @@ namespace GitCredentialManager
{
public static class Constants
{
+ public const string DefaultWindowTitle = "Git Credential Manager";
public const string PersonalAccessTokenUserName = "PersonalAccessToken";
public const string DefaultCredentialNamespace = "git";
public const int DefaultAutoDetectProviderTimeoutMs = 2000; // 2 seconds
@@ -15,6 +16,8 @@ public static class Constants
public const string GcmDataDirectoryName = ".gcm";
+ public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");
+
public static class CredentialStoreNames
{
public const string WindowsCredentialManager = "wincredman";
@@ -56,6 +59,7 @@ public static class EnvironmentVariables
public const string GcmAllowWia = "GCM_ALLOW_WINDOWSAUTH";
public const string GitTrace2Event = "GIT_TRACE2_EVENT";
public const string GitTrace2Normal = "GIT_TRACE2";
+ public const string GitTrace2Performance = "GIT_TRACE2_PERF";
/*
* Unlike other environment variables, these proxy variables are normally lowercase only.
@@ -81,6 +85,7 @@ public static class EnvironmentVariables
public const string GcmParentWindow = "GCM_MODAL_PARENTHWND";
public const string MsAuthFlow = "GCM_MSAUTH_FLOW";
public const string MsAuthUseBroker = "GCM_MSAUTH_USEBROKER";
+ public const string MsAuthUseDefaultAccount = "GCM_MSAUTH_USEDEFAULTACCOUNT";
public const string GcmCredNamespace = "GCM_NAMESPACE";
public const string GcmCredentialStore = "GCM_CREDENTIAL_STORE";
public const string GcmCredCacheOptions = "GCM_CREDENTIAL_CACHE_OPTIONS";
@@ -101,6 +106,7 @@ public static class EnvironmentVariables
public const string OAuthDeviceEndpoint = "GCM_OAUTH_DEVICE_ENDPOINT";
public const string OAuthClientAuthHeader = "GCM_OAUTH_USE_CLIENT_AUTH_HEADER";
public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME";
+ public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS";
}
public static class Http
@@ -119,6 +125,10 @@ public static class Credential
{
public const string SectionName = "credential";
public const string Helper = "helper";
+ public const string Trace = "trace";
+ public const string TraceSecrets = "traceSecrets";
+ public const string TraceMsAuth = "traceMsAuth";
+ public const string Debug = "debug";
public const string Provider = "provider";
public const string Authority = "authority";
public const string AllowWia = "allowWindowsAuth";
@@ -137,6 +147,8 @@ public static class Credential
public const string AutoDetectTimeout = "autoDetectTimeout";
public const string GuiPromptsEnabled = "guiPrompt";
public const string UiHelper = "uiHelper";
+ public const string DevUseLegacyUiHelpers = "devUseLegacyUiHelpers";
+ public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount";
public const string OAuthAuthenticationModes = "oauthAuthModes";
public const string OAuthClientId = "oauthClientId";
@@ -158,6 +170,7 @@ public static class Http
public const string SslBackend = "sslBackend";
public const string SslVerify = "sslVerify";
public const string SslCaInfo = "sslCAInfo";
+ public const string SslAutoClientCert = "sslAutoClientCert";
}
public static class Remote
@@ -169,9 +182,10 @@ public static class Remote
public static class Trace2
{
- public const string SectionName = "trace2";
- public const string EventTarget = "eventtarget";
- public const string NormalTarget = "normaltarget";
+ public const string SectionName = "trace2";
+ public const string EventTarget = "eventtarget";
+ public const string NormalTarget = "normaltarget";
+ public const string PerformanceTarget = "perftarget";
}
}
@@ -179,6 +193,10 @@ public static class WindowsRegistry
{
public const string HKAppBasePath = @"SOFTWARE\GitCredentialManager";
public const string HKConfigurationPath = HKAppBasePath + @"\Configuration";
+
+ public const string HKWindows365Path = @"SOFTWARE\Microsoft\Windows365";
+ public const string IsW365EnvironmentKeyName = "IsW365Environment";
+ public const string W365PartnerIdKeyName = "PartnerId";
}
public static class HelpUrls
@@ -192,6 +210,7 @@ public static class HelpUrls
public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin";
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
public const string GcmExecRename = "https://aka.ms/gcm/rename";
+ public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount";
}
private static Version _gcmVersion;
@@ -227,9 +246,9 @@ public static Version GcmVersion
/// Get the HTTP user-agent for Git Credential Manager.
///
/// User-agent string for HTTP requests.
- public static string GetHttpUserAgent()
+ public static string GetHttpUserAgent(ITrace2 trace2)
{
- PlatformInformation info = PlatformUtils.GetPlatformInformation();
+ PlatformInformation info = PlatformUtils.GetPlatformInformation(trace2);
string osType = info.OperatingSystemType;
string cpuArch = info.CpuArchitecture;
string clrVersion = info.ClrVersion;
diff --git a/src/shared/Core/Core.csproj b/src/shared/Core/Core.csproj
index 835d2e038..3376b435d 100644
--- a/src/shared/Core/Core.csproj
+++ b/src/shared/Core/Core.csproj
@@ -13,14 +13,25 @@
-
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/Core/CredentialStore.cs b/src/shared/Core/CredentialStore.cs
index bf8c0be47..8245b41c9 100644
--- a/src/shared/Core/CredentialStore.cs
+++ b/src/shared/Core/CredentialStore.cs
@@ -79,7 +79,7 @@ private void EnsureBackingStore()
case StoreNames.Gpg:
ValidateGpgPass(out string gpgStoreRoot, out string gpgExec);
- IGpg gpg = new Gpg(gpgExec, _context.SessionManager);
+ IGpg gpg = new Gpg(gpgExec, _context.SessionManager, _context.ProcessManager, _context.Trace2);
_backingStore = new GpgPassCredentialStore(_context.FileSystem, gpg, gpgStoreRoot, ns);
break;
@@ -98,6 +98,7 @@ private void EnsureBackingStore()
sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName)
? "No credential store has been selected."
: $"Unknown credential store '{credStoreName}'.");
+ _context.Trace2.WriteError(sb.ToString());
sb.AppendFormat(
"{3}Set the {0} environment variable or the {1}.{2} Git configuration setting to one of the following options:{3}{3}",
Constants.EnvironmentVariables.GcmCredentialStore,
@@ -166,18 +167,18 @@ private void ValidateWindowsCredentialManager()
{
if (!PlatformUtils.IsWindows())
{
- throw new Exception(
- $"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = $"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
if (!WindowsCredentialManager.CanPersist())
{
- throw new Exception(
- $"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store." +
- Environment.NewLine +
+ var message = $"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
$"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
@@ -187,9 +188,9 @@ private void ValidateDpapi(out string storeRoot)
{
if (!PlatformUtils.IsWindows())
{
- throw new Exception(
- $"Can only use the '{StoreNames.Dpapi}' credential store on Windows." +
- Environment.NewLine +
+ var message = $"Can only use the '{StoreNames.Dpapi}' credential store on Windows.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
$"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
@@ -210,10 +211,10 @@ private void ValidateMacOSKeychain()
{
if (!PlatformUtils.IsMacOS())
{
- throw new Exception(
- $"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = $"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
}
@@ -222,19 +223,19 @@ private void ValidateSecretService()
{
if (!PlatformUtils.IsLinux())
{
- throw new Exception(
- $"Can only use the '{StoreNames.SecretService}' credential store on Linux." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = $"Can only use the '{StoreNames.SecretService}' credential store on Linux.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
if (!_context.SessionManager.IsDesktopSession)
{
- throw new Exception(
- $"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = $"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
}
@@ -243,10 +244,10 @@ private void ValidateGpgPass(out string storeRoot, out string execPath)
{
if (!PlatformUtils.IsPosix())
{
- throw new Exception(
- $"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = $"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
@@ -258,10 +259,10 @@ private void ValidateGpgPass(out string storeRoot, out string execPath)
!_context.Environment.Variables.ContainsKey("GPG_TTY") &&
!_context.Environment.Variables.ContainsKey("SSH_TTY"))
{
- throw new Exception(
- "GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = "GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
@@ -279,10 +280,12 @@ private void ValidateGpgPass(out string storeRoot, out string execPath)
string gpgIdFile = Path.Combine(storeRoot, ".gpg-id");
if (!_context.FileSystem.FileExists(gpgIdFile))
{
- throw new Exception(
- $"Password store has not been initialized at '{storeRoot}'; run `pass init ` to initialize the store." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var format =
+ "Password store has not been initialized at '{0}'; run `pass init ` to initialize the store.";
+ var message = string.Format(format, storeRoot);
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
}
@@ -291,10 +294,10 @@ private void ValidateCredentialCache(out string options)
{
if (PlatformUtils.IsWindows())
{
- throw new Exception(
- $"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows." +
- Environment.NewLine +
- $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
+ var message = $"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows.";
+ _context.Trace2.WriteError(message);
+ throw new Exception(message + Environment.NewLine +
+ $"See {Constants.HelpUrls.GcmCredentialStores} for more information."
);
}
@@ -337,7 +340,9 @@ private string GetGpgPath()
return gpgPath;
}
- throw new Exception($"GPG executable does not exist with path '{gpgPath}'");
+ var format = "GPG executable does not exist with path '{0}'";
+ var message = string.Format(format, gpgPath);
+ throw new Trace2Exception(_context.Trace2, message, format);
}
// If no explicit GPG path is specified, mimic the way `pass`
diff --git a/src/shared/Core/Diagnostics/CredentialStoreDiagnostic.cs b/src/shared/Core/Diagnostics/CredentialStoreDiagnostic.cs
index 2e77c214c..74f9ca2ed 100644
--- a/src/shared/Core/Diagnostics/CredentialStoreDiagnostic.cs
+++ b/src/shared/Core/Diagnostics/CredentialStoreDiagnostic.cs
@@ -7,19 +7,13 @@ namespace GitCredentialManager.Diagnostics
{
public class CredentialStoreDiagnostic : Diagnostic
{
- private readonly ICredentialStore _credentialStore;
-
- public CredentialStoreDiagnostic(ICredentialStore credentialStore)
- : base("Credential storage")
- {
- EnsureArgument.NotNull(credentialStore, nameof(credentialStore));
-
- _credentialStore = credentialStore;
- }
+ public CredentialStoreDiagnostic(ICommandContext commandContext)
+ : base("Credential storage", commandContext)
+ { }
protected override Task RunInternalAsync(StringBuilder log, IList additionalFiles)
{
- log.AppendLine($"ICredentialStore instance is of type: {_credentialStore.GetType().Name}");
+ log.AppendLine($"ICredentialStore instance is of type: {CommandContext.CredentialStore.GetType().Name}");
// Create a service that is guaranteed to be unique
string service = $"https://example.com/{Guid.NewGuid():N}";
@@ -29,11 +23,11 @@ protected override Task RunInternalAsync(StringBuilder log, IList
try
{
log.Append("Writing test credential...");
- _credentialStore.AddOrUpdate(service, account, password);
+ CommandContext.CredentialStore.AddOrUpdate(service, account, password);
log.AppendLine(" OK");
log.Append("Reading test credential...");
- ICredential outCredential = _credentialStore.Get(service, account);
+ ICredential outCredential = CommandContext.CredentialStore.Get(service, account);
if (outCredential is null)
{
log.AppendLine(" Failed");
@@ -62,7 +56,7 @@ protected override Task RunInternalAsync(StringBuilder log, IList
finally
{
log.Append("Deleting test credential...");
- _credentialStore.Remove(service, account);
+ CommandContext.CredentialStore.Remove(service, account);
log.AppendLine(" OK");
}
diff --git a/src/shared/Core/Diagnostics/Diagnostic.cs b/src/shared/Core/Diagnostics/Diagnostic.cs
index 7db135ee6..45813bd1e 100644
--- a/src/shared/Core/Diagnostics/Diagnostic.cs
+++ b/src/shared/Core/Diagnostics/Diagnostic.cs
@@ -16,9 +16,12 @@ public interface IDiagnostic
public abstract class Diagnostic : IDiagnostic
{
- protected Diagnostic(string name)
+ protected ICommandContext CommandContext;
+
+ protected Diagnostic(string name, ICommandContext commandContext)
{
Name = name;
+ CommandContext = commandContext;
}
public string Name { get; }
diff --git a/src/shared/Core/Diagnostics/EnvironmentDiagnostic.cs b/src/shared/Core/Diagnostics/EnvironmentDiagnostic.cs
index f2a84fb65..dbec71f02 100644
--- a/src/shared/Core/Diagnostics/EnvironmentDiagnostic.cs
+++ b/src/shared/Core/Diagnostics/EnvironmentDiagnostic.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
@@ -8,19 +9,13 @@ namespace GitCredentialManager.Diagnostics
{
public class EnvironmentDiagnostic : Diagnostic
{
- private readonly IEnvironment _env;
-
- public EnvironmentDiagnostic(IEnvironment env)
- : base("Environment")
- {
- EnsureArgument.NotNull(env, nameof(env));
-
- _env = env;
- }
+ public EnvironmentDiagnostic(ICommandContext commandContext)
+ : base("Environment", commandContext)
+ { }
protected override Task RunInternalAsync(StringBuilder log, IList additionalFiles)
{
- PlatformInformation platformInfo = PlatformUtils.GetPlatformInformation();
+ PlatformInformation platformInfo = PlatformUtils.GetPlatformInformation(CommandContext.Trace2);
log.AppendLine($"OSType: {platformInfo.OperatingSystemType}");
log.AppendLine($"OSVersion: {platformInfo.OperatingSystemVersion}");
diff --git a/src/shared/Core/Diagnostics/FileSystemDiagnostic.cs b/src/shared/Core/Diagnostics/FileSystemDiagnostic.cs
index 2455d9f56..a6711036f 100644
--- a/src/shared/Core/Diagnostics/FileSystemDiagnostic.cs
+++ b/src/shared/Core/Diagnostics/FileSystemDiagnostic.cs
@@ -8,15 +8,9 @@ namespace GitCredentialManager.Diagnostics
{
public class FileSystemDiagnostic : Diagnostic
{
- private readonly IFileSystem _fs;
-
- public FileSystemDiagnostic(IFileSystem fs)
- : base("File system")
- {
- EnsureArgument.NotNull(fs, nameof(fs));
-
- _fs = fs;
- }
+ public FileSystemDiagnostic(ICommandContext commandContext)
+ : base("File system", commandContext)
+ { }
protected override Task RunInternalAsync(StringBuilder log, IList additionalFiles)
{
@@ -49,9 +43,9 @@ protected override Task RunInternalAsync(StringBuilder log, IList
log.AppendLine(" OK");
log.AppendLine("Testing IFileSystem instance...");
- log.AppendLine($"UserHomePath: {_fs.UserHomePath}");
- log.AppendLine($"UserDataDirectoryPath: {_fs.UserDataDirectoryPath}");
- log.AppendLine($"GetCurrentDirectory(): {_fs.GetCurrentDirectory()}");
+ log.AppendLine($"UserHomePath: {CommandContext.FileSystem.UserHomePath}");
+ log.AppendLine($"UserDataDirectoryPath: {CommandContext.FileSystem.UserDataDirectoryPath}");
+ log.AppendLine($"GetCurrentDirectory(): {CommandContext.FileSystem.GetCurrentDirectory()}");
return Task.FromResult(true);
}
diff --git a/src/shared/Core/Diagnostics/GitDiagnostic.cs b/src/shared/Core/Diagnostics/GitDiagnostic.cs
index 8066f2aaf..e09f091dd 100644
--- a/src/shared/Core/Diagnostics/GitDiagnostic.cs
+++ b/src/shared/Core/Diagnostics/GitDiagnostic.cs
@@ -7,31 +7,25 @@ namespace GitCredentialManager.Diagnostics
{
public class GitDiagnostic : Diagnostic
{
- private readonly IGit _git;
-
- public GitDiagnostic(IGit git)
- : base("Git")
- {
- EnsureArgument.NotNull(git, nameof(git));
-
- _git = git;
- }
+ public GitDiagnostic(ICommandContext commandContext)
+ : base("Git", commandContext)
+ { }
protected override Task RunInternalAsync(StringBuilder log, IList additionalFiles)
{
log.Append("Getting Git version...");
- GitVersion gitVersion = _git.Version;
+ GitVersion gitVersion = CommandContext.Git.Version;
log.AppendLine(" OK");
log.AppendLine($"Git version is '{gitVersion.OriginalString}'");
log.Append("Locating current repository...");
- string thisRepo =_git.GetCurrentRepository();
+ string thisRepo =CommandContext.Git.GetCurrentRepository();
log.AppendLine(" OK");
log.AppendLine(thisRepo is null ? "Not inside a Git repository." : $"Git repository at '{thisRepo}'");
log.Append("Listing all Git configuration...");
- Process configProc = _git.CreateProcess("config --list --show-origin");
- configProc.Start();
+ ChildProcess configProc = CommandContext.Git.CreateProcess("config --list --show-origin");
+ configProc.Start(Trace2ProcessClass.Git);
// To avoid deadlocks, always read the output stream first and then wait
// TODO: don't read in all the data at once; stream it
string gitConfig = configProc.StandardOutput.ReadToEnd().TrimEnd();
diff --git a/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs b/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs
index 9e3d1aaa0..05ed9200c 100644
--- a/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs
+++ b/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs
@@ -9,39 +9,14 @@ namespace GitCredentialManager.Diagnostics
{
public class MicrosoftAuthenticationDiagnostic : Diagnostic
{
- private readonly ICommandContext _context;
-
public MicrosoftAuthenticationDiagnostic(ICommandContext context)
- : base("Microsoft authentication (AAD/MSA)")
- {
- EnsureArgument.NotNull(context, nameof(context));
-
- _context = context;
- }
+ : base("Microsoft authentication (AAD/MSA)", context)
+ { }
protected override async Task RunInternalAsync(StringBuilder log, IList additionalFiles)
{
- if (MicrosoftAuthentication.CanUseBroker(_context))
- {
- log.Append("Checking broker initialization state...");
- if (MicrosoftAuthentication.IsBrokerInitialized)
- {
- log.AppendLine(" Initialized");
- }
- else
- {
- log.AppendLine(" Not initialized");
- log.Append("Initializing broker...");
- MicrosoftAuthentication.InitializeBroker();
- log.AppendLine("OK");
- }
- }
- else
- {
- log.AppendLine("Broker not supported.");
- }
-
- var msAuth = new MicrosoftAuthentication(_context);
+ var msAuth = new MicrosoftAuthentication(CommandContext);
+ log.AppendLine(msAuth.CanUseBroker() ? "Broker is enabled." : "Broker is not enabled.");
log.AppendLine($"Flow type is: {msAuth.GetFlowType()}");
log.Append("Gathering MSAL token cache data...");
diff --git a/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs b/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs
index 6faee71fd..310b26361 100644
--- a/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs
+++ b/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs
@@ -11,23 +11,18 @@ namespace GitCredentialManager.Diagnostics
{
public class NetworkingDiagnostic : Diagnostic
{
- private readonly IHttpClientFactory _httpFactory;
private const string TestHttpUri = "http://example.com";
private const string TestHttpsUri = "https://example.com";
- public NetworkingDiagnostic(IHttpClientFactory httpFactory)
- : base("Networking")
- {
- EnsureArgument.NotNull(httpFactory, nameof(httpFactory));
-
- _httpFactory = httpFactory;
- }
+ public NetworkingDiagnostic(ICommandContext commandContext)
+ : base("Networking", commandContext)
+ { }
protected override async Task RunInternalAsync(StringBuilder log, IList additionalFiles)
{
log.AppendLine("Checking networking and HTTP stack...");
log.Append("Creating HTTP client...");
- using var httpClient = _httpFactory.CreateClient();
+ using var httpClient = CommandContext.HttpClientFactory.CreateClient();
log.AppendLine(" OK");
bool hasNetwork = NetworkInterface.GetIsNetworkAvailable();
diff --git a/src/shared/Core/EnvironmentBase.cs b/src/shared/Core/EnvironmentBase.cs
index 63790589a..6a3967193 100644
--- a/src/shared/Core/EnvironmentBase.cs
+++ b/src/shared/Core/EnvironmentBase.cs
@@ -45,18 +45,6 @@ public interface IEnvironment
/// True if the executable was found, false otherwise.
bool TryLocateExecutable(string program, out string path);
- ///
- /// Create a process ready to start, with redirected streams.
- ///
- /// Absolute file path of executable or command to start.
- /// Command line arguments to pass to executable.
- ///
- /// True to resolve using the OS shell, false to use as an absolute file path.
- ///
- /// Working directory for the new process.
- /// object ready to start.
- Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory);
-
///
/// Set an environment variable at the specified target level.
///
@@ -65,20 +53,45 @@ public interface IEnvironment
/// Target level of environment variable to set (Machine, Process, or User).
void SetEnvironmentVariable(string variable, string value,
EnvironmentVariableTarget target = EnvironmentVariableTarget.Process);
+
+ ///
+ /// Refresh the current process environment variables. See .
+ ///
+ /// This is automatically called after .
+ void Refresh();
}
public abstract class EnvironmentBase : IEnvironment
{
+ private IReadOnlyDictionary _variables;
+
protected EnvironmentBase(IFileSystem fileSystem)
{
EnsureArgument.NotNull(fileSystem, nameof(fileSystem));
-
FileSystem = fileSystem;
}
- public IReadOnlyDictionary Variables { get; protected set; }
+ internal EnvironmentBase(IFileSystem fileSystem, IReadOnlyDictionary variables)
+ : this(fileSystem)
+ {
+ EnsureArgument.NotNull(variables, nameof(variables));
+ _variables = variables;
+ }
+
+ public IReadOnlyDictionary Variables
+ {
+ get
+ {
+ // Variables are lazily loaded
+ if (_variables is null)
+ {
+ Refresh();
+ }
- protected ITrace Trace { get; }
+ Debug.Assert(_variables != null);
+ return _variables;
+ }
+ }
protected IFileSystem FileSystem { get; }
@@ -99,20 +112,6 @@ public bool IsDirectoryOnPath(string directoryPath)
protected abstract string[] SplitPathVariable(string value);
- public virtual Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
- {
- var psi = new ProcessStartInfo(path, args)
- {
- RedirectStandardInput = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = useShellExecute,
- WorkingDirectory = workingDirectory ?? string.Empty
- };
-
- return new Process { StartInfo = psi };
- }
-
public virtual bool TryLocateExecutable(string program, out string path)
{
return TryLocateExecutable(program, null, out path);
@@ -154,9 +153,22 @@ internal virtual bool TryLocateExecutable(string program, ICollection pa
public void SetEnvironmentVariable(string variable, string value,
EnvironmentVariableTarget target = EnvironmentVariableTarget.Process)
{
- if (Variables.Keys.Contains(variable)) return;
+ // Don't bother setting the variable if it already has the same value
+ if (Variables.TryGetValue(variable, out var currentValue) &&
+ StringComparer.Ordinal.Equals(currentValue, value))
+ {
+ return;
+ }
+
Environment.SetEnvironmentVariable(variable, value, target);
- Variables = GetCurrentVariables();
+
+ // Immediately refresh the variables so that the new value is available to callers using IEnvironment
+ Refresh();
+ }
+
+ public void Refresh()
+ {
+ _variables = GetCurrentVariables();
}
protected abstract IReadOnlyDictionary GetCurrentVariables();
@@ -181,30 +193,16 @@ public static string LocateExecutable(this IEnvironment environment, string prog
}
///
- /// Create a process ready to start, with redirected streams.
- ///
- /// The .
- /// Absolute file path of executable or command to start.
- /// Command line arguments to pass to executable.
- ///
- /// True to resolve using the OS shell, false to use as an absolute file path.
- ///
- /// object ready to start.
- public static Process CreateProcess(this IEnvironment environment, string path, string args, bool useShellExecute)
- {
- return environment.CreateProcess(path, args, useShellExecute, string.Empty);
- }
-
- ///
- /// Create a process ready to start, with redirected streams.
+ /// Retrieves the value of an environment variable from the current process.
///
/// The .
- /// Absolute file path of executable to start.
- /// Command line arguments to pass to executable.
- /// object ready to start.
- public static Process CreateProcess(this IEnvironment environment, string path, string args)
+ /// The name of the environment variable.
+ ///
+ /// The value of the environment variable specified by variable, or null if the environment variable is not found.
+ ///
+ public static string GetEnvironmentVariable(this IEnvironment environment, string variable)
{
- return environment.CreateProcess(path, args, false, string.Empty);
+ return environment.Variables.TryGetValue(variable, out string value) ? value : null;
}
}
}
diff --git a/src/shared/Core/FileSystem.cs b/src/shared/Core/FileSystem.cs
index c1cdcfd13..aeacfd51d 100644
--- a/src/shared/Core/FileSystem.cs
+++ b/src/shared/Core/FileSystem.cs
@@ -84,6 +84,29 @@ public interface IFileSystem
///
IEnumerable EnumerateFiles(string path, string searchPattern);
+ ///
+ /// Returns an enumerable collection of directory full names in a specified path.
+ ///
+ /// The relative or absolute path to the directory to search. This string is not case-sensitive.
+ ///
+ /// An enumerable collection of the full names (including paths) for the directories
+ /// in the directory specified by path.
+ ///
+ IEnumerable EnumerateDirectories(string path);
+
+ ///
+ /// Opens a text file, reads all the text in the file, and then closes the file
+ ///
+ /// The file to open for reading.
+ /// A string containing all the text in the file.
+ string ReadAllText(string path);
+
+ ///
+ /// Opens a text file, reads all lines of the file, and then closes the file.
+ ///
+ /// The file to open for reading.
+ /// A string array containing all lines of the file.
+ string[] ReadAllLines(string path);
}
///
@@ -111,5 +134,11 @@ public Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAcce
public void DeleteFile(string path) => File.Delete(path);
public IEnumerable EnumerateFiles(string path, string searchPattern) => Directory.EnumerateFiles(path, searchPattern);
+
+ public IEnumerable EnumerateDirectories(string path) => Directory.EnumerateDirectories(path);
+
+ public string ReadAllText(string path) => File.ReadAllText(path);
+
+ public string[] ReadAllLines(string path) => File.ReadAllLines(path);
}
}
diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs
index 2b05537e1..cdba0ba8a 100644
--- a/src/shared/Core/GenericHostProvider.cs
+++ b/src/shared/Core/GenericHostProvider.cs
@@ -76,7 +76,7 @@ public override async Task GenerateCredentialAsync(InputArguments i
Context.Trace.WriteLine($"\tUseAuthHeader = {oauthConfig.UseAuthHeader}");
Context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}");
- return await GetOAuthAccessToken(uri, input.UserName, oauthConfig);
+ return await GetOAuthAccessToken(uri, input.UserName, oauthConfig, Context.Trace2);
}
// Try detecting WIA for this remote, if permitted
else if (IsWindowsAuthAllowed)
@@ -100,7 +100,7 @@ public override async Task GenerateCredentialAsync(InputArguments i
}
else
{
- string osType = PlatformUtils.GetPlatformInformation().OperatingSystemType;
+ string osType = PlatformUtils.GetPlatformInformation(Context.Trace2).OperatingSystemType;
Context.Trace.WriteLine($"Skipping check for Windows Integrated Authentication on {osType}.");
}
}
@@ -114,7 +114,7 @@ public override async Task GenerateCredentialAsync(InputArguments i
return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName);
}
- private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config)
+ private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config, ITrace2 trace2)
{
// TODO: Determined user info from a webcall? ID token? Need OIDC support
string oauthUser = userName ?? config.DefaultUserName;
@@ -123,9 +123,9 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa
HttpClient,
config.Endpoints,
config.ClientId,
+ trace2,
config.RedirectUri,
config.ClientSecret,
- Context.Trace,
config.UseAuthHeader);
//
@@ -143,16 +143,26 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa
ICredential refreshToken = Context.CredentialStore.Get(refreshService, userName);
if (refreshToken != null)
{
- var refreshResult = await client.GetTokenByRefreshTokenAsync(refreshToken.Password, CancellationToken.None);
+ try
+ {
+ var refreshResult = await client.GetTokenByRefreshTokenAsync(refreshToken.Password, CancellationToken.None);
- // Store new refresh token if we have been given one
- if (!string.IsNullOrWhiteSpace(refreshResult.RefreshToken))
- {
- Context.CredentialStore.AddOrUpdate(refreshService, refreshToken.Account, refreshToken.Password);
- }
+ // Store new refresh token if we have been given one
+ if (!string.IsNullOrWhiteSpace(refreshResult.RefreshToken))
+ {
+ Context.CredentialStore.AddOrUpdate(refreshService, refreshToken.Account, refreshToken.Password);
+ }
- // Return the new access token
- return new GitCredential(oauthUser,refreshResult.AccessToken);
+ // Return the new access token
+ return new GitCredential(oauthUser,refreshResult.AccessToken);
+ }
+ catch (OAuth2Exception ex)
+ {
+ // Failed to use refresh token. It may have expired or been revoked.
+ // Fall through to an interactive OAuth flow.
+ Context.Trace.WriteLine("Failed to use refresh token.");
+ Context.Trace.WriteException(ex);
+ }
}
// Determine which interactive OAuth mode to use. Start by checking for mode preference in config
@@ -194,7 +204,7 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa
break;
default:
- throw new Exception("No authentication mode selected!");
+ throw new Trace2Exception(Context.Trace2, "No authentication mode selected!");
}
// Store the refresh token if we have one
diff --git a/src/shared/Core/Git.cs b/src/shared/Core/Git.cs
index d83254413..b63e0fe3d 100644
--- a/src/shared/Core/Git.cs
+++ b/src/shared/Core/Git.cs
@@ -18,7 +18,7 @@ public interface IGit
///
/// Arguments to pass to the Git process.
/// Process object ready to be started.
- Process CreateProcess(string args);
+ ChildProcess CreateProcess(string args);
///
/// Return the path to the current repository, or null if this instance is not
@@ -65,18 +65,21 @@ public GitRemote(string name, string fetchUrl, string pushUrl)
public class GitProcess : IGit
{
private readonly ITrace _trace;
- private readonly IEnvironment _environment;
+ private readonly ITrace2 _trace2;
+ private readonly IProcessManager _processManager;
private readonly string _gitPath;
private readonly string _workingDirectory;
- public GitProcess(ITrace trace, IEnvironment environment, string gitPath, string workingDirectory = null)
+ public GitProcess(ITrace trace, ITrace2 trace2, IProcessManager processManager, string gitPath, string workingDirectory = null)
{
EnsureArgument.NotNull(trace, nameof(trace));
- EnsureArgument.NotNull(environment, nameof(environment));
+ EnsureArgument.NotNull(trace2, nameof(trace2));
+ EnsureArgument.NotNull(processManager, nameof(processManager));
EnsureArgument.NotNullOrWhiteSpace(gitPath, nameof(gitPath));
_trace = trace;
- _environment = environment;
+ _trace2 = trace2;
+ _processManager = processManager;
_gitPath = gitPath;
_workingDirectory = workingDirectory;
}
@@ -90,7 +93,7 @@ public GitVersion Version
{
using (var git = CreateProcess("version"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
string data = git.StandardOutput.ReadToEnd();
git.WaitForExit();
@@ -120,9 +123,7 @@ public string GetCurrentRepository()
{
using (var git = CreateProcess("rev-parse --absolute-git-dir"))
{
- git.Start();
- // To avoid deadlocks, always read the output stream first and then wait
- // TODO: don't read in all the data at once; stream it
+ git.Start(Trace2ProcessClass.Git);
string data = git.StandardOutput.ReadToEnd();
git.WaitForExit();
@@ -133,8 +134,9 @@ public string GetCurrentRepository()
case 128: // Not inside a Git repository
return null;
default:
- _trace.WriteLine($"Failed to get current Git repository (exit={git.ExitCode})");
- throw CreateGitException(git, "Failed to get current Git repository");
+ var message = "Failed to get current Git repository";
+ _trace.WriteLine($"{message} (exit={git.ExitCode})");
+ throw CreateGitException(git, message, _trace2);
}
}
}
@@ -143,7 +145,7 @@ public IEnumerable GetRemotes()
{
using (var git = CreateProcess("remote -v show"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
// To avoid deadlocks, always read the output stream first and then wait
// TODO: don't read in all the data at once; stream it
string data = git.StandardOutput.ReadToEnd();
@@ -157,8 +159,9 @@ public IEnumerable GetRemotes()
case 128 when stderr.Contains("not a git repository"): // Not inside a Git repository
yield break;
default:
- _trace.WriteLine($"Failed to enumerate Git remotes (exit={git.ExitCode})");
- throw CreateGitException(git, "Failed to enumerate Git remotes");
+ var message = "Failed to enumerate Git remotes";
+ _trace.WriteLine($"{message} (exit={git.ExitCode})");
+ throw CreateGitException(git, message, _trace2);
}
string[] lines = data.Split('\n');
@@ -184,13 +187,13 @@ public IEnumerable GetRemotes()
}
}
- public Process CreateProcess(string args)
+ public ChildProcess CreateProcess(string args)
{
- return _environment.CreateProcess(_gitPath, args, false, _workingDirectory);
+ return _processManager.CreateProcess(_gitPath, args, false, _workingDirectory);
}
// This code was originally copied from
- // src/shared/GitCredentialManager/Authentication/AuthenticationBase.cs
+ // src/shared/Core/Authentication/AuthenticationBase.cs
// That code is for GUI helpers in this codebase, while the below is for
// communicating over Git's stdin/stdout helper protocol. The GUI helper
// protocol will one day use a different IPC mechanism, whereas this code
@@ -206,10 +209,12 @@ public async Task> InvokeHelperAsync(string args, ID
UseShellExecute = false
};
- var process = Process.Start(procStartInfo);
- if (process is null)
+ var process = _processManager.CreateProcess(procStartInfo);
+ if (!process.Start(Trace2ProcessClass.Git))
{
- throw new Exception($"Failed to start Git helper '{args}'");
+ var format = "Failed to start Git helper '{0}'";
+ var message = string.Format(format, args);
+ throw new Trace2Exception(_trace2, message, format);
}
if (!(standardInput is null))
@@ -238,9 +243,13 @@ public async Task> InvokeHelperAsync(string args, ID
return resultDict;
}
- public static GitException CreateGitException(Process git, string message)
+ public static GitException CreateGitException(ChildProcess git, string message, ITrace2 trace2 = null)
{
- string gitMessage = git.StandardError.ReadToEnd();
+ var gitMessage = git.StandardError.ReadToEnd();
+
+ if (trace2 != null)
+ throw new Trace2GitException(trace2, message, git.ExitCode, gitMessage);
+
throw new GitException(message, gitMessage, git.ExitCode);
}
}
diff --git a/src/shared/Core/GitConfiguration.cs b/src/shared/Core/GitConfiguration.cs
index e1412cf8c..9603b2db5 100644
--- a/src/shared/Core/GitConfiguration.cs
+++ b/src/shared/Core/GitConfiguration.cs
@@ -127,9 +127,9 @@ internal GitProcessConfiguration(ITrace trace, GitProcess git)
public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb)
{
string levelArg = GetLevelFilterArg(level);
- using (Process git = _git.CreateProcess($"config --null {levelArg} --list"))
+ using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} --list"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
// To avoid deadlocks, always read the output stream first and then wait
// TODO: don't read in all the data at once; stream it
string data = git.StandardOutput.ReadToEnd();
@@ -196,9 +196,12 @@ public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, strin
{
string levelArg = GetLevelFilterArg(level);
string typeArg = GetCanonicalizeTypeArg(type);
- using (Process git = _git.CreateProcess($"config --null {levelArg} {typeArg} {QuoteCmdArg(name)}"))
+ using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} {typeArg} {QuoteCmdArg(name)}"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
+ // To avoid deadlocks, always read the output stream first and then wait
+ // TODO: don't read in all the data at once; stream it
+ string data = git.StandardOutput.ReadToEnd();
git.WaitForExit();
switch (git.ExitCode)
@@ -214,7 +217,6 @@ public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, strin
return false;
}
- string data = git.StandardOutput.ReadToEnd();
string[] entries = data.Split('\0');
if (entries.Length > 0)
{
@@ -232,9 +234,9 @@ public void Set(GitConfigurationLevel level, string name, string value)
EnsureSpecificLevel(level);
string levelArg = GetLevelFilterArg(level);
- using (Process git = _git.CreateProcess($"config {levelArg} {QuoteCmdArg(name)} {QuoteCmdArg(value)}"))
+ using (ChildProcess git = _git.CreateProcess($"config {levelArg} {QuoteCmdArg(name)} {QuoteCmdArg(value)}"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
git.WaitForExit();
switch (git.ExitCode)
@@ -253,9 +255,9 @@ public void Add(GitConfigurationLevel level, string name, string value)
EnsureSpecificLevel(level);
string levelArg = GetLevelFilterArg(level);
- using (Process git = _git.CreateProcess($"config {levelArg} --add {QuoteCmdArg(name)} {QuoteCmdArg(value)}"))
+ using (ChildProcess git = _git.CreateProcess($"config {levelArg} --add {QuoteCmdArg(name)} {QuoteCmdArg(value)}"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
git.WaitForExit();
switch (git.ExitCode)
@@ -274,9 +276,9 @@ public void Unset(GitConfigurationLevel level, string name)
EnsureSpecificLevel(level);
string levelArg = GetLevelFilterArg(level);
- using (Process git = _git.CreateProcess($"config {levelArg} --unset {QuoteCmdArg(name)}"))
+ using (ChildProcess git = _git.CreateProcess($"config {levelArg} --unset {QuoteCmdArg(name)}"))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
git.WaitForExit();
switch (git.ExitCode)
@@ -298,10 +300,10 @@ public IEnumerable GetAll(GitConfigurationLevel level, GitConfigurationT
var gitArgs = $"config --null {levelArg} {typeArg} --get-all {QuoteCmdArg(name)}";
- using (Process git = _git.CreateProcess(gitArgs))
+ using (ChildProcess git = _git.CreateProcess(gitArgs))
{
- git.Start();
-
+ git.Start(Trace2ProcessClass.Git);
+ // To avoid deadlocks, always read the output stream first and then wait
// TODO: don't read in all the data at once; stream it
string data = git.StandardOutput.ReadToEnd();
git.WaitForExit();
@@ -340,9 +342,9 @@ public IEnumerable GetRegex(GitConfigurationLevel level, GitConfiguratio
gitArgs += $" {QuoteCmdArg(valueRegex)}";
}
- using (Process git = _git.CreateProcess(gitArgs))
+ using (ChildProcess git = _git.CreateProcess(gitArgs))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
// To avoid deadlocks, always read the output stream first and then wait
// TODO: don't read in all the data at once; stream it
string data = git.StandardOutput.ReadToEnd();
@@ -382,9 +384,9 @@ public void ReplaceAll(GitConfigurationLevel level, string name, string valueReg
gitArgs += $" {QuoteCmdArg(valueRegex)}";
}
- using (Process git = _git.CreateProcess(gitArgs))
+ using (ChildProcess git = _git.CreateProcess(gitArgs))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
git.WaitForExit();
switch (git.ExitCode)
@@ -409,9 +411,9 @@ public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex
gitArgs += $" {QuoteCmdArg(valueRegex)}";
}
- using (Process git = _git.CreateProcess(gitArgs))
+ using (ChildProcess git = _git.CreateProcess(gitArgs))
{
- git.Start();
+ git.Start(Trace2ProcessClass.Git);
git.WaitForExit();
switch (git.ExitCode)
diff --git a/src/shared/Core/Gpg.cs b/src/shared/Core/Gpg.cs
index 92de09be6..686cf0db9 100644
--- a/src/shared/Core/Gpg.cs
+++ b/src/shared/Core/Gpg.cs
@@ -14,14 +14,19 @@ public class Gpg : IGpg
{
private readonly string _gpgPath;
private readonly ISessionManager _sessionManager;
+ private readonly IProcessManager _processManager;
+ private readonly ITrace2 _trace2;
- public Gpg(string gpgPath, ISessionManager sessionManager)
+ public Gpg(string gpgPath, ISessionManager sessionManager, IProcessManager processManager, ITrace2 trace2)
{
EnsureArgument.NotNullOrWhiteSpace(gpgPath, nameof(gpgPath));
EnsureArgument.NotNull(sessionManager, nameof(sessionManager));
+ EnsureArgument.NotNull(trace2, nameof(trace2));
_gpgPath = gpgPath;
_sessionManager = sessionManager;
+ _processManager = processManager;
+ _trace2 = trace2;
}
public string DecryptFile(string path)
@@ -30,16 +35,18 @@ public string DecryptFile(string path)
{
UseShellExecute = false,
RedirectStandardOutput = true,
- RedirectStandardError = true, // Suppress verbose decryption messages
+ // Suppress verbose decryption messages
+ // Ok to redirect stderr for non-Git-related processes
+ RedirectStandardError = true,
};
PrepareEnvironment(psi);
- using (var gpg = Process.Start(psi))
+ using (var gpg = _processManager.CreateProcess(psi))
{
- if (gpg is null)
+ if (!gpg.Start(Trace2ProcessClass.Other))
{
- throw new Exception("Failed to start gpg.");
+ throw new Trace2Exception(_trace2, "Failed to start gpg.");
}
gpg.WaitForExit();
@@ -48,7 +55,9 @@ public string DecryptFile(string path)
{
string stdout = gpg.StandardOutput.ReadToEnd();
string stderr = gpg.StandardError.ReadToEnd();
- throw new Exception($"Failed to decrypt file '{path}' with gpg. exit={gpg.ExitCode}, out={stdout}, err={stderr}");
+ var format = "Failed to decrypt file '{0}' with gpg. exit={1}, out={2}, err={3}";
+ var message = string.Format(format, path, gpg.ExitCode, stdout, stderr);
+ throw new Trace2Exception(_trace2, message, format);
}
return gpg.StandardOutput.ReadToEnd();
@@ -62,16 +71,16 @@ public void EncryptFile(string path, string gpgId, string contents)
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
- RedirectStandardError = true,
+ RedirectStandardError = true, // Ok to redirect stderr for non-git-related processes
};
PrepareEnvironment(psi);
- using (var gpg = Process.Start(psi))
+ using (var gpg = _processManager.CreateProcess(psi))
{
- if (gpg is null)
+ if (!gpg.Start(Trace2ProcessClass.Other))
{
- throw new Exception("Failed to start gpg.");
+ throw new Trace2Exception(_trace2, "Failed to start gpg.");
}
gpg.StandardInput.Write(contents);
@@ -83,7 +92,9 @@ public void EncryptFile(string path, string gpgId, string contents)
{
string stdout = gpg.StandardOutput.ReadToEnd();
string stderr = gpg.StandardError.ReadToEnd();
- throw new Exception($"Failed to encrypt file '{path}' with gpg. exit={gpg.ExitCode}, out={stdout}, err={stderr}");
+ var format = "Failed to encrypt file '{0}' with gpg. exit={1}, out={2}, err={3}";
+ var message = string.Format(format, path, gpg.ExitCode, stdout, stderr);
+ throw new Trace2Exception(_trace2, message, format);
}
}
}
diff --git a/src/shared/Core/HostProviderRegistry.cs b/src/shared/Core/HostProviderRegistry.cs
index 237c32e54..7907ff7dc 100644
--- a/src/shared/Core/HostProviderRegistry.cs
+++ b/src/shared/Core/HostProviderRegistry.cs
@@ -151,7 +151,7 @@ public async Task GetProviderAsync(InputArguments input)
var uri = input.GetRemoteUri();
if (uri is null)
{
- throw new Exception("Unable to detect host provider without a remote URL");
+ throw new Trace2Exception(_context.Trace2, "Unable to detect host provider without a remote URL");
}
// We can only probe HTTP(S) URLs - for SMTP, IMAP, etc we cannot do network probing
@@ -240,8 +240,10 @@ await MatchProviderAsync(HostProviderPriority.Low, canProbeUri) ??
}
catch (Exception ex)
{
- _context.Trace.WriteLine("Failed to set host provider!");
+ var message = "Failed to set host provider!";
+ _context.Trace.WriteLine(message);
_context.Trace.WriteException(ex);
+ _context.Trace2.WriteError(message);
_context.Streams.Error.WriteLine("warning: failed to remember result of host provider detection!");
_context.Streams.Error.WriteLine($"warning: try setting this manually: `git config --global {keyName} {match.Id}`");
diff --git a/src/shared/Core/HttpClientFactory.cs b/src/shared/Core/HttpClientFactory.cs
index de6beb7a1..b34aecc1a 100644
--- a/src/shared/Core/HttpClientFactory.cs
+++ b/src/shared/Core/HttpClientFactory.cs
@@ -37,10 +37,11 @@ public class HttpClientFactory : IHttpClientFactory
{
private readonly IFileSystem _fileSystem;
private readonly ITrace _trace;
+ private readonly ITrace2 _trace2;
private readonly ISettings _settings;
private readonly IStandardStreams _streams;
- public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ISettings settings, IStandardStreams streams)
+ public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ITrace2 trace2, ISettings settings, IStandardStreams streams)
{
EnsureArgument.NotNull(fileSystem, nameof(fileSystem));
EnsureArgument.NotNull(trace, nameof(trace));
@@ -49,6 +50,7 @@ public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ISettings setting
_fileSystem = fileSystem;
_trace = trace;
+ _trace2 = trace2;
_settings = settings;
_streams = streams;
}
@@ -74,6 +76,20 @@ public HttpClient CreateClient()
handler = new HttpClientHandler();
}
+ // Trace Git's chosen SSL/TLS backend
+ _trace.WriteLine($"Git's SSL/TLS backend is: {_settings.TlsBackend}");
+
+ // Mirror Git for Windows and only send client TLS certificates automatically if we're using
+ // the schannel backend _and_ the user has opted in to sending them.
+ if (_settings.TlsBackend == TlsBackend.Schannel &&
+ _settings.AutomaticallyUseClientCertificates)
+ {
+ _trace.WriteLine("Configured to automatically send TLS client certificates.");
+ handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
+ }
+
+ // Configure server certificate verification and warn if we're bypassing validation
+
// IsCertificateVerificationEnabled takes precedence over custom TLS cert verification
if (!_settings.IsCertificateVerificationEnabled)
{
@@ -99,7 +115,9 @@ public HttpClient CreateClient()
// Throw exception if cert bundle file not found
if (!_fileSystem.FileExists(certBundlePath))
{
- throw new FileNotFoundException($"Custom certificate bundle not found at path: {certBundlePath}", certBundlePath);
+ var format = "Custom certificate bundle not found at path: {0}";
+ var message = string.Format(format, certBundlePath);
+ throw new Trace2FileNotFoundException(_trace2, message, format, certBundlePath);
}
Func validationCallback = (cert, chain, errors) =>
@@ -182,7 +200,7 @@ public HttpClient CreateClient()
var client = new HttpClient(handler);
// Add default headers
- client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent());
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent(_trace2));
client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
{
NoCache = true
@@ -264,8 +282,11 @@ public bool TryCreateProxy(out IWebProxy proxy)
}
catch (Exception ex)
{
- _trace.WriteLine("Failed to convert proxy bypass hosts to regular expressions; ignoring bypass list");
+ var message =
+ "Failed to convert proxy bypass hosts to regular expressions; ignoring bypass list";
+ _trace.WriteLine(message);
_trace.WriteException(ex);
+ _trace2.WriteError(message);
dict["bypass"] = "<< failed to convert >>";
}
}
diff --git a/src/shared/Core/ISessionManager.cs b/src/shared/Core/ISessionManager.cs
index 1f707c046..3804518e7 100644
--- a/src/shared/Core/ISessionManager.cs
+++ b/src/shared/Core/ISessionManager.cs
@@ -7,5 +7,30 @@ public interface ISessionManager
///
/// True if the session can display UI, false otherwise.
bool IsDesktopSession { get; }
+
+ ///
+ /// Determine if the current session has access to a web browser.
+ ///
+ /// True if the session can display a web browser, false otherwise.
+ bool IsWebBrowserAvailable { get; }
+ }
+
+ public abstract class SessionManager : ISessionManager
+ {
+ protected IEnvironment Environment { get; }
+ protected IFileSystem FileSystem { get; }
+
+ protected SessionManager(IEnvironment env, IFileSystem fs)
+ {
+ EnsureArgument.NotNull(env, nameof(env));
+ EnsureArgument.NotNull(fs, nameof(fs));
+
+ Environment = env;
+ FileSystem = fs;
+ }
+
+ public abstract bool IsDesktopSession { get; }
+
+ public virtual bool IsWebBrowserAvailable => IsDesktopSession;
}
}
diff --git a/src/shared/Core/ITrace2Writer.cs b/src/shared/Core/ITrace2Writer.cs
index 4474555cd..426c69f1e 100644
--- a/src/shared/Core/ITrace2Writer.cs
+++ b/src/shared/Core/ITrace2Writer.cs
@@ -1,10 +1,63 @@
using System;
+using System.Text;
namespace GitCredentialManager;
+///
+/// The different format targets supported in the TRACE2 tracing
+/// system.
+///
+public enum Trace2FormatTarget
+{
+ Event,
+ Normal,
+ Performance
+}
+
public interface ITrace2Writer : IDisposable
{
bool Failed { get; }
void Write(Trace2Message message);
}
+
+public class Trace2Writer : DisposableObject, ITrace2Writer
+{
+ private readonly Trace2FormatTarget _formatTarget;
+
+ public bool Failed { get; protected set; }
+
+ protected Trace2Writer(Trace2FormatTarget formatTarget)
+ {
+ _formatTarget = formatTarget;
+ }
+
+ protected string Format(Trace2Message message)
+ {
+ EnsureArgument.NotNull(message, nameof(message));
+ var sb = new StringBuilder();
+
+ switch (_formatTarget)
+ {
+ case Trace2FormatTarget.Event:
+ sb.Append(message.ToJson());
+ break;
+ case Trace2FormatTarget.Normal:
+ sb.Append(message.ToNormalString());
+ break;
+ case Trace2FormatTarget.Performance:
+ sb.Append(message.ToPerformanceString());
+ break;
+ default:
+ Console.WriteLine($"warning: unrecognized format target '{_formatTarget}', disabling TRACE2 tracing.");
+ Failed = true;
+ break;
+ }
+
+ sb.Append('\n');
+ return sb.ToString();
+ }
+
+ public virtual void Write(Trace2Message message)
+ { }
+}
diff --git a/src/shared/Core/IniFile.cs b/src/shared/Core/IniFile.cs
new file mode 100644
index 000000000..cf438d49d
--- /dev/null
+++ b/src/shared/Core/IniFile.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace GitCredentialManager
+{
+ public class IniFile
+ {
+ public IniFile()
+ {
+ Sections = new Dictionary();
+ }
+
+ public IDictionary Sections { get; }
+
+ public bool TryGetSection(string name, string subName, out IniSection section)
+ {
+ return Sections.TryGetValue(new IniSectionName(name, subName), out section);
+ }
+
+ public bool TryGetSection(string name, out IniSection section)
+ {
+ return Sections.TryGetValue(new IniSectionName(name), out section);
+ }
+ }
+
+ [DebuggerDisplay("{DebuggerDisplay}")]
+ public readonly struct IniSectionName : IEquatable
+ {
+ public IniSectionName(string name, string subName = null)
+ {
+ Name = name;
+ SubName = string.IsNullOrEmpty(subName) ? null : subName;
+ }
+
+ public string Name { get; }
+
+ public string SubName { get; }
+
+ public bool Equals(IniSectionName other)
+ {
+ // Main section name is case-insensitive, but subsection name IS case-sensitive!
+ return StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name) &&
+ StringComparer.Ordinal.Equals(SubName, other.SubName);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is IniSectionName other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((Name != null ? Name.ToLowerInvariant().GetHashCode() : 0) * 397) ^
+ (SubName != null ? SubName.GetHashCode() : 0);
+ }
+ }
+
+ private string DebuggerDisplay => SubName is null ? Name : $"{Name} \"{SubName}\"";
+ }
+
+ [DebuggerDisplay("{DebuggerDisplay}")]
+ public class IniSection
+ {
+ public IniSection(IniSectionName name)
+ {
+ Name = name;
+ Properties = new List();
+ }
+
+ public IniSectionName Name { get; }
+
+ public IList Properties { get; }
+
+ public bool TryGetProperty(string name, out string value)
+ {
+ if (TryGetMultiProperty(name, out IEnumerable values))
+ {
+ value = values.Last();
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ public bool TryGetMultiProperty(string name, out IEnumerable values)
+ {
+ IniProperty[] props = Properties
+ .Where(x => StringComparer.OrdinalIgnoreCase.Equals(x.Name, name))
+ .ToArray();
+
+ if (props.Length == 0)
+ {
+ values = Array.Empty();
+ return false;
+ }
+
+ values = props.Select(x => x.Value);
+ return true;
+ }
+
+ private string DebuggerDisplay => Name.SubName is null
+ ? $"{Name.Name} [Properties: {Properties.Count}]"
+ : $"{Name.Name} \"{Name.SubName}\" [Properties: {Properties.Count}]";
+ }
+
+ [DebuggerDisplay("{DebuggerDisplay}")]
+ public class IniProperty
+ {
+ public IniProperty(string name, string value)
+ {
+ Name = name;
+ Value = value;
+ }
+
+ public string Name { get; }
+ public string Value { get; }
+
+ private string DebuggerDisplay => $"{Name}={Value}";
+ }
+
+ public static class IniSerializer
+ {
+ private static readonly Regex SectionRegex =
+ new Regex(@"^\[[^\S#]*(?'name'[^\s#\]]*?)(?:\s+""(?'sub'.+)"")?\s*\]", RegexOptions.Compiled);
+
+ private static readonly Regex PropertyRegex =
+ new Regex(@"^[^\S#]*?(?'name'[^\s#]+)\s*=(?'value'.*)?$", RegexOptions.Compiled);
+
+ public static IniFile Deserialize(IFileSystem fs, string path)
+ {
+ IEnumerable lines = fs.ReadAllLines(path).Select(x => x.Trim());
+
+ var iniFile = new IniFile();
+ IniSection section = null;
+
+ foreach (string line in lines)
+ {
+ Match match = SectionRegex.Match(line);
+ if (match.Success)
+ {
+ string mainName = match.Groups["name"].Value;
+ string subName = match.Groups["sub"].Value;
+
+ // Skip empty-named sections
+ if (string.IsNullOrWhiteSpace(mainName))
+ {
+ continue;
+ }
+
+ if (!iniFile.TryGetSection(mainName, subName, out section))
+ {
+ var sectionName = new IniSectionName(mainName, subName);
+ section = new IniSection(sectionName);
+ iniFile.Sections[sectionName] = section;
+ }
+
+ continue;
+ }
+
+ match = PropertyRegex.Match(line);
+ if (match.Success)
+ {
+ if (section is null)
+ {
+ throw new Exception("Missing section header");
+ }
+
+ string propName = match.Groups["name"].Value;
+ string propValue = match.Groups["value"].Value.Trim();
+
+ // Trim trailing comments
+ int firstDQuote = propValue.IndexOf('"');
+ int lastDQuote = propValue.LastIndexOf('"');
+ int commentIdx = propValue.LastIndexOf('#');
+ if (commentIdx > -1)
+ {
+ bool insideDQuotes = firstDQuote > -1 && lastDQuote > -1 &&
+ (firstDQuote < commentIdx && commentIdx < lastDQuote);
+
+ if (!insideDQuotes)
+ {
+ propValue = propValue.Substring(0, commentIdx).Trim();
+ }
+ }
+
+ // Trim book-ending double quotes: "foo" => foo
+ if (propValue.Length > 1 && propValue[0] == '"' &&
+ propValue[propValue.Length - 1] == '"')
+ {
+ propValue = propValue.Substring(1, propValue.Length - 2);
+ }
+
+ var property = new IniProperty(propName, propValue);
+ section.Properties.Add(property);
+ }
+ }
+
+ return iniFile;
+ }
+ }
+}
diff --git a/src/shared/Core/InputArguments.cs b/src/shared/Core/InputArguments.cs
index c6524d65a..53aab181a 100644
--- a/src/shared/Core/InputArguments.cs
+++ b/src/shared/Core/InputArguments.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Linq;
namespace GitCredentialManager
{
@@ -94,10 +95,7 @@ public Uri GetRemoteUri(bool includeUser = false)
string[] hostParts = Host.Split(':');
if (hostParts.Length > 0)
{
- var ub = new UriBuilder(Protocol, hostParts[0])
- {
- Path = Path
- };
+ var ub = new UriBuilder(Protocol, hostParts[0]);
if (hostParts.Length > 1 && int.TryParse(hostParts[1], out int port))
{
@@ -109,6 +107,28 @@ public Uri GetRemoteUri(bool includeUser = false)
ub.UserName = Uri.EscapeDataString(UserName);
}
+ if (Path != null)
+ {
+ string[] pathParts = Path.Split('?', '#');
+ // We know the first piece is the path
+ ub.Path = pathParts[0];
+
+ switch (pathParts.Length)
+ {
+ // If we have 3 items, that means path, query, and fragment
+ case 3:
+ ub.Query = pathParts[1];
+ ub.Fragment = pathParts[2];
+ break;
+ // If we have 2 items, we must distinguish between query and fragment
+ case 2 when Path.Contains('?'):
+ ub.Query = pathParts[1];
+ break;
+ case 2 when Path.Contains('#'):
+ ub.Fragment = pathParts[1];
+ break;
+ }
+ }
return ub.Uri;
}
diff --git a/src/shared/Core/Interop/Linux/LinuxSessionManager.cs b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs
new file mode 100644
index 000000000..2147289ac
--- /dev/null
+++ b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs
@@ -0,0 +1,64 @@
+using GitCredentialManager.Interop.Posix;
+
+namespace GitCredentialManager.Interop.Linux;
+
+public class LinuxSessionManager : PosixSessionManager
+{
+ private bool? _isWebBrowserAvailable;
+
+ public LinuxSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
+ {
+ PlatformUtils.EnsureLinux();
+ }
+
+ public override bool IsWebBrowserAvailable
+ {
+ get
+ {
+ return _isWebBrowserAvailable ??= GetWebBrowserAvailable();
+ }
+ }
+
+ private bool GetWebBrowserAvailable()
+ {
+ // If this is a Windows Subsystem for Linux distribution we may
+ // be able to launch the web browser of the host Windows OS.
+ if (WslUtils.IsWslDistribution(Environment, FileSystem, out _))
+ {
+ // We need a shell execute handler to be able to launch to browser
+ if (!BrowserUtils.TryGetLinuxShellExecuteHandler(Environment, out _))
+ {
+ return false;
+ }
+
+ //
+ // If we are in Windows logon session 0 then the user can never interact,
+ // even in the WinSta0 window station. This is typical when SSH-ing into a
+ // Windows 10+ machine using the default OpenSSH Server configuration,
+ // which runs in the 'services' session 0.
+ //
+ // If we're in any other session, and in the WinSta0 window station then
+ // the user can possibly interact. However, since it's hard to determine
+ // the window station from PowerShell cmdlets (we'd need to write P/Invoke
+ // code and that's just messy and too many levels of indirection quite
+ // frankly!) we just assume any non session 0 is interactive.
+ //
+ // This assumption doesn't hold true if the user has changed the user that
+ // the OpenSSH Server service runs as (not a built-in NT service) *AND*
+ // they've SSH-ed into the Windows host (and then started a WSL shell).
+ // This feels like a very small subset of users...
+ //
+ if (WslUtils.GetWindowsSessionId(FileSystem) == 0)
+ {
+ return false;
+ }
+
+ // If we are not in session 0, or we cannot get the Windows session ID,
+ // assume that we *CAN* launch the browser so that users are never blocked.
+ return true;
+ }
+
+ // We require an interactive desktop session to be able to launch a browser
+ return IsDesktopSession;
+ }
+}
diff --git a/src/shared/Core/Interop/Linux/LinuxTerminal.cs b/src/shared/Core/Interop/Linux/LinuxTerminal.cs
index 1ad8b7d24..f7ea6f89a 100644
--- a/src/shared/Core/Interop/Linux/LinuxTerminal.cs
+++ b/src/shared/Core/Interop/Linux/LinuxTerminal.cs
@@ -7,12 +7,12 @@ namespace GitCredentialManager.Interop.Linux
{
public class LinuxTerminal : PosixTerminal
{
- public LinuxTerminal(ITrace trace)
- : base(trace) { }
+ public LinuxTerminal(ITrace trace, ITrace2 trace2)
+ : base(trace, trace2) { }
protected override IDisposable CreateTtyContext(int fd, bool echo)
{
- return new TtyContext(Trace, fd, echo);
+ return new TtyContext(Trace, Trace2, fd, echo);
}
private class TtyContext : IDisposable
@@ -23,7 +23,7 @@ private class TtyContext : IDisposable
private termios_Linux _originalTerm;
private bool _isDisposed;
- public TtyContext(ITrace trace, int fd, bool echo)
+ public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo)
{
EnsureArgument.NotNull(trace, nameof(trace));
EnsureArgument.PositiveOrZero(fd, nameof(fd));
@@ -36,7 +36,7 @@ public TtyContext(ITrace trace, int fd, bool echo)
// Capture current terminal settings so we can restore them later
if ((error = Termios_Linux.tcgetattr(_fd, out termios_Linux t)) != 0)
{
- throw new InteropException("Failed to get initial terminal settings", error);
+ throw new Trace2InteropException(trace2, "Failed to get initial terminal settings", error);
}
_originalTerm = t;
@@ -50,7 +50,7 @@ public TtyContext(ITrace trace, int fd, bool echo)
if ((error = Termios_Linux.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0)
{
- throw new InteropException("Failed to set terminal settings", error);
+ throw new Trace2InteropException(trace2, "Failed to set terminal settings", error);
}
}
diff --git a/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs b/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs
index a29f1f4df..256e81cb9 100644
--- a/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs
+++ b/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs
@@ -15,11 +15,7 @@ public MacOSEnvironment(IFileSystem fileSystem)
: base(fileSystem) { }
internal MacOSEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables)
- : base(fileSystem)
- {
- EnsureArgument.NotNull(variables, nameof(variables));
- Variables = variables;
- }
+ : base(fileSystem, variables) { }
public override bool TryLocateExecutable(string program, out string path)
{
@@ -35,4 +31,4 @@ public override bool TryLocateExecutable(string program, out string path)
return TryLocateExecutable(program, _pathsToIgnore, out path);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
index febcd20c2..584965ca1 100644
--- a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
+++ b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
@@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.MacOS
{
public class MacOSSessionManager : PosixSessionManager
{
- public MacOSSessionManager()
+ public MacOSSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureMacOS();
}
diff --git a/src/shared/Core/Interop/MacOS/MacOSTerminal.cs b/src/shared/Core/Interop/MacOS/MacOSTerminal.cs
index e56a3fa54..a4c9d2120 100644
--- a/src/shared/Core/Interop/MacOS/MacOSTerminal.cs
+++ b/src/shared/Core/Interop/MacOS/MacOSTerminal.cs
@@ -7,12 +7,12 @@ namespace GitCredentialManager.Interop.MacOS
{
public class MacOSTerminal : PosixTerminal
{
- public MacOSTerminal(ITrace trace)
- : base(trace) { }
+ public MacOSTerminal(ITrace trace, ITrace2 trace2)
+ : base(trace, trace2) { }
protected override IDisposable CreateTtyContext(int fd, bool echo)
{
- return new TtyContext(Trace, fd, echo);
+ return new TtyContext(Trace, Trace2, fd, echo);
}
private class TtyContext : IDisposable
@@ -23,7 +23,7 @@ private class TtyContext : IDisposable
private termios_MacOS _originalTerm;
private bool _isDisposed;
- public TtyContext(ITrace trace, int fd, bool echo)
+ public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo)
{
EnsureArgument.NotNull(trace, nameof(trace));
EnsureArgument.PositiveOrZero(fd, nameof(fd));
@@ -36,7 +36,7 @@ public TtyContext(ITrace trace, int fd, bool echo)
// Capture current terminal settings so we can restore them later
if ((error = Termios_MacOS.tcgetattr(_fd, out termios_MacOS t)) != 0)
{
- throw new InteropException("Failed to get initial terminal settings", error);
+ throw new Trace2InteropException(trace2, "Failed to get initial terminal settings", error);
}
_originalTerm = t;
@@ -50,7 +50,7 @@ public TtyContext(ITrace trace, int fd, bool echo)
if ((error = Termios_MacOS.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0)
{
- throw new InteropException("Failed to set terminal settings", error);
+ throw new Trace2InteropException(trace2, "Failed to set terminal settings", error);
}
}
diff --git a/src/shared/Core/Interop/Posix/PosixEnvironment.cs b/src/shared/Core/Interop/Posix/PosixEnvironment.cs
index c725c18e1..ec3d91c92 100644
--- a/src/shared/Core/Interop/Posix/PosixEnvironment.cs
+++ b/src/shared/Core/Interop/Posix/PosixEnvironment.cs
@@ -7,13 +7,10 @@ namespace GitCredentialManager.Interop.Posix
public class PosixEnvironment : EnvironmentBase
{
public PosixEnvironment(IFileSystem fileSystem)
- : this(fileSystem, null) { }
+ : base(fileSystem) { }
internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables)
- : base(fileSystem)
- {
- Variables = variables ?? GetCurrentVariables();
- }
+ : base(fileSystem, variables) { }
#region EnvironmentBase
diff --git a/src/shared/Core/Interop/Posix/PosixSessionManager.cs b/src/shared/Core/Interop/Posix/PosixSessionManager.cs
index e4ff05cbc..8709e12e7 100644
--- a/src/shared/Core/Interop/Posix/PosixSessionManager.cs
+++ b/src/shared/Core/Interop/Posix/PosixSessionManager.cs
@@ -1,15 +1,15 @@
-using System;
-
namespace GitCredentialManager.Interop.Posix
{
- public class PosixSessionManager : ISessionManager
+ public abstract class PosixSessionManager : SessionManager
{
- public PosixSessionManager()
+ protected PosixSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsurePosix();
}
- // Check if we have an X11 environment available
- public virtual bool IsDesktopSession => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY"));
+ // Check if we have an X11 or Wayland display environment available
+ public override bool IsDesktopSession =>
+ !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("DISPLAY")) ||
+ !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
}
}
diff --git a/src/shared/Core/Interop/Posix/PosixTerminal.cs b/src/shared/Core/Interop/Posix/PosixTerminal.cs
index d110b1408..b37d6d6cf 100644
--- a/src/shared/Core/Interop/Posix/PosixTerminal.cs
+++ b/src/shared/Core/Interop/Posix/PosixTerminal.cs
@@ -13,8 +13,9 @@ public abstract class PosixTerminal : ITerminal
private const byte DeleteChar = 127;
protected readonly ITrace Trace;
+ protected readonly ITrace2 Trace2;
- public PosixTerminal(ITrace trace)
+ public PosixTerminal(ITrace trace, ITrace2 trace2)
{
PlatformUtils.EnsurePosix();
EnsureArgument.NotNull(trace, nameof(trace));
diff --git a/src/shared/Core/Interop/Windows/Native/Kernel32.cs b/src/shared/Core/Interop/Windows/Native/Kernel32.cs
index 403ddf2d7..b5b7fd0a8 100644
--- a/src/shared/Core/Interop/Windows/Native/Kernel32.cs
+++ b/src/shared/Core/Interop/Windows/Native/Kernel32.cs
@@ -251,6 +251,16 @@ public static extern bool SetConsoleMode(
///
[DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LocalFree(IntPtr ptr);
+
+ ///
+ /// Retrieves the window handle used by the console associated with the calling process.
+ ///
+ ///
+ /// The return value is a handle to the window used by the console associated with the calling process or
+ /// NULL if there is no such associated console.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern IntPtr GetConsoleWindow();
}
[Flags]
diff --git a/src/shared/Core/Interop/Windows/Native/User32.cs b/src/shared/Core/Interop/Windows/Native/User32.cs
index 0a28a28d3..8cf05a8d8 100644
--- a/src/shared/Core/Interop/Windows/Native/User32.cs
+++ b/src/shared/Core/Interop/Windows/Native/User32.cs
@@ -35,6 +35,36 @@ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr value)
[DllImport(LibraryName, SetLastError = true)]
public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);
+
+ ///
+ /// Retrieves the handle to the ancestor of the specified window.
+ ///
+ ///
+ /// A handle to the window whose ancestor is to be retrieved.
+ /// If this parameter is the desktop window, the function returns NULL.
+ ///
+ /// The ancestor to be retrieved.
+ /// The return value is the handle to the ancestor window.
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags);
+ }
+
+ public enum GetAncestorFlags
+ {
+ ///
+ /// Retrieves the parent window. This does not include the owner, as it does with the GetParent function.
+ ///
+ GetParent = 1,
+
+ ///
+ /// Retrieves the root window by walking the chain of parent windows.
+ ///
+ GetRoot = 2,
+
+ ///
+ /// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent.
+ ///
+ GetRootOwner = 3
}
[StructLayout(LayoutKind.Sequential)]
diff --git a/src/shared/Core/Interop/Windows/Native/Win32Error.cs b/src/shared/Core/Interop/Windows/Native/Win32Error.cs
index f5c436339..f6a170bda 100644
--- a/src/shared/Core/Interop/Windows/Native/Win32Error.cs
+++ b/src/shared/Core/Interop/Windows/Native/Win32Error.cs
@@ -97,6 +97,18 @@ public static int GetLastError(bool success)
return Marshal.GetLastWin32Error();
}
+ ///
+ /// Throw an if is not true.
+ ///
+ /// The application's TRACE2 tracer.
+ /// Windows API return code.
+ /// Default error message.
+ /// Throw if is not true.
+ public static void ThrowIfError(ITrace2 trace2, bool succeeded, string defaultErrorMessage = "Unknown error.")
+ {
+ ThrowIfError(GetLastError(succeeded), defaultErrorMessage, trace2);
+ }
+
///
/// Throw an if is not true.
///
@@ -113,8 +125,9 @@ public static void ThrowIfError(bool succeeded, string defaultErrorMessage = "Un
///
/// Windows API error code.
/// Default error message.
+ /// The application's TRACE2 tracer.
/// Throw if is not .
- public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.")
+ public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.", ITrace2 trace2 = null)
{
switch (error)
{
@@ -123,6 +136,8 @@ public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown
default:
// The Win32Exception constructor will automatically get the human-readable
// message for the error code.
+ if (trace2 != null)
+ throw new Trace2InteropException(trace2, defaultErrorMessage, new Win32Exception(error));
throw new InteropException(defaultErrorMessage, new Win32Exception(error));
}
}
diff --git a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs
index 67aea7d64..6c3450a38 100644
--- a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs
+++ b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs
@@ -10,13 +10,10 @@ namespace GitCredentialManager.Interop.Windows
public class WindowsEnvironment : EnvironmentBase
{
public WindowsEnvironment(IFileSystem fileSystem)
- : this(fileSystem, null) { }
+ : base(fileSystem) { }
internal WindowsEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables)
- : base(fileSystem)
- {
- Variables = variables ?? GetCurrentVariables();
- }
+ : base(fileSystem, variables) { }
#region EnvironmentBase
@@ -48,7 +45,7 @@ public override void AddDirectoryToPath(string directoryPath, EnvironmentVariabl
Environment.SetEnvironmentVariable("PATH", newValue, target);
// Update the cached PATH variable to the latest value (as well as all other variables)
- Variables = GetCurrentVariables();
+ Refresh();
}
public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target)
@@ -66,20 +63,8 @@ public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVa
Environment.SetEnvironmentVariable("PATH", newValue, target);
// Update the cached PATH variable to the latest value (as well as all other variables)
- Variables = GetCurrentVariables();
- }
- }
-
- public override Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
- {
- // If we're asked to start a WSL executable we must launch via the wsl.exe command tool
- if (!useShellExecute && WslUtils.IsWslPath(path))
- {
- string wslPath = WslUtils.ConvertToDistroPath(path, out string distro);
- return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", workingDirectory);
+ Refresh();
}
-
- return base.CreateProcess(path, args, useShellExecute, workingDirectory);
}
#endregion
diff --git a/src/shared/Core/Interop/Windows/WindowsProcessManager.cs b/src/shared/Core/Interop/Windows/WindowsProcessManager.cs
new file mode 100644
index 000000000..85d47b0de
--- /dev/null
+++ b/src/shared/Core/Interop/Windows/WindowsProcessManager.cs
@@ -0,0 +1,21 @@
+namespace GitCredentialManager.Interop.Windows;
+
+public class WindowsProcessManager : ProcessManager
+{
+ public WindowsProcessManager(ITrace2 trace2) : base(trace2)
+ {
+ PlatformUtils.EnsureWindows();
+ }
+
+ public override ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
+ {
+ // If we're asked to start a WSL executable we must launch via the wsl.exe command tool
+ if (!useShellExecute && WslUtils.IsWslPath(path))
+ {
+ string wslPath = WslUtils.ConvertToDistroPath(path, out string distro);
+ return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", Trace2, workingDirectory);
+ }
+
+ return base.CreateProcess(path, args, useShellExecute, workingDirectory);
+ }
+}
diff --git a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs
index 2296a9455..d87d76347 100644
--- a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs
+++ b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs
@@ -3,14 +3,14 @@
namespace GitCredentialManager.Interop.Windows
{
- public class WindowsSessionManager : ISessionManager
+ public class WindowsSessionManager : SessionManager
{
- public WindowsSessionManager()
+ public WindowsSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureWindows();
}
- public unsafe bool IsDesktopSession
+ public override unsafe bool IsDesktopSession
{
get
{
diff --git a/src/shared/Core/Interop/Windows/WindowsSettings.cs b/src/shared/Core/Interop/Windows/WindowsSettings.cs
index f6e92eca9..abdd9ee0e 100644
--- a/src/shared/Core/Interop/Windows/WindowsSettings.cs
+++ b/src/shared/Core/Interop/Windows/WindowsSettings.cs
@@ -17,7 +17,7 @@ public WindowsSettings(IEnvironment environment, IGit git, ITrace trace)
PlatformUtils.EnsureWindows();
}
- protected override bool TryGetExternalDefault(string section, string property, out string value)
+ protected override bool TryGetExternalDefault(string section, string scope, string property, out string value)
{
value = null;
@@ -32,7 +32,10 @@ protected override bool TryGetExternalDefault(string section, string property, o
return false;
}
- string name = $"{section}.{property}";
+ string name = string.IsNullOrWhiteSpace(scope)
+ ? $"{section}.{property}"
+ : $"{section}.{scope}.{property}";
+
object registryValue = configKey.GetValue(name);
if (registryValue is null)
{
@@ -46,7 +49,7 @@ protected override bool TryGetExternalDefault(string section, string property, o
return true;
}
#else
- return base.TryGetExternalDefault(section, property, out value);
+ return base.TryGetExternalDefault(section, scope, property, out value);
#endif
}
}
diff --git a/src/shared/Core/Interop/Windows/WindowsTerminal.cs b/src/shared/Core/Interop/Windows/WindowsTerminal.cs
index 140f69ad3..b8f5f3475 100644
--- a/src/shared/Core/Interop/Windows/WindowsTerminal.cs
+++ b/src/shared/Core/Interop/Windows/WindowsTerminal.cs
@@ -17,12 +17,14 @@ public class WindowsTerminal : ITerminal
private const string ConsoleOutName = "CONOUT$";
private readonly ITrace _trace;
+ private readonly ITrace2 _trace2;
- public WindowsTerminal(ITrace trace)
+ public WindowsTerminal(ITrace trace, ITrace2 trace2)
{
PlatformUtils.EnsureWindows();
_trace = trace;
+ _trace2 = trace2;
}
public void WriteLine(string format, params object[] args)
@@ -58,7 +60,7 @@ public void WriteLine(string format, params object[] args)
numberOfCharsWritten: out uint written,
reserved: IntPtr.Zero))
{
- Win32Error.ThrowIfError(Marshal.GetLastWin32Error());
+ Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), trace2: _trace2);
}
}
}
@@ -116,13 +118,13 @@ private string Prompt(string prompt, bool echo)
numberOfCharsWritten: out written,
reserved: IntPtr.Zero))
{
- Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to write prompt text");
+ Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to write prompt text", _trace2);
}
sb.Clear();
// Read input from the user
- using (new TtyContext(_trace, stdin, echo))
+ using (new TtyContext(_trace, _trace2, stdin, echo))
{
if (!Kernel32.ReadConsole(buffer: sb,
consoleInputHandle: stdin,
@@ -130,7 +132,8 @@ private string Prompt(string prompt, bool echo)
numberOfCharsRead: out read,
reserved: IntPtr.Zero))
{
- Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Unable to read prompt input from standard input");
+ Win32Error.ThrowIfError(Marshal.GetLastWin32Error(),
+ "Unable to read prompt input from standard input", _trace2);
}
// Record input from the user into local storage, stripping any EOL chars
@@ -150,7 +153,8 @@ private string Prompt(string prompt, bool echo)
numberOfCharsWritten: out written,
reserved: IntPtr.Zero))
{
- Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to write final newline in secret prompting");
+ Win32Error.ThrowIfError(Marshal.GetLastWin32Error(),
+ "Failed to write final newline in secret prompting", _trace2);
}
}
@@ -161,23 +165,25 @@ private string Prompt(string prompt, bool echo)
private class TtyContext : IDisposable
{
private readonly ITrace _trace;
+ private readonly ITrace2 _trace2;
private readonly SafeFileHandle _stream;
private ConsoleMode _originalMode;
private bool _isDisposed;
- public TtyContext(ITrace trace, SafeFileHandle stream, bool echo)
+ public TtyContext(ITrace trace, ITrace2 trace2, SafeFileHandle stream, bool echo)
{
EnsureArgument.NotNull(stream, nameof(stream));
_trace = trace;
+ _trace2 = trace2;
_stream = stream;
// Capture current console mode so we can restore it later
ConsoleMode consoleMode;
if (!Kernel32.GetConsoleMode(consoleMode: out consoleMode, consoleHandle: stream))
{
- Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to get initial console mode");
+ Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to get initial console mode", trace2);
}
_originalMode = consoleMode;
@@ -189,7 +195,8 @@ public TtyContext(ITrace trace, SafeFileHandle stream, bool echo)
ConsoleMode newConsoleMode = consoleMode ^ ConsoleMode.EchoInput;
if (!Kernel32.SetConsoleMode(consoleMode: newConsoleMode, consoleHandle: _stream))
{
- Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to set console mode");
+ Win32Error.ThrowIfError(
+ Marshal.GetLastWin32Error(), "Failed to set console mode", trace2);
}
}
}
diff --git a/src/shared/Core/PlatformUtils.cs b/src/shared/Core/PlatformUtils.cs
index 7a267c743..d484d0227 100644
--- a/src/shared/Core/PlatformUtils.cs
+++ b/src/shared/Core/PlatformUtils.cs
@@ -12,16 +12,45 @@ public static class PlatformUtils
/// Get information about the current platform (OS and CLR details).
///
/// Platform information.
- public static PlatformInformation GetPlatformInformation()
+ public static PlatformInformation GetPlatformInformation(ITrace2 trace2)
{
string osType = GetOSType();
- string osVersion = GetOSVersion();
+ string osVersion = GetOSVersion(trace2);
string cpuArch = GetCpuArchitecture();
string clrVersion = GetClrVersion();
return new PlatformInformation(osType, osVersion, cpuArch, clrVersion);
}
+ public static bool IsDevBox()
+ {
+ if (!IsWindows())
+ {
+ return false;
+ }
+
+#if NETFRAMEWORK
+ // Check for machine (HKLM) registry keys for Cloud PC indicators
+ // Note that the keys are only found in the 64-bit registry view
+ using (Microsoft.Win32.RegistryKey hklm64 = Microsoft.Win32.RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64))
+ using (Microsoft.Win32.RegistryKey w365Key = hklm64.OpenSubKey(Constants.WindowsRegistry.HKWindows365Path))
+ {
+ if (w365Key is null)
+ {
+ // No Windows365 key exists
+ return false;
+ }
+
+ object w365Value = w365Key.GetValue(Constants.WindowsRegistry.IsW365EnvironmentKeyName);
+ string partnerValue = w365Key.GetValue(Constants.WindowsRegistry.W365PartnerIdKeyName)?.ToString();
+
+ return w365Value is not null && Guid.TryParse(partnerValue, out Guid partnerId) && partnerId == Constants.DevBoxPartnerId;
+ }
+#else
+ return false;
+#endif
+ }
+
public static bool IsWindowsBrokerSupported()
{
if (!IsWindows())
@@ -320,7 +349,7 @@ private static string GetOSType()
return "Unknown";
}
- private static string GetOSVersion()
+ private static string GetOSVersion(ITrace2 trace2)
{
if (IsWindows() && RtlGetVersionEx(out RTL_OSVERSIONINFOEX osvi) == 0)
{
@@ -336,10 +365,11 @@ private static string GetOSVersion()
RedirectStandardOutput = true
};
- using (var swvers = new Process { StartInfo = psi })
+ using (var swvers = new ChildProcess(trace2, psi))
{
- swvers.Start();
+ swvers.Start(Trace2ProcessClass.Other);
swvers.WaitForExit();
+
if (swvers.ExitCode == 0)
{
return swvers.StandardOutput.ReadToEnd().Trim();
@@ -356,10 +386,11 @@ private static string GetOSVersion()
RedirectStandardOutput = true
};
- using (var uname = new Process { StartInfo = psi })
+ using (var uname = new ChildProcess(trace2, psi))
{
- uname.Start();
- uname.WaitForExit();
+ uname.Start(Trace2ProcessClass.Other);
+ uname.Process.WaitForExit();
+
if (uname.ExitCode == 0)
{
return uname.StandardOutput.ReadToEnd().Trim();
diff --git a/src/shared/Core/ProcessManager.cs b/src/shared/Core/ProcessManager.cs
new file mode 100644
index 000000000..4c5988c4d
--- /dev/null
+++ b/src/shared/Core/ProcessManager.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Diagnostics;
+
+namespace GitCredentialManager;
+
+public interface IProcessManager
+{
+ ///
+ /// Create a process ready to start.
+ ///
+ /// Absolute file path of executable or command to start.
+ /// Command line arguments to pass to executable.
+ ///
+ /// True to resolve using the OS shell, false to use as an absolute file path.
+ ///
+ /// Working directory for the new process.
+ /// object ready to start.
+ ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory);
+
+ ///
+ /// Create a process ready to start.
+ ///
+ /// Process start info.
+ /// object ready to start.
+ ChildProcess CreateProcess(ProcessStartInfo psi);
+}
+
+public class ProcessManager : IProcessManager
+{
+ private const string SidEnvar = "GIT_TRACE2_PARENT_SID";
+
+ protected readonly ITrace2 Trace2;
+
+ public static string Sid { get; internal set; }
+
+ public static int Depth { get; internal set; }
+
+ public ProcessManager(ITrace2 trace2)
+ {
+ EnsureArgument.NotNull(trace2, nameof(trace2));
+
+ Trace2 = trace2;
+ }
+
+ public virtual ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
+ {
+ var psi = new ProcessStartInfo(path, args)
+ {
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled
+ UseShellExecute = useShellExecute,
+ WorkingDirectory = workingDirectory ?? string.Empty
+ };
+
+ return CreateProcess(psi);
+ }
+
+ public virtual ChildProcess CreateProcess(ProcessStartInfo psi)
+ {
+ return new ChildProcess(Trace2, psi);
+ }
+
+ ///
+ /// Create a TRACE2 "session id" (sid) for this process.
+ ///
+ public static void CreateSid()
+ {
+ Sid = Environment.GetEnvironmentVariable(SidEnvar);
+
+ if (!string.IsNullOrEmpty(Sid))
+ {
+ // Use trim to ensure no accidental leading or trailing slashes
+ Sid = $"{Sid.Trim('/')}/{Guid.NewGuid():D}";
+ // Only check for process depth if there is a parent.
+ // If there is not a parent, depth defaults to 0.
+ Depth = GetProcessDepth();
+ }
+ else
+ {
+ // We are the root process; create our own 'root' SID
+ Sid = Guid.NewGuid().ToString("D");
+ }
+
+ Environment.SetEnvironmentVariable(SidEnvar, Sid);
+ }
+
+ ///
+ /// Get "depth" of current process relative to top-level GCM process.
+ ///
+ /// Depth of current process.
+ internal static int GetProcessDepth()
+ {
+ char processSeparator = '/';
+
+ int count = 0;
+ // Use AsSpan() for slight performance bump over traditional foreach loop.
+ foreach (var c in Sid.AsSpan())
+ {
+ if (c == processSeparator)
+ count++;
+ }
+
+ return count;
+ }
+}
diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs
index 3eafa0baa..6fbbdf49a 100644
--- a/src/shared/Core/Settings.cs
+++ b/src/shared/Core/Settings.cs
@@ -12,6 +12,7 @@ namespace GitCredentialManager
{
///
/// Component that represents settings for Git Credential Manager as found from the environment and Git configuration.
+ /// Setting values from Git configuration may be cached for performance reasons.
///
public interface ISettings : IDisposable
{
@@ -26,7 +27,6 @@ public interface ISettings : IDisposable
/// True if a setting value was found, false otherwise.
bool TryGetSetting(string envarName, string section, string property, out string value);
-
///
/// Try and get the value of a specified setting as specified in the environment and Git configuration,
/// with the environment taking precedence over Git. If the value is pulled from the Git configuration,
@@ -119,6 +119,11 @@ public interface ISettings : IDisposable
///
bool IsCertificateVerificationEnabled { get; }
+ ///
+ /// Automatically send client TLS certificates.
+ ///
+ bool AutomaticallyUseClientCertificates { get; }
+
///
/// Get the proxy setting if configured, or null otherwise.
///
@@ -167,6 +172,12 @@ public interface ISettings : IDisposable
///
int AutoDetectProviderTimeout { get; }
+ ///
+ /// Automatically use the default/current operating system account if no other account information is given
+ /// for Microsoft Authentication.
+ ///
+ bool UseMsAuthDefaultAccount { get; }
+
///
/// Get TRACE2 settings.
///
@@ -286,6 +297,8 @@ public class Settings : ISettings
private readonly IEnvironment _environment;
private readonly IGit _git;
+ private Dictionary _configEntries;
+
public Settings(IEnvironment environment, IGit git)
{
EnsureArgument.NotNull(environment, nameof(environment));
@@ -329,6 +342,30 @@ public IEnumerable GetSettingValues(string envarName, string section, st
{
IGitConfiguration config = _git.GetConfiguration();
+ //
+ // Enumerate all configuration entries for all sections and property names and make a
+ // local copy of them here to avoid needing to call `TryGetValue` on the IGitConfiguration
+ // object multiple times in a loop below.
+ //
+ // This is a performance optimisation to avoid calling `TryGet` on the IGitConfiguration
+ // object multiple times in a loop below, or each time this entire method is called.
+ // The assumption is that the configuration entries will not change during a single invocation
+ // of Git Credential Manager, which is reasonable given process lifetime is typically less
+ // than a few seconds. For some entries (type=path), we still need to ask Git in order to
+ // expand the path correctly.
+ //
+ if (_configEntries is null)
+ {
+ _configEntries = new Dictionary(GitConfigurationKeyComparer.Instance);
+ config.Enumerate(entry =>
+ {
+ _configEntries[entry.Key] = entry.Value;
+
+ // Continue the enumeration
+ return true;
+ });
+ }
+
if (RemoteUri != null)
{
/*
@@ -374,26 +411,14 @@ public IEnumerable GetSettingValues(string envarName, string section, st
*
*/
- // Enumerate all configuration entries with the correct section and property name
- // and make a local copy of them here to avoid needing to call `TryGetValue` on the
- // IGitConfiguration object multiple times in a loop below.
- var configEntries = new Dictionary(GitConfigurationKeyComparer.Instance);
- config.Enumerate(section, property, entry =>
- {
- configEntries[entry.Key] = entry.Value;
-
- // Continue the enumeration
- return true;
- });
-
foreach (string scope in RemoteUri.GetGitConfigurationScopes())
{
string queryName = $"{section}.{scope}.{property}";
// Look for a scoped entry that includes the scheme "protocol://example.com" first as
// this is more specific. If `isPath` is true, then re-get the value from the
// `GitConfiguration` with `isPath` specified.
- if (configEntries.TryGetValue(queryName, out value) &&
- (!isPath || config.TryGet(queryName, isPath, out value)))
+ if ((isPath && config.TryGet(queryName, true, out value)) ||
+ _configEntries.TryGetValue(queryName, out value))
{
yield return value;
}
@@ -403,8 +428,14 @@ public IEnumerable GetSettingValues(string envarName, string section, st
// `isPath` specified.
string scopeWithoutScheme = scope.TrimUntilIndexOf(Uri.SchemeDelimiter);
string queryWithSchemeName = $"{section}.{scopeWithoutScheme}.{property}";
- if (configEntries.TryGetValue(queryWithSchemeName, out value) &&
- (!isPath || config.TryGet(queryWithSchemeName, isPath, out value)))
+ if ((isPath && config.TryGet(queryWithSchemeName, true, out value)) ||
+ _configEntries.TryGetValue(queryWithSchemeName, out value))
+ {
+ yield return value;
+ }
+
+ // Check for an externally specified default value
+ if (TryGetExternalDefault(section, scope, property, out value))
{
yield return value;
}
@@ -418,15 +449,17 @@ public IEnumerable GetSettingValues(string envarName, string section, st
* property = value
*
*/
- if (config.TryGet($"{section}.{property}", isPath, out value))
+ string name = $"{section}.{property}";
+ if ((isPath && config.TryGet(name, true, out value)) ||
+ _configEntries.TryGetValue(name, out value))
{
yield return value;
}
- // Check for an externally specified default value
- if (TryGetExternalDefault(section, property, out string defaultValue))
+ // Check for an externally specified default value without a scope
+ if (TryGetExternalDefault(section, null, property, out value))
{
- yield return defaultValue;
+ yield return value;
}
}
}
@@ -436,10 +469,11 @@ public IEnumerable GetSettingValues(string envarName, string section, st
/// This may come from external policies or the Operating System.
///
/// Configuration section name.
+ /// Optional configuration scope.
/// Configuration property name.
/// Value of the configuration setting, or null.
/// True if a default setting has been set, false otherwise.
- protected virtual bool TryGetExternalDefault(string section, string property, out string value)
+ protected virtual bool TryGetExternalDefault(string section, string scope, string property, out string value)
{
value = null;
return false;
@@ -447,7 +481,11 @@ protected virtual bool TryGetExternalDefault(string section, string property, ou
public Uri RemoteUri { get; set; }
- public bool IsDebuggingEnabled => _environment.Variables.GetBooleanyOrDefault(KnownEnvars.GcmDebug, false);
+ public bool IsDebuggingEnabled =>
+ TryGetSetting(KnownEnvars.GcmDebug,
+ KnownGitCfg.Credential.SectionName,
+ KnownGitCfg.Credential.Debug,
+ out string str) && str.IsTruthy();
public bool IsTerminalPromptsEnabled => _environment.Variables.GetBooleanyOrDefault(KnownEnvars.GitTerminalPrompts, true);
@@ -508,7 +546,11 @@ public bool IsInteractionAllowed
}
}
- public bool GetTracingEnabled(out string value) => _environment.Variables.TryGetValue(KnownEnvars.GcmTrace, out value) && !value.IsFalsey();
+ public bool GetTracingEnabled(out string value) =>
+ TryGetSetting(KnownEnvars.GcmTrace,
+ KnownGitCfg.Credential.SectionName,
+ KnownGitCfg.Credential.Trace,
+ out value) && !value.IsFalsey();
public Trace2Settings GetTrace2Settings()
{
@@ -526,12 +568,26 @@ public Trace2Settings GetTrace2Settings()
settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Normal, value);
}
+ if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Performance, KnownGitCfg.Trace2.SectionName,
+ Constants.GitConfiguration.Trace2.PerformanceTarget, out value))
+ {
+ settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Performance, value);
+ }
+
return settings;
}
- public bool IsSecretTracingEnabled => _environment.Variables.GetBooleanyOrDefault(KnownEnvars.GcmTraceSecrets, false);
+ public bool IsSecretTracingEnabled =>
+ TryGetSetting(KnownEnvars.GcmTraceSecrets,
+ KnownGitCfg.Credential.SectionName,
+ KnownGitCfg.Credential.TraceSecrets,
+ out string str) && str.IsTruthy();
- public bool IsMsalTracingEnabled => _environment.Variables.GetBooleanyOrDefault(Constants.EnvironmentVariables.GcmTraceMsAuth, false);
+ public bool IsMsalTracingEnabled =>
+ TryGetSetting(KnownEnvars.GcmTraceMsAuth,
+ KnownGitCfg.Credential.SectionName,
+ KnownGitCfg.Credential.TraceMsAuth,
+ out string str) && str.IsTruthy();
public string ProviderOverride =>
TryGetSetting(KnownEnvars.GcmProvider, GitCredCfg.SectionName, GitCredCfg.Provider, out string providerId) ? providerId : null;
@@ -563,6 +619,9 @@ public bool IsCertificateVerificationEnabled
}
}
+ public bool AutomaticallyUseClientCertificates =>
+ TryGetSetting(null, KnownGitCfg.Credential.SectionName, KnownGitCfg.Http.SslAutoClientCert, out string value) && value.ToBooleanyOrDefault(false);
+
public string CustomCertificateBundlePath =>
TryGetPathSetting(KnownEnvars.GitSslCaInfo, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SslCaInfo, out string value) ? value : null;
@@ -731,6 +790,15 @@ ProxyConfiguration CreateConfiguration(Uri uri, bool isLegacy = false)
? credStore
: null;
+ public bool UseMsAuthDefaultAccount =>
+ TryGetSetting(
+ KnownEnvars.MsAuthUseDefaultAccount,
+ KnownGitCfg.Credential.SectionName,
+ KnownGitCfg.Credential.MsAuthUseDefaultAccount,
+ out string str)
+ ? str.IsTruthy()
+ : PlatformUtils.IsDevBox(); // default to true in DevBox environment
+
#region IDisposable
public void Dispose()
diff --git a/src/shared/Core/StringExtensions.cs b/src/shared/Core/StringExtensions.cs
index 5c9a37455..1cab09807 100644
--- a/src/shared/Core/StringExtensions.cs
+++ b/src/shared/Core/StringExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Text;
namespace GitCredentialManager
{
@@ -240,5 +241,28 @@ public static bool Contains(this string str, string value, StringComparison comp
{
return str?.IndexOf(value, comparisonType) >= 0;
}
+
+ ///
+ /// Convert string to snake case.
+ ///
+ /// String to convert.
+ /// Input string converted to snake case.
+ public static string ToSnakeCase(this string str)
+ {
+ int len = str.Length;
+ var sb = new StringBuilder(2*len);
+ for (int i = 0; i < len; i++)
+ {
+ if (i > 0 && char.IsUpper(str[i]) &&
+ (char.IsLower(str[i - 1]) || i < len - 1 && char.IsLower(str[i + 1])))
+ {
+ sb.Append('_');
+
+ }
+ sb.Append(char.ToLower(str[i]));
+ }
+
+ return sb.ToString();
+ }
}
}
diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs
index 116bf6612..d8eba64b5 100644
--- a/src/shared/Core/Trace2.cs
+++ b/src/shared/Core/Trace2.cs
@@ -1,14 +1,9 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
-using System.Linq;
-using System.Runtime.Serialization;
using System.Text;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-using Newtonsoft.Json.Serialization;
+using System.Threading;
namespace GitCredentialManager;
@@ -18,42 +13,108 @@ namespace GitCredentialManager;
///
public enum Trace2Event
{
- [EnumMember(Value = "version")]
Version = 0,
- [EnumMember(Value = "start")]
Start = 1,
- [EnumMember(Value = "exit")]
- Exit = 2
+ Exit = 2,
+ ChildStart = 3,
+ ChildExit = 4,
+ Error = 5,
+ RegionEnter = 6,
+ RegionLeave = 7,
}
+///
+/// Classifications of processes invoked by GCM.
+///
+public enum Trace2ProcessClass
+{
+ None = 0,
+ UIHelper = 1,
+ Git = 2,
+ Other = 3
+}
+
+///
+/// Stores various TRACE2 format targets user has enabled.
+/// Check for supported formats.
+///
public class Trace2Settings
{
public IDictionary FormatTargetsAndValues { get; set; } =
new Dictionary();
}
+///
+/// Specifies a "text span" (i.e. space between two pipes) for the performance format target.
+///
+public class PerformanceFormatSpan
+{
+ public int Size { get; set; }
+
+ public int BeginPadding { get; set; }
+
+ public int EndPadding { get; set; }
+}
+
+///
+/// Class that manages regions.
+///
+public class Region : DisposableObject
+{
+ private readonly ITrace2 _trace2;
+ private readonly string _category;
+ private readonly string _label;
+ private readonly string _filePath;
+ private readonly int _lineNumber;
+ private readonly string _message;
+ private readonly DateTimeOffset _startTime;
+
+ public Region(ITrace2 trace2, string category, string label, string filePath, int lineNumber, string message = "")
+ {
+ _trace2 = trace2;
+ _category = category;
+ _label = label;
+ _filePath = filePath;
+ _lineNumber = lineNumber;
+ _message = message;
+
+ _startTime = DateTimeOffset.UtcNow;
+
+ _trace2.WriteRegionEnter(_category, _label, _message, _filePath, _lineNumber);
+ }
+
+ protected override void ReleaseManagedResources()
+ {
+ double relativeTime = (DateTimeOffset.UtcNow - _startTime).TotalSeconds;
+ _trace2.WriteRegionLeave(relativeTime, _category, _label, _message, _filePath, _lineNumber);
+ }
+}
+
///
/// Represents the application's TRACE2 tracing system.
///
public interface ITrace2 : IDisposable
{
///
- /// Initialize TRACE2 tracing by setting up any configured target formats and
- /// writing Version and Start events.
+ /// Initialize TRACE2 tracing by initializing multi-use fields and setting up any configured target formats.
+ ///
+ /// Approximate time calling application began executing.
+ void Initialize(DateTimeOffset startTime);
+
+ ///
+ /// Write Version and Start events.
///
- /// The standard error text stream connected back to the calling process.
- /// File system abstraction.
- /// The path to the GCM application.
+ /// The path to the application.
+ /// Args passed to the application (if applicable).
/// Path of the file this method is called from.
/// Line number of file this method is called from.
- void Start(TextWriter error,
- IFileSystem fileSystem,
- string appPath,
+ void Start(string appPath,
+ string[] args,
[System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0);
///
- /// Shut down TRACE2 tracing by writing Exit event and disposing of writers.
+ /// Write Exit event and dispose of writers.
///
/// The exit code of the GCM application.
/// Path of the file this method is called from.
@@ -61,40 +122,157 @@ void Start(TextWriter error,
void Stop(int exitCode,
[System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0);
+
+ ///
+ /// Writes information related to startup of child process to trace writer.
+ ///
+ /// Time at which child process began executing.
+ /// Process classification.
+ /// Specifies whether or not OS shell was used to start the process.
+ /// Name of application running in child process.
+ /// Arguments specific to the child process.
+ /// The child process's session id.
+ /// Path of the file this method is called from.
+ /// Line number of file this method is called from.
+ void WriteChildStart(DateTimeOffset startTime,
+ Trace2ProcessClass processClass,
+ bool useShell,
+ string appName,
+ string argv,
+ [System.Runtime.CompilerServices.CallerFilePath]
+ string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber]
+ int lineNumber = 0);
+
+ ///
+ /// Writes information related to exit of child process to trace writer.
+ ///
+ /// Runtime of child process.
+ /// Id of exiting process.
+ /// Process exit code.
+ /// Path of the file this method is called from.
+ /// Line number of file this method is called from.
+ void WriteChildExit(
+ double relativeTime,
+ int pid,
+ int code,
+ [System.Runtime.CompilerServices.CallerFilePath]
+ string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber]
+ int lineNumber = 0);
+
+ ///
+ /// Writes an error as a message to the trace writer.
+ ///
+ /// The error message to write.
+ /// The error format string.
+ /// Path of the file this method is called from.
+ /// Line number of file this method is called from.
+ void WriteError(
+ string errorMessage,
+ string parameterizedMessage = null,
+ [System.Runtime.CompilerServices.CallerFilePath]
+ string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber]
+ int lineNumber = 0);
+
+ ///
+ /// Creates a region and manages entry/leaving.
+ ///
+ /// Category of region.
+ /// Description of region.
+ /// Message associated with entering region.
+ /// Path of the file this method is called from.
+ /// Line number of file this method is called from.
+ Region CreateRegion(
+ string category,
+ string label,
+ string message = "",
+ [System.Runtime.CompilerServices.CallerFilePath]
+ string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber]
+ int lineNumber = 0);
+
+ ///
+ /// Writes a region enter message to the trace writer.
+ ///
+ /// Category of region.
+ /// Description of region.
+ /// Message associated with entering region.
+ /// Path of the file this method is called from.
+ /// Line number of file this method is called from.
+ void WriteRegionEnter(
+ string category,
+ string label,
+ string message = "",
+ [System.Runtime.CompilerServices.CallerFilePath]
+ string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber]
+ int lineNumber = 0);
+
+ ///
+ /// Writes a region leave message to the trace writer.
+ ///
+ /// Time of region execution.
+ /// Category of region.
+ /// Description of region.
+ /// Message associated with entering region.
+ /// Path of the file this method is called from.
+ /// Line number of file this method is called from.
+ void WriteRegionLeave(
+ double relativeTime,
+ string category,
+ string label,
+ string message = "",
+ [System.Runtime.CompilerServices.CallerFilePath]
+ string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber]
+ int lineNumber = 0);
}
public class Trace2 : DisposableObject, ITrace2
{
+ private readonly ICommandContext _commandContext;
private readonly object _writersLock = new object();
private readonly Encoding _utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ private readonly List _writers = new List();
private const string GitSidVariable = "GIT_TRACE2_PARENT_SID";
- private List _writers = new List();
- private IEnvironment _environment;
- private Trace2Settings _settings;
- private string[] _argv;
private DateTimeOffset _applicationStartTime;
+ private Trace2Settings _settings;
private string _sid;
- public Trace2(IEnvironment environment, Trace2Settings settings, string[] argv, DateTimeOffset applicationStartTime)
+ private bool _initialized;
+ // Increment with each new child process that is tracked
+ private int _childProcCounter = 0;
+
+ public Trace2(ICommandContext commandContext)
+ {
+ _commandContext = commandContext;
+ }
+
+ public void Initialize(DateTimeOffset startTime)
{
- _environment = environment;
- _settings = settings;
- _argv = argv;
- _applicationStartTime = applicationStartTime;
+ if (_initialized)
+ {
+ return;
+ }
+
+ _applicationStartTime = startTime;
+ _settings = _commandContext.Settings.GetTrace2Settings();
+ _sid = ProcessManager.Sid;
+
+ InitializeWriters();
- _sid = SetSid();
+ _initialized = true;
}
- public void Start(TextWriter error,
- IFileSystem fileSystem,
- string appPath,
+ public void Start(string appPath,
+ string[] args,
string filePath,
int lineNumber)
{
- TryParseSettings(error, fileSystem);
-
if (!AssemblyUtils.TryGetAssemblyVersion(out string version))
{
// A version is required for TRACE2, so if this call fails
@@ -102,13 +280,178 @@ public void Start(TextWriter error,
version = "0.0.0";
}
WriteVersion(version, filePath, lineNumber);
- WriteStart(appPath, filePath, lineNumber);
+ WriteStart(appPath, args, filePath, lineNumber);
}
public void Stop(int exitCode, string filePath, int lineNumber)
{
WriteExit(exitCode, filePath, lineNumber);
- ReleaseManagedResources();
+ }
+
+ public void WriteChildStart(DateTimeOffset startTime,
+ Trace2ProcessClass processClass,
+ bool useShell,
+ string appName,
+ string argv,
+ string filePath = "",
+ int lineNumber = 0)
+ {
+ // Some child processes are started before TRACE2 can be initialized.
+ // Since certain dependencies are not available until initialization,
+ // we must immediately return if this method is invoked prior to
+ // initialization.
+ if (!_initialized)
+ {
+ return;
+ }
+
+ // Always add name of the application the process is executing
+ var procArgs = new List()
+ {
+ Path.GetFileName(appName)
+ };
+
+ // If the process has arguments, append them.
+ if (!string.IsNullOrEmpty(argv))
+ {
+ procArgs.AddRange(argv.Split(' '));
+ }
+
+ WriteMessage(new ChildStartMessage()
+ {
+ Event = Trace2Event.ChildStart,
+ Sid = _sid,
+ Time = startTime,
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
+ Line = lineNumber,
+ Id = ++_childProcCounter,
+ Classification = processClass,
+ UseShell = useShell,
+ Argv = procArgs,
+ ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,
+ Depth = ProcessManager.Depth,
+ });
+ }
+
+ public void WriteChildExit(
+ double relativeTime,
+ int pid,
+ int code,
+ string filePath = "",
+ int lineNumber = 0)
+ {
+ // Some child processes are started before TRACE2 can be initialized.
+ // Since certain dependencies are not available until initialization,
+ // we must immediately return if this method is invoked prior to
+ // initialization.
+ if (!_initialized)
+ {
+ return;
+ }
+
+ WriteMessage(new ChildExitMessage()
+ {
+ Event = Trace2Event.ChildExit,
+ Sid = _sid,
+ Time = DateTimeOffset.UtcNow,
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
+ Line = lineNumber,
+ Id = _childProcCounter,
+ Pid = pid,
+ Code = code,
+ ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,
+ RelativeTime = relativeTime,
+ Depth = ProcessManager.Depth
+ });
+ }
+
+ public void WriteError(
+ string errorMessage,
+ string parameterizedMessage = null,
+ [System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
+ [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0)
+ {
+ // It is possible for an error to be thrown before TRACE2 can be initialized.
+ // Since certain dependencies are not available until initialization,
+ // we must immediately return if this method is invoked prior to
+ // initialization.
+ if (!_initialized)
+ {
+ return;
+ }
+
+ WriteMessage(new ErrorMessage()
+ {
+ Event = Trace2Event.Error,
+ Sid = _sid,
+ Time = DateTimeOffset.UtcNow,
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
+ Line = lineNumber,
+ Message = errorMessage,
+ ParameterizedMessage = parameterizedMessage ?? errorMessage,
+ Depth = ProcessManager.Depth
+ });
+ }
+
+ public Region CreateRegion(
+ string category,
+ string label,
+ string message,
+ string filePath,
+ int lineNumber)
+ {
+ return new Region(this, category, label, filePath, lineNumber, message);
+ }
+
+ public void WriteRegionEnter(
+ string category,
+ string label,
+ string message = "",
+ string filePath = "",
+ int lineNumber = 0)
+ {
+ WriteMessage(new RegionEnterMessage()
+ {
+ Event = Trace2Event.RegionEnter,
+ Sid = _sid,
+ Time = DateTimeOffset.UtcNow,
+ Category = category,
+ Label = label,
+ Message = message == "" ? label : message,
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
+ Line = lineNumber,
+ ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,
+ Depth = ProcessManager.Depth
+ });
+ }
+
+ public void WriteRegionLeave(
+ double relativeTime,
+ string category,
+ string label,
+ string message = "",
+ string filePath = "",
+ int lineNumber = 0)
+ {
+ WriteMessage(new RegionLeaveMessage()
+ {
+ Event = Trace2Event.RegionLeave,
+ Sid = _sid,
+ Time = DateTimeOffset.UtcNow,
+ Category = category,
+ Label = label,
+ Message = message == "" ? label : message,
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
+ Line = lineNumber,
+ ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds,
+ RelativeTime = relativeTime,
+ Depth = ProcessManager.Depth
+ });
}
protected override void ReleaseManagedResources()
@@ -134,23 +477,7 @@ protected override void ReleaseManagedResources()
base.ReleaseManagedResources();
}
- internal string SetSid()
- {
- var sids = new List();
- if (_environment.Variables.TryGetValue(GitSidVariable, out string parentSid))
- {
- sids.Add(parentSid);
- }
-
- // Add GCM "child" sid
- sids.Add(Guid.NewGuid().ToString("D"));
- var combinedSid = string.Join("/", sids);
-
- _environment.SetEnvironmentVariable(GitSidVariable, combinedSid);
- return combinedSid;
- }
-
- internal bool TryGetPipeName(string eventTarget, out string name)
+ internal static bool TryGetPipeName(string eventTarget, out string name)
{
// Use prefixes to determine whether target is a named pipe/socket
if (eventTarget.Contains("af_unix:", StringComparison.OrdinalIgnoreCase) ||
@@ -167,14 +494,14 @@ internal bool TryGetPipeName(string eventTarget, out string name)
return false;
}
- private void TryParseSettings(TextWriter error, IFileSystem fileSystem)
+ private void InitializeWriters()
{
// Set up the correct writer for every enabled format target.
foreach (var formatTarget in _settings.FormatTargetsAndValues)
{
if (TryGetPipeName(formatTarget.Value, out string name)) // Write to named pipe/socket
{
- AddWriter(new Trace2CollectorWriter((
+ AddWriter(new Trace2CollectorWriter(formatTarget.Key, (
() => new NamedPipeClientStream(".", name,
PipeDirection.Out,
PipeOptions.Asynchronous)
@@ -183,20 +510,17 @@ private void TryParseSettings(TextWriter error, IFileSystem fileSystem)
}
else if (formatTarget.Value.IsTruthy()) // Write to stderr
{
- AddWriter(new Trace2StreamWriter(error, formatTarget.Key));
+ AddWriter(new Trace2StreamWriter(formatTarget.Key, _commandContext.Streams.Error));
}
else if (Path.IsPathRooted(formatTarget.Value)) // Write to file
{
try
{
- Stream stream = fileSystem.OpenFileStream(formatTarget.Value, FileMode.Append,
- FileAccess.Write, FileShare.ReadWrite);
- AddWriter(new Trace2StreamWriter(new StreamWriter(stream, _utf8NoBomEncoding,
- 4096, leaveOpen: false), formatTarget.Key));
+ AddWriter(new Trace2FileWriter(formatTarget.Key, formatTarget.Value));
}
catch (Exception ex)
{
- error.WriteLine($"warning: unable to trace to file '{formatTarget.Value}': {ex.Message}");
+ Console.Error.WriteLine($"warning: unable to trace to file '{formatTarget.Value}': {ex.Message}");
}
}
}
@@ -215,7 +539,8 @@ private void WriteVersion(
Event = Trace2Event.Version,
Sid = _sid,
Time = DateTimeOffset.UtcNow,
- File = Path.GetFileName(filePath).ToLower(),
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
Line = lineNumber,
Evt = eventFormatVersion,
Exe = gcmVersion
@@ -224,6 +549,7 @@ private void WriteVersion(
private void WriteStart(
string appPath,
+ string[] args,
string filePath,
int lineNumber)
{
@@ -232,14 +558,19 @@ private void WriteStart(
{
Path.GetFileName(appPath),
};
- argv.AddRange(_argv);
+
+ if (args.Length > 0)
+ {
+ argv.AddRange(args);
+ }
WriteMessage(new StartMessage()
{
Event = Trace2Event.Start,
Sid = _sid,
Time = DateTimeOffset.UtcNow,
- File = Path.GetFileName(filePath).ToLower(),
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
Line = lineNumber,
Argv = argv,
ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds
@@ -254,8 +585,9 @@ private void WriteExit(int code, string filePath = "", int lineNumber = 0)
{
Event = Trace2Event.Exit,
Sid = _sid,
- Time = DateTimeOffset.Now,
- File = Path.GetFileName(filePath).ToLower(),
+ Time = DateTimeOffset.UtcNow,
+ Thread = BuildThreadName(),
+ File = Path.GetFileName(filePath),
Line = lineNumber,
Code = code,
ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds
@@ -280,6 +612,11 @@ private void WriteMessage(Trace2Message message)
{
ThrowIfDisposed();
+ if (!_initialized)
+ {
+ return;
+ }
+
lock (_writersLock)
{
if (_writers.Count == 0)
@@ -296,123 +633,28 @@ private void WriteMessage(Trace2Message message)
}
}
}
-}
-
-public abstract class Trace2Message
-{
- protected const string TimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffff'Z'";
- private const int SourceColumnMaxWidth = 23;
-
- [JsonProperty("event", Order = 1)]
- public Trace2Event Event { get; set; }
-
- [JsonProperty("sid", Order = 2)]
- public string Sid { get; set; }
-
- // TODO: Remove this default value when TRACE2 regions are introduced.
- [JsonProperty("thread", Order = 3)]
- public string Thread { get; set; } = "main";
-
- [JsonProperty("time", Order = 4)]
- public DateTimeOffset Time { get; set; }
-
- [JsonProperty("file", Order = 5)]
-
- public string File { get; set; }
-
- [JsonProperty("line", Order = 6)]
- public int Line { get; set; }
-
- public abstract string ToJson();
-
- public abstract string ToNormalString();
- protected string BuildNormalString(string message)
+ private static string BuildThreadName()
{
- // The normal format uses local time rather than UTC time.
- string time = Time.ToLocalTime().ToString("HH:mm:ss.ffffff");
-
- // Source column format is file:line
- string source = $"{File.ToLower()}:{Line}";
- if (source.Length > SourceColumnMaxWidth)
+ // If this is the entry thread, call it "main", per Trace2 convention
+ if (Thread.CurrentThread.ManagedThreadId == 0)
{
- source = TraceUtils.FormatSource(source, SourceColumnMaxWidth);
+ return "main";
}
- // Git's TRACE2 normal format is:
- // [