diff --git a/.azure-pipelines/continuous-integration.yml b/.azure-pipelines/continuous-integration.yml
deleted file mode 100644
index cd9b304a0..000000000
--- a/.azure-pipelines/continuous-integration.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-trigger:
- - main
-
-variables:
- configuration: Release
- winImage: vs2017-win2016
- osxImage: macos-latest
-
-jobs:
-- job: windows
- displayName: Windows
- pool:
- vmImage: $(winImage)
- steps:
- - template: templates/windows/compile.yml
- - template: templates/windows/pack.yml
-
-- job: osx
- displayName: macOS
- pool:
- vmImage: $(osxImage)
- steps:
- - template: templates/osx/compile.yml
- - template: templates/osx/pack.unsigned.yml
-
-- job: ubuntu1804_x86_64
- displayName: Ubuntu 18.04 LTS x86_64
- pool:
- vmImage: ubuntu-18.04
- steps:
- - template: templates/linux/compile.yml
- - template: templates/linux/pack.unsigned.yml
diff --git a/.azure-pipelines/pull-request.yml b/.azure-pipelines/pull-request.yml
deleted file mode 100644
index e24d3d6e1..000000000
--- a/.azure-pipelines/pull-request.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-pr:
- - main
- - release
-
-variables:
- configuration: Release
- winImage: vs2017-win2016
- osxImage: macos-latest
-
-jobs:
-- job: windows
- displayName: Windows
- pool:
- vmImage: $(winImage)
- steps:
- - template: templates/windows/compile.yml
- - template: templates/windows/pack.yml
-
-- job: osx
- displayName: macOS
- pool:
- vmImage: $(osxImage)
- steps:
- - template: templates/osx/compile.yml
- - template: templates/osx/pack.unsigned.yml
-
-- job: ubuntu1804_x86_64
- displayName: Ubuntu 18.04 LTS x86_64
- pool:
- vmImage: ubuntu-18.04
- steps:
- - template: templates/linux/compile.yml
- - template: templates/linux/pack.unsigned.yml
diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml
deleted file mode 100644
index 934942932..000000000
--- a/.azure-pipelines/release.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-trigger:
- - release
-
-variables:
- configuration: Release
- signPool: MSEngSS-MicroBuild2019-1ES
- winImage: vs2017-win2016
- osxImage: macos-latest
-
-jobs:
-- job: windows
- displayName: Windows
- pool:
- name: $(signPool)
- steps:
- - template: templates/windows/compile.signed.yml
- - template: templates/windows/pack.yml
-
-- job: osx_step1
- displayName: macOS (Build & Layout)
- pool:
- vmImage: $(osxImage)
- steps:
- - template: templates/osx/compile.yml
- - template: templates/osx/pack.signed/step1-layout.yml
-
-- job: osx_step2
- displayName: macOS (Sign payload)
- dependsOn: osx_step1
- condition: succeeded()
- pool:
- name: $(signPool)
- steps:
- - template: templates/osx/pack.signed/step2-signpayload.yml
-
-- job: osx_step3
- displayName: macOS (Pack)
- dependsOn: osx_step2
- condition: succeeded()
- pool:
- vmImage: $(osxImage)
- steps:
- - template: templates/osx/pack.signed/step3-pack.yml
-
-- job: osx_step4
- displayName: macOS (Sign package)
- dependsOn: osx_step3
- condition: succeeded()
- pool:
- name: $(signPool)
- steps:
- - template: templates/osx/pack.signed/step4-signpack.yml
-
-- job: osx_step5
- displayName: macOS (Prepare for distribution)
- dependsOn: osx_step4
- condition: succeeded()
- pool:
- vmImage: $(osxImage)
- steps:
- - template: templates/osx/pack.signed/step5-dist.yml
-
-- job: ubuntu1804_x86_64
- displayName: Ubuntu 18.04 LTS x86_64
- pool:
- vmImage: ubuntu-18.04
- steps:
- - template: templates/linux/compile.yml
- - template: templates/linux/pack.unsigned.yml
diff --git a/.azure-pipelines/templates/linux/compile.yml b/.azure-pipelines/templates/linux/compile.yml
deleted file mode 100644
index a45ab6b30..000000000
--- a/.azure-pipelines/templates/linux/compile.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-steps:
- - task: UseDotNet@2
- displayName: Use .NET SDK 6.0.201
- inputs:
- packageType: sdk
- version: 6.0.201
-
- - task: DotNetCoreCLI@2
- displayName: Compile common code
- inputs:
- command: build
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Linux$(configuration)'
-
- - task: DotNetCoreCLI@2
- displayName: Run common unit tests
- inputs:
- command: test
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Linux$(configuration)'
- publishTestResults: true
- testRunTitle: 'Unit tests (Linux)'
diff --git a/.azure-pipelines/templates/linux/pack.unsigned.yml b/.azure-pipelines/templates/linux/pack.unsigned.yml
deleted file mode 100644
index 08f9ccffd..000000000
--- a/.azure-pipelines/templates/linux/pack.unsigned.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-steps:
- - script: |
- mkdir -p "$(Build.StagingDirectory)/publish/"
- cp "out/linux/Packaging.Linux/tar/$(configuration)/"*.tar.gz "$(Build.StagingDirectory)/publish/"
- cp "out/linux/Packaging.Linux/deb/$(configuration)/"*.deb "$(Build.StagingDirectory)/publish/"
- displayName: Prepare final build artifacts
-
- - task: PublishPipelineArtifact@0
- displayName: Publish unsigned installer artifacts
- inputs:
- artifactName: 'Installer.Linux.Unsigned'
- targetPath: '$(Build.StagingDirectory)/publish'
diff --git a/.azure-pipelines/templates/osx/compile.yml b/.azure-pipelines/templates/osx/compile.yml
deleted file mode 100644
index d480a9213..000000000
--- a/.azure-pipelines/templates/osx/compile.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-steps:
- - task: UseDotNet@2
- displayName: Use .NET SDK 6.0.201
- inputs:
- packageType: sdk
- version: 6.0.201
-
- - task: DotNetCoreCLI@2
- displayName: Compile common code and macOS Helpers
- inputs:
- command: build
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Mac$(configuration)'
-
- - task: DotNetCoreCLI@2
- displayName: Run common unit tests
- inputs:
- command: test
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Mac$(configuration)'
- publishTestResults: true
- testRunTitle: 'Unit tests (macOS)'
diff --git a/.azure-pipelines/templates/osx/pack.signed/step1-layout.yml b/.azure-pipelines/templates/osx/pack.signed/step1-layout.yml
deleted file mode 100644
index cb93812b6..000000000
--- a/.azure-pipelines/templates/osx/pack.signed/step1-layout.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-steps:
- - script: src/osx/Installer.Mac/layout.sh --configuration='$(configuration)' --output='$(Build.StagingDirectory)/payload' --symbol-output='$(Build.StagingDirectory)/symbols'
- displayName: Layout installer payload
-
- - task: PublishPipelineArtifact@0
- displayName: Upload unsigned payload
- inputs:
- artifactName: 'tmp.macpayload_unsigned'
- targetPath: '$(Build.StagingDirectory)/payload'
-
- - task: PublishPipelineArtifact@0
- displayName: Upload symbols
- inputs:
- artifactName: 'tmp.macsymbols'
- targetPath: '$(Build.StagingDirectory)/symbols'
diff --git a/.azure-pipelines/templates/osx/pack.signed/step2-signpayload.yml b/.azure-pipelines/templates/osx/pack.signed/step2-signpayload.yml
deleted file mode 100644
index 01439a347..000000000
--- a/.azure-pipelines/templates/osx/pack.signed/step2-signpayload.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-steps:
- - task: NuGetAuthenticate@0
- displayName: Authenticate to MicroBuild NuGet feed
- inputs:
- nuGetServiceConnections: 'MicroBuild Toolset Nuget Feed (Read)'
-
- - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@3
- displayName: Install signing plugin
- inputs:
- signType: '$(SignType)'
-
- - task: DownloadPipelineArtifact@1
- displayName: Download unsigned payload
- inputs:
- buildType: 'current'
- artifactName: 'tmp.macpayload_unsigned'
- downloadPath: '$(Build.StagingDirectory)\payload'
-
- - task: UseDotNet@2
- displayName: Use .NET SDK 6.0.201
- inputs:
- packageType: sdk
- version: 6.0.201
-
- - task: NuGetToolInstaller@0
- displayName: Install NuGet tool >=4.3.0
- inputs:
- versionSpec: '>=4.3.0'
-
- # Must use the NuGet & MSBuild toolchain here rather than `dotnet`
- # because the signing tasks target the netfx MSBuild runtime only.
- - task: NuGetCommand@2
- displayName: Restore MicroBuild packages
- inputs:
- command: restore
- restoreSolution: 'src\osx\SignFiles.Mac\SignFiles.Mac.csproj'
-
- - task: MSBuild@1
- displayName: Sign payload
- inputs:
- solution: 'src\osx\SignFiles.Mac\SignFiles.Mac.csproj'
- msbuildArguments: '/p:RootDir="$(Build.StagingDirectory)\payload"'
-
- - task: PublishPipelineArtifact@0
- displayName: Upload signed payload
- inputs:
- artifactName: 'tmp.macpayload_signed'
- targetPath: '$(Build.StagingDirectory)\payload'
diff --git a/.azure-pipelines/templates/osx/pack.signed/step3-pack.yml b/.azure-pipelines/templates/osx/pack.signed/step3-pack.yml
deleted file mode 100644
index 897aca172..000000000
--- a/.azure-pipelines/templates/osx/pack.signed/step3-pack.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-steps:
- - task: DownloadPipelineArtifact@1
- displayName: Download signed payload
- inputs:
- buildType: 'current'
- artifactName: 'tmp.macpayload_signed'
- downloadPath: '$(Build.StagingDirectory)/payload'
-
- - task: UseDotNet@2
- displayName: Use .NET SDK 6.0.201
- inputs:
- packageType: sdk
- version: 6.0.201
-
- - script: dotnet tool install --global nbgv
- displayName: Install Nerdbank.GitVersioning tool
-
- - script: nbgv cloud --common-vars
- displayName: Set version variables
-
- - script: src/osx/Installer.Mac/pack.sh --payload='$(Build.StagingDirectory)/payload' --version='$(GitBuildVersionSimple)' --output='$(Build.StagingDirectory)/components/com.microsoft.gitcredentialmanager.component.pkg'
- displayName: Create component package
-
- - script: src/osx/Installer.Mac/dist.sh --package-path='$(Build.StagingDirectory)/components' --version='$(GitBuildVersionSimple)' --output='$(Build.StagingDirectory)/pkg/gcmcore-osx-$(GitBuildVersionSimple).pkg' || exit 1
- displayName: Create product archive
-
- - task: PublishPipelineArtifact@0
- displayName: Upload unsigned package
- inputs:
- artifactName: 'tmp.macinstaller_unsigned'
- targetPath: '$(Build.StagingDirectory)/pkg/gcmcore-osx-$(GitBuildVersionSimple).pkg'
diff --git a/.azure-pipelines/templates/osx/pack.signed/step4-signpack.yml b/.azure-pipelines/templates/osx/pack.signed/step4-signpack.yml
deleted file mode 100644
index 0a7e05d6d..000000000
--- a/.azure-pipelines/templates/osx/pack.signed/step4-signpack.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-steps:
- - task: NuGetAuthenticate@0
- displayName: Authenticate to MicroBuild NuGet feed
- inputs:
- nuGetServiceConnections: 'MicroBuild Toolset Nuget Feed (Read)'
-
- - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@3
- displayName: Install signing plugin
- inputs:
- signType: '$(SignType)'
-
- - task: DownloadPipelineArtifact@1
- displayName: Download unsigned package
- inputs:
- buildType: 'current'
- artifactName: 'tmp.macinstaller_unsigned'
- downloadPath: '$(Build.StagingDirectory)\pkg'
-
- - powershell: |
- $dir="$(Build.StagingDirectory)\pkg"
- Compress-Archive -Path $dir\*.pkg $dir\gcmcorepkg.zip
- Remove-Item $dir\*.pkg
- displayName: 'Zip package file for signing'
-
- - task: ms-vseng.MicroBuildTasks.7973a23b-33e3-4b00-a7d9-c06d90f8297f.MicroBuildSignMacFiles@1
- displayName: Sign package
- inputs:
- SigningTarget: '$(Build.StagingDirectory)\pkg\gcmcorepkg.zip'
- SigningCert: 8003
- condition: and(succeeded(), ne(variables['SignType'], 'test'))
-
- - powershell: |
- $dir="$(Build.StagingDirectory)\pkg"
- Expand-Archive -LiteralPath $dir\gcmcorepkg.zip -DestinationPath $dir -Force
- Remove-Item $dir\gcmcorepkg.zip -Force
- displayName: 'Unzip signed package file'
-
- - task: PublishPipelineArtifact@0
- displayName: Upload signed installer
- inputs:
- artifactName: 'tmp.macinstaller_signed'
- targetPath: '$(Build.StagingDirectory)\pkg'
diff --git a/.azure-pipelines/templates/osx/pack.signed/step5-dist.yml b/.azure-pipelines/templates/osx/pack.signed/step5-dist.yml
deleted file mode 100644
index 31a4cbcc7..000000000
--- a/.azure-pipelines/templates/osx/pack.signed/step5-dist.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-steps:
- - task: DownloadPipelineArtifact@1
- displayName: Download signed installer
- inputs:
- buildType: 'current'
- artifactName: 'tmp.macinstaller_signed'
- downloadPath: '$(Build.StagingDirectory)/pkg'
-
- - task: DownloadPipelineArtifact@1
- displayName: Download signed payload
- inputs:
- buildType: 'current'
- artifactName: 'tmp.macpayload_signed'
- downloadPath: '$(Build.StagingDirectory)/payload'
-
- - task: DownloadPipelineArtifact@1
- displayName: Download symbols
- inputs:
- buildType: 'current'
- artifactName: 'tmp.macsymbols'
- downloadPath: '$(Build.StagingDirectory)/symbols'
-
- # Skip notarization until we can preserve the hardened runtime bit and sign the .NET runtime bits
- # Tracked: https://github.com/microsoft/Git-Credential-Manager-Core/issues/108
- #- script: src/osx/SignFiles.Mac/notarize-pkg.sh -id "$(AppleId)" -p "$(AppleIdPassword)" -pkg "$(Build.StagingDirectory)"/pkg/*.pkg
- # displayName: Notarize and staple installer package
-
- - script: |
- mkdir -p "$(Build.StagingDirectory)/publish/payload" "$(Build.StagingDirectory)/publish/payload.sym"
- cp -f "$(Build.StagingDirectory)"/pkg/*.pkg "$(Build.StagingDirectory)/publish/"
- cp -Rf "$(Build.StagingDirectory)/payload/" "$(Build.StagingDirectory)/publish/payload/"
- cp -Rf "$(Build.StagingDirectory)/symbols/" "$(Build.StagingDirectory)/publish/payload.sym/"
- displayName: Prepare final build artifacts
-
- - script: dotnet tool install --global nbgv
- displayName: Install Nerdbank.GitVersioning tool
-
- - script: nbgv cloud --common-vars
- displayName: Set version variables
-
- - task: ArchiveFiles@2
- displayName: Create payload archive
- inputs:
- rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload/'
- includeRootFolder: false
- archiveType: 'tar'
- archiveFile: '$(Build.StagingDirectory)/publish/gcmcore-osx-$(GitBuildVersionSimple).tar.gz'
- replaceExistingArchive: true
-
- - task: ArchiveFiles@2
- displayName: Create symbol archive
- inputs:
- rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload.sym/'
- includeRootFolder: false
- archiveType: 'tar'
- archiveFile: '$(Build.StagingDirectory)/publish/symbols-osx.tar.gz'
- replaceExistingArchive: true
-
- - task: PublishPipelineArtifact@0
- displayName: Publish signed installer artifacts
- inputs:
- artifactName: 'Installer.Mac.Signed'
- targetPath: '$(Build.StagingDirectory)/publish'
diff --git a/.azure-pipelines/templates/osx/pack.unsigned.yml b/.azure-pipelines/templates/osx/pack.unsigned.yml
deleted file mode 100644
index e225d31ef..000000000
--- a/.azure-pipelines/templates/osx/pack.unsigned.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-steps:
- - script: |
- cp -R "out/osx/Installer.Mac/pkg/$(configuration)" "$(Build.StagingDirectory)/publish/"
- displayName: Prepare final build artifacts
-
- - script: dotnet tool install --global nbgv
- displayName: Install Nerdbank.GitVersioning tool
-
- - script: nbgv cloud --common-vars
- displayName: Set version variables
-
- - task: ArchiveFiles@2
- displayName: Create payload archive
- inputs:
- rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload'
- includeRootFolder: false
- archiveType: 'tar'
- archiveFile: '$(Build.StagingDirectory)/publish/gcmcore-osx-$(GitBuildVersionSimple).tar.gz'
- replaceExistingArchive: true
-
- - task: ArchiveFiles@2
- displayName: Create symbol archive
- inputs:
- rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload.sym/'
- includeRootFolder: false
- archiveType: 'tar'
- archiveFile: '$(Build.StagingDirectory)/publish/symbols-osx.tar.gz'
- replaceExistingArchive: true
-
- - task: PublishPipelineArtifact@0
- displayName: Publish unsigned installer artifacts
- inputs:
- artifactName: 'Installer.Mac.Unsigned'
- targetPath: '$(Build.StagingDirectory)/publish'
diff --git a/.azure-pipelines/templates/windows/compile.signed.yml b/.azure-pipelines/templates/windows/compile.signed.yml
deleted file mode 100644
index ab2afefaf..000000000
--- a/.azure-pipelines/templates/windows/compile.signed.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-steps:
- - task: NuGetAuthenticate@0
- displayName: Authenticate to MicroBuild NuGet feed
- inputs:
- nuGetServiceConnections: 'MicroBuild Toolset Nuget Feed (Read)'
-
- - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@3
- displayName: Install signing plugin
- condition: and(succeeded(), eq(variables['SignType'], 'real'))
- inputs:
- signType: '$(SignType)'
-
- - task: UseDotNet@2
- displayName: Use .NET SDK 6.0.201
- inputs:
- packageType: sdk
- version: 6.0.201
-
- - task: NuGetToolInstaller@0
- displayName: Install NuGet tool >=4.3.0
- inputs:
- versionSpec: '>=4.3.0'
-
- # Must use the NuGet & MSBuild toolchain here rather than `dotnet`
- # because the signing tasks target the netfx MSBuild runtime only.
- - task: NuGetCommand@2
- displayName: Restore packages
- inputs:
- command: restore
- restoreSolution: 'Git-Credential-Manager.sln'
- configuration: 'Windows$(configuration)'
-
- - task: MSBuild@1
- displayName: Compile common code and Windows helpers
- inputs:
- solution: 'Git-Credential-Manager.sln'
- configuration: 'Windows$(configuration)'
-
- - task: VSTest@2
- displayName: Run common unit tests
- inputs:
- testAssemblyVer2: |
- out\shared\*.Tests\bin\**\*.Tests.dll
- configuration: 'Windows$(configuration)'
- otherConsoleOptions: '/Framework:.NETCoreApp,Version=2.1'
- testRunTitle: 'Unit tests (Windows)'
diff --git a/.azure-pipelines/templates/windows/compile.yml b/.azure-pipelines/templates/windows/compile.yml
deleted file mode 100644
index 48d664869..000000000
--- a/.azure-pipelines/templates/windows/compile.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-steps:
- - task: UseDotNet@2
- displayName: Use .NET SDK 6.0.201
- inputs:
- packageType: sdk
- version: 6.0.201
-
- - task: DotNetCoreCLI@2
- displayName: Restore packages
- inputs:
- command: restore
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Windows$(configuration)'
-
- - task: DotNetCoreCLI@2
- displayName: Compile common code and Windows Helpers
- inputs:
- command: build
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Windows$(configuration)'
-
- - task: DotNetCoreCLI@2
- displayName: Run common unit tests
- inputs:
- command: test
- projects: 'Git-Credential-Manager.sln'
- arguments: '--configuration=Mac$(configuration)'
- publishTestResults: true
- testRunTitle: 'Unit tests (Windows)'
diff --git a/.azure-pipelines/templates/windows/pack.yml b/.azure-pipelines/templates/windows/pack.yml
deleted file mode 100644
index c6e6fde63..000000000
--- a/.azure-pipelines/templates/windows/pack.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-steps:
- - script: dotnet tool install --tool-path .tools nbgv
- displayName: Install Nerdbank.GitVersioning tool
-
- - script: .tools\nbgv cloud --common-vars
- displayName: Set version variables
-
- - script: |
- xcopy "out\windows\Installer.Windows\bin\$(configuration)\net472" "$(Build.StagingDirectory)\publish\"
- xcopy "out\windows\Payload.Windows\bin\$(configuration)\net472\win-x86" "$(Build.StagingDirectory)\publish\payload\"
- mkdir "$(Build.StagingDirectory)\publish\payload.sym\"
- move "$(Build.StagingDirectory)\publish\payload\*.pdb" "$(Build.StagingDirectory)\publish\payload.sym\"
- displayName: Prepare final build artifacts
-
- - task: ArchiveFiles@2
- displayName: Create payload archive
- inputs:
- rootFolderOrFile: '$(Build.StagingDirectory)\publish\payload\'
- includeRootFolder: false
- archiveType: 'zip'
- archiveFile: '$(Build.StagingDirectory)\publish\gcmcore-win-x86-$(GitBuildVersionSimple).zip'
- replaceExistingArchive: true
-
- - task: ArchiveFiles@2
- displayName: Create symbol archive
- inputs:
- rootFolderOrFile: '$(Build.StagingDirectory)\publish\payload.sym\'
- includeRootFolder: false
- archiveType: 'zip'
- archiveFile: '$(Build.StagingDirectory)\publish\symbols-win-x86.zip'
- replaceExistingArchive: true
-
- - task: PublishPipelineArtifact@0
- displayName: Publish unsigned installer artifacts
- condition: and(succeeded(), ne(variables['SignType'], 'real'))
- inputs:
- artifactName: 'Installer.Windows.Unsigned'
- targetPath: '$(Build.StagingDirectory)\publish'
-
- - task: PublishPipelineArtifact@0
- displayName: Publish signed installer artifacts
- condition: and(succeeded(), eq(variables['SignType'], 'real'))
- inputs:
- artifactName: 'Installer.Windows.Signed'
- targetPath: '$(Build.StagingDirectory)\publish'
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..b0f8d194a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+
+updates:
+
+ # Enable version updates for GitHub ecosystem
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/run_developer_signing.sh b/.github/run_developer_signing.sh
new file mode 100755
index 000000000..8b3de88a3
--- /dev/null
+++ b/.github/run_developer_signing.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+SIGN_DIR=$1
+DEVELOPER_ID=$2
+ENTITLEMENTS_FILE=$3
+
+if [ -z "$SIGN_DIR" ]; then
+ echo "error: missing directory argument"
+ exit 1
+elif [ -z "$DEVELOPER_ID" ]; then
+ echo "error: missing developer id argument"
+ exit 1
+elif [ -z "$ENTITLEMENTS_FILE" ]; then
+ echo "error: missing entitlements file argument"
+ exit 1
+fi
+
+echo "======== INPUTS ========"
+echo "Directory: $SIGN_DIR"
+echo "Developer ID: $DEVELOPER_ID"
+echo "Entitlements: $ENTITLEMENTS_FILE"
+echo "======== END INPUTS ========"
+
+cd $SIGN_DIR
+for f in *
+do
+ macho=$(file --mime $f | grep mach)
+ # Runtime sign dylibs and Mach-O binaries
+ if [[ $f == *.dylib ]] || [ ! -z "$macho" ];
+ then
+ echo "Runtime Signing $f"
+ codesign -s "$DEVELOPER_ID" $f --timestamp --force --options=runtime --entitlements $ENTITLEMENTS_FILE
+ elif [ -d "$f" ];
+ then
+ echo "Signing files in subdirectory $f"
+ cd $f
+ for i in *
+ do
+ codesign -s "$DEVELOPER_ID" $i --timestamp --force
+ done
+ cd ..
+ else
+ echo "Signing $f"
+ codesign -s "$DEVELOPER_ID" $f --timestamp --force
+ fi
+done
\ No newline at end of file
diff --git a/.github/run_esrp_signing.py b/.github/run_esrp_signing.py
index fd137976e..dc598fdcd 100644
--- a/.github/run_esrp_signing.py
+++ b/.github/run_esrp_signing.py
@@ -1,3 +1,4 @@
+import argparse
import json
import os
import glob
@@ -6,40 +7,44 @@
import sys
import re
+parser = argparse.ArgumentParser(description='Sign binaries for Windows, macOS, and Linux')
+parser.add_argument('path', help='Path to file for signing')
+parser.add_argument('keycode', help='Platform-specific key code for signing')
+parser.add_argument('opcode', help='Platform-specific operation code for signing')
+# Setting nargs=argparse.REMAINDER allows us to pass in params that begin with `--`
+parser.add_argument('--params', nargs=argparse.REMAINDER, help='Parameters for signing')
+args = parser.parse_args()
+
esrp_tool = os.path.join("esrp", "tools", "EsrpClient.exe")
aad_id = os.environ['AZURE_AAD_ID'].strip()
+# We temporarily need two AAD IDs, as we're using an SSL certificate associated
+# with an older App Registration until we have the required hardware to approve
+# the new certificate in SSL Admin.
+aad_id_ssl = os.environ['AZURE_AAD_ID_SSL'].strip()
workspace = os.environ['GITHUB_WORKSPACE'].strip()
-source_root_location = os.path.join(workspace, "deb", "Release")
-destination_location = os.path.join(workspace)
-
-files = glob.glob(os.path.join(source_root_location, "*.deb"))
+source_location = args.path
+files = glob.glob(os.path.join(source_location, "*"))
print("Found files:")
pprint.pp(files)
-if len(files) < 1 or not files[0].endswith(".deb"):
- print("Error: cannot find .deb to sign")
- exit(1)
-
-file_to_sign = os.path.basename(files[0])
-
auth_json = {
- "Version": "1.0.0",
- "AuthenticationType": "AAD_CERT",
- "TenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
- "ClientId": aad_id,
- "AuthCert": {
- "SubjectName": f"CN={aad_id}.microsoft.com",
- "StoreLocation": "LocalMachine",
- "StoreName": "My",
- },
- "RequestSigningCert": {
- "SubjectName": f"CN={aad_id}",
- "StoreLocation": "LocalMachine",
- "StoreName": "My",
- }
+ "Version": "1.0.0",
+ "AuthenticationType": "AAD_CERT",
+ "TenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
+ "ClientId": f"{aad_id}",
+ "AuthCert": {
+ "SubjectName": f"CN={aad_id_ssl}.microsoft.com",
+ "StoreLocation": "LocalMachine",
+ "StoreName": "My"
+ },
+ "RequestSigningCert": {
+ "SubjectName": f"CN={aad_id}",
+ "StoreLocation": "LocalMachine",
+ "StoreName": "My"
+ }
}
input_json = {
@@ -47,21 +52,15 @@
"SignBatches": [
{
"SourceLocationType": "UNC",
- "SourceRootDirectory": source_root_location,
+ "SourceRootDirectory": source_location,
"DestinationLocationType": "UNC",
- "DestinationRootDirectory": destination_location,
- "SignRequestFiles": [
- {
- "CustomerCorrelationId": "01A7F55F-6CDD-4123-B255-77E6F212CDAD",
- "SourceLocation": file_to_sign,
- "DestinationLocation": os.path.join("Signed", file_to_sign),
- }
- ],
+ "DestinationRootDirectory": workspace,
+ "SignRequestFiles": [],
"SigningInfo": {
"Operations": [
{
- "KeyCode": "CP-450779-Pgp",
- "OperationCode": "LinuxSign",
+ "KeyCode": f"{args.keycode}",
+ "OperationCode": f"{args.opcode}",
"Parameters": {},
"ToolName": "sign",
"ToolVersion": "1.0",
@@ -72,10 +71,27 @@
]
}
+# add files to sign
+for f in files:
+ name = os.path.basename(f)
+ input_json["SignBatches"][0]["SignRequestFiles"].append(
+ {
+ "SourceLocation": name,
+ "DestinationLocation": os.path.join("signed", name),
+ }
+ )
+
+# add parameters to input.json (e.g. enabling the hardened runtime for macOS)
+if args.params is not None:
+ i = 0
+ while i < len(args.params):
+ input_json["SignBatches"][0]["SigningInfo"]["Operations"][0]["Parameters"][args.params[i]] = args.params[i + 1]
+ i += 2
+
policy_json = {
"Version": "1.0.0",
"Intent": "production release",
- "ContentType": "Debian package",
+ "ContentType": "binary",
}
configs = [
@@ -106,7 +122,7 @@
'***',
result.stdout,
flags=re.IGNORECASE|re.MULTILINE)
-printf(log)
+print(log)
if result.returncode != 0:
print("Failed to run ESRPClient.exe")
@@ -117,6 +133,6 @@
with open(esrp_out, 'r') as fp:
pprint.pp(json.load(fp))
-signed_file = os.path.join(destination_location, "Signed", file_to_sign)
-if os.path.isfile(signed_file):
- print(f"Success!\nSigned {signed_file}")
+for file in files:
+ if os.path.isfile(os.path.join("signed", file)):
+ print(f"Success!\nSigned {file}")
\ No newline at end of file
diff --git a/.github/set_up_esrp.ps1 b/.github/set_up_esrp.ps1
new file mode 100644
index 000000000..099cd50c2
--- /dev/null
+++ b/.github/set_up_esrp.ps1
@@ -0,0 +1,12 @@
+# Install ESRP client
+az storage blob download --file esrp.zip --account-key "$env:AZURE_STORAGE_KEY" --account-name gcmesrp --container microsoft-esrp-client --name microsoft.esrpclient.1.2.76.nupkg
+Expand-Archive -Path esrp.zip -DestinationPath .\esrp
+
+# Install certificates
+az keyvault secret download --vault-name "$env:AZURE_VAULT" --name "$env:AUTH_CERT" --file out.pfx
+certutil -f -importpfx out.pfx
+Remove-Item out.pfx
+
+az keyvault secret download --vault-name "$env:AZURE_VAULT" --name "$env:REQUEST_SIGNING_CERT" --file out.pfx
+certutil -f -importpfx out.pfx
+Remove-Item out.pfx
\ No newline at end of file
diff --git a/.github/workflows/build-installers.yml b/.github/workflows/build-installers.yml
deleted file mode 100644
index ebd8b1c8a..000000000
--- a/.github/workflows/build-installers.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-name: Build-Installers
-
-on:
- workflow_dispatch:
- push:
- branches: [ main, release ]
- pull_request:
- branches: [ main ]
-
-jobs:
- linux:
- name: "Linux"
-
- runs-on: ubuntu-18.04
- steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: 6.0.201
-
- - name: Install dependencies
- run: dotnet restore --force
-
- - name: Build Linux Payloads
- run: dotnet build -c Release src/linux/Packaging.Linux/Packaging.Linux.csproj
-
- - name: Upload Installers
- uses: actions/upload-artifact@v2
- with:
- name: Installers
- path: |
- out/linux/Packaging.Linux/deb/Release/*.deb
- out/linux/Packaging.Linux/tar/Release/*.tar.gz
diff --git a/.github/workflows/build-signed-deb.yml b/.github/workflows/build-signed-deb.yml
deleted file mode 100644
index 6437225d1..000000000
--- a/.github/workflows/build-signed-deb.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-name: "Build Signed Debian Installer"
-
-on:
- workflow_dispatch:
- release:
- types: [released]
-
-jobs:
- build:
- name: "Build"
- runs-on: ubuntu-18.04
- steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: 6.0.201
-
- - name: Install dependencies
- run: dotnet restore --force
-
- - name: Build Linux Payloads
- run: dotnet build -c Release src/linux/Packaging.Linux/Packaging.Linux.csproj
-
- - name: Upload Installers
- uses: actions/upload-artifact@v2
- with:
- name: LinuxInstallers
- path: |
- out/linux/Packaging.Linux/deb/Release/*.deb
- out/linux/Packaging.Linux/tar/Release/*.tar.gz
-
- sign:
- name: 'Sign'
- runs-on: windows-latest
- needs: build
- steps:
- - name: setup python
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
-
- - uses: actions/checkout@v2
-
- - name: 'Download Installer Artifact'
- uses: actions/download-artifact@v2
- with:
- name: LinuxInstallers
-
- - uses: azure/login@v1
- with:
- creds: ${{ secrets.AZURE_CREDENTIALS }}
-
- - name: 'Install ESRP Client'
- shell: pwsh
- env:
- AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }}
- run: |
- az storage blob download --subscription "$env:AZ_SUB" --account-name gitcitoolstore -c tools -n microsoft.esrpclient.1.2.47.nupkg -f esrp.zip
- Expand-Archive -Path esrp.zip -DestinationPath .\esrp
-
- - name: Install Certs
- shell: pwsh
- env:
- AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }}
- AZ_VAULT: ${{ secrets.AZURE_VAULT }}
- SSL_CERT: ${{ secrets.VAULT_SSL_CERT_NAME }}
- ESRP_CERT: ${{ secrets.VAULT_ESRP_CERT_NAME }}
- run: |
- az keyvault secret download --subscription "$env:AZ_SUB" --vault-name "$env:AZ_VAULT" --name "$env:SSL_CERT" -f out.pfx
- certutil -f -importpfx out.pfx
- Remove-Item out.pfx
-
- az keyvault secret download --subscription "$env:AZ_SUB" --vault-name "$env:AZ_VAULT" --name "$env:ESRP_CERT" -f out.pfx
- certutil -f -importpfx out.pfx
- Remove-Item out.pfx
-
- - name: Run ESRP Client
- shell: pwsh
- env:
- AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
- run: |
- python .github/run_esrp_signing.py
-
- - name: Upload Installer
- uses: actions/upload-artifact@v2
- with:
- name: DebianInstallerSigned
- path: |
- Signed/*.deb
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index bea685ec4..e4ead0dc0 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -23,13 +23,13 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
fetch-depth: 0 # patch around Nerdbank.GitVersioning failure
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
@@ -37,4 +37,4 @@ jobs:
dotnet build
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 47c68a10a..7a06deff1 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -17,12 +17,12 @@ jobs:
os: [ubuntu-18.04, ubuntu-20.04, windows-2019, macos-10.15]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
- name: Setup .NET
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.201
@@ -42,4 +42,4 @@ jobs:
run: dotnet build --configuration MacRelease
- name: Test
- run: dotnet test --no-restore --verbosity normal
+ run: dotnet test --verbosity normal
diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml
new file mode 100644
index 000000000..c723963ea
--- /dev/null
+++ b/.github/workflows/lint-docs.yml
@@ -0,0 +1,19 @@
+name: "Lint documentation"
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ main, linux ]
+ pull_request:
+ branches: [ main, linux ]
+
+jobs:
+ lint-markdown:
+ name: Lint markdown files
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: DavidAnson/markdownlint-cli2-action@744f913a124058ee903768d3adb92a4847e5d132
+ with:
+ globs: "**/*.md"
diff --git a/.github/workflows/release-homebrew.yaml b/.github/workflows/release-homebrew.yaml
index b847388f9..ca657863b 100644
--- a/.github/workflows/release-homebrew.yaml
+++ b/.github/workflows/release-homebrew.yaml
@@ -14,5 +14,5 @@ jobs:
tap: microsoft/git
name: git-credential-manager-core
type: cask
- releaseAsset: gcmcore-osx-(.*)\.pkg
+ releaseAsset: gcm-osx-x64-(.*)\.pkg
alwaysUsePullRequest: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..db6dc6807
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,563 @@
+name: release
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ release ]
+
+jobs:
+# ================================
+# macOS
+# ================================
+ osx-build:
+ name: Build macOS
+ runs-on: macos-latest
+ strategy:
+ matrix:
+ runtime: [ osx-x64, osx-arm64 ]
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+
+ - name: Set up dotnet
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 6.0.201
+
+ - name: Install dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: |
+ 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
+
+ - name: Lay out payload and symbols
+ run: |
+ src/osx/Installer.Mac/layout.sh \
+ --configuration=MacRelease --output=payload \
+ --symbol-output=symbols --runtime=${{ matrix.runtime }}
+
+ - name: Create keychain
+ env:
+ CERT_BASE64: ${{ secrets.DEVELOPER_CERTIFICATE_BASE64 }}
+ CERT_PASSPHRASE: ${{ secrets.DEVELOPER_CERTIFICATE_PASSWORD }}
+ run: |
+ security create-keychain -p pwd $RUNNER_TEMP/buildagent.keychain
+ security default-keychain -s $RUNNER_TEMP/buildagent.keychain
+ security unlock-keychain -p pwd $RUNNER_TEMP/buildagent.keychain
+ 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 }}
+ run: |
+ .github/run_developer_signing.sh payload $APPLE_TEAM_ID $GITHUB_WORKSPACE/src/osx/Installer.Mac/entitlements.xml
+
+ - name: Upload macOS artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: tmp.${{ matrix.runtime }}-build
+ path: |
+ payload
+ symbols
+
+ osx-payload-sign:
+ name: Sign macOS payload
+ # ESRP service requires signing to run on Windows
+ runs-on: windows-latest
+ strategy:
+ matrix:
+ runtime: [ osx-x64, osx-arm64 ]
+ needs: osx-build
+ steps:
+ - name: Check out repository
+ 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:
+ AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }}
+ AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
+ AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }}
+ REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
+ run: |
+ .github\set_up_esrp.ps1
+
+ - name: Run ESRP client
+ shell: pwsh
+ env:
+ AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
+ # We temporarily need two AAD IDs, as we're using an SSL certificate associated
+ # with an older App Registration until we have the required hardware to approve
+ # the new certificate in SSL Admin.
+ AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }}
+ APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }}
+ APPLE_SIGNING_OP_CODE: ${{ secrets.APPLE_SIGNING_OPERATION_CODE }}
+ run: |
+ 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: |
+ Expand-Archive signed/payload.zip -DestinationPath signed
+ Remove-Item signed/payload.zip
+
+ - name: Upload signed payload
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ matrix.runtime }}-payload-sign
+ path: |
+ signed
+
+ osx-pack:
+ name: Package macOS payload
+ runs-on: macos-latest
+ strategy:
+ matrix:
+ runtime: [ osx-x64, osx-arm64 ]
+ needs: osx-payload-sign
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+
+ - name: Set up dotnet
+ uses: actions/setup-dotnet@v2
+ 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 \
+ --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
+
+ - 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 ]
+ needs: osx-pack
+ steps:
+ - name: Check out repository
+ 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:
+ AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }}
+ AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
+ AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }}
+ REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
+ run: |
+ .github\set_up_esrp.ps1
+
+ - name: Sign package
+ shell: pwsh
+ env:
+ AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
+ # We temporarily need two AAD IDs, as we're using an SSL certificate associated
+ # with an older App Registration until we have the required hardware to approve
+ # the new certificate in SSL Admin.
+ AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }}
+ APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }}
+ 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:
+ AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
+ # We temporarily need two AAD IDs, as we're using an SSL certificate associated
+ # with an older App Registration until we have the required hardware to approve
+ # the new certificate in SSL Admin.
+ AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }}
+ APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }}
+ 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:
+ name: ${{ matrix.runtime }}-sign
+ path: signed/*.pkg
+
+# ================================
+# Windows
+# ================================
+ win-sign:
+ name: Build and Sign Windows
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+
+ - name: Set up dotnet
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 6.0.201
+
+ # Install Nerdbank.GitVersioning
+ - uses: dotnet/nbgv@master
+ with:
+ setCommonVars: true
+
+ - name: Install dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: |
+ dotnet build --configuration=WindowsRelease
+
+ - name: Run Windows unit tests
+ run: |
+ dotnet test --configuration=WindowsRelease
+
+ - name: Lay out Windows payload and symbols
+ shell: pwsh
+ run: |
+ cd src/windows/Installer.Windows/
+ ./layout.ps1 -Configuration WindowsRelease -Output payload -SymbolOutput symbols
+ mkdir unsigned-payload
+ Get-ChildItem -Path payload/* -Include *.exe, *.dll | Move-Item -Destination unsigned-payload
+
+ - uses: azure/login@v1
+ with:
+ creds: ${{ secrets.AZURE_CREDENTIALS }}
+
+ - name: Set up ESRP client
+ shell: pwsh
+ env:
+ AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }}
+ AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
+ AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }}
+ 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:
+ AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
+ # We temporarily need two AAD IDs, as we're using an SSL certificate associated
+ # with an older App Registration until we have the required hardware to approve
+ # the new certificate in SSL Admin.
+ AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }}
+ WINDOWS_KEY_CODE: ${{ secrets.WINDOWS_KEY_CODE }}
+ WINDOWS_OP_CODE: ${{ secrets.WINDOWS_OPERATION_CODE }}
+ run: |
+ python .github\run_esrp_signing.py `
+ src/windows/Installer.Windows/unsigned-payload `
+ $env:WINDOWS_KEY_CODE $env:WINDOWS_OP_CODE `
+ --params 'OpusName' 'Microsoft' `
+ '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: |
+ mkdir signed-payload
+ Move-Item -Path signed/* -Destination signed-payload
+ # ESRP will not sign the *.exe.config or NOTICE files, but they are needed to build the installers.
+ # Due to this, we copy them after signing.
+ Get-ChildItem -Path src/windows/Installer.Windows/payload/* -Include *.exe.config, NOTICE | Move-Item -Destination signed-payload
+ Remove-Item signed -Recurse -Force
+
+ - name: Build with signed payload
+ 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:
+ AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
+ # We temporarily need two AAD IDs, as we're using an SSL certificate associated
+ # with an older App Registration until we have the required hardware to approve
+ # the new certificate in SSL Admin.
+ AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }}
+ WINDOWS_KEY_CODE: ${{ secrets.WINDOWS_KEY_CODE }}
+ WINDOWS_OP_CODE: ${{ secrets.WINDOWS_OPERATION_CODE }}
+ run: |
+ python .github\run_esrp_signing.py `
+ .\out\windows\Installer.Windows\bin\WindowsRelease\net472 `
+ $env:WINDOWS_KEY_CODE `
+ $env:WINDOWS_OP_CODE `
+ --params 'OpusName' 'Microsoft' `
+ '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:
+ name: win-sign
+ path: |
+ signed
+ signed-payload
+ src/windows/Installer.Windows/symbols
+
+# ================================
+# Linux
+# ================================
+ linux-build:
+ name: Build Linux
+ 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@v2
+ with:
+ dotnet-version: 6.0.201
+
+ - name: Install dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --configuration=LinuxRelease
+
+ - name: Lay out
+ run: |
+ mkdir -p linux-build/deb linux-build/tar
+ mv out/linux/Packaging.Linux/deb/Release/*.deb linux-build/deb
+ mv out/linux/Packaging.Linux/tar/Release/*.tar.gz linux-build/tar
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux-build
+ path: |
+ linux-build
+
+ linux-sign:
+ name: Sign Debian package
+ # ESRP service requires signing to run on Windows
+ runs-on: windows-latest
+ needs: linux-build
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: linux-build
+ path: artifacts
+
+ - uses: azure/login@v1
+ with:
+ creds: ${{ secrets.AZURE_CREDENTIALS }}
+
+ - name: Set up ESRP client
+ shell: pwsh
+ env:
+ AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }}
+ AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
+ AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }}
+ REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }}
+ run: |
+ .github\set_up_esrp.ps1
+
+ - name: Run ESRP client
+ shell: pwsh
+ env:
+ AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }}
+ # We temporarily need two AAD IDs, as we're using an SSL certificate associated
+ # with an older App Registration until we have the required hardware to approve
+ # the new certificate in SSL Admin.
+ AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }}
+ LINUX_KEY_CODE: ${{ secrets.LINUX_KEY_CODE }}
+ LINUX_OP_CODE: ${{ secrets.LINUX_OPERATION_CODE }}
+ run: |
+ python .github/run_esrp_signing.py artifacts/deb $env:LINUX_KEY_CODE $env:LINUX_OP_CODE
+
+ - name: Upload signed Debian package
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux-sign
+ path: |
+ signed
+
+# ================================
+# Publish
+# ================================
+ create-github-release:
+ name: Publish GitHub draft release
+ runs-on: ubuntu-latest
+ needs: [ osx-sign, win-sign, linux-sign ]
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
+
+ - name: Set up dotnet
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 6.0.201
+
+ # Install Nerdbank.GitVersioning
+ - uses: dotnet/nbgv@master
+ with:
+ setCommonVars: true
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v3
+
+ - name: Archive macOS payload and symbols
+ 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-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 .
+
+ - name: Archive Windows payload and symbols
+ shell: pwsh
+ run: |
+ mkdir win-x86-payload-and-symbols
+ Compress-Archive -Path win-sign/signed-payload/* win-x86-payload-and-symbols/gcm-win-x86-$env:GitBuildVersionSimple.zip
+ Compress-Archive -Path win-sign/src/windows/Installer.Windows/symbols/* win-x86-payload-and-symbols/gcm-win-x86-$env:GitBuildVersionSimple-symbols.zip
+
+ - uses: actions/github-script@v6
+ with:
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+ const version = process.env.GitBuildVersionSimple
+
+ var releaseMetadata = {
+ owner: context.repo.owner,
+ repo: context.repo.repo
+ };
+
+ // Create the release
+ var tagName = `v${version}`;
+ var createdRelease = await github.rest.repos.createRelease({
+ ...releaseMetadata,
+ draft: true,
+ tag_name: tagName,
+ name: `GCM ${version}`
+ });
+ releaseMetadata.release_id = createdRelease.data.id;
+
+ // Uploads contents of directory to the release created above
+ async function uploadDirectoryToRelease(directory, includeExtensions=[]) {
+ return fs.promises.readdir(directory)
+ .then(async(files) => Promise.all(
+ files.filter(file => {
+ return includeExtensions.length==0 || includeExtensions.includes(path.extname(file).toLowerCase());
+ })
+ .map(async (file) => {
+ var filePath = path.join(directory, file);
+ github.rest.repos.uploadReleaseAsset({
+ ...releaseMetadata,
+ name: file,
+ headers: {
+ "content-length": (await fs.promises.stat(filePath)).size
+ },
+ data: fs.createReadStream(filePath)
+ });
+ }))
+ );
+ }
+
+ await Promise.all([
+ // Upload Windows artifacts
+ uploadDirectoryToRelease('win-sign/signed'),
+ uploadDirectoryToRelease('win-x86-payload-and-symbols'),
+
+ // Upload macOS artifacts
+ uploadDirectoryToRelease('osx-x64-sign'),
+ uploadDirectoryToRelease('osx-arm64-sign'),
+ uploadDirectoryToRelease('osx-payload-and-symbols'),
+
+ // Upload Linux artifacts
+ uploadDirectoryToRelease('linux-sign'),
+ uploadDirectoryToRelease('linux-build/tar')
+ ]);
diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml
index 23e61aa89..f6648e602 100644
--- a/.github/workflows/validate-install-from-source.yml
+++ b/.github/workflows/validate-install-from-source.yml
@@ -22,7 +22,9 @@ jobs:
- image: alpine
container: ${{matrix.vector.image}}
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works.
- run: |
if [ ${{matrix.vector.image}} == "centos" ]; then
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-Linux-*
diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc
new file mode 100644
index 000000000..6e0ac4ada
--- /dev/null
+++ b/.markdownlint.jsonc
@@ -0,0 +1,6 @@
+// For information on writing markdownlint configuration see:
+// https://github.com/DavidAnson/markdownlint/blob/main/README.md#optionsconfig
+{
+ "MD013": false, // Line length and line breaking convention not yet standardised across docs
+ "MD024": false // The format for some files require repeated headings, e.g. "Example"
+}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 3a64696bc..a7a7e0e63 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -67,10 +67,12 @@ members of the project's leadership.
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+This Code of Conduct is adapted from the [Contributor Covenant][cc-homepage], version 1.4,
+available at [Contributor Covenant Code of Conduct][cc-coc].
-[homepage]: https://www.contributor-covenant.org
+For answers to common questions about this code of conduct, see the
+[Contributor Covenant FAQ][cc-faq]
-For answers to common questions about this code of conduct, see
-https://www.contributor-covenant.org/faq
+[cc-homepage]: https://www.contributor-covenant.org
+[cc-coc]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+[cc-faq]: https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ff2f5b2ee..41c925a91 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,43 +1,44 @@
-## Contributing
+# Contributing
-[issue]: https://github.com/GitCredentialManager/git-credential-manager/issues
+[issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose
[fork]: https://github.com/GitCredentialManager/git-credential-manager/fork
[pr]: https://github.com/GitCredentialManager/git-credential-manager/compare
[code-of-conduct]: CODE_OF_CONDUCT.md
+[commits]: https://www.youtube.com/watch?v=4qLtKx9S9a8
-Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
+Hi there! We're thrilled that you'd like to contribute to GCM :tada:. Your help is essential for keeping it great.
-Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
+Contributions to GCM are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
## Start with an issue
-0. Open an [issue][issue] to discuss the change you want to see.
+1. Open an [issue][issue] to discuss the change you want to see.
This helps us coordinate and reduce duplication.
-0. Once we've had some discussion, you're ready to code!
+1. Once we've had some discussion, you're ready to code!
## Submitting a pull request
-0. [Fork][fork] and clone the repository
-0. Configure and install the dependencies: `dotnet restore`
-0. Make sure the tests pass on your machine: `dotnet test`
-0. Create a new branch: `git switch -c my-branch-name`
-0. Make your change, add tests, and make sure the tests still pass
-0. For UI updates, test your changes by executing a `dotnet run` in applicable UI-related project directories:
+1. [Fork][fork] and clone the repository
+1. Configure and install the dependencies: `dotnet restore`
+1. Make sure the tests pass on your machine: `dotnet test`
+1. Create a new branch: `git switch -c my-branch-name`
+1. Make your change, add tests, and make sure the tests still pass
+1. For UI updates, test your changes by executing a `dotnet run` in applicable UI-related project directories:
- `Atlassian.Bitbucket.UI.Avalonia`
- `GitHub.UI.Avalonia`
- `Atlassian.Bitbucket.UI.Windows`
- `GitHub.UI.Windows`
-0. Push to your fork and [submit a pull request][pr]
-0. Pat your self on the back and wait for your pull request to be reviewed and merged.
+1. Organize your changes into one or more [logical, descriptive commits][commits].
+1. Push to your fork and [submit a pull request][pr]
+1. Pat your self on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Match existing code style.
- Write tests.
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
-- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
## Resources
diff --git a/Directory.Build.props b/Directory.Build.props
index 7f78555c7..3abe378a3 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,6 +18,12 @@
$(RepoPath)src\
$(RepoPath)out\
$(RepoPath)assets\
+
+
+ <_IsExeProject Condition="'$(OutputType)' == 'Exe' OR '$(OutputType)' == 'WinExe'">true
+
+
+ true
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 000000000..3c84f230d
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+ $(IntermediateOutputPath)app.manifest
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln
index 0330e693c..9b0e76ba8 100644
--- a/Git-Credential-Manager.sln
+++ b/Git-Credential-Manager.sln
@@ -31,8 +31,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer.Mac", "src\osx\In
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer.Windows", "src\windows\Installer.Windows\Installer.Windows.csproj", "{85903170-9E52-4B53-A6E4-3F416F684FAE}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Payload.Windows", "src\windows\Payload.Windows\Payload.Windows.csproj", "{8DBBAB0A-970D-4BE3-958C-8CDC92F76549}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket", "src\shared\Atlassian.Bitbucket\Atlassian.Bitbucket.csproj", "{B49881A6-E734-490E-8EA7-FB0D9E296CFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.Tests", "src\shared\Atlassian.Bitbucket.Tests\Atlassian.Bitbucket.Tests.csproj", "{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}"
@@ -229,16 +227,6 @@ Global
{85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
{85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -499,7 +487,6 @@ Global
{3D279E2D-E011-45CF-8EA8-3D71D1300443} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}
{74FA0AA4-B5C1-4F3B-B182-277FC2D50715} = {3D279E2D-E011-45CF-8EA8-3D71D1300443}
{85903170-9E52-4B53-A6E4-3F416F684FAE} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
- {8DBBAB0A-970D-4BE3-958C-8CDC92F76549} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{B49881A6-E734-490E-8EA7-FB0D9E296CFB} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{025E5329-A0B1-4BA9-9203-B70B44A5F9E0} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{2B3CD8FF-84A6-4B53-A28B-D7A75B0AB4D7} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
diff --git a/README.md b/README.md
index 0536d8364..e7e4c7472 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,13 @@
[Git Credential Manager](https://github.com/GitCredentialManager/git-credential-manager) (GCM) is a secure Git credential helper built on [.NET](https://dotnet.microsoft.com) that runs on Windows, macOS, and Linux.
-Compared to Git's [built-in credential helpers]((https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)) (Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring/libsecret) which provides single-factor authentication support working on any HTTP-enabled Git repository, GCM provides multi-factor authentication support for [Azure DevOps](https://dev.azure.com/), Azure DevOps Server (formerly Team Foundation Server), GitHub, and Bitbucket.
+Compared to Git's [built-in credential helpers]((https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)) (Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring/libsecret) which provides single-factor authentication support working on any HTTP-enabled Git repository, GCM provides multi-factor authentication support for [Azure DevOps](https://dev.azure.com/), Azure DevOps Server (formerly Team Foundation Server), GitHub, Bitbucket, and GitLab.
Git Credential Manager (GCM) replaces the .NET Framework-based [Git Credential Manager for Windows](https://github.com/microsoft/Git-Credential-Manager-for-Windows) (GCM), and the Java-based [Git Credential Manager for Mac and Linux](https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux) (Java GCM), providing a consistent authentication experience across all platforms.
## Current status
-Git Credential Manager is currently available for Windows, macOS, and Linux. GCM only works with HTTP(S) remotes; you can still use Git with SSH:
+Git Credential Manager is currently available for Windows, macOS, and Linux\*. GCM only works with HTTP(S) remotes; you can still use Git with SSH:
- [Azure DevOps SSH](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops)
- [GitHub SSH](https://help.github.com/en/articles/connecting-to-github-with-ssh)
@@ -21,7 +21,7 @@ Git Credential Manager is currently available for Windows, macOS, and Linux. GCM
Feature|Windows|macOS|Linux
-|:-:|:-:|:-:
Installer/uninstaller|✓|✓|✓\*
-Secure platform credential storage|✓
[(see more)](docs/credstores.md)|✓
[(see more)](docs/credstores.md)|✓
[(see more)](docs/credstores.md)
+Secure platform credential storage|✓ [(see more)](docs/credstores.md)|✓ [(see more)](docs/credstores.md)|✓ [(see more)](docs/credstores.md)
Multi-factor authentication support for Azure DevOps|✓|✓|✓
Two-factor authentication support for GitHub|✓|✓|✓
Two-factor authentication support for Bitbucket|✓|✓|✓
@@ -34,9 +34,11 @@ Proxy support|✓|✓|✓
`arm64` support|best effort|via Rosetta 2|best effort, no packages
`armhf` support|_N/A_|_N/A_|best effort, no packages
-**Notes:**
+(\*) GCM guarantees support for the below Linux distributions. GCM maintainers also monitor and evaluate issues opened against other distributions to determine community interest/engagement and whether an emerging platform should become fully-supported.
-(\*) Fedora packages planned but not yet available.
+- Debian/Ubuntu/Linux Mint
+- Fedora/CentOS/RHEL
+- Alpine
## Download and Install
@@ -86,34 +88,10 @@ sudo /usr/local/share/gcm-core/uninstall.sh
---
+
-### Linux
-
-#### Experimental: install from source helper script
-
-If you would like to help dogfood our new install from source helper script,
-run the following:
-
-1. To ensure `curl` is installed:
-
-```shell
-curl --version
-```
-
-If `curl` is not installed, please use your distribution's package manager
-to install it.
-
-0. To download and run the script:
-
-```shell
-curl -LO https://raw.githubusercontent.com/GitCredentialManager/git-credential-manager/main/src/linux/Packaging.Linux/install-from-source.sh &&
-sh ./install-from-source.sh &&
-git-credential-manager-core configure
-```
-__Note:__ You will be prompted to enter your credentials so that the script
-can download GCM's dependencies using your distribution's package
-manager.
+### Linux
#### Ubuntu/Debian distributions
@@ -123,7 +101,8 @@ Download the latest [.deb package](https://github.com/GitCredentialManager/git-c
sudo dpkg -i
git-credential-manager-core configure
```
-__Note:__ Although packages were previously offered on certain
+
+**Note:** Although packages were previously offered on certain
[Microsoft Ubuntu package feeds](https://packages.microsoft.com/repos/),
GCM no longer publishes to these repositories. Please install the
Debian package using the above instructions instead.
@@ -137,6 +116,8 @@ sudo dpkg -r gcmcore
#### Other distributions
+##### Option 1: Tarball
+
Download the latest [tarball](https://github.com/GitCredentialManager/git-credential-manager/releases/latest), and run the following:
```shell
@@ -151,6 +132,33 @@ git-credential-manager-core unconfigure
rm $(command -v git-credential-manager-core)
```
+#### Option 2: Install from source helper script
+
+1. Ensure `curl` is installed:
+
+ ```shell
+ curl --version
+ ```
+
+ If `curl` is not installed, please use your distribution's package manager
+ to install it.
+
+1. Download and run the script:
+
+ ```shell
+ curl -LO https://raw.githubusercontent.com/GitCredentialManager/git-credential-manager/main/src/linux/Packaging.Linux/install-from-source.sh &&
+ sh ./install-from-source.sh &&
+ git-credential-manager-core configure
+ ```
+
+ **Note:** You will be prompted to enter your credentials so that the script
+ can download GCM's dependencies using your distribution's package
+ manager.
+
+To uninstall:
+
+[Follow these instructions](docs/linux-fromsrc-uninstall.md) for your distribution.
+
**Note:** all Linux distributions [require additional configuration](https://aka.ms/gcm/credstores) to use GCM.
---
@@ -171,11 +179,11 @@ Installing GCM as a standalone package on Windows will forcibly override the ver
There are two flavors of standalone installation on Windows:
-- User (preferred) (`gcmcoreuser-win*`):
+- User (preferred) (`gcmuser-win*`):
Does not require administrator rights. Will install only for the current user and updates only the current user's Git configuration.
-- System (`gcmcore-win*`):
+- System (`gcm-win*`):
Requires administrator rights. Will install for all users on the system and update the system-wide Git configuration.
@@ -239,15 +247,16 @@ See detailed information [here](https://aka.ms/gcm/httpproxy).
- [Credential stores](docs/credstores.md)
- [Architectural overview](docs/architecture.md)
- [Host provider specification](docs/hostprovider.md)
+- [Azure Repos OAuth tokens](docs/azrepos-users-and-tokens.md)
+- [GitLab support](docs/gitlab.md)
## Experimental Features
- [Windows broker (experimental)](docs/windows-broker.md)
-- [Azure Repos OAuth tokens (experimental)](docs/azrepos-users-and-tokens.md)
## Contributing
-This project welcomes contributions and suggestions.
+This project welcomes contributions and suggestions.
See the [contributing guide](CONTRIBUTING.md) to get started.
This project follows [GitHub's Open Source Code of Conduct](CODE_OF_CONDUCT.md).
diff --git a/SECURITY.md b/SECURITY.md
index 5b2208269..8785fd5ba 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,5 @@
+# Security
+
If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github)
Thanks for helping make GitHub products safe for everyone.
diff --git a/build/GCM.MSBuild.csproj b/build/GCM.MSBuild.csproj
new file mode 100644
index 000000000..7dbe90afe
--- /dev/null
+++ b/build/GCM.MSBuild.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+ false
+
+
+
+
+
+
+
+
diff --git a/build/GCM.tasks b/build/GCM.tasks
new file mode 100644
index 000000000..fe7031f34
--- /dev/null
+++ b/build/GCM.tasks
@@ -0,0 +1,16 @@
+
+
+ <_TaskAssembly>$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll
+ <_TaskFactory>CodeTaskFactory
+
+
+ <_TaskAssembly>$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll
+ <_TaskFactory>RoslynCodeTaskFactory
+
+
+
+
+
+
+
+
diff --git a/build/GenerateWindowsAppManifest.cs b/build/GenerateWindowsAppManifest.cs
new file mode 100644
index 000000000..58a94c5a1
--- /dev/null
+++ b/build/GenerateWindowsAppManifest.cs
@@ -0,0 +1,51 @@
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using System.IO;
+
+namespace GitCredentialManager.MSBuild
+{
+ public class GenerateWindowsAppManifest : Task
+ {
+ [Required]
+ public string Version { get; set; }
+
+ [Required]
+ public string ApplicationName { get; set; }
+
+ [Required]
+ public string OutputFile { get; set; }
+
+ public override bool Execute()
+ {
+ Log.LogMessage(MessageImportance.Normal, "Creating application manifest file for '{0}'...", ApplicationName);
+
+ string manifestDirectory = Path.GetDirectoryName(OutputFile);
+ if (!Directory.Exists(manifestDirectory))
+ {
+ Directory.CreateDirectory(manifestDirectory);
+ }
+
+ File.WriteAllText(
+ OutputFile,
+ $@"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+");
+
+ return true;
+ }
+ }
+}
diff --git a/docs/architecture.md b/docs/architecture.md
index eee92e3e3..7790dfc8c 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -31,13 +31,13 @@
| | | | | |
+-v---v----v--------------v------------+ +-v-----------------v----------------+
| | | |
-| Core <--+ Core.UI |
+| Core <--+ Core.UI |
| | | |
+--------------------------------------+ +------------------------------------+
```
Git Credential Manager (GCM) is built to be Git host and platform/OS
-agonstic. Most of the shared logic (command execution, the abstract platform
+agnostic. Most of the shared logic (command execution, the abstract platform
subsystems, etc) can be found in the `Core` class
library (C#). The library targets .NET Standard as well as .NET Framework.
@@ -74,7 +74,7 @@ the shared, core binaries shell out to. Currently the Bitbucket and GitHub
providers each have a WPF (Windows only) helper executable that shows
authentication prompts and messages.
-The `Microsoft.Git.CredentialHelper.UI` project is a WPF (Windows only) assembly
+The `Core.UI` project is a WPF (Windows only) assembly
that contains common WPF components and styles that are shared between provider
helpers on Windows.
@@ -163,7 +163,7 @@ appropriate host provider. The default registry implementation select the a host
provider by asking each registered provider in turn if they understand the
request. The provider selection can be overridden by the user via the
[`credential.provider`](configuration.md#credentialprovider) or [`GCM_PROVIDER`](environment.md#GCM_PROVIDER)
-configuration and environment variable respectively (3)).
+configuration and environment variable respectively (3).
The `Get|Store|EraseCommand`s call the corresponding
`Get|Store|EraseCredentialAsync` methods on the `IHostProvider`, passing the
diff --git a/docs/azrepos-users-and-tokens.md b/docs/azrepos-users-and-tokens.md
index 153e7f8d4..1aab32262 100644
--- a/docs/azrepos-users-and-tokens.md
+++ b/docs/azrepos-users-and-tokens.md
@@ -5,11 +5,11 @@
The Azure Repos host provider supports creating multiple types of credential:
- Azure DevOps personal access tokens
-- Microsoft identity OAuth tokens (experimental)
+- Microsoft identity OAuth tokens
To select which type of credential the Azure Repos host provider will create
-and use, you can set the [`credential.azreposCredentialType`](configuration.md#credentialazreposcredentialtype-experimental)
-configuration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`](environment.md#GCM_AZREPOS_CREDENTIALTYPE-experimental)
+and use, you can set the [`credential.azreposCredentialType`](configuration.md#credentialazreposcredentialtype)
+configuration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`](environment.md#GCM_AZREPOS_CREDENTIALTYPE)
environment variable).
### Azure DevOps personal access tokens
@@ -24,7 +24,7 @@ PATs have a limited lifetime and new tokens must be created once they expire. In
Git Credential Manager, when a PAT expired (or was manually revoked) this
resulted in a new authentication prompt.
-### Microsoft identity OAuth tokens (experimental)
+### Microsoft identity OAuth tokens
"Microsoft identity OAuth token" is the generic term for OAuth-based access
tokens issued by Azure Active Directory for either Work and School Accounts
@@ -64,7 +64,7 @@ credential. This may change in the future.
Normally you won't need to worry about managing which user accounts Git
Credential Manager is using as this is configured automatically when you first
-authenticate for a particular Azure DevOps organziation.
+authenticate for a particular Azure DevOps organization.
In advanced scenarios (such as using multiple accounts) you can interact with
and manage remembered user accounts using the 'azure-repos' provider command:
@@ -181,7 +181,7 @@ fabrikam:
```
In the above example, the `~/myrepo` repository has a single Git remote named
-`origin` that points to the `contoso` Azure DevOps organziation. There is no
+`origin` that points to the `contoso` Azure DevOps organization. There is no
user account specifically associated with the `origin` remote, so the global
user account binding for `contoso` will be used (the global binding is
inherited).
diff --git a/docs/bitbucket-development.md b/docs/bitbucket-development.md
index 26dddda90..dc7140770 100644
--- a/docs/bitbucket-development.md
+++ b/docs/bitbucket-development.md
@@ -63,20 +63,20 @@ Assuming the user successfully logins into Bitbucket and authorizes the GCM this
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
-On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC, has a number of differences compared to the cloud instance of Bitbucket, https://bitbucket.org.
+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](https://bitbucket.org).
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:
- ❯ atlas-run-standalone --product bitbucket
+ ❯ atlas-run-standalone --product bitbucket
-See https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/.
+See the developer documentation for [atlas-run-standalone](https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/).
This will download and run a standalone instance of Bitbucket Server which can be accessed using the credentials `admin`/`admin` at
- https://localhost:7990/bitbucket
+ https://localhost:7990/bitbucket
-Instructions on how to download and install the Atlassian SDK can be found here: https://developer.atlassian.com/server/framework/atlassian-sdk/
+Atlassian has [documentation](https://developer.atlassian.com/server/framework/atlassian-sdk/) on how to download and install their SDK.
diff --git a/docs/configuration.md b/docs/configuration.md
index c1c14761a..e295c68d7 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -39,8 +39,8 @@ The following table summarizes the change in behavior and the mapping of older v
Value(s)|Old meaning|New meaning
-|-|-
`auto`|Prompt if required – use cached credentials if possible|_(unchanged)_
-`never`,
`false`| Never prompt – fail if interaction is required|_(unchanged)_
-`always`,
`force`,
`true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)
+`never`, `false`| Never prompt – fail if interaction is required|_(unchanged)_
+`always`, `force`, `true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)
#### Example
@@ -64,7 +64,7 @@ ID|Provider
`azure-repos`|Azure Repos
`github`|GitHub
`bitbucket`|Bitbucket
-`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_
+`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_
`generic`|Generic (any other provider not listed above)
Automatic provider selection is based on the remote URL.
@@ -92,11 +92,11 @@ Select the host provider to use when authenticating by which authority is suppor
Authority|Provider(s)
-|-
`auto` _(default)_|_\[automatic\]_
-`msa`, `microsoft`, `microsoftaccount`,
`aad`, `azure`, `azuredirectory`,`live`, `liveconnect`, `liveid`|Azure Repos
_(supports Microsoft Authentication)_
-`github`|GitHub
_(supports GitHub Authentication)_
-`bitbucket`|Bitbucket.org
_(supports Basic Authentication and OAuth)_
Bitbucket Server
_(supports Basic Authentication)_
-`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_
-`basic`, `integrated`, `windows`, `kerberos`, `ntlm`,
`tfs`, `sso`|Generic
_(supports Basic and Windows Integrated Authentication)_
+`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|Azure Repos _(supports Microsoft Authentication)_
+`github`|GitHub _(supports GitHub Authentication)_
+`bitbucket`|Bitbucket.org _(supports Basic Authentication and OAuth)_, Bitbucket Server _(supports Basic Authentication)_
+`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_
+`basic`, `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|Generic _(supports Basic and Windows Integrated Authentication)_
#### Example
@@ -196,7 +196,6 @@ git config --global credential.httpsProxy http://john.doe:password@proxy.contoso
Override the available authentication modes presented during Bitbucket authentication.
If this option is not set, then the available authentication modes will be automatically detected.
-
**Note:** This setting only applies to Bitbucket.org, and not Server or DC instances.
**Note:** This setting supports multiple values separated by commas.
@@ -227,7 +226,6 @@ Enabling this option will improve performance when using Oauth2 and interacting
Enabling this option will decrease performance when using Basic Auth by requiring the user the re-enter credentials everytime.
-
Value|Refresh Credentials Before Returning
-|-
`true`, `1`, `yes`, `on` |Always
@@ -295,7 +293,6 @@ git config --global credential.gitLabAuthModes "browser"
---
-
### credential.namespace
Use a custom namespace prefix for credentials read and written in the OS credential store.
@@ -323,7 +320,7 @@ Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset o
Value|Credential Store|Platforms
-|-|-
-_(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|-
+_(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|-
`wincredman`|Windows Credential Manager (not available over SSH).|Windows
`dpapi`|DPAPI protected files. Customize the DPAPI store location with [credential.dpapiStorePath](#credentialdpapistorepath)|Windows
`keychain`|macOS Keychain.|macOS
@@ -332,7 +329,7 @@ _(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|-
`cache`|Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache).|Windows, macOS, Linux
`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`](#credentialplaintextstorepath).|Windows, macOS, Linux
-##### Example
+#### Example
```bash
git config --global credential.credentialStore gpg
@@ -475,6 +472,7 @@ Credential: "git:https://github.com" (user = alice)
https://github.com/contoso/widgets
https://alice@github.com/contoso/widgets
```
+
```text
Credential: "git:https://bob@github.com" (user = bob)
@@ -489,17 +487,20 @@ Credential: "git:https://github.com/foo/bar" (user = alice)
https://github.com/foo/bar
```
+
```text
Credential: "git:https://github.com/contoso/widgets" (user = alice)
https://github.com/contoso/widgets
https://alice@github.com/contoso/widgets
```
+
```text
Credential: "git:https://bob@github.com/foo/bar" (user = bob)
https://bob@github.com/foo/bar
```
+
```text
Credential: "git:https://bob@github.com/example/myrepo" (user = bob)
@@ -508,7 +509,7 @@ Credential: "git:https://bob@github.com/example/myrepo" (user = bob)
---
-### credential.azreposCredentialType _(experimental)_
+### credential.azreposCredentialType
Specify the type of credential the Azure Repos host provider should return.
@@ -519,7 +520,7 @@ Value|Description
`pat` _(default)_|Azure DevOps personal access tokens
`oauth`|Microsoft identity OAuth tokens (AAD or MSA tokens)
-More information about Azure Access tokens can be found [here](azrepos-azuretokens.md).
+More information about Azure Access tokens can be found [here](azrepos-users-and-tokens.md).
#### Example
@@ -527,4 +528,4 @@ More information about Azure Access tokens can be found [here](azrepos-azuretoke
git config --global credential.azreposCredentialType oauth
```
-**Also see: [GCM_AZREPOS_CREDENTIALTYPE](environment.md#GCM_AZREPOS_CREDENTIALTYPE-experimental)**
+**Also see: [GCM_AZREPOS_CREDENTIALTYPE](environment.md#GCM_AZREPOS_CREDENTIALTYPE)**
diff --git a/docs/credstores.md b/docs/credstores.md
index 3e4a232f2..bb206e647 100644
--- a/docs/credstores.md
+++ b/docs/credstores.md
@@ -178,7 +178,7 @@ TTY device path, as returned by the `tty` utility.
## Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache)
-**Available on:** _Windows, macOS, Linux_
+**Available on:** _macOS, Linux_
```shell
export GCM_CREDENTIAL_STORE=cache
diff --git a/docs/development.md b/docs/development.md
index 0e8f2dc41..fdec0d8b7 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -101,34 +101,35 @@ $ GCM_TRACE=1 git-credential-manager-core version
If you want code coverage metrics these can be generated either from the command line:
```shell
-$ dotnet test --collect:"XPlat Code Coverage" --settings=./.code-coverage/coverlet.settings.xml
+dotnet test --collect:"XPlat Code Coverage" --settings=./.code-coverage/coverlet.settings.xml
```
Or via the VSCode Terminal/Run Task:
-```
+```console
test with coverage
```
HTML reports can be generated using ReportGenerator, this should be installed during the build process, from the command line:
```shell
-$ dotnet ~/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage
+dotnet ~/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage
```
+
or
```shell
-$ dotnet {$env:USERPROFILE}/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage
+dotnet {$env:USERPROFILE}/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage
```
Or via VSCode Terminal/Run Task:
-```
+```console
report coverage - nix
```
or
-```
+```console
report coverage - win
-```
\ No newline at end of file
+```
diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md
index 754d0766d..96ce891d5 100644
--- a/docs/enterprise-config.md
+++ b/docs/enterprise-config.md
@@ -4,12 +4,12 @@ Git Credential Manager (GCM) can be configured using multiple
different mechanisms. In order of preference, those mechanisms are:
1. [Environment variables](environment.md)
-2. [Standard Git configuration files](configuration.md)
+1. [Standard Git configuration files](configuration.md)
1. Repository/local configuration (`.git/config`)
- 2. User/global configuration (`$HOME/.gitconfig` or `%HOME%\.gitconfig`)
- 3. Installation/system configuration (`etc/gitconfig`)
-3. Enterprise system administrator defaults
-4. Compiled default values
+ 1. User/global configuration (`$HOME/.gitconfig` or `%HOME%\.gitconfig`)
+ 1. Installation/system configuration (`etc/gitconfig`)
+1. Enterprise system administrator defaults
+1. Compiled default values
This model largely matches what Git itself supports, namely environment
variables that take precedence over Git configuration files.
@@ -18,25 +18,25 @@ The addition of the enterprise system administrator defaults enables those
administrators to configure many GCM settings using familiar MDM tooling, rather
than having to modify the Git installation configuration files.
-### User Freedom
+## User Freedom
We believe the user should _always_ be at liberty to configure
-Git and GCM exactly as they wish. By prefering environment variables and Git
+Git and GCM exactly as they wish. By preferring environment variables and Git
configuration files over system admin values, these only act as _default values_
-that can always be overriden by the user in the usual ways.
+that can always be overridden by the user in the usual ways.
## Windows
Default setting values come from the Windows Registry, specifically the
following keys:
-**32-bit Windows**
+### 32-bit Windows
```text
HKEY_LOCAL_MACHINE\SOFTWARE\GitCredentialManager\Configuration
```
-**64-bit Windows**
+### 64-bit Windows
```text
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GitCredentialManager\Configuration
@@ -55,7 +55,6 @@ those of the [Git configuration](configuration.md) settings.
The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD`
(integer).
-
## macOS/Linux
Default configuration setting stores has not been implemented.
diff --git a/docs/environment.md b/docs/environment.md
index bf50971a3..18cfa4774 100644
--- a/docs/environment.md
+++ b/docs/environment.md
@@ -137,8 +137,8 @@ The following table summarizes the change in behavior and the mapping of older v
Value(s)|Old meaning|New meaning
-|-|-
`auto`|Prompt if required – use cached credentials if possible|_(unchanged)_
-`never`,
`false`| Never prompt – fail if interaction is required|_(unchanged)_
-`always`,
`force`,
`true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)
+`never`, `false`| Never prompt – fail if interaction is required|_(unchanged)_
+`always`, `force`, `true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)
#### Example
@@ -169,7 +169,7 @@ ID|Provider
`auto` _(default)_|_\[automatic\]_ ([learn more](autodetect.md))
`azure-repos`|Azure Repos
`github`|GitHub
-`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_
+`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_
`generic`|Generic (any other provider not listed above)
Automatic provider selection is based on the remote URL.
@@ -205,10 +205,10 @@ Select the host provider to use when authenticating by which authority is suppor
Authority|Provider(s)
-|-
`auto` _(default)_|_\[automatic\]_
-`msa`, `microsoft`, `microsoftaccount`,
`aad`, `azure`, `azuredirectory`,`live`, `liveconnect`, `liveid`|Azure Repos
_(supports Microsoft Authentication)_
-`github`|GitHub
_(supports GitHub Authentication)_
-`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_
-`basic`, `integrated`, `windows`, `kerberos`, `ntlm`,
`tfs`, `sso`|Generic
_(supports Basic and Windows Integrated Authentication)_
+`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|Azure Repos _(supports Microsoft Authentication)_
+`github`|GitHub _(supports GitHub Authentication)_
+`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_
+`basic`, `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|Generic _(supports Basic and Windows Integrated Authentication)_
#### Example
@@ -325,13 +325,13 @@ Configure GCM to use the a proxy for network operations.
**Note:** Git itself does _not_ respect this setting; this affects GCM _only_.
-##### Windows
+#### Windows
```batch
SET GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com
@@ -356,13 +356,13 @@ _(unset)_|Automatically detect modes
`oauth`|OAuth-based authentication
`basic`|Basic/PAT-based authentication
-##### Windows
+#### Windows
```batch
SET GCM_BITBUCKET_AUTHMODES="oauth,basic"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_BITBUCKET_AUTHMODES="oauth,basic"
@@ -380,21 +380,20 @@ This is especially relevant to OAuth credentials. Bitbucket.org access tokens ex
Enabling this option will improve performance when using Oauth2 and interacting with Bitbucket.org if, on average, commits are done less frequently than every 2 hours.
-Enabling this option will decrease performance when using Basic Auth by requiring the user the re-enter credentials everytime.
-
+Enabling this option will decrease performance when using Basic Auth by requiring the user the re-enter credentials every time.
Value|Refresh Credentials Before Returning
-|-
`true`, `1`, `yes`, `on` |Always
`false`, `0`, `no`, `off`_(default)_|Only when the credentials are found to be invalid
-##### Windows
+#### Windows
```batch
SET GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1
@@ -422,13 +421,13 @@ _(unset)_|Automatically detect modes
`basic`|Basic authentication using username and password
`pat`|Personal Access Token (pat)-based authentication
-##### Windows
+#### Windows
```batch
SET GCM_GITHUB_AUTHMODES="oauth,basic"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_GITHUB_AUTHMODES="oauth,basic"
@@ -452,13 +451,13 @@ _(unset)_|Automatically detect modes
`basic`|Basic authentication using username and password
`pat`|Personal Access Token (pat)-based authentication
-##### Windows
+#### Windows
```batch
SET GCM_GITLAB_AUTHMODES="browser"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_GITLAB_AUTHMODES="browser"
@@ -475,13 +474,13 @@ Credentials will be stored in the format `{namespace}:{service}`.
Defaults to the value `git`.
-##### Windows
+#### Windows
```batch
SET GCM_NAMESPACE="my-namespace"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_NAMESPACE="my-namespace"
@@ -501,7 +500,7 @@ Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset o
Value|Credential Store|Platforms
-|-|-
-_(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|-
+_(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|-
`wincredman`|Windows Credential Manager (not available over SSH).|Windows
`dpapi`|DPAPI protected files. Customize the DPAPI store location with [`GCM_DPAPI_STORE_PATH`](#gcm_dpapi_store_path)|Windows
`keychain`|macOS Keychain.|macOS
@@ -510,13 +509,13 @@ _(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|-
`cache`|Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache).|Windows, macOS, Linux
`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`](#gcm_plaintext_store_path).|Windows, macOS, Linux
-##### Windows
+#### Windows
```batch
SET GCM_CREDENTIAL_STORE="gpg"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_CREDENTIAL_STORE="gpg"
@@ -597,7 +596,7 @@ Specify the path (_including_ the executable name) to the version of `gpg` used
If not specified, GCM defaults to using the version of `gpg2` on the `$PATH`, falling back on `gpg` if `gpg2` is not found.
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_GPG_PATH="/usr/local/bin/gpg2"
@@ -625,13 +624,13 @@ Value|Authentication Flow
`system`|Open the user's default web browser.
`devicecode`|Show a device code.
-##### Windows
+#### Windows
```batch
SET GCM_MSAUTH_FLOW="devicecode"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_MSAUTH_FLOW="devicecode"
@@ -654,13 +653,13 @@ Value|Description
`true`|Use the operating system account manager as an authentication broker.
`false` _(default)_|Do not use the broker.
-##### Windows
+#### Windows
```batch
SET GCM_MSAUTH_USEBROKER="true"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_MSAUTH_USEBROKER="false"
@@ -670,7 +669,7 @@ export GCM_MSAUTH_USEBROKER="false"
---
-### GCM_AZREPOS_CREDENTIALTYPE _(experimental)_
+### GCM_AZREPOS_CREDENTIALTYPE
Specify the type of credential the Azure Repos host provider should return.
@@ -681,18 +680,18 @@ Value|Description
`pat` _(default)_|Azure DevOps personal access tokens
`oauth`|Microsoft identity OAuth tokens (AAD or MSA tokens)
-More information about Azure Access tokens can be found [here](azrepos-azuretokens.md).
+More information about Azure Access tokens can be found [here](azrepos-users-and-tokens.md).
-##### Windows
+#### Windows
```batch
SET GCM_AZREPOS_CREDENTIALTYPE="oauth"
```
-##### macOS/Linux
+#### macOS/Linux
```bash
export GCM_AZREPOS_CREDENTIALTYPE="oauth"
```
-**Also see: [credential.azreposCredentialType](configuration.md#azreposcredentialtype-experimental)**
+**Also see: [credential.azreposCredentialType](configuration.md#azreposcredentialtype)**
diff --git a/docs/faq.md b/docs/faq.md
index d95365cf9..b61d43d0e 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -14,17 +14,17 @@ Please follow these steps to diagnose or resolve the problem:
1. If all else fails, create an issue [here](https://github.com/GitCredentialManager/git-credential-manager/issues/create), making sure to include the trace log.
-### Q: I got an error saying unsecure HTTP is not supported.
+### Q: I got an error saying unsecure HTTP is not supported
To keep your data secure, Git Credential Manager will not send credentials for Azure Repos, Azure DevOps Server (TFS), GitHub, and Bitbucket, over HTTP connections that are not secured using TLS (HTTPS).
Please make sure your remote URLs use "https://" rather than "http://".
-### Q: I got an authentication error and I am behind a network proxy.
+### Q: I got an authentication error and I am behind a network proxy
You probably need to configure Git and GCM to use a proxy. Please see detailed information [here](https://aka.ms/gcm/httpproxy).
-### Q: I'm getting errors about picking a credential store on Linux.
+### Q: I'm getting errors about picking a credential store on Linux
On Linux you must [select and configure a credential store](https://aka.ms/gcm/credstores), as due to the varied nature of distributions and installations, we cannot guarantee a suitable storage solution is available.
@@ -65,7 +65,7 @@ GCM Windows was not designed with a cross-platform architecture.
### What level of support does GCM have?
-Support will be best-effort. We would really appreciate your feedback to make this a great experience across each platform we support.
+Support will be best-effort. We would really appreciate your feedback to make this a great experience across each platform we support.
### Q: Why does GCM not support operating system/distribution 'X', or Git hosting provider 'Y'?
@@ -136,3 +136,31 @@ information.
You may also set these variables to the empty string `""` to force terminal/
text-based prompts instead.
+
+### How do I revoke consent for GCM for GitHub.com?
+
+In your GitHub user settings, navigate to
+[Integrations > Applications > Authorized OAuth Apps > Git Credential Manager](https://github.com/settings/connections/applications/0120e057bd645470c1ed)
+and pick "Revoke access".
+
+
+
+After revoking access, any tokens created by GCM will be invalidated and can no longer be used to access your repositories. The next time GCM attempts to access GitHub.com you will be prompted to consent again.
+
+### I used the install from source script to install GCM on my Linux distribution. Now how can I uninstall GCM and its dependencies?
+
+Please see full instructions [here](./linux-fromsrc-uninstall.md).
+
+### How do I revoke access for a GitLab OAuth application?
+
+There are some scenarios (e.g. updated scopes) for which you will need to manually revoke and re-authorize access for a GitLab OAuth application. You can do so by:
+
+1. Navigating to [the **Applications** page within your **User Settings**](https://gitlab.com/-/profile/applications).
+2. Scrolling to **Authorized applications**.
+3. Clicking the **Revoke** button next to the name of the application for which you would like to revoke access (Git Credential Manager is used here for demonstration purposes).
+
+ 
+4. Waiting for a notification stating **The application was revoked access**.
+
+ 
+5. Re-authorizing the application with the new scope (GCM should automatically initiate this flow for you next time access is requested).
diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md
index a9c010714..9d5c3b9a3 100644
--- a/docs/github-apideprecation.md
+++ b/docs/github-apideprecation.md
@@ -53,42 +53,42 @@ GCM for Windows bundled with the Git for Windows installation.
If you are unable to use Git Credential Manager due to a bug or
compatibility issue we'd [like to know why](https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose)!
-## Help! I cannot make any changes to my Windows machine without an Administrator!
+## Help! I cannot make any changes to my Windows machine without an Administrator
If you do not have permission to change your installation (for example in a
corporate environment) you can use the per-user installer. Check out the [latest
release](https://aka.ms/gcm/latest) and download the `gcmcoreuser-win-*.exe`
executable.
-### Help! I still cannot or don't want to install anything!
+### Help! I still cannot or don't want to install anything
There is a workaround which should work and doesn't require installing anything.
1. Tell your system administrator they should start planning to upgrade the
installed version of Git for Windows to at least 2.29! 😁
-2. [Create a new personal access token](https://github.com/settings/tokens/new?scopes=repo,gist,workflow) (see official [documentation](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token))
+1. [Create a new personal access token](https://github.com/settings/tokens/new?scopes=repo,gist,workflow) (see official [documentation](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token))
-3. Enter a name ("note") for the token and ensure the `repo`, `gist`, and
+1. Enter a name ("note") for the token and ensure the `repo`, `gist`, and
`workflow` scopes are selected:
-
-...
-
-...
-
-...
-
+ 
+ ...
+ 
+ ...
+ 
+ ...
+ 
-3. Click "Generate Token"
+1. Click "Generate Token"
-
+ 
-4. **[IMPORTANT]** Keep the resulting page open as this contains your new token
+1. **[IMPORTANT]** Keep the resulting page open as this contains your new token
(this will only be displayed once!)
-
+ 
-5. Save the generated PAT in the Windows Credential Manager:
+1. Save the generated PAT in the Windows Credential Manager:
1. If you prefer to use the command-line, open a command prompt (cmd.exe) and
type the following:
@@ -98,7 +98,7 @@ There is a workaround which should work and doesn't require installing anything.
```
You will be prompted to enter a password – copy the newly generated PAT in
- step 4 and paste it here, and press Enter
+ step 4 and paste it here, and press the `Enter` key

diff --git a/docs/gitlab.md b/docs/gitlab.md
index c7eddbc06..94065398e 100644
--- a/docs/gitlab.md
+++ b/docs/gitlab.md
@@ -6,16 +6,16 @@ Git Credential Manager supports [gitlab.com](https://gitlab.com) out the box.
To use on another instance, eg. `https://gitlab.example.com` requires setup and configuration:
-1. [Create an OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html). This can be at the user, group or instance level. Specify a name and use a redirect URI of `http://127.0.0.1/`. _Unselect_ the 'Confidential' option, and ensure the 'Expire access tokens' option is selected. Set the scope to 'write_repository'.
-2. Copy the application ID and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientId `
-3. Copy the application secret and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientSecret `
-4. Configure authentication modes to include 'browser' `git config --global credential.https://gitlab.example.com.gitLabAuthModes browser`
-5. For good measure, configure `git config --global credential.https://gitlab.example.com.provider gitlab`
-6. Verify the config is as expected `git config --global --get-urlmatch credential https://gitlab.example.com`
+1. [Create an OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html). This can be at the user, group or instance level. Specify a name and use a redirect URI of `http://127.0.0.1/`. _Unselect_ the 'Confidential' option, and ensure the 'Expire access tokens' option is selected. Set the 'write_repository' and 'read_repository' scopes.
+1. Copy the application ID and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientId `
+1. Copy the application secret and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientSecret `
+1. Configure authentication modes to include 'browser' `git config --global credential.https://gitlab.example.com.gitLabAuthModes browser`
+1. For good measure, configure `git config --global credential.https://gitlab.example.com.provider gitlab`. This may be necessary to recognise the domain as a GitLab instance.
+1. Verify the config is as expected `git config --global --get-urlmatch credential https://gitlab.example.com`
### Clearing config
-```
+```console
git config --global --unset-all credential.https://gitlab.example.com.GitLabDevClientId
git config --global --unset-all credential.https://gitlab.example.com.GitLabDevClientSecret
git config --global --unset-all credential.https://gitlab.example.com.provider
@@ -23,22 +23,24 @@ To use on another instance, eg. `https://gitlab.example.com` requires setup and
## Preferences
-```
+```console
Select an authentication method for 'https://gitlab.com/':
1. Web browser (default)
2. Personal access token
3. Username/password
-option (enter for default):
+option (enter for default):
```
If you have a preferred authentication mode, you can specify [credential.gitLabAuthModes](configuration.md#credential.gitLabAuthModes):
- `git config --global credential.gitlabauthmodes browser`
+```console
+git config --global credential.gitlabauthmodes browser
+```
## Caveats
Improved support requires changes in GitLab. Please vote for these issues if they affect you:
-1. No support for OAuth device authorization (necessary for machines without web browser) https://gitlab.com/gitlab-org/gitlab/-/issues/332682
-2. Only domains with prefix `gitlab.` are recognised as GitLab remotes https://gitlab.com/gitlab-org/gitlab/-/issues/349464
-3. Username/password authentication is suggested even if disabled on server https://gitlab.com/gitlab-org/gitlab/-/issues/349463
+1. No support for OAuth device authorization (necessary for machines without web browser): [GitLab issue 332682](https://gitlab.com/gitlab-org/gitlab/-/issues/332682)
+1. Only domains with prefix `gitlab.` are recognised as GitLab remotes: [GitLab issue 349464](https://gitlab.com/gitlab-org/gitlab/-/issues/349464)
+1. Username/password authentication is suggested even if disabled on server: [GitLab issue 349463](https://gitlab.com/gitlab-org/gitlab/-/issues/349463)
diff --git a/docs/img/github-oauthapp-revoke.png b/docs/img/github-oauthapp-revoke.png
new file mode 100644
index 000000000..38151bdcf
Binary files /dev/null and b/docs/img/github-oauthapp-revoke.png differ
diff --git a/docs/img/gitlab-oauthapp-revoke.png b/docs/img/gitlab-oauthapp-revoke.png
new file mode 100644
index 000000000..261fff96a
Binary files /dev/null and b/docs/img/gitlab-oauthapp-revoke.png differ
diff --git a/docs/img/gitlab-oauthapp-revoked.png b/docs/img/gitlab-oauthapp-revoked.png
new file mode 100644
index 000000000..f478c152e
Binary files /dev/null and b/docs/img/gitlab-oauthapp-revoked.png differ
diff --git a/docs/linux-fromsrc-uninstall.md b/docs/linux-fromsrc-uninstall.md
new file mode 100644
index 000000000..878528804
--- /dev/null
+++ b/docs/linux-fromsrc-uninstall.md
@@ -0,0 +1,55 @@
+# Uninstalling after installing from source
+
+These instructions will guide you in removing GCM after running the [install from source script](../src/linux/Packaging.Linux/install-from-source.sh) on your Linux distribution.
+
+:rotating_light: PROCEED WITH CAUTION :rotating_light:
+
+For completeness, we provide uninstall instructions for _the GCM application, the GCM repo, and the maximum number of dependencies*_ for all distributions. This repo and these dependencies may or may not have already been present on your system when you ran the install from source script, and uninstalling them could impact other programs and/or your normal workflows. Please keep this in mind when following the instructions below.
+
+*Certain distributions require some dependencies of the script to function as expected, so we only include instructions to remove the non-required dependencies.
+
+## All distributions
+
+**Note:** If you ran the install from source script from a pre-existing clone of the `git-credential-manager` repo or outside of your `$HOME` directory, you will need to modify the final two commands below to point to the location of your pre-existing clone or the directory from which you ran the install from source script.
+
+```console
+git-credential-manager-core unconfigure &&
+sudo rm $(command -v git-credential-manager-core) &&
+sudo rm -rf /usr/local/share/gcm-core &&
+sudo rm -rf ~/git-credential-manager &&
+sudo rm ~/install-from-source.sh
+```
+
+## Debian/Ubuntu
+
+**Note:** If you had a pre-existing installation of dotnet that was not installed via `apt` or `apt-get` when you ran the install from source script, you will need to remove it using [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net) and remove `dotnet-*` from the below command.
+
+```console
+sudo apt remove dotnet-* dpkg-dev apt-transport-https git curl wget
+```
+
+## Linux Mint
+
+**Note:** If you had a pre-existing installation of dotnet when you ran the install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed and keep other versions, you can do so with [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net).
+
+```console
+sudo rm -rf ~/.dotnet &&
+sudo apt remove git curl
+```
+
+## Fedora/CentOS/RHEL
+
+**Note:** If you had a pre-existing installation of dotnet when you ran the install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed and keep other versions, you can do so with [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net).
+
+```console
+sudo rm -rf ~/.dotnet
+```
+
+## Alpine
+
+**Note:** If you had a pre-existing installation of dotnet when you ran the install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed and keep other versions, you can do so with [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net).
+
+```console
+sudo rm -rf ~/.dotnet &&
+sudo apk del icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which bash coreutils gcompat git curl
+```
diff --git a/docs/migration.md b/docs/migration.md
index a2be4cfe7..88ba8da89 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -10,12 +10,12 @@ Because both Basic HTTP authentication and Windows Integrated Authentication (WI
The following table shows the correct replacement for all legacy authorities values:
-GCM_AUTHORITY
(credential.authority)|→|GCM_PROVIDER
(credential.provider)|GCM_ALLOW_WINDOWSAUTH
(credential.allowWindowsAuth)
+GCM_AUTHORITY (credential.authority)|→|GCM_PROVIDER (credential.provider)|GCM_ALLOW_WINDOWSAUTH (credential.allowWindowsAuth)
-|-|-|-
-`msa`, `microsoft`, `microsoftaccount`,
`aad`, `azure`, `azuredirectory`,`live`, `liveconnect`, `liveid`|→|`azure-repos`|_N/A_
+`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|→|`azure-repos`|_N/A_
`github`|→|`github`|_N/A_
`basic`|→|`generic`|`false`
-`integrated`, `windows`, `kerberos`, `ntlm`,
`tfs`, `sso`|→|`generic`|`true` _(default)_
+`integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|→|`generic`|`true` _(default)_
For example if you had previous set the authority for the `example.com` host to `basic`..
diff --git a/docs/multiple-users.md b/docs/multiple-users.md
index 20f1fd3cf..7446aef0e 100644
--- a/docs/multiple-users.md
+++ b/docs/multiple-users.md
@@ -10,11 +10,11 @@ Separate from the user strings in commits, Git recognizes the "user" part of a r
Git hosting providers (like GitHub or Bitbucket) _do_ have a concept of "user". Typically it's an identity like a username or email address, plus a password or other credential to perform actions as that user. You may have guessed by now that GCM (the Git **Credential** Manager) does work with this notion of a user.
-## People, identities, credentials, oh my!
+## People, identities, credentials, oh my
-You (a physical person) may have one or more user accounts (identities) with one or more Git hosting providers. Since most Git hosts don't put a "user" part in their URLs, by default, Git will treat the user part for a remote as the empty string. If you have multiple identites on one domain, you'll need to insert a unique user part per-identity yourself.
+You (a physical person) may have one or more user accounts (identities) with one or more Git hosting providers. Since most Git hosts don't put a "user" part in their URLs, by default, Git will treat the user part for a remote as the empty string. If you have multiple identities on one domain, you'll need to insert a unique user part per-identity yourself.
-There are good reasons for having multiple identities on one domain. You might use one GitHub identity for your personal work, another for your open source work, and a third for your employer's work. You can ask Git to assign a different credential to different repositories hosted on the same provider. HTTPS URLs include an optional "name" part before an `@` sign in the domain name, and you can use this to force Git to distiguish multiple users. This should likely be your username on the Git hosting service, since there are cases where GCM will use it like a username.
+There are good reasons for having multiple identities on one domain. You might use one GitHub identity for your personal work, another for your open source work, and a third for your employer's work. You can ask Git to assign a different credential to different repositories hosted on the same provider. HTTPS URLs include an optional "name" part before an `@` sign in the domain name, and you can use this to force Git to distinguish multiple users. This should likely be your username on the Git hosting service, since there are cases where GCM will use it like a username.
## Setting it up
diff --git a/docs/netconfig.md b/docs/netconfig.md
index 7a6d5f51c..35cf9dc80 100644
--- a/docs/netconfig.md
+++ b/docs/netconfig.md
@@ -49,11 +49,11 @@ GCM supports other ways of configuring a proxy for convenience and compatibility
1. GCM-specific configuration options (_**only** respected by GCM; **deprecated**_):
- `credential.httpProxy`
- `credential.httpsProxy`
-2. cURL environment variables (_also respected by Git_):
+1. cURL environment variables (_also respected by Git_):
- `http_proxy`
- `https_proxy`/`HTTPS_PROXY`
- `all_proxy`/`ALL_PROXY`
-3. `GCM_HTTP_PROXY` environment variable (_**only** respected by GCM; **deprecated**_)
+1. `GCM_HTTP_PROXY` environment variable (_**only** respected by GCM; **deprecated**_)
Note that with the cURL environment variables there are both lowercase and
uppercase variants.
@@ -76,7 +76,7 @@ addresses. GCM supports the cURL environment variable `no_proxy` (and
Like with the [other cURL proxy environment variables](#other-proxy-options),
the lowercase variant will take precedence over the uppercase form.
-This environment variable should contain a comma (`,`) or space (` `) separated
+This environment variable should contain a comma-separated or space-separated
list of host names that should not be proxied (should connect directly).
GCM attempts to match [libcurl's behaviour](https://curl.se/libcurl/c/CURLOPT_NOPROXY.html),
diff --git a/docs/usage.md b/docs/usage.md
index bb0426e2b..1f64cbd1a 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -31,7 +31,7 @@ Set your user-level Git configuration (`~/.gitconfig`) to use GCM. If you pass
`--system` to these commands, they act on the system-level Git configuration
(`/etc/gitconfig`) instead.
-### azure-repos (experimental)
+### azure-repos
Interact with the Azure Repos host provider to bind/unbind user accounts to Azure DevOps
organizations or specific remote URLs, and manage the authentication authority cache.
diff --git a/docs/windows-broker.md b/docs/windows-broker.md
index 1ab1cc73b..62b422ee5 100644
--- a/docs/windows-broker.md
+++ b/docs/windows-broker.md
@@ -31,6 +31,7 @@ The GCM team isn't responsible for the user experience or choices made by WAM, b
Therefore, we want you to be aware of some defaults and experiences if you choose to use WAM integration.
### For work or school accounts (Azure AD-backed identities)
+
When you sign into an Azure DevOps organization backed by Azure AD (often your company or school email), if your machine is already joined to Azure AD matching that Azure DevOps organization, you'll get a seamless and easy-to-use experience.
If your machine isn't Azure AD-joined, or is Azure AD-joined to a different tenant, WAM will present you with a dialog box suggesting you stay signed in and allow the organization to manage your device.
@@ -57,6 +58,7 @@ Similar to the above, your organization's Conditional Access policies may preven
If Conditional Access is required to access your organization's Git repositories, you can [enable WAM integration](environment.md#GCM_MSAUTH_USEBROKER-experimental) (or follow other instructions your organization provides).
#### Removing device management
+
If you've allowed your computer to be managed and want to undo it, you can go into **Settings**, **Accounts**, **Access work or school**.
In the section where you see your email address and organization name, click **Disconnect**.
@@ -65,6 +67,7 @@ In the section where you see your email address and organization name, click **D

### For Microsoft accounts
+
When you sign into an Azure DevOps organization backed by Microsoft account (MSA) identities (email addresses like `@outlook.com` or `@gmail.com` fall into this category), you may be prompted to select an existing "work or school account" or use a different one.
In order to sign in with an MSA you should continue and select "Use a different [work or school] account", but enter your MSA credentials when prompted.
@@ -81,8 +84,9 @@ For any connected MSA, you can control whether or not the account is available t

Two very important things to note:
-* If you haven't connected any Microsoft accounts to Windows before, the first account you connect will cause the local Windows user account to be converted to a connected account.
-* In addition, you can't change the usage preference for the first Microsoft account connected to Windows: all Microsoft apps will be able to sign you in with that account.
+
+- If you haven't connected any Microsoft accounts to Windows before, the first account you connect will cause the local Windows user account to be converted to a connected account.
+- In addition, you can't change the usage preference for the first Microsoft account connected to Windows: all Microsoft apps will be able to sign you in with that account.
As far as we can tell, there are no workarounds for either of these behaviors (other than to not use the WAM broker).
diff --git a/docs/wsl.md b/docs/wsl.md
index 63b99e9ea..b5852f367 100644
--- a/docs/wsl.md
+++ b/docs/wsl.md
@@ -22,7 +22,7 @@ _Inside your WSL installation_, run the following command to set GCM as the Git
credential helper:
```shell
-git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/libexec/git-core/git-credential-manager-core.exe"
+git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager-core.exe"
```
If you intend to use Azure DevOps you must _also_ set the following Git
@@ -102,7 +102,7 @@ installation, and not shared with others or the Windows host.
Yes. Rather than install GCM as a Windows application (and have WSL Git invoke
the Windows GCM), can you install GCM as a Linux application instead.
-To do this, simply follow the [GCM installation instructions for Linux](../README.md#linux-install-instructions).
+To do this, simply follow the [GCM installation instructions for Linux](../README.md#linux).
**Note:** In this scenario, because GCM is running as a Linux application
it cannot utilize authentication or credential storage features of the host
diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh
index 5250f4664..a9430850d 100755
--- a/src/linux/Packaging.Linux/build.sh
+++ b/src/linux/Packaging.Linux/build.sh
@@ -75,12 +75,12 @@ SYMBOLOUT="$PROJ_OUT/payload.sym/$CONFIGURATION"
if [ $INSTALL_FROM_SOURCE = false ]; then
TAROUT="$PROJ_OUT/tar/$CONFIGURATION"
- TARBALL="$TAROUT/gcmcore-linux_$ARCH.$VERSION.tar.gz"
- SYMTARBALL="$TAROUT/symbols-linux_$ARCH.$VERSION.tar.gz"
+ TARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION.tar.gz"
+ SYMTARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION-symbols.tar.gz"
DEBOUT="$PROJ_OUT/deb/$CONFIGURATION"
DEBROOT="$DEBOUT/root"
- DEBPKG="$DEBOUT/gcmcore-linux_$ARCH.$VERSION.deb"
+ DEBPKG="$DEBOUT/gcm-linux_$ARCH.$VERSION.deb"
else
INSTALL_LOCATION="/usr/local"
fi
@@ -106,9 +106,13 @@ else
mkdir -p "$INSTALL_LOCATION"
fi
+if [ -z "$DOTNET_ROOT" ]; then
+ DOTNET_ROOT="$(dirname $(which dotnet))"
+fi
+
# Publish core application executables
echo "Publishing core application..."
-dotnet publish "$GCM_SRC" \
+$DOTNET_ROOT/dotnet publish "$GCM_SRC" \
--configuration="$CONFIGURATION" \
--framework="$FRAMEWORK" \
--runtime="$RUNTIME" \
@@ -117,7 +121,7 @@ dotnet publish "$GCM_SRC" \
--output="$(make_absolute "$PAYLOAD")" || exit 1
echo "Publishing Bitbucket UI helper..."
-dotnet publish "$BITBUCKET_UI_SRC" \
+$DOTNET_ROOT/dotnet publish "$BITBUCKET_UI_SRC" \
--configuration="$CONFIGURATION" \
--framework="$FRAMEWORK" \
--runtime="$RUNTIME" \
@@ -126,7 +130,7 @@ dotnet publish "$BITBUCKET_UI_SRC" \
--output="$(make_absolute "$PAYLOAD")" || exit 1
echo "Publishing GitHub UI helper..."
-dotnet publish "$GITHUB_UI_SRC" \
+$DOTNET_ROOT/dotnet publish "$GITHUB_UI_SRC" \
--configuration="$CONFIGURATION" \
--framework="$FRAMEWORK" \
--runtime="$RUNTIME" \
@@ -135,7 +139,7 @@ dotnet publish "$GITHUB_UI_SRC" \
--output="$(make_absolute "$PAYLOAD")" || exit 1
echo "Publishing GitLab UI helper..."
-dotnet publish "$GITLAB_UI_SRC" \
+$DOTNET_ROOT/dotnet publish "$GITLAB_UI_SRC" \
--configuration="$CONFIGURATION" \
--framework="$FRAMEWORK" \
--runtime="$RUNTIME" \
@@ -189,7 +193,7 @@ if [ $INSTALL_FROM_SOURCE = false ]; then
# https://stackoverflow.com/questions/9349616/bash-eof-in-if-statement
# for details
cat >"$DEBROOT/DEBIAN/control" <
-
-
+
+
diff --git a/src/osx/Installer.Mac/build.sh b/src/osx/Installer.Mac/build.sh
index ba5308da1..e52419601 100755
--- a/src/osx/Installer.Mac/build.sh
+++ b/src/osx/Installer.Mac/build.sh
@@ -22,6 +22,10 @@ case "$i" in
CONFIGURATION="${i#*=}"
shift # past argument=value
;;
+ --runtime=*)
+ RUNTIME="${i#*=}"
+ shift
+ ;;
--version=*)
VERSION="${i#*=}"
shift # past argument=value
@@ -38,15 +42,30 @@ if [ -z "$VERSION" ]; then
die "--version was not set"
fi
+if [ -z "$RUNTIME" ]; then
+ TEST_RUNTIME=`uname -m`
+ case $TEST_RUNTIME in
+ "x86_64")
+ RUNTIME="osx-x64"
+ ;;
+ "arm64")
+ RUNTIME="osx-arm64"
+ ;;
+ *)
+ die "Unknown runtime '$TEST_RUNTIME'"
+ ;;
+ esac
+fi
+
OUTDIR="$INSTALLER_OUT/pkg/$CONFIGURATION"
PAYLOAD="$OUTDIR/payload"
COMPONENTDIR="$OUTDIR/components"
COMPONENTOUT="$COMPONENTDIR/com.microsoft.gitcredentialmanager.component.pkg"
-DISTOUT="$OUTDIR/gcmcore-osx-$VERSION.pkg"
+DISTOUT="$OUTDIR/gcm-osx-x64-$VERSION.pkg"
# Layout and pack
-"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --output="$PAYLOAD" || exit 1
+"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --output="$PAYLOAD" --runtime="$RUNTIME" || exit 1
"$INSTALLER_SRC/pack.sh" --payload="$PAYLOAD" --version="$VERSION" --output="$COMPONENTOUT" || exit 1
-"$INSTALLER_SRC/dist.sh" --package-path="$COMPONENTDIR" --version="$VERSION" --output="$DISTOUT" || exit 1
+"$INSTALLER_SRC/dist.sh" --package-path="$COMPONENTDIR" --version="$VERSION" --output="$DISTOUT" --runtime="$RUNTIME" || exit 1
echo "Build of Installer.Mac complete."
diff --git a/src/osx/Installer.Mac/dist.sh b/src/osx/Installer.Mac/dist.sh
index 749231583..c1f5b9328 100755
--- a/src/osx/Installer.Mac/dist.sh
+++ b/src/osx/Installer.Mac/dist.sh
@@ -11,7 +11,6 @@ SRC="$ROOT/src"
OUT="$ROOT/out"
INSTALLER_SRC="$SRC/osx/Installer.Mac"
RESXPATH="$INSTALLER_SRC/resources"
-DISTPATH="$INSTALLER_SRC/distribution.xml"
# Product information
IDENTIFIER="com.microsoft.gitcredentialmanager.dist"
@@ -32,6 +31,10 @@ case "$i" in
DISTOUT="${i#*=}"
shift # past argument=value
;;
+ --runtime=*)
+ RUNTIME="${i#*=}"
+ shift
+ ;;
*)
# unknown option
;;
@@ -50,6 +53,28 @@ fi
if [ -z "$DISTOUT" ]; then
die "--output was not set"
fi
+if [ -z "$RUNTIME" ]; then
+ TEST_RUNTIME=`uname -m`
+ case $TEST_RUNTIME in
+ "x86_64")
+ RUNTIME="osx-x64"
+ ;;
+ "arm64")
+ RUNTIME="osx-arm64"
+ ;;
+ *)
+ die "Unknown runtime '$TEST_RUNTIME'"
+ ;;
+ esac
+fi
+
+echo "Building for runtime '$RUNTIME'"
+
+if [ "$RUNTIME" == "osx-x64"]; then
+ DISTPATH="$INSTALLER_SRC/distribution.x64.xml"
+else
+ DISTPATH="$INSTALLER_SRC/distribution.arm64.xml"
+fi
# Cleanup any old package
if [ -e "$DISTOUT" ]; then
diff --git a/src/osx/Installer.Mac/distribution.xml b/src/osx/Installer.Mac/distribution.arm64.xml
similarity index 93%
rename from src/osx/Installer.Mac/distribution.xml
rename to src/osx/Installer.Mac/distribution.arm64.xml
index 657397513..531bdbe33 100644
--- a/src/osx/Installer.Mac/distribution.xml
+++ b/src/osx/Installer.Mac/distribution.arm64.xml
@@ -2,7 +2,7 @@
Git Credential Manager
-
+
diff --git a/src/osx/Installer.Mac/distribution.x64.xml b/src/osx/Installer.Mac/distribution.x64.xml
new file mode 100644
index 000000000..45deec220
--- /dev/null
+++ b/src/osx/Installer.Mac/distribution.x64.xml
@@ -0,0 +1,21 @@
+
+
+ Git Credential Manager
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.microsoft.gitcredentialmanager.component.pkg
+
+
diff --git a/src/osx/Installer.Mac/entitlements.xml b/src/osx/Installer.Mac/entitlements.xml
new file mode 100644
index 000000000..9acbcfa5c
--- /dev/null
+++ b/src/osx/Installer.Mac/entitlements.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+
+
\ No newline at end of file
diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh
index 288216165..b9991713d 100755
--- a/src/osx/Installer.Mac/layout.sh
+++ b/src/osx/Installer.Mac/layout.sh
@@ -27,7 +27,6 @@ GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia"
# Build parameters
FRAMEWORK=net6.0
-RUNTIME=osx-x64
# Parse script arguments
for i in "$@"
@@ -41,6 +40,10 @@ case "$i" in
PAYLOAD="${i#*=}"
shift # past argument=value
;;
+ --runtime=*)
+ RUNTIME="${i#*=}"
+ shift # past argument=value
+ ;;
--symbol-output=*)
SYMBOLOUT="${i#*=}"
;;
@@ -50,6 +53,24 @@ case "$i" in
esac
done
+# Determine a runtime if one was not provided
+if [ -z "$RUNTIME" ]; then
+ TEST_RUNTIME=`uname -m`
+ case $TEST_RUNTIME in
+ "x86_64")
+ RUNTIME="osx-x64"
+ ;;
+ "arm64")
+ RUNTIME="osx-arm64"
+ ;;
+ *)
+ die "Unknown runtime '$TEST_RUNTIME'"
+ ;;
+ esac
+fi
+
+echo "Building for runtime '$RUNTIME'"
+
# Perform pre-execution checks
CONFIGURATION="${CONFIGURATION:=Debug}"
if [ -z "$PAYLOAD" ]; then
diff --git a/src/osx/Installer.Mac/uninstall.sh b/src/osx/Installer.Mac/uninstall.sh
index 989ed9956..f26f2b189 100755
--- a/src/osx/Installer.Mac/uninstall.sh
+++ b/src/osx/Installer.Mac/uninstall.sh
@@ -12,7 +12,7 @@ fi
# Unconfigure (as the current user)
echo "Unconfiguring credential helper..."
-sudo -u `/usr/bin/logname` "$GCMBIN" unconfigure
+sudo -u `/usr/bin/logname` -E "$GCMBIN" unconfigure
# Remove symlink
if [ -L /usr/local/bin/git-credential-manager-core ]
diff --git a/src/osx/SignFiles.Mac/SignFiles.Mac.csproj b/src/osx/SignFiles.Mac/SignFiles.Mac.csproj
deleted file mode 100644
index 3de4601d9..000000000
--- a/src/osx/SignFiles.Mac/SignFiles.Mac.csproj
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
- net6.0
- false
-
- $(RootDir)
- $(BaseIntermediateOutputPath)\tmp\macsign
- 8003
-
-
-
-
-
- all
-
-
-
-
-
- Microsoft400
- false
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/osx/SignFiles.Mac/notarize-pkg.sh b/src/osx/SignFiles.Mac/notarize-pkg.sh
deleted file mode 100755
index e7566b851..000000000
--- a/src/osx/SignFiles.Mac/notarize-pkg.sh
+++ /dev/null
@@ -1,163 +0,0 @@
-#!/bin/bash
-
-# This file was based on https://github.com/microsoft/BuildXL/blob/8c2348ff04e6ca78726bb945fb2a0f6a55a5c7d6/Private/macOS/notarize.sh
-#
-# For detailed explanation see: https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution/customizing_the_notarization_workflow
-
-usage() {
- cat < -p -pkg
- -id or --appleid # A valid Apple ID email address, account must have correct certificates available
- -p or --password # The password for the specified Apple ID or Apple One-Time password (to avoid 2FA)
- -pkg or --package # The path to an already signed flat-package
-EOM
- exit 0
-}
-
-declare arg_AppleId=""
-declare arg_Password=""
-declare arg_PackagePath=""
-
-[ $# -eq 0 ] && { usage; }
-
-function parseArgs() {
- arg_Positional=()
- while [[ $# -gt 0 ]]; do
- cmd="$1"
- case $cmd in
- --help | -h)
- usage
- shift
- exit 0
- ;;
- --appleid | -id)
- arg_AppleId=$2
- shift
- ;;
- --password | -p)
- arg_Password="$2"
- shift
- ;;
- --package | -pkg)
- arg_PackagePath="$2"
- shift
- ;;
- *)
- arg_Positional+=("$1")
- shift
- ;;
- esac
- done
-}
-
-function getPackageId {
- local PKG=$(cd "$(dirname "$1")"; pwd)/$(basename "$1")
- local PKGDEST=$(mktemp -d | tr -d '\r')
- xar -x -f "${PKG}" --exclude '^(?:(?!PackageInfo).)*$' -C "${PKGDEST}"
- if [ ! -e "${PKGDEST}/PackageInfo" ]; then
- echo "error: can't find 'PackageInfo'; maybe meta-package"
- return 1
- fi
- cat "${PKGDEST}/PackageInfo" | tr -d '\r' | tr -d '\n' | sed 's:^.*identifier="\([^"]*\)".*$:\1:g'
- rm -rf "${PKGDEST}"
-}
-
-parseArgs "$@"
-
-if [[ -z $arg_AppleId ]]; then
- echo "[ERROR] Must supply valid / non-empty Apple ID!"
- exit 1
-fi
-
-if [[ -z $arg_Password ]]; then
- echo "[ERROR] Must supply valid / non-empty password!"
- exit 1
-fi
-
-if [[ ! -f $arg_PackagePath ]]; then
- echo "[ERROR] Must supply valid / non-empty path to package!"
- exit 1
-fi
-
-declare bundle_id=$(getPackageId ${arg_PackagePath})
-
-if [[ -z $bundle_id ]]; then
- echo "[ERROR] No identifier found in package info!"
- exit 1
-fi
-
-echo "Notarizating $arg_PackagePath"
-
-echo -e "Current state:\n"
-xcrun stapler validate -v "$arg_PackagePath"
-
-if [[ $? -eq 0 ]]; then
- echo "$arg_PackagePath already notarized and stapled, nothing to do!"
- exit 0
-fi
-
-set -e
-
-declare start_time=$(date +%s)
-
-declare output="/tmp/progress.xml"
-
-echo "Uploading package to notarization service, please wait..."
-xcrun altool --notarize-app -t osx -f $arg_PackagePath --primary-bundle-id $bundle_id -u $arg_AppleId -p $arg_Password --output-format xml | tee $output
-
-declare request_id=$(/usr/libexec/PlistBuddy -c "print :notarization-upload:RequestUUID" $output)
-
-echo "Checking notarization request validity..."
-if [[ $request_id =~ ^\{?[A-F0-9a-f]{8}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{12}\}?$ ]]; then
- declare attempts=5
-
- while :
- do
- echo "Waiting a bit before checking on notarization status again..."
-
- sleep 20
- xcrun altool --notarization-info $request_id -u $arg_AppleId -p $arg_Password --output-format xml | tee $output
-
- declare status=$(/usr/libexec/PlistBuddy -c "print :notarization-info:Status" $output)
- echo "Status: $status"
-
- if [[ -z $status ]]; then
- echo "Left attempts: $attempts"
-
- if (($attempts <= 0)); then
- break
- fi
-
- ((attempts--))
- else
- if [[ $status != "in progress" ]]; then
- break
- fi
- fi
- done
-
- declare end_time=$(date +%s)
- echo -e "Completed in $(($end_time-$start_time)) seconds\n"
-
- if [[ "$status" != "success" ]]; then
- echo "Error notarizing, exiting..." >&2
- exit 1
- else
- declare url=$(/usr/libexec/PlistBuddy -c "print :notarization-info:LogFileURL" $output)
-
- if [ "$url" ]; then
- curl $url
- fi
-
- # Staple the ticket to the package
- xcrun stapler staple "$arg_PackagePath"
-
- echo -e "State after notarization:\n"
- xcrun stapler validate -v "$arg_PackagePath"
- echo -e "Stapler exit code: $? (must be zero on success!)\n"
- fi
-else
- echo "Invalid request id found in 'altool' output, aborting!" >&2
- exit 1
-fi
diff --git a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj b/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj
index 61f8f2ae5..18d7f90d7 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj
+++ b/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj
@@ -12,7 +12,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
index 708d1205a..aace44ad7 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
+++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
@@ -1,6 +1,10 @@
using System;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using GitCredentialManager;
using GitCredentialManager.Tests.Objects;
+using Moq;
using Xunit;
namespace Atlassian.Bitbucket.Tests
@@ -105,18 +109,111 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_NoDesktopSessi
}
[Fact]
- public async Task BitbucketAuthentication_ShowOAuthRequiredPromptAsync_SucceedsAfterUserInput()
+ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBCloud_HelperCmdLine()
{
+ var targetUri = new Uri("https://bitbucket.org");
+
+ var helperPath = "/usr/bin/test-helper";
+ var expectedUserName = "jsquire";
+ var expectedPassword = "password";
+ var resultDict = new Dictionary
+ {
+ ["username"] = expectedUserName,
+ ["password"] = expectedPassword
+ };
+
+ string expectedArgs = $"prompt --show-basic --show-oauth";
+
var context = new TestCommandContext();
- context.Terminal.Prompts["Press enter to continue..."] = " ";
+ context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection
- var bitbucketAuthentication = new BitbucketAuthentication(context);
+ var authMock = new Mock(context) { CallBase = true };
+ authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath))
+ .Returns(true);
+ authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None))
+ .ReturnsAsync(resultDict);
+
+ BitbucketAuthentication auth = authMock.Object;
+ CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);
+
+ Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
+ Assert.Equal(result.Credential.Account, expectedUserName);
+ Assert.Equal(result.Credential.Password, expectedPassword);
+
+ authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBCloud_HelperCmdLine()
+ {
+ var targetUri = new Uri("https://bitbucket.org");
+
+ var helperPath = "/usr/bin/test-helper";
+ var expectedUserName = "jsquire";
+ var expectedPassword = "password";
+ var resultDict = new Dictionary
+ {
+ ["username"] = expectedUserName,
+ ["password"] = expectedPassword
+ };
+
+ string expectedArgs = $"prompt --username {expectedUserName} --show-basic";
+
+ var context = new TestCommandContext();
+ context.SessionManager.IsDesktopSession = true; // Enable UI helper selection
- var result = await bitbucketAuthentication.ShowOAuthRequiredPromptAsync();
+ var authMock = new Mock(context) { CallBase = true };
+ authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath))
+ .Returns(true);
+ authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None))
+ .ReturnsAsync(resultDict);
+
+ BitbucketAuthentication auth = authMock.Object;
+ CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, expectedUserName, AuthenticationModes.Basic);
+
+ Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
+ Assert.Equal(result.Credential.Account, expectedUserName);
+ Assert.Equal(result.Credential.Password, expectedPassword);
+
+ authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBServerDC_HelperCmdLine()
+ {
+ var targetUri = new Uri("https://example.com/bitbucket");
+
+ var helperPath = "/usr/bin/test-helper";
+ var expectedUserName = "jsquire";
+ var expectedPassword = "password";
+ var resultDict = new Dictionary
+ {
+ ["username"] = expectedUserName,
+ ["password"] = expectedPassword
+ };
+
+ string expectedArgs = $"prompt --url {targetUri} --show-basic --show-oauth";
+
+ var context = new TestCommandContext();
+ context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection
+
+ var authMock = new Mock(context) { CallBase = true };
+ authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath))
+ .Returns(true);
+ authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None))
+ .ReturnsAsync(resultDict);
+
+ BitbucketAuthentication auth = authMock.Object;
+ CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);
+
+ Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
+ Assert.Equal(result.Credential.Account, expectedUserName);
+ Assert.Equal(result.Credential.Password, expectedPassword);
- Assert.True(result);
- Assert.Equal($"Your account has two-factor authentication enabled.{Environment.NewLine}" +
- $"To continue you must complete authentication in your web browser.{Environment.NewLine}", context.Terminal.Messages[0].Item1);
+ authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None),
+ Times.Once);
}
}
}
diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs
index 6afc52f45..cf8216afc 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs
+++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs
@@ -15,12 +15,66 @@ public class BitbucketHostProviderTest
#region Tests
private const string MOCK_ACCESS_TOKEN = "at-0987654321";
+ private const string MOCK_ACCESS_TOKEN_ALT = "at-onetwothreefour-1234";
+ private const string MOCK_EXPIRED_ACCESS_TOKEN = "at-1234567890-expired";
private const string MOCK_REFRESH_TOKEN = "rt-1234567809";
private const string BITBUCKET_DOT_ORG_HOST = "bitbucket.org";
private const string DC_SERVER_HOST = "example.com";
private Mock bitbucketAuthentication = new Mock(MockBehavior.Strict);
private Mock bitbucketApi = new Mock(MockBehavior.Strict);
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ [InlineData(" ", false)]
+ [InlineData("bitbucket.org", true)]
+ [InlineData("BITBUCKET.ORG", true)]
+ [InlineData("BiTbUcKeT.OrG", true)]
+ [InlineData("bitbucket.example.com", false)]
+ [InlineData("bitbucket.example.org", false)]
+ [InlineData("bitbucket.org.com", false)]
+ [InlineData("bitbucket.org.org", false)]
+ public void BitbucketHostProvider_IsBitbucketOrg_StringHost(string str, bool expected)
+ {
+ bool actual = BitbucketHostProvider.IsBitbucketOrg(str);
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("http://bitbucket.org", true)]
+ [InlineData("https://bitbucket.org", true)]
+ [InlineData("http://bitbucket.org/path", true)]
+ [InlineData("https://bitbucket.org/path", true)]
+ [InlineData("http://BITBUCKET.ORG", true)]
+ [InlineData("https://BITBUCKET.ORG", true)]
+ [InlineData("http://BITBUCKET.ORG/PATH", true)]
+ [InlineData("https://BITBUCKET.ORG/PATH", true)]
+ [InlineData("http://BiTbUcKeT.OrG", true)]
+ [InlineData("https://BiTbUcKeT.OrG", true)]
+ [InlineData("http://BiTbUcKeT.OrG/pAtH", true)]
+ [InlineData("https://BiTbUcKeT.OrG/pAtH", true)]
+ [InlineData("http://bitbucket.example.com", false)]
+ [InlineData("https://bitbucket.example.com", false)]
+ [InlineData("http://bitbucket.example.com/path", false)]
+ [InlineData("https://bitbucket.example.com/path", false)]
+ [InlineData("http://bitbucket.example.org", false)]
+ [InlineData("https://bitbucket.example.org", false)]
+ [InlineData("http://bitbucket.example.org/path", false)]
+ [InlineData("https://bitbucket.example.org/path", false)]
+ [InlineData("http://bitbucket.org.com", false)]
+ [InlineData("https://bitbucket.org.com", false)]
+ [InlineData("http://bitbucket.org.com/path", false)]
+ [InlineData("https://bitbucket.org.com/path", false)]
+ [InlineData("http://bitbucket.org.org", false)]
+ [InlineData("https://bitbucket.org.org", false)]
+ [InlineData("http://bitbucket.org.org/path", false)]
+ [InlineData("https://bitbucket.org.org/path", false)]
+ public void BitbucketHostProvider_IsBitbucketOrg_Uri(string str, bool expected)
+ {
+ bool actual = BitbucketHostProvider.IsBitbucketOrg(new Uri(str));
+ Assert.Equal(expected, actual);
+ }
+
[Theory]
[InlineData("https", null, false)]
// We report that we support unencrypted HTTP here so that we can fail and
@@ -86,210 +140,262 @@ public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header,
[Theory]
[InlineData("https", DC_SERVER_HOST, "jsquire", "password")]
[InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")]
- public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasicAuthAccount(string protocol, string host, string username,string password)
+ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic(
+ string protocol, string host, string username, string password)
{
InputArguments input = MockInput(protocol, host, username);
var context = new TestCommandContext();
MockStoredAccount(context, input, password);
- MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password);
+ MockRemoteBasicValid(input, password);
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
- var credential = provider.GetCredentialAsync(input);
+ var credential = await provider.GetCredentialAsync(input);
- //verify bitbucket.org credentials were validated
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(password, credential.Password);
+
+ // Verify bitbucket.org credentials were validated
if (BITBUCKET_DOT_ORG_HOST.Equals(host))
{
- VerifyValidateBasicAuthCredentialsRan();
+ VerifyValidateBasicAuthCredentialsRan(input, password);
}
else
{
- //verify DC/Server credentials were not validated
+ // Verify DC/Server credentials were not validated
VerifyValidateBasicAuthCredentialsNeverRan();
}
// Stored credentials so don't ask for more
- VerifyInteractiveBasicAuthFlowNeverRan(password, input, credential);
-
- // Valid Basic Auth credentials so don't run Oauth
- VerifyInteractiveOAuthFlowNeverRan(input, credential);
+ VerifyInteractiveAuthNeverRan();
}
[Theory]
// DC/Server does not currently support OAuth
[InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")]
- public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAuthAccount(string protocol, string host, string username,string token)
+ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth(
+ string protocol, string host, string username, string token)
{
InputArguments input = MockInput(protocol, host, username);
var context = new TestCommandContext();
MockStoredAccount(context, input, token);
- MockRemoteOAuthAccountIsValid(bitbucketApi, input, token, false);
+ MockRemoteAccessTokenValid(input, token);
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
- var credential = provider.GetCredentialAsync(input);
+ var credential = await provider.GetCredentialAsync(input);
- //verify bitbucket.org credentials were validated
- VerifyValidateOAuthCredentialsRan();
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(token, credential.Password);
- // Stored credentials so don't ask for more
- VerifyInteractiveBasicAuthFlowNeverRan(token, input, credential);
+ // Verify bitbucket.org credentials were validated
+ VerifyValidateAccessTokenRan(input, token);
- // Valid Basic Auth credentials so don't run Oauth
- VerifyInteractiveOAuthFlowNeverRan(input, credential);
+ // Stored credentials so don't ask for more
+ VerifyInteractiveAuthNeverRan();
}
[Theory]
// DC
[InlineData("https", DC_SERVER_HOST, "jsquire", "password")]
- // cloud
+ // Cloud
[InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")]
- public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasicAuthAccount(string protocol, string host, string username, string password)
+ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_Basic(
+ string protocol, string host, string username, string password)
{
InputArguments input = MockInput(protocol, host, username);
var context = new TestCommandContext();
- MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password);
+ MockPromptBasic(input, password);
+ MockRemoteBasicValid(input, password);
- if (BITBUCKET_DOT_ORG_HOST.Equals(host))
- {
- MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true);
- }
+ var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
+
+ var credential = await provider.GetCredentialAsync(input);
+
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(password, credential.Password);
+
+ VerifyInteractiveAuthRan(input);
+ }
+
+ [Theory]
+ // DC/Server does not currently support OAuth
+ [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)]
+ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_OAuth(
+ string protocol, string host, string username, string refreshToken, string accessToken)
+ {
+ InputArguments input = MockInput(protocol, host, username);
- MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password);
+ var context = new TestCommandContext();
+
+ MockPromptOAuth(input);
+ MockRemoteOAuthTokenCreate(input, accessToken, refreshToken);
+ MockRemoteAccessTokenValid(input, accessToken);
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
- var credential = provider.GetCredentialAsync(input);
+ var credential = await provider.GetCredentialAsync(input);
- VerifyBasicAuthFlowRan(password, true, input, credential, null);
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(accessToken, credential.Password);
- VerifyOAuthFlowDidNotRun(password, true, input, credential);
+ VerifyInteractiveAuthRan(input);
+ VerifyOAuthFlowRan(input, accessToken);
+ VerifyValidateAccessTokenRan(input, accessToken);
+ VerifyOAuthRefreshTokenStored(context, input, refreshToken);
}
[Theory]
// DC/Server does not currently support OAuth
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN)]
- public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAcccount(string protocol, string host, string username, string password)
+ [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)]
+ public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refresh(
+ string protocol, string host, string username, string refreshToken, string accessToken)
{
var input = MockInput(protocol, host, username);
var context = new TestCommandContext();
- // user is prompted for basic auth credentials
- MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password);
- // basic auth credentials are valid but 2FA is ON
- MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password);
- MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true);
- MockRemoteValidRefreshToken();
+ // AT has does not exist, but RT is still valid
+ MockStoredRefreshToken(context, input, refreshToken);
+ MockRemoteAccessTokenValid(input, accessToken);
+ MockRemoteRefreshTokenValid(refreshToken, accessToken);
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
- var credential = provider.GetCredentialAsync(input);
+ var credential = await provider.GetCredentialAsync(input);
- VerifyOAuthFlowRan(password, false, true, input, credential, null);
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(accessToken, credential.Password);
- VerifyBasicAuthFlowNeverRan(password, input, false, null);
+ VerifyValidateAccessTokenRan(input, accessToken);
+ VerifyOAuthRefreshRan(refreshToken);
+ VerifyInteractiveAuthNeverRan();
}
[Theory]
- // cloud
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "basic")]
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "oauth")]
- // Basic Auth works
- public void BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected(string protocol, string host, string username, string password,
- string preconfiguredAuthModes)
+ // DC/Server does not currently support OAuth
+ [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_EXPIRED_ACCESS_TOKEN, MOCK_ACCESS_TOKEN)]
+ public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refresh(
+ string protocol, string host, string username, string refreshToken, string expiredAccessToken, string accessToken)
{
var input = MockInput(protocol, host, username);
var context = new TestCommandContext();
- if (preconfiguredAuthModes != null)
- {
- context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, preconfiguredAuthModes);
- }
- MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password);
- MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password);
- bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true);
+ // AT exists but has expired, but RT is still valid
+ MockStoredAccount(context, input, expiredAccessToken);
+ MockRemoteAccessTokenExpired(input, expiredAccessToken);
+
+ MockStoredRefreshToken(context, input, refreshToken);
+ MockRemoteAccessTokenValid(input, accessToken);
+ MockRemoteRefreshTokenValid(refreshToken, accessToken);
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
- var credential = provider.GetCredentialAsync(input);
+ var credential = await provider.GetCredentialAsync(input);
+
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(accessToken, credential.Password);
+
+ VerifyValidateAccessTokenRan(input, accessToken);
+ VerifyOAuthRefreshRan(refreshToken);
+ VerifyInteractiveAuthNeverRan();
+ }
+
+ [Theory]
+ // Cloud
+ [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)]
+ public async Task BitbucketHostProvider_GetCredentialAsync_PreconfiguredMode_OAuth_ValidRT_IsRespected(
+ string protocol, string host, string username, string refreshToken, string accessToken)
+ {
+ var input = MockInput(protocol, host, username);
+
+ var context = new TestCommandContext();
+ context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, "oauth");
+
+ // We have a stored RT so we can just use that without any prompts
+ MockStoredRefreshToken(context, input, refreshToken);
+ MockRemoteAccessTokenValid(input, accessToken);
+ MockRemoteRefreshTokenValid(refreshToken, accessToken);
+
+ var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
+
+ var credential = await provider.GetCredentialAsync(input);
Assert.NotNull(credential);
- if (preconfiguredAuthModes.Contains("basic"))
- {
- VerifyInteractiveBasicAuthFlowRan(password, input, credential);
- VerifyInteractiveOAuthFlowNeverRan(input, credential);
- }
+ VerifyInteractiveAuthNeverRan();
+ VerifyOAuthRefreshRan(refreshToken);
+ }
- if (preconfiguredAuthModes.Contains("oauth"))
- {
- VerifyInteractiveBasicAuthFlowNeverRan(password, input, credential);
- VerifyInteractiveOAuthFlowRan(password, input, credential);
- }
+ [Theory]
+ // DC/Server does not currently support OAuth
+ [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN, MOCK_ACCESS_TOKEN_ALT, MOCK_REFRESH_TOKEN)]
+ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_OAuth_IsRespected(
+ string protocol, string host, string username, string storedToken, string newToken, string refreshToken)
+ {
+ var input = MockInput(protocol, host, username);
+
+ var context = new TestCommandContext();
+ context.Environment.Variables.Add(
+ BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString);
+
+ // User has stored access token that we shouldn't use - RT should be used to mint new AT
+ MockStoredAccount(context, input, storedToken);
+ MockStoredRefreshToken(context, input, refreshToken);
+ MockRemoteAccessTokenValid(input, newToken);
+ MockRemoteRefreshTokenValid(refreshToken, newToken);
+
+ var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
+
+ var credential = await provider.GetCredentialAsync(input);
+
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(newToken, credential.Password);
+
+ VerifyInteractiveAuthNeverRan();
+ VerifyOAuthRefreshRan(refreshToken);
}
[Theory]
- // cloud
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "false")]
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "0")]
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "true")]
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "1")]
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", null)]
+ // Cloud
+ [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "old-password", "new-password")]
// DC
- [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "false")]
- [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "0")]
- [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "1")]
- [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "true")]
- [InlineData("https", DC_SERVER_HOST, "jsquire", "password", null)]
- public void BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_IsRespected(string protocol, string host, string username, string password,
- string alwaysRefreshCredentials)
+ [InlineData("https", DC_SERVER_HOST, "jsquire", "old-password", "new-password")]
+ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_Basic_IsRespected(
+ string protocol, string host, string username, string storedPassword, string freshPassword)
{
var input = MockInput(protocol, host, username);
var context = new TestCommandContext();
- if (alwaysRefreshCredentials != null)
- {
- context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, alwaysRefreshCredentials);
- }
+ context.Environment.Variables.Add(
+ BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString);
- MockStoredAccount(context, input, password);
- MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password);
- MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true);
- MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password);
+ // User has stored password that we shouldn't use
+ MockStoredAccount(context, input, storedPassword);
+ MockPromptBasic(input, freshPassword);
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
- var credential = provider.GetCredentialAsync(input);
+ var credential = await provider.GetCredentialAsync(input);
- var alwaysRefreshCredentialsBool = "1".Equals(alwaysRefreshCredentials)
- || "on".Equals(alwaysRefreshCredentials)
- || "true".Equals(alwaysRefreshCredentials) ? true : false;
+ Assert.Equal(username, credential.Account);
+ Assert.Equal(freshPassword, credential.Password);
- if (alwaysRefreshCredentialsBool)
- {
- VerifyBasicAuthFlowRan(password, true, input, credential, null);
- }
- else
- {
- VerifyBasicAuthFlowNeverRan(password, input, true, null);
- }
-
- VerifyOAuthFlowDidNotRun(password, true, input, credential);
+ VerifyInteractiveAuthRan(input);
}
[Theory]
// DC - supports Basic
[InlineData("https://example.com", "basic", AuthenticationModes.Basic)]
[InlineData("https://example.com", "oauth", AuthenticationModes.Basic)]
- // cloud - supports Basic, OAuth
+ // Cloud - supports Basic, OAuth
[InlineData("https://bitbucket.org", "oauth", AuthenticationModes.OAuth)]
[InlineData("https://bitbucket.org", "basic", AuthenticationModes.Basic)]
[InlineData("https://bitbucket.org", "NOT-A-REAL-VALUE", BitbucketConstants.DotOrgAuthenticationModes)]
@@ -302,7 +408,7 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri
{
var targetUri = new Uri(uriString);
- var context = new TestCommandContext { };
+ var context = new TestCommandContext();
if (bitbucketAuthModes != null)
{
context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, bitbucketAuthModes);
@@ -315,40 +421,6 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri
Assert.Equal(expectedModes, actualModes);
}
- [Theory]
- // DC
- [InlineData("https", DC_SERVER_HOST, "jsquire", "password")]
- [InlineData("http", DC_SERVER_HOST, "jsquire", "password")]
- // cloud
- [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")]
- [InlineData("http", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")]
- public async Task BitbucketHostProvider_GetCredentialAsync_ValidateTargetUriAsync(string protocol, string host, string username, string password)
- {
- var input = MockInput(protocol, host, username);
-
- var context = new TestCommandContext();
-
- var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object);
-
- if (protocol.ToLower().Equals("http") && host.ToLower().Equals(BITBUCKET_DOT_ORG_HOST))
- {
- // only fail for http://bitbucket.org
- await Assert.ThrowsAsync(async () => await provider.GetCredentialAsync(input));
- }
- else
- {
- MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password);
- MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password);
- MockRemoteValidRefreshToken();
- bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true);
- bitbucketAuthentication.Setup(m => m.CreateOAuthCredentialsAsync(It.IsAny())).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token"));
- var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = false };
- bitbucketApi.Setup(x => x.GetUserInformationAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo));
-
- var credential = await provider.GetCredentialAsync(input);
- }
- }
-
[Theory]
[InlineData("https", DC_SERVER_HOST, "jsquire")]
public async Task BitbucketHostProvider_StoreCredentialAsync(string protocol, string host, string username)
@@ -388,6 +460,7 @@ public async Task BitbucketHostProvider_EraseCredentialAsync(string protocol, st
#endregion
#region Test helpers
+
private static InputArguments MockInput(string protocol, string host, string username)
{
return new InputArguments(new Dictionary
@@ -398,224 +471,102 @@ private static InputArguments MockInput(string protocol, string host, string use
});
}
- private void VerifyBasicAuthFlowRan(string password, bool expected, InputArguments input, Task credential,
- string preconfiguredAuthModes)
+ private void VerifyOAuthFlowRan(InputArguments input, string token)
{
- Assert.Equal(expected, credential != null);
-
var remoteUri = input.GetRemoteUri();
- bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once);
+ // Get new access token and refresh token
+ bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(remoteUri), Times.Once);
- // check username/password for Bitbucket.org
- if ((preconfiguredAuthModes == null && BITBUCKET_DOT_ORG_HOST == remoteUri.Host)
- || (preconfiguredAuthModes != null && preconfiguredAuthModes.Contains("oauth")))
- {
- bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once);
- }
+ // Check access token works/resolve username
+ bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once);
}
- private void VerifyInteractiveBasicAuthFlowRan(string password, InputArguments input, Task credential)
- {
- var remoteUri = input.GetRemoteUri();
-
- // verify users was prompted for username/password credentials
- bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once);
-
- // check username/password for Bitbucket.org
- if (BITBUCKET_DOT_ORG_HOST == remoteUri.Host)
- {
- bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once);
- }
- }
-
- private void VerifyBasicAuthFlowNeverRan(string password, InputArguments input, bool storedAccount,
- string preconfiguredAuthModes)
- {
- var remoteUri = input.GetRemoteUri();
-
- if (!storedAccount &&
- (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic")) )
- {
- // never prompt the user for basic credentials
- bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once);
- }
- else
- {
- // never prompt the user for basic credentials
- bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never);
- }
- }
-
- private void VerifyInteractiveBasicAuthFlowNeverRan(string password, InputArguments input, Task credential)
- {
- var remoteUri = input.GetRemoteUri();
-
- bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never);
- }
-
- private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, Task credential,
- string preconfiguredAuthModes)
+ private void VerifyValidateBasicAuthCredentialsNeverRan()
{
- Assert.Equal(expected, credential != null);
-
- var remoteUri = input.GetRemoteUri();
-
- if (storedAccount)
- {
- // use refresh token to get new access token and refresh token
- bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN), Times.Once);
-
- // check access token works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Once);
- }
- else
- {
- if (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic"))
- {
- // prompt user for basic auth, if basic auth is not excluded
- bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once);
-
- // check if entered Basic Auth credentials work, if basic auth is not excluded
- bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once);
- }
-
- // Basic Auth 403-ed so push user through OAuth flow
- bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Once);
- }
+ // Never check username/password works
+ bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Never);
}
- private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input, System.Threading.Tasks.Task credential)
+ private void VerifyValidateBasicAuthCredentialsRan(InputArguments input, string password)
{
- var remoteUri = input.GetRemoteUri();
-
- // Basic Auth 403-ed so push user through OAuth flow
- bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Once);
-
+ // Check username/password works
+ bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once);
}
- private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArguments input, System.Threading.Tasks.Task credential)
+ private void VerifyValidateAccessTokenRan(InputArguments input, string token)
{
- Assert.Equal(expected, credential != null);
-
- var remoteUri = input.GetRemoteUri();
-
- // never prompt user through OAuth flow
- bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Never);
-
- // Never try to refresh Access Token
- bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(It.IsAny()), Times.Never);
-
- // never check access token works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never);
+ // Check tokens works
+ bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once);
}
- private void VerifyInteractiveOAuthFlowNeverRan(InputArguments input, System.Threading.Tasks.Task credential)
+ private void VerifyInteractiveAuthRan(InputArguments input)
{
var remoteUri = input.GetRemoteUri();
- // never prompt user through OAuth flow
- bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Never);
-
- // Never try to refresh Access Token
- bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(It.IsAny()), Times.Never);
-
- // never check access token works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never);
- }
-
- private void VerifyValidateBasicAuthCredentialsNeverRan()
- {
- // never check username/password works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Never);
- }
-
- private void VerifyValidateBasicAuthCredentialsRan()
- {
- // check username/password works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Once);
- }
-
- private void VerifyValidateOAuthCredentialsNeverRan()
- {
- // never check username/password works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), false), Times.Never);
+ bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once);
}
- private void VerifyValidateOAuthCredentialsRan()
+ private void VerifyInteractiveAuthNeverRan()
{
- // check username/password works
- bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), true), Times.Once);
+ bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
- private void MockStoredOAuthAccount(TestCommandContext context, InputArguments input)
+ private void VerifyOAuthRefreshRan(string refreshToken)
{
- // refresh token
- context.CredentialStore.Add("https://bitbucket.org/refresh_token", new TestCredential(input.Host, input.UserName, MOCK_REFRESH_TOKEN));
- // auth token
- context.CredentialStore.Add("https://bitbucket.org", new TestCredential(input.Host, input.UserName, MOCK_ACCESS_TOKEN));
+ // Check refresh was called
+ bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(refreshToken), Times.Once);
}
- private void MockRemoteValidRefreshToken()
+ private void MockRemoteRefreshTokenValid(string refreshToken, string accessToken)
{
- bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN)).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token"));
+ bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(refreshToken)).ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token"));
}
- private static void MockInvalidRemoteBasicAccount(Mock bitbucketApi, Mock bitbucketAuthentication)
- {
- bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, null));
-
- bitbucketApi.Setup(x => x.GetUserInformationAsync(It.IsAny(), It.IsAny(), false))
- .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized));
-
- }
- private static void MockUserEntersValidBasicCredentials(Mock bitbucketAuthentication, InputArguments input, string password)
+ private void MockPromptBasic(InputArguments input, string password)
{
var remoteUri = input.GetRemoteUri();
bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()))
.ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password)));
}
- private static void MockUserDoesNotEntersValidBasicCredentials(Mock bitbucketAuthentication)
+ private void MockPromptOAuth(InputArguments input)
{
- bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, null));
+ var remoteUri = input.GetRemoteUri();
+ bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()))
+ .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.OAuth));
}
- private static void MockRemoteBasicAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled)
+ private void MockRemoteBasicValid(InputArguments input, string password, bool twoFactor = true)
{
- var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled };
+ var userInfo = new UserInfo
+ {
+ UserName = input.UserName,
+ IsTwoFactorAuthenticationEnabled = twoFactor
+ };
+
// Basic
bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false))
.ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo));
-
}
- private static void MockRemoteBasicAuthAccountIsValidRequires2FA(Mock bitbucketApi, InputArguments input, string password)
+ private void MockRemoteAccessTokenExpired(InputArguments input, string token)
{
- MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, true);
- }
-
- private static void MockRemoteBasicAuthAccountIsValidNo2FA(Mock bitbucketApi, InputArguments input, string password)
- {
- MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, false);
+ // OAuth
+ bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true))
+ .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized));
}
- private static void MockRemoteBasicAuthAccountIsInvalid(Mock bitbucketApi, InputArguments input, string password)
+ private void MockRemoteAccessTokenValid(InputArguments input, string token, bool twoFactor = true)
{
- var userInfo = new UserInfo();
- // Basic
- bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false))
- .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Forbidden, userInfo));
-
- }
+ var userInfo = new UserInfo
+ {
+ UserName = input.UserName,
+ IsTwoFactorAuthenticationEnabled = twoFactor
+ };
- private static void MockRemoteOAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled)
- {
- var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled };
// OAuth
- bitbucketApi.Setup(x => x.GetUserInformationAsync(null, password, true))
+ bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true))
.ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo));
}
@@ -626,12 +577,28 @@ private static void MockStoredAccount(TestCommandContext context, InputArguments
context.CredentialStore.Add(remoteUrl, new TestCredential(input.Host, input.UserName, password));
}
- private static void MockValidStoredOAuthUser(TestCommandContext context, Mock bitbucketApi)
+ private static void MockStoredRefreshToken(TestCommandContext context, InputArguments input, string token)
{
- var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = false };
- bitbucketApi.Setup(x => x.GetUserInformationAsync("jsquire", "password1", false))
- .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo));
- context.CredentialStore.Add("https://bitbucket.org", new TestCredential("https://bitbucket.org", "jsquire", "password1"));
+ var remoteUri = input.GetRemoteUri();
+ var refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri);
+ context.CredentialStore.Add(refreshService, new TestCredential(refreshService, input.UserName, token));
+ }
+
+ private void MockRemoteOAuthTokenCreate(InputArguments input, string accessToken, string refreshToken)
+ {
+ var remoteUri = input.GetRemoteUri();
+ bitbucketAuthentication.Setup(x => x.CreateOAuthCredentialsAsync(remoteUri))
+ .ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token") { RefreshToken = refreshToken });
+ }
+
+ private void VerifyOAuthRefreshTokenStored(TestCommandContext context, InputArguments input, string refreshToken)
+ {
+ var remoteUri = input.GetRemoteUri();
+ string refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri);
+ bool result = context.CredentialStore.TryGet(refreshService, input.UserName, out var credential);
+
+ Assert.True(result);
+ Assert.Equal(refreshToken, credential.Password);
}
#endregion
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
index e1ae6d4af..a9185278a 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj
+++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj
@@ -3,7 +3,7 @@
WinExe
net6.0
- osx-x64;linux-x64
+ osx-x64;linux-x64;osx-arm64
Atlassian.Bitbucket.UI
Atlassian.Bitbucket.UI
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs
deleted file mode 100644
index f3ee88591..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.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 OAuthCommandImpl : OAuthCommand
- {
- public OAuthCommandImpl(CommandContext context) : base(context) { }
-
- protected override Task ShowAsync(OAuthViewModel 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
index 86b010696..fca8daf48 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml
+++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml
@@ -6,8 +6,24 @@
x:Class="Atlassian.Bitbucket.UI.Controls.TesterWindow"
Title="Bitbucket Authentication Dialog Tester"
Height="240" Width="420" CanResize="False">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
index c5c31b047..25ae2cef8 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
+++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
@@ -1,3 +1,4 @@
+using System;
using Atlassian.Bitbucket.UI.ViewModels;
using Atlassian.Bitbucket.UI.Views;
using Avalonia;
@@ -53,17 +54,17 @@ private void ShowCredentials(object sender, RoutedEventArgs e)
{
var vm = new CredentialsViewModel(_environment)
{
- ShowOAuth = true
+ ShowOAuth = this.FindControl("showOAuth").IsChecked ?? false,
+ ShowBasic = this.FindControl("showBasic").IsChecked ?? false,
+ UserName = this.FindControl("username").Text
};
- var view = new CredentialsView();
- var window = new DialogWindow(view) {DataContext = vm};
- window.ShowDialog(this);
- }
- private void ShowOAuth(object sender, RoutedEventArgs e)
- {
- var vm = new OAuthViewModel(_environment);
- var view = new OAuthView();
+ 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
index 714356044..d84abdfe1 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs
+++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs
@@ -50,7 +50,6 @@ private static void AppMain(object o)
using (var app = new HelperApplication(context))
{
app.RegisterCommand(new CredentialsCommandImpl(context));
- app.RegisterCommand(new OAuthCommandImpl(context));
int exitCode = app.RunAsync(args)
.ConfigureAwait(false)
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
index 0f4ad4e96..f42feced3 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
+++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
@@ -3,6 +3,7 @@
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Atlassian.Bitbucket.UI.Views.CredentialsView">
@@ -10,40 +11,71 @@
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml.cs
index f3b7b305b..cf3d1b11a 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml.cs
+++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml.cs
@@ -7,6 +7,8 @@ namespace Atlassian.Bitbucket.UI.Views
{
public class CredentialsView : UserControl, IFocusable
{
+ private TabControl _tabControl;
+ private Button _oauthLoginButton;
private TextBox _userNameTextBox;
private TextBox _passwordTextBox;
@@ -19,6 +21,8 @@ private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
+ _tabControl = this.FindControl("authModesTabControl");
+ _oauthLoginButton = this.FindControl
-
-
-
+
+
+
diff --git a/src/shared/Core/Authentication/AuthenticationBase.cs b/src/shared/Core/Authentication/AuthenticationBase.cs
index 467a9fffb..fdbe75a11 100644
--- a/src/shared/Core/Authentication/AuthenticationBase.cs
+++ b/src/shared/Core/Authentication/AuthenticationBase.cs
@@ -25,7 +25,7 @@ protected Task> InvokeHelperAsync(string path, strin
return InvokeHelperAsync(path, args, null, CancellationToken.None);
}
- internal protected virtual async Task> InvokeHelperAsync(string path, string args,
+ protected internal virtual async Task> InvokeHelperAsync(string path, string args,
IDictionary standardInput, CancellationToken ct)
{
var procStartInfo = new ProcessStartInfo(path)
diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs
index 0a52f627a..397d57bbb 100644
--- a/src/shared/Core/CommandContext.cs
+++ b/src/shared/Core/CommandContext.cs
@@ -107,7 +107,7 @@ public CommandContext(string appPath)
FileSystem = new MacOSFileSystem();
SessionManager = new MacOSSessionManager();
SystemPrompts = new MacOSSystemPrompts();
- Environment = new PosixEnvironment(FileSystem);
+ Environment = new MacOSEnvironment(FileSystem);
Terminal = new MacOSTerminal(Trace);
string gitPath = GetGitPath(Environment, FileSystem, Trace);
Git = new GitProcess(
diff --git a/src/shared/Core/CredentialStore.cs b/src/shared/Core/CredentialStore.cs
index 812c301ee..bf8c0be47 100644
--- a/src/shared/Core/CredentialStore.cs
+++ b/src/shared/Core/CredentialStore.cs
@@ -152,8 +152,11 @@ private static void AppendAvailableStoreList(StringBuilder sb)
Environment.NewLine, StoreNames.Gpg);
}
- sb.AppendFormat(" {1,-13} : Git's in-memory credential cache{0}",
- Environment.NewLine, StoreNames.Cache);
+ if (!PlatformUtils.IsWindows())
+ {
+ sb.AppendFormat(" {1,-13} : Git's in-memory credential cache{0}",
+ Environment.NewLine, StoreNames.Cache);
+ }
sb.AppendFormat(" {1,-13} : store credentials in plain-text files (UNSECURE){0}",
Environment.NewLine, StoreNames.Plaintext);
@@ -286,6 +289,15 @@ private void ValidateGpgPass(out string storeRoot, out string execPath)
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."
+ );
+ }
+
// allow for --timeout and other options
if (!_context.Settings.TryGetSetting(
Constants.EnvironmentVariables.GcmCredCacheOptions,
diff --git a/src/shared/Core/EnvironmentBase.cs b/src/shared/Core/EnvironmentBase.cs
index 5ca12ee6b..a2aa36cf2 100644
--- a/src/shared/Core/EnvironmentBase.cs
+++ b/src/shared/Core/EnvironmentBase.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Linq;
namespace GitCredentialManager
@@ -89,8 +90,6 @@ public bool IsDirectoryOnPath(string directoryPath)
protected abstract string[] SplitPathVariable(string value);
- public abstract bool TryLocateExecutable(string program, out string path);
-
public virtual Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
{
var psi = new ProcessStartInfo(path, args)
@@ -104,6 +103,44 @@ public virtual Process CreateProcess(string path, string args, bool useShellExec
return new Process { StartInfo = psi };
}
+
+ public virtual bool TryLocateExecutable(string program, out string path)
+ {
+ return TryLocateExecutable(program, null, out path);
+ }
+
+ internal virtual bool TryLocateExecutable(string program, ICollection pathsToIgnore, out string path)
+ {
+ // On UNIX-like systems we would normally use the "which" utility to locate a program,
+ // but since distributions don't always place "which" in a consistent location we cannot
+ // find it! Oh the irony..
+ // We could also try using "env" to then locate "which", but the same problem exists in
+ // that "env" isn't always in a standard location.
+ //
+ // On Windows we should avoid using the equivalent utility "where.exe" because this will
+ // include the current working directory in the search, and we don't want this.
+ //
+ // The upshot of the above means we cannot use either of "which" or "where.exe" and must
+ // instead manually scan the PATH variable looking for the program.
+ // At least both Windows and UNIX use the same name for the $PATH or %PATH% variable!
+ if (Variables.TryGetValue("PATH", out string pathValue))
+ {
+ string[] paths = SplitPathVariable(pathValue);
+ foreach (var basePath in paths)
+ {
+ string candidatePath = Path.Combine(basePath, program);
+ if (FileSystem.FileExists(candidatePath) && (pathsToIgnore is null ||
+ !pathsToIgnore.Contains(candidatePath, StringComparer.OrdinalIgnoreCase)))
+ {
+ path = candidatePath;
+ return true;
+ }
+ }
+ }
+
+ path = null;
+ return false;
+ }
}
public static class EnvironmentExtensions
diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs
index cedbd6cdf..c0e794d4d 100644
--- a/src/shared/Core/GenericHostProvider.cs
+++ b/src/shared/Core/GenericHostProvider.cs
@@ -40,8 +40,8 @@ public GenericHostProvider(ICommandContext context,
public override bool IsSupported(InputArguments input)
{
- return input != null && (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") ||
- StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https"));
+ // The generic provider should support all possible protocols (HTTP, HTTPS, SMTP, IMAP, etc)
+ return input != null && !string.IsNullOrWhiteSpace(input.Protocol);
}
public override async Task GenerateCredentialAsync(InputArguments input)
@@ -51,7 +51,12 @@ public override async Task GenerateCredentialAsync(InputArguments i
Uri uri = input.GetRemoteUri();
// Determine the if the host supports Windows Integration Authentication (WIA)
- if (IsWindowsAuthAllowed)
+ if (!StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "http") &&
+ !StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "https"))
+ {
+ // Cannot check WIA support for non-HTTP based protocols
+ }
+ else if (IsWindowsAuthAllowed)
{
if (PlatformUtils.IsWindows())
{
diff --git a/src/shared/Core/HostProviderRegistry.cs b/src/shared/Core/HostProviderRegistry.cs
index d45d0678f..237c32e54 100644
--- a/src/shared/Core/HostProviderRegistry.cs
+++ b/src/shared/Core/HostProviderRegistry.cs
@@ -154,12 +154,16 @@ public async Task GetProviderAsync(InputArguments input)
throw new Exception("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
+ bool canProbeUri = StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "http") ||
+ StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "https");
+
var probeTimeout = TimeSpan.FromMilliseconds(_context.Settings.AutoDetectProviderTimeout);
_context.Trace.WriteLine($"Auto-detect probe timeout is {probeTimeout.TotalSeconds} ms.");
HttpResponseMessage probeResponse = null;
- async Task MatchProviderAsync(HostProviderPriority priority)
+ async Task MatchProviderAsync(HostProviderPriority priority, bool probe)
{
if (_hostProviders.TryGetValue(priority, out ICollection providers))
{
@@ -174,7 +178,7 @@ async Task MatchProviderAsync(HostProviderPriority priority)
// Try matching using the HTTP response from a query to the remote URL (expensive).
// The user may have disabled this feature with a zero or negative timeout for performance reasons.
// We only probe the remote once and reuse the same response for all providers.
- if (probeTimeout.TotalMilliseconds > 0)
+ if (probe && probeTimeout.TotalMilliseconds > 0)
{
if (probeResponse is null)
{
@@ -215,9 +219,9 @@ async Task MatchProviderAsync(HostProviderPriority priority)
}
// Match providers starting with the highest priority
- IHostProvider match = await MatchProviderAsync(HostProviderPriority.High) ??
- await MatchProviderAsync(HostProviderPriority.Normal) ??
- await MatchProviderAsync(HostProviderPriority.Low) ??
+ IHostProvider match = await MatchProviderAsync(HostProviderPriority.High, canProbeUri) ??
+ await MatchProviderAsync(HostProviderPriority.Normal, canProbeUri) ??
+ await MatchProviderAsync(HostProviderPriority.Low, canProbeUri) ??
throw new Exception("No host provider available to service this request.");
// If we ended up making a network call then set the host provider explicitly
diff --git a/src/shared/Core/InternalsVisibleTo.cs b/src/shared/Core/InternalsVisibleTo.cs
index 3b65c60e6..e0a0c782f 100644
--- a/src/shared/Core/InternalsVisibleTo.cs
+++ b/src/shared/Core/InternalsVisibleTo.cs
@@ -2,3 +2,4 @@
[assembly: InternalsVisibleTo("Core.Tests")]
[assembly: InternalsVisibleTo("GitHub.Tests")]
+[assembly: InternalsVisibleTo("Atlassian.Bitbucket.Tests")]
diff --git a/src/shared/Core/Interop/InteropUtils.cs b/src/shared/Core/Interop/InteropUtils.cs
index 9a72e243e..2964fa276 100644
--- a/src/shared/Core/Interop/InteropUtils.cs
+++ b/src/shared/Core/Interop/InteropUtils.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Runtime.InteropServices;
namespace GitCredentialManager.Interop
@@ -11,5 +12,21 @@ public static byte[] ToByteArray(IntPtr ptr, long count)
Marshal.Copy(ptr, destination, 0, destination.Length);
return destination;
}
+
+ public static bool AreEqual(byte[] bytes, IntPtr ptr, uint length)
+ {
+ if (bytes.Length == 0 && (ptr == IntPtr.Zero || length == 0))
+ {
+ return true;
+ }
+
+ if (bytes.Length != length)
+ {
+ return false;
+ }
+
+ byte[] ptrBytes = ToByteArray(ptr, length);
+ return bytes.SequenceEqual(ptrBytes);
+ }
}
}
diff --git a/src/shared/Core/Interop/Linux/SecretServiceCollection.cs b/src/shared/Core/Interop/Linux/SecretServiceCollection.cs
index ab3063b38..e6a61286a 100644
--- a/src/shared/Core/Interop/Linux/SecretServiceCollection.cs
+++ b/src/shared/Core/Interop/Linux/SecretServiceCollection.cs
@@ -114,6 +114,16 @@ public unsafe void AddOrUpdate(string service, string account, string secret)
SecretValue* secretValue = null;
GError *error = null;
+ // If there is an existing credential that matches the same account and password
+ // then don't bother writing out anything because they're the same!
+ ICredential existingCred = Get(service, account);
+ if (existingCred != null &&
+ StringComparer.Ordinal.Equals(existingCred.Account, account) &&
+ StringComparer.Ordinal.Equals(existingCred.Password, secret))
+ {
+ return;
+ }
+
try
{
SecretService* secService = GetSecretService();
diff --git a/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs b/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs
new file mode 100644
index 000000000..a29f1f4df
--- /dev/null
+++ b/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using GitCredentialManager.Interop.Posix;
+
+namespace GitCredentialManager.Interop.MacOS
+{
+ public class MacOSEnvironment : PosixEnvironment
+ {
+ private ICollection _pathsToIgnore;
+
+ public MacOSEnvironment(IFileSystem fileSystem)
+ : base(fileSystem) { }
+
+ internal MacOSEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables)
+ : base(fileSystem)
+ {
+ EnsureArgument.NotNull(variables, nameof(variables));
+ Variables = variables;
+ }
+
+ public override bool TryLocateExecutable(string program, out string path)
+ {
+ if (_pathsToIgnore is null)
+ {
+ _pathsToIgnore = new List();
+ if (Variables.TryGetValue("HOMEBREW_PREFIX", out string homebrewPrefix))
+ {
+ string homebrewGit = Path.Combine(homebrewPrefix, "Homebrew/Library/Homebrew/shims/shared/git");
+ _pathsToIgnore.Add(homebrewGit);
+ }
+ }
+ return TryLocateExecutable(program, _pathsToIgnore, out path);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs
index 23d992b17..61df622b9 100644
--- a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs
+++ b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs
@@ -103,7 +103,6 @@ public void AddOrUpdate(string service, string account, string secret)
string serviceName = CreateServiceName(service);
-
uint serviceNameLength = (uint) serviceName.Length;
uint accountLength = (uint) (account?.Length ?? 0);
@@ -112,12 +111,12 @@ public void AddOrUpdate(string service, string account, string secret)
// Check if an entry already exists in the keychain
int findResult = SecKeychainFindGenericPassword(
IntPtr.Zero, serviceNameLength, serviceName, accountLength, account,
- out uint _, out passwordData, out itemRef);
+ out uint passwordDataLength, out passwordData, out itemRef);
switch (findResult)
{
- // Update existing entry
- case OK:
+ // Update existing entry only if the password/secret is different
+ case OK when !InteropUtils.AreEqual(secretBytes, passwordData, passwordDataLength):
ThrowIfError(
SecKeychainItemModifyAttributesAndData(itemRef, IntPtr.Zero, (uint) secretBytes.Length, secretBytes),
"Could not update existing item"
diff --git a/src/shared/Core/Interop/Posix/PosixEnvironment.cs b/src/shared/Core/Interop/Posix/PosixEnvironment.cs
index 8c4e4b4f1..da2e76c72 100644
--- a/src/shared/Core/Interop/Posix/PosixEnvironment.cs
+++ b/src/shared/Core/Interop/Posix/PosixEnvironment.cs
@@ -1,15 +1,18 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
namespace GitCredentialManager.Interop.Posix
{
public class PosixEnvironment : EnvironmentBase
{
- public PosixEnvironment(IFileSystem fileSystem) : base(fileSystem)
+ public PosixEnvironment(IFileSystem fileSystem)
+ : this(fileSystem, GetCurrentVariables()) { }
+
+ internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables)
+ : base(fileSystem)
{
- Variables = GetCurrentVariables();
+ EnsureArgument.NotNull(variables, nameof(variables));
+ Variables = variables;
}
#region EnvironmentBase
@@ -29,40 +32,6 @@ protected override string[] SplitPathVariable(string value)
return value.Split(':');
}
- public override bool TryLocateExecutable(string program, out string path)
- {
- // The "which" utility scans over the PATH and does not include the current working directory
- // (unlike the equivalent "where.exe" on Windows), which is exactly what we want. Let's use it.
- const string whichPath = "/usr/bin/which";
- var psi = new ProcessStartInfo(whichPath, program)
- {
- UseShellExecute = false,
- RedirectStandardOutput = true
- };
-
- using (var where = new Process {StartInfo = psi})
- {
- where.Start();
- where.WaitForExit();
-
- switch (where.ExitCode)
- {
- case 0: // found
- string stdout = where.StandardOutput.ReadToEnd();
- string[] results = stdout.Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
- path = results.First();
- return true;
-
- case 1: // not found
- path = null;
- return false;
-
- default:
- throw new Exception($"Unknown error locating '{program}' using {whichPath}. Exit code: {where.ExitCode}.");
- }
- }
- }
-
#endregion
private static IReadOnlyDictionary GetCurrentVariables()
diff --git a/src/shared/Core/Interop/Windows/Native/Advapi32.cs b/src/shared/Core/Interop/Windows/Native/Advapi32.cs
index 924eb4349..14d4a3303 100644
--- a/src/shared/Core/Interop/Windows/Native/Advapi32.cs
+++ b/src/shared/Core/Interop/Windows/Native/Advapi32.cs
@@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
+using System.Text;
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
namespace GitCredentialManager.Interop.Windows.Native
@@ -93,5 +94,16 @@ public struct Win32Credential
public string TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)]
public string UserName;
+
+ public string GetCredentialBlobAsString()
+ {
+ if (CredentialBlobSize != 0 && CredentialBlob != IntPtr.Zero)
+ {
+ byte[] passwordBytes = InteropUtils.ToByteArray(CredentialBlob, CredentialBlobSize);
+ return Encoding.Unicode.GetString(passwordBytes);
+ }
+
+ return null;
+ }
}
}
diff --git a/src/shared/Core/Interop/Windows/WindowsCredentialManager.cs b/src/shared/Core/Interop/Windows/WindowsCredentialManager.cs
index c9c3dd605..2645ef293 100644
--- a/src/shared/Core/Interop/Windows/WindowsCredentialManager.cs
+++ b/src/shared/Core/Interop/Windows/WindowsCredentialManager.cs
@@ -9,12 +9,10 @@ namespace GitCredentialManager.Interop.Windows
{
public class WindowsCredentialManager : ICredentialStore
{
- private const string TargetNameLegacyGenericPrefix = "LegacyGeneric:target=";
+ internal const string TargetNameLegacyGenericPrefix = "LegacyGeneric:target=";
private readonly string _namespace;
- #region Constructors
-
///
/// Open the Windows Credential Manager vault for the current user.
///
@@ -26,10 +24,6 @@ public WindowsCredentialManager(string @namespace = null)
_namespace = @namespace;
}
- #endregion
-
- #region ICredentialStore
-
public ICredential Get(string service, string account)
{
return Enumerate(service, account).FirstOrDefault();
@@ -66,6 +60,15 @@ public void AddOrUpdate(string service, string account, string secret)
// Create new entry with the account in the target name
targetName = CreateTargetName(service, account);
}
+ else
+ {
+ // No need to write out credential if the account and secret/password are the same
+ string existingSecret = existingCred.GetCredentialBlobAsString();
+ if (StringComparer.Ordinal.Equals(existingSecret, secret))
+ {
+ return;
+ }
+ }
}
byte[] secretBytes = Encoding.Unicode.GetBytes(secret);
@@ -129,8 +132,6 @@ public bool Remove(string service, string account)
return false;
}
- #endregion
-
///
/// Check if we can persist credentials to for the current process and logon session.
///
@@ -205,14 +206,7 @@ private IEnumerable Enumerate(string service, string account)
private WindowsCredential CreateCredentialFromStructure(Win32Credential credential)
{
- string password = null;
- if (credential.CredentialBlobSize != 0 && credential.CredentialBlob != IntPtr.Zero)
- {
- byte[] passwordBytes = InteropUtils.ToByteArray(
- credential.CredentialBlob,
- credential.CredentialBlobSize);
- password = Encoding.Unicode.GetString(passwordBytes);
- }
+ string password = credential.GetCredentialBlobAsString();
// Recover the target name we gave from the internal (raw) target name
string targetName = credential.TargetName.TrimUntilIndexOf(TargetNameLegacyGenericPrefix);
@@ -266,7 +260,7 @@ private WindowsCredential CreateCredentialFromStructure(Win32Credential credenti
return url;
}
- private bool IsMatch(string service, string account, Win32Credential credential)
+ internal /* for testing */ bool IsMatch(string service, string account, Win32Credential credential)
{
// Match against the username first
if (!string.IsNullOrWhiteSpace(account) &&
@@ -292,6 +286,12 @@ private bool IsMatch(string service, string account, Win32Credential credential)
if (Uri.TryCreate(service, UriKind.Absolute, out Uri serviceUri) &&
Uri.TryCreate(targetName, UriKind.Absolute, out Uri targetUri))
{
+ // Match scheme/protocol
+ if (!StringComparer.OrdinalIgnoreCase.Equals(serviceUri.Scheme, targetUri.Scheme))
+ {
+ return false;
+ }
+
// Match host name
if (!StringComparer.OrdinalIgnoreCase.Equals(serviceUri.Host, targetUri.Host))
{
@@ -299,7 +299,7 @@ private bool IsMatch(string service, string account, Win32Credential credential)
}
// Match port number
- if (!serviceUri.IsDefaultPort && serviceUri.Port == targetUri.Port)
+ if (serviceUri.Port != targetUri.Port)
{
return false;
}
@@ -319,7 +319,7 @@ private bool IsMatch(string service, string account, Win32Credential credential)
return false;
}
- private string CreateTargetName(string service, string account)
+ internal /* for testing */ string CreateTargetName(string service, string account)
{
var serviceUri = new Uri(service, UriKind.Absolute);
var sb = new StringBuilder();
@@ -345,9 +345,15 @@ private string CreateTargetName(string service, string account)
sb.Append(serviceUri.Host);
}
- if (!string.IsNullOrWhiteSpace(serviceUri.AbsolutePath.TrimEnd('/')))
+ if (!serviceUri.IsDefaultPort)
+ {
+ sb.AppendFormat(":{0}", serviceUri.Port);
+ }
+
+ string trimmedPath = serviceUri.AbsolutePath.TrimEnd('/');
+ if (!string.IsNullOrWhiteSpace(trimmedPath))
{
- sb.Append(serviceUri.AbsolutePath);
+ sb.Append(trimmedPath);
}
return sb.ToString();
diff --git a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs
index 918579d10..c438582c9 100644
--- a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs
+++ b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs
@@ -67,28 +67,6 @@ public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVa
}
}
- public override bool TryLocateExecutable(string program, out string path)
- {
- // Don't use "where.exe" on Windows as this includes the current working directory
- // and we don't want to enumerate this location; only the PATH.
- if (Variables.TryGetValue("PATH", out string pathValue))
- {
- string[] paths = SplitPathVariable(pathValue);
- foreach (var basePath in paths)
- {
- string candidatePath = Path.Combine(basePath, program);
- if (FileSystem.FileExists(candidatePath))
- {
- path = candidatePath;
- return true;
- }
- }
- }
-
- path = null;
- return false;
- }
-
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
diff --git a/src/shared/Core/PlaintextCredentialStore.cs b/src/shared/Core/PlaintextCredentialStore.cs
index b82c6eb63..0ddd21357 100644
--- a/src/shared/Core/PlaintextCredentialStore.cs
+++ b/src/shared/Core/PlaintextCredentialStore.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
namespace GitCredentialManager
@@ -22,37 +23,9 @@ public PlaintextCredentialStore(IFileSystem fileSystem, string storeRoot, string
protected string Namespace { get; }
protected virtual string CredentialFileExtension => ".credential";
- #region ICredentialStore
-
public ICredential Get(string service, string account)
{
- string serviceSlug = CreateServiceSlug(service);
- string searchPath = Path.Combine(StoreRoot, serviceSlug);
- bool anyAccount = string.IsNullOrWhiteSpace(account);
-
- if (!FileSystem.DirectoryExists(searchPath))
- {
- return null;
- }
-
- IEnumerable allFiles = FileSystem.EnumerateFiles(searchPath, $"*{CredentialFileExtension}");
-
- foreach (string fullPath in allFiles)
- {
- string accountFile = Path.GetFileNameWithoutExtension(fullPath);
- if (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, accountFile))
- {
- // Validate the credential metadata also matches our search
- if (TryDeserializeCredential(fullPath, out FileCredential credential) &&
- StringComparer.OrdinalIgnoreCase.Equals(service, credential.Service) &&
- (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, credential.Account)))
- {
- return credential;
- }
- }
- }
-
- return null;
+ return Enumerate(service, account).FirstOrDefault();
}
public void AddOrUpdate(string service, string account, string secret)
@@ -60,6 +33,16 @@ public void AddOrUpdate(string service, string account, string secret)
// Ensure the store root exists and permissions are set
EnsureStoreRoot();
+ FileCredential existingCredential = Enumerate(service, account).FirstOrDefault();
+
+ // No need to update existing credential if nothing has changed
+ if (existingCredential != null &&
+ StringComparer.Ordinal.Equals(account, existingCredential.Account) &&
+ StringComparer.Ordinal.Equals(secret, existingCredential.Password))
+ {
+ return;
+ }
+
string serviceSlug = CreateServiceSlug(service);
string servicePath = Path.Combine(StoreRoot, serviceSlug);
@@ -75,39 +58,16 @@ public void AddOrUpdate(string service, string account, string secret)
public bool Remove(string service, string account)
{
- string serviceSlug = CreateServiceSlug(service);
- string searchPath = Path.Combine(StoreRoot, serviceSlug);
- bool anyAccount = string.IsNullOrWhiteSpace(account);
-
- if (!FileSystem.DirectoryExists(searchPath))
- {
- return false;
- }
-
- IEnumerable allFiles = FileSystem.EnumerateFiles(searchPath, $"*{CredentialFileExtension}");
-
- foreach (string fullPath in allFiles)
+ foreach (FileCredential credential in Enumerate(service, account))
{
- string accountFile = Path.GetFileNameWithoutExtension(fullPath);
- if (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, accountFile))
- {
- // Validate the credential metadata also matches our search
- if (TryDeserializeCredential(fullPath, out FileCredential credential) &&
- StringComparer.OrdinalIgnoreCase.Equals(service, credential.Service) &&
- (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, credential.Account)))
- {
- // Delete the credential file
- FileSystem.DeleteFile(fullPath);
- return true;
- }
- }
+ // Only delete the first match
+ FileSystem.DeleteFile(credential.FullPath);
+ return true;
}
return false;
}
- #endregion
-
protected virtual bool TryDeserializeCredential(string path, out FileCredential credential)
{
string text;
@@ -162,6 +122,35 @@ protected virtual void SerializeCredential(FileCredential credential)
}
}
+ private IEnumerable Enumerate(string service, string account)
+ {
+ string serviceSlug = CreateServiceSlug(service);
+ string searchPath = Path.Combine(StoreRoot, serviceSlug);
+ bool anyAccount = string.IsNullOrWhiteSpace(account);
+
+ if (!FileSystem.DirectoryExists(searchPath))
+ {
+ yield break;
+ }
+
+ IEnumerable allFiles = FileSystem.EnumerateFiles(searchPath, $"*{CredentialFileExtension}");
+
+ foreach (string fullPath in allFiles)
+ {
+ string accountFile = Path.GetFileNameWithoutExtension(fullPath);
+ if (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, accountFile))
+ {
+ // Validate the credential metadata also matches our search
+ if (TryDeserializeCredential(fullPath, out FileCredential credential) &&
+ StringComparer.OrdinalIgnoreCase.Equals(service, credential.Service) &&
+ (anyAccount || StringComparer.OrdinalIgnoreCase.Equals(account, credential.Account)))
+ {
+ yield return credential;
+ }
+ }
+ }
+ }
+
///
/// Ensure the store root directory exists. If it does not, create a new directory with
/// permissions that only permit the owner to read/write/execute. Permissions on an existing
diff --git a/src/shared/Core/UriExtensions.cs b/src/shared/Core/UriExtensions.cs
index 939a45029..074b1e84f 100644
--- a/src/shared/Core/UriExtensions.cs
+++ b/src/shared/Core/UriExtensions.cs
@@ -102,6 +102,16 @@ public static IEnumerable GetGitConfigurationScopes(this Uri uri)
}
}
+ // Check whether the URL only contains hostname.
+ // This usually means the host is on your local network.
+ if (!string.IsNullOrWhiteSpace(host) &&
+ !host.Contains("."))
+ {
+ yield return $"{schemeAndDelim}{host}";
+ // If we have reached this point, there are no more subdomains to unfold, so exit early.
+ yield break;
+ }
+
// Unfold the host by sub-domain, left-to-right
while (!string.IsNullOrWhiteSpace(host))
{
diff --git a/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj b/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj
index 10f598f61..441ee82bc 100644
--- a/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj
+++ b/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj
@@ -4,7 +4,7 @@
Exe
net6.0
net472;net6.0
- win-x86;osx-x64;linux-x64
+ win-x86;osx-x64;linux-x64;osx-arm64
x86
git-credential-manager-core
GitCredentialManager
diff --git a/src/shared/GitHub.Tests/GitHub.Tests.csproj b/src/shared/GitHub.Tests/GitHub.Tests.csproj
index 3caa998ed..13408d24f 100644
--- a/src/shared/GitHub.Tests/GitHub.Tests.csproj
+++ b/src/shared/GitHub.Tests/GitHub.Tests.csproj
@@ -12,7 +12,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/src/shared/GitHub.UI.Avalonia/GitHub.UI.Avalonia.csproj b/src/shared/GitHub.UI.Avalonia/GitHub.UI.Avalonia.csproj
index d1e040118..828677fcc 100644
--- a/src/shared/GitHub.UI.Avalonia/GitHub.UI.Avalonia.csproj
+++ b/src/shared/GitHub.UI.Avalonia/GitHub.UI.Avalonia.csproj
@@ -3,7 +3,7 @@
WinExe
net6.0
- osx-x64;linux-x64
+ osx-x64;linux-x64;osx-arm64
GitHub.UI
GitHub.UI
diff --git a/src/shared/GitHub/GitHubConstants.cs b/src/shared/GitHub/GitHubConstants.cs
index a564e290d..b6f222b48 100644
--- a/src/shared/GitHub/GitHubConstants.cs
+++ b/src/shared/GitHub/GitHubConstants.cs
@@ -9,6 +9,7 @@ public static class GitHubConstants
public const string DefaultAuthenticationHelper = "GitHub.UI";
+ // https://github.com/settings/connections/applications/0120e057bd645470c1ed
public const string OAuthClientId = "0120e057bd645470c1ed";
// [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="OAuth2 public client application 'secrets' are required and permitted to be public")]
diff --git a/src/shared/GitLab.Tests/GitLab.Tests.csproj b/src/shared/GitLab.Tests/GitLab.Tests.csproj
index 341b7bc51..3212501c7 100644
--- a/src/shared/GitLab.Tests/GitLab.Tests.csproj
+++ b/src/shared/GitLab.Tests/GitLab.Tests.csproj
@@ -12,7 +12,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/src/shared/GitLab.UI.Avalonia/GitLab.UI.Avalonia.csproj b/src/shared/GitLab.UI.Avalonia/GitLab.UI.Avalonia.csproj
index 0a64307ee..26927c272 100644
--- a/src/shared/GitLab.UI.Avalonia/GitLab.UI.Avalonia.csproj
+++ b/src/shared/GitLab.UI.Avalonia/GitLab.UI.Avalonia.csproj
@@ -3,7 +3,7 @@
WinExe
net6.0
- osx-x64;linux-x64
+ osx-x64;linux-x64;osx-arm64
GitLab.UI
GitLab.UI
diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs
index 5aca828c7..2836f41c6 100644
--- a/src/shared/GitLab/GitLabHostProvider.cs
+++ b/src/shared/GitLab/GitLabHostProvider.cs
@@ -13,6 +13,7 @@ public class GitLabHostProvider : HostProvider
private static readonly string[] GitLabOAuthScopes =
{
"write_repository",
+ "read_repository"
};
private readonly IGitLabAuthentication _gitLabAuth;
diff --git a/src/shared/Microsoft.AzureRepos.Tests/Microsoft.AzureRepos.Tests.csproj b/src/shared/Microsoft.AzureRepos.Tests/Microsoft.AzureRepos.Tests.csproj
index cb620ba3b..f10e3ddda 100644
--- a/src/shared/Microsoft.AzureRepos.Tests/Microsoft.AzureRepos.Tests.csproj
+++ b/src/shared/Microsoft.AzureRepos.Tests/Microsoft.AzureRepos.Tests.csproj
@@ -12,7 +12,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs
index 48809e4b6..55fb408f4 100644
--- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs
+++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs
@@ -389,7 +389,7 @@ private static string GetAccountNameForCredentialQuery(InputArguments input)
/// True if Personal Access Tokens should be used, false otherwise.
private bool UsePersonalAccessTokens()
{
- // Default to using PATs whilst the Azure AT functionality is being tested
+ // Default to using PATs
const bool defaultValue = true;
if (_context.Settings.TryGetSetting(
@@ -410,7 +410,7 @@ private bool UsePersonalAccessTokens()
default:
_context.Streams.Error.WriteLine(
- $"warning: unknown Azure Repos credential type '{valueStr}' - using default option");
+ $"warning: unknown Azure Repos credential type '{valueStr}' - using PATs");
return defaultValue;
}
}
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Commands/OAuthCommandImpl.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Commands/OAuthCommandImpl.cs
deleted file mode 100644
index 4917d98e8..000000000
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Commands/OAuthCommandImpl.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 OAuthCommandImpl : OAuthCommand
- {
- public OAuthCommandImpl(ICommandContext context) : base(context) { }
-
- protected override Task ShowAsync(OAuthViewModel viewModel, CancellationToken ct)
- {
- return Gui.ShowDialogWindow(viewModel, () => new OAuthView(), GetParentHandle());
- }
- }
-}
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml b/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml
index b87ca5c9f..35b74cc50 100644
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml
+++ b/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml
@@ -6,8 +6,30 @@
mc:Ignorable="d"
Title="Bitbucket Authentication Dialog Tester"
Height="240" Width="420" ResizeMode="NoResize">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml.cs
index cc6c3e3a0..3873b2e55 100644
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml.cs
+++ b/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml.cs
@@ -1,3 +1,4 @@
+using System;
using System.Windows;
using Atlassian.Bitbucket.UI.ViewModels;
using Atlassian.Bitbucket.UI.Views;
@@ -19,17 +20,17 @@ private void ShowCredentials(object sender, RoutedEventArgs e)
{
var vm = new CredentialsViewModel(_environment)
{
- ShowOAuth = true
+ ShowOAuth = showOAuth.IsChecked ?? false,
+ ShowBasic = showBasic.IsChecked ?? false,
+ UserName = username.Text
};
- var view = new CredentialsView();
- var window = new DialogWindow(view) { DataContext = vm };
- window.ShowDialog();
- }
- private void ShowOAuth(object sender, RoutedEventArgs e)
- {
- var vm = new OAuthViewModel(_environment);
- var view = new OAuthView();
+ if (Uri.TryCreate(url.Text, UriKind.Absolute, out Uri uri))
+ {
+ vm.Url = uri;
+ }
+
+ var view = new CredentialsView();
var window = new DialogWindow(view) { DataContext = vm };
window.ShowDialog();
}
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs
index 4b65b7a30..8f4b3cf02 100644
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs
+++ b/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs
@@ -22,7 +22,6 @@ public static async Task Main(string[] args)
}
app.RegisterCommand(new CredentialsCommandImpl(context));
- app.RegisterCommand(new OAuthCommandImpl(context));
int exitCode = app.RunAsync(args)
.ConfigureAwait(false)
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml b/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml
index 6c36ee173..eadb912a7 100644
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml
+++ b/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml
@@ -15,6 +15,7 @@
+
@@ -34,38 +35,87 @@
-
+
+
-
-
-
-
-
-
- Sign in with OAuth
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml.cs
index 8a3421342..3839517c7 100644
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml.cs
+++ b/src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml.cs
@@ -1,4 +1,6 @@
+using System.Windows;
using System.Windows.Controls;
+using System.Windows.Input;
using Atlassian.Bitbucket.UI.ViewModels;
using GitCredentialManager.UI.Controls;
@@ -11,6 +13,22 @@ public CredentialsView()
InitializeComponent();
}
+ // Set focus on a UIElement the next time it becomes visible
+ private static void OnIsVisibleChangedOneTime(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (sender is UIElement element)
+ {
+ // Unsubscribe to prevent re-triggering
+ element.IsVisibleChanged -= OnIsVisibleChangedOneTime;
+
+ // Set logical focus
+ element.Focus();
+
+ // Set keyboard focus
+ Keyboard.Focus(element);
+ }
+ }
+
public void SetFocus()
{
if (!(DataContext is CredentialsViewModel vm))
@@ -18,13 +36,37 @@ public void SetFocus()
return;
}
- if (string.IsNullOrWhiteSpace(vm.UserName))
+ //
+ // Select the best available authentication mechanism that is visible
+ // and make the textbox/button focused when it next made visible.
+ //
+ // In WPF the controls in a TabItem are not part of the visual tree until
+ // the TabControl has been switched to that tab, so we must delay focusing
+ // on the textbox/button until it becomes visible.
+ //
+ // This means as the user first moves through the tabs, the "correct" control
+ // will be given focus in that tab.
+ //
+ void SetFocusOnNextVisible(UIElement element)
+ {
+ element.IsVisibleChanged += OnIsVisibleChangedOneTime;
+ }
+
+ // Set up focus events on all controls
+ SetFocusOnNextVisible(
+ string.IsNullOrWhiteSpace(vm.UserName)
+ ? userNameTextBox
+ : passwordTextBox);
+ SetFocusOnNextVisible(oauthButton);
+
+ // Switch to the preferred tab
+ if (vm.ShowOAuth)
{
- userNameTextBox.Focus();
+ tabControl.SelectedIndex = 0;
}
- else
+ else if (vm.ShowBasic)
{
- passwordTextBox.Focus();
+ tabControl.SelectedIndex = 1;
}
}
}
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/OAuthView.xaml b/src/windows/Atlassian.Bitbucket.UI.Windows/Views/OAuthView.xaml
deleted file mode 100644
index 5230e0d91..000000000
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/OAuthView.xaml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Can't log in?
-
-
-
-
- Sign up for an account
-
-
-
-
-
-
-
-
-
-
-
-
- Learn more about 2-Factor Authentication
-
-
-
-
-
-
diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/OAuthView.xaml.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Views/OAuthView.xaml.cs
deleted file mode 100644
index f7132135a..000000000
--- a/src/windows/Atlassian.Bitbucket.UI.Windows/Views/OAuthView.xaml.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Windows.Controls;
-using GitCredentialManager.UI.Controls;
-
-namespace Atlassian.Bitbucket.UI.Views
-{
- public partial class OAuthView : UserControl, IFocusable
- {
- public OAuthView()
- {
- InitializeComponent();
- }
-
- public void SetFocus()
- {
- okButton.Focus();
- }
- }
-}
diff --git a/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs b/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs
index d83759779..09f5550b6 100644
--- a/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs
+++ b/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs
@@ -18,4 +18,18 @@ public virtual object ConvertBack(object value, Type targetType, object paramete
return Binding.DoNothing;
}
}
+
+ [ValueConversion(typeof(string), typeof(Visibility))]
+ public class NonNullToVisibleConverter : IValueConverter
+ {
+ public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return ConverterHelper.GetConditionalVisibility(value != null, parameter);
+ }
+
+ public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Binding.DoNothing;
+ }
+ }
}
diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj
index 74830d703..ce9e4b9f5 100644
--- a/src/windows/Installer.Windows/Installer.Windows.csproj
+++ b/src/windows/Installer.Windows/Installer.Windows.csproj
@@ -5,51 +5,29 @@
net472
false
- $(PlatformOutPath)Payload.Windows\bin\$(Configuration)\net472\win-x86
false
+ $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\win-x86
-
-
-
-
-
- all
-
-
-
-
-
- Microsoft400
- false
-
-
-
-
-
-
-
-
-
-
"$(NuGetPackageRoot)Tools.InnoSetup\6.0.5\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=system "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)"
"$(NuGetPackageRoot)Tools.InnoSetup\6.0.5\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=user "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)"
+
+
+
diff --git a/src/windows/Installer.Windows/Setup.iss b/src/windows/Installer.Windows/Setup.iss
index 972f0ebb5..8dff30c84 100644
--- a/src/windows/Installer.Windows/Setup.iss
+++ b/src/windows/Installer.Windows/Setup.iss
@@ -18,12 +18,12 @@
#if InstallTarget == "user"
#define GcmAppId "{{aa76d31d-432c-42ee-844c-bc0bc801cef3}}"
#define GcmLongName "Git Credential Manager (User)"
- #define GcmSetupExe "gcmcoreuser"
+ #define GcmSetupExe "gcmuser"
#define GcmConfigureCmdArgs ""
#elif InstallTarget == "system"
#define GcmAppId "{{fdfae50a-1bc1-4ead-9228-1e1c275e8d12}}"
#define GcmLongName "Git Credential Manager"
- #define GcmSetupExe "gcmcore"
+ #define GcmSetupExe "gcm"
#define GcmConfigureCmdArgs "--system"
#else
#error Installer target property 'InstallTarget' must be 'user' or 'system'
diff --git a/src/windows/Installer.Windows/layout.ps1 b/src/windows/Installer.Windows/layout.ps1
new file mode 100644
index 000000000..6b43a2cb9
--- /dev/null
+++ b/src/windows/Installer.Windows/layout.ps1
@@ -0,0 +1,67 @@
+# Inputs
+param ([Parameter(Mandatory)] $CONFIGURATION, [Parameter(Mandatory)] $OUTPUT, $SYMBOLOUTPUT)
+
+Write-Output "Output: $OUTPUT"
+
+# Directories
+$THISDIR = $pwd.path
+$ROOT = (Get-Item $THISDIR).parent.parent.parent.FullName
+$SRC = "$ROOT/src"
+$GCM_SRC = "$SRC/shared/Git-Credential-Manager"
+$BITBUCKET_UI_SRC = "$SRC/windows/Atlassian.Bitbucket.UI.Windows"
+$GITHUB_UI_SRC = "$SRC/windows/GitHub.UI.Windows"
+$GITLAB_UI_SRC = "$SRC/windows/GitLab.UI.Windows"
+
+# Perform pre-execution checks
+$PAYLOAD = "$OUTPUT"
+if ($SYMBOLOUTPUT)
+{
+ $SYMBOLS = "$SYMBOLOUTPUT"
+} else {
+ $SYMBOLS = "$PAYLOAD.sym"
+}
+
+# Clean up any old payload and symbols directories
+if (Test-Path -Path $PAYLOAD)
+{
+ Write-Output "Cleaning old payload directory '$PAYLOAD'..."
+ Remove-Item -Recurse "$PAYLOAD" -Force
+}
+
+if (Test-Path -Path $SYMBOLS)
+{
+ Write-Output "Cleaning old symbols directory '$SYMBOLS'..."
+ Remove-Item -Recurse "$SYMBOLS" -Force
+}
+
+# Ensure payload and symbol directories exist
+mkdir -p "$PAYLOAD","$SYMBOLS"
+
+# Publish core application executables
+Write-Output "Publishing core application..."
+dotnet publish "$GCM_SRC" `
+ --framework net472 `
+ --configuration "$CONFIGURATION" `
+ --runtime win-x86 `
+ --output "$PAYLOAD"
+
+Write-Output "Publishing Bitbucket UI helper..."
+dotnet publish "$BITBUCKET_UI_SRC" `
+ --configuration "$CONFIGURATION" `
+ --output "$PAYLOAD"
+
+Write-Output "Publishing GitHub UI helper..."
+dotnet publish "$GITHUB_UI_SRC" `
+ --configuration "$CONFIGURATION" `
+ --output "$PAYLOAD"
+
+Write-Output "Publishing GitLab UI helper..."
+dotnet publish "$GITLAB_UI_SRC" `
+ --configuration "$CONFIGURATION" `
+ --output "$PAYLOAD"
+
+# Collect symbols
+Write-Output "Collecting managed symbols..."
+Move-Item -Path "$PAYLOAD/*.pdb" -Destination "$SYMBOLS"
+
+Write-Output "Layout complete."
\ No newline at end of file
diff --git a/src/windows/Payload.Windows/Payload.Windows.csproj b/src/windows/Payload.Windows/Payload.Windows.csproj
deleted file mode 100644
index a147461ad..000000000
--- a/src/windows/Payload.Windows/Payload.Windows.csproj
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
- net472
- win-x86
- false
-
-
-
-
-
-
-
-
-
-
-
- all
-
-
-
-
-
- Microsoft400
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-