-1. In the Team Explorer pane, click the **Manage Connectios** toolbar icon.
+1. In the Team Explorer pane, click the **Manage Connections** toolbar icon.
1. Click the **Connect** link in the GitHub section.
diff --git a/docs/using/cloning-a-repository-to-visual-studio.md b/docs/using/cloning-a-repository-to-visual-studio.md
index 27e5cb0534..49765274d7 100644
--- a/docs/using/cloning-a-repository-to-visual-studio.md
+++ b/docs/using/cloning-a-repository-to-visual-studio.md
@@ -2,7 +2,7 @@
After you provide your GitHub or GitHub Enterprise credentials to GitHub for Visual Studio, the extension automatically detects the personal, collaborator and organization repositories you have access to on your account.
-## Opening the clone dialog
+## Opening the clone dialog
### From **Team Explorer**
@@ -17,7 +17,12 @@ Next to the account you want to clone from, click **Clone**.
### From the **Start Page**
-Using Visual Studio 2017, click the `GitHub` button on the `Start Page` to open the clone dialog.
+Using Visual Studio 2017, click the `GitHub` button on the `Start Page` to open the clone dialog.
+
+
+### From the **Start Window**
+
+Using Visual Studio 2019, on the `Start Window` select `Clone or check out code` and then click the `GitHub` button to open the clone dialog.
### From the **File** menu
@@ -26,14 +31,19 @@ Go to `File > Open > Open From GitHub...`
## Clone repositories
-1. In the list of repositories, scroll until you find the repository you'd like to clone. You can also filter the repository results by using the *Filter* text box.
+1. In the list of repositories, scroll until you find the repository you'd like to clone.
-
+You can also filter the repository results by using the *Filter* text box.
-In addition to using the list of personal, collaborator and organization repositories, you can use the URL tab to clone a public repository by its URL or using the repository owner and name.
+In addition to using the list of personal, collaborator and organization repositories, you can enter a repository URL to clone a public repository.
-
+
2. If desired, change the local path that the repository will be cloned into, or leave the default as-is.
3. Once a repository is selected and the path is set, Click **Clone**.
4. In Team Explorer, under the list of solutions, double-click on a solution to open it in Visual Studio.
+
+## Open repositories
+For any repository that you select from the list or provide a URL for that you already have cloned locally, the **Open** button becomes enabled and a message shows that you have already cloned the repository to that location.
+
+
diff --git a/docs/using/images/clone-dialog.png b/docs/using/images/clone-dialog.png
deleted file mode 100644
index 43d44666ee..0000000000
Binary files a/docs/using/images/clone-dialog.png and /dev/null differ
diff --git a/docs/using/images/clone-url-dialog.png b/docs/using/images/clone-url-dialog.png
deleted file mode 100644
index 99c490e978..0000000000
Binary files a/docs/using/images/clone-url-dialog.png and /dev/null differ
diff --git a/docs/using/images/open-cloned-repository.png b/docs/using/images/open-cloned-repository.png
new file mode 100644
index 0000000000..9aa0596e53
Binary files /dev/null and b/docs/using/images/open-cloned-repository.png differ
diff --git a/docs/using/images/unified-clone-dialog.png b/docs/using/images/unified-clone-dialog.png
new file mode 100644
index 0000000000..814fc5b658
Binary files /dev/null and b/docs/using/images/unified-clone-dialog.png differ
diff --git a/docs/using/images/view-conversation.png b/docs/using/images/view-conversation.png
new file mode 100644
index 0000000000..ea037cf511
Binary files /dev/null and b/docs/using/images/view-conversation.png differ
diff --git a/docs/using/publishing-an-existing-project-to-github.md b/docs/using/publishing-an-existing-project-to-github.md
index 7b296056ce..d24874b10b 100644
--- a/docs/using/publishing-an-existing-project-to-github.md
+++ b/docs/using/publishing-an-existing-project-to-github.md
@@ -10,5 +10,5 @@
5. Click the **Publish to GitHub** button.

6. Enter a name and description for the repository on GitHub.
-7. Check the **Private Repository** box if you want to upload the repository as a private repository on GitHub. You must have a [Developer, Team or Business account](https://github.com/pricing) to create private repositories.
+7. Check the **Private Repository** box if you want to upload the repository as a private repository on GitHub.
8. Click the **Publish** button.
diff --git a/docs/using/reviewing-a-pull-request-in-visual-studio.md b/docs/using/reviewing-a-pull-request-in-visual-studio.md
index 89bf3beb81..f046b15023 100644
--- a/docs/using/reviewing-a-pull-request-in-visual-studio.md
+++ b/docs/using/reviewing-a-pull-request-in-visual-studio.md
@@ -15,13 +15,19 @@ GitHub for Visual Studio provides facilities for reviewing a pull request direct
The Pull Request Details view shows the current state of the pull request, including:
- information about who created the pull request
- the source and target branch
-- a description of the pull request
+- a description of the pull request (collapsed by default)
- reviewers and the status of their review
- checks (if checks have been enabled for the repository)
- the files changed

+## Viewing conversation details
+
+Click the comment count link in the GitHub pane to open up the conversation view. The conversation view shows the Pull Request description, a history of commits, and comments made.
+
+
+
## Checking out a pull request
To check out the pull request branch, click the **Checkout [branch]** link where [branch] is the name of the branch that will be checked out.
diff --git a/global.json b/global.json
index e620a36770..a75963ff54 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
- "msbuild-sdks": {
- "MSBuild.Sdk.Extras": "1.6.52"
- }
+ "msbuild-sdks": {
+ "MSBuild.Sdk.Extras": "1.6.61"
+ }
}
\ No newline at end of file
diff --git a/install.cmd b/install.cmd
deleted file mode 100644
index 0f46241066..0000000000
--- a/install.cmd
+++ /dev/null
@@ -1,3 +0,0 @@
-@if "%1" == "" echo Please specify Debug or Release && EXIT /B
-tools\VsixUtil\vsixutil /install "build\%1\GitHub.VisualStudio.vsix"
-@echo Installed %1 build of GitHub for Visual Studio
diff --git a/lib/14.0/Microsoft.VisualStudio.Shell.ViewManager.dll b/lib/14.0/Microsoft.VisualStudio.Shell.ViewManager.dll
new file mode 100644
index 0000000000..85c116f284
Binary files /dev/null and b/lib/14.0/Microsoft.VisualStudio.Shell.ViewManager.dll differ
diff --git a/lib/16.0/Microsoft.TeamFoundation.Client.dll b/lib/16.0/Microsoft.TeamFoundation.Client.dll
index 2280436cf0..d42abde8f0 100644
Binary files a/lib/16.0/Microsoft.TeamFoundation.Client.dll and b/lib/16.0/Microsoft.TeamFoundation.Client.dll differ
diff --git a/lib/16.0/Microsoft.TeamFoundation.Common.dll b/lib/16.0/Microsoft.TeamFoundation.Common.dll
index 5f96474375..d537d70f35 100644
Binary files a/lib/16.0/Microsoft.TeamFoundation.Common.dll and b/lib/16.0/Microsoft.TeamFoundation.Common.dll differ
diff --git a/lib/16.0/Microsoft.TeamFoundation.Controls.dll b/lib/16.0/Microsoft.TeamFoundation.Controls.dll
index 598e9fe14a..bdb525c762 100644
Binary files a/lib/16.0/Microsoft.TeamFoundation.Controls.dll and b/lib/16.0/Microsoft.TeamFoundation.Controls.dll differ
diff --git a/lib/16.0/Microsoft.TeamFoundation.Git.Client.dll b/lib/16.0/Microsoft.TeamFoundation.Git.Client.dll
index 917f58f4ae..5911b7e179 100644
Binary files a/lib/16.0/Microsoft.TeamFoundation.Git.Client.dll and b/lib/16.0/Microsoft.TeamFoundation.Git.Client.dll differ
diff --git a/lib/16.0/Microsoft.TeamFoundation.Git.Controls.dll b/lib/16.0/Microsoft.TeamFoundation.Git.Controls.dll
index bda68a21df..29f24fe79e 100644
Binary files a/lib/16.0/Microsoft.TeamFoundation.Git.Controls.dll and b/lib/16.0/Microsoft.TeamFoundation.Git.Controls.dll differ
diff --git a/lib/16.0/Microsoft.TeamFoundation.Git.Provider.dll b/lib/16.0/Microsoft.TeamFoundation.Git.Provider.dll
index 0ccb6da2e7..7d28d587ad 100644
Binary files a/lib/16.0/Microsoft.TeamFoundation.Git.Provider.dll and b/lib/16.0/Microsoft.TeamFoundation.Git.Provider.dll differ
diff --git a/lib/Octokit.GraphQL.0.1.1-beta.nupkg b/lib/Octokit.GraphQL.0.1.1-beta.nupkg
deleted file mode 100644
index ac26783672..0000000000
Binary files a/lib/Octokit.GraphQL.0.1.1-beta.nupkg and /dev/null differ
diff --git a/nuget.config b/nuget.config
index 4eb3008283..e8c7639ce6 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,8 +1,7 @@
-
-
+
diff --git a/script b/script
deleted file mode 160000
index 5ed9b3d7bc..0000000000
--- a/script
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 5ed9b3d7bceee50d27a4a5838d4c0265bd35cc8e
diff --git a/scripts/Bump-Version.ps1 b/scripts/Bump-Version.ps1
deleted file mode 100644
index f9918f1892..0000000000
--- a/scripts/Bump-Version.ps1
+++ /dev/null
@@ -1,93 +0,0 @@
-<#
-.SYNOPSIS
- Bumps the version number of GitHub for Visual Studio
-.DESCRIPTION
- By default, just bumps the last component of the version number by one. An
- alternate version number can be specified on the command line.
-
- The new version number is committed to the local repository and pushed to
- GitHub.
-#>
-
-Param(
- # It would be nice to use our Validate-Version function here, but we
- # can't because this Param definition has to come before any other code in the
- # file.
- [ValidateScript({ ($_.Major -ge 0) -and ($_.Minor -ge 0) -and ($_.Build -ge 0) })]
- [System.Version]
- $NewVersion = $null
- ,
- [switch]
- $BumpMajor = $false
- ,
- [switch]
- $BumpMinor = $false
- ,
- [switch]
- $BumpPatch = $false
- ,
- [switch]
- $BumpBuild = $false
- ,
- [int]
- $BuildNumber = -1
- ,
- [switch]
- $Commit = $false
- ,
- [switch]
- $Push = $false
- ,
- [switch]
- $Force = $false
- ,
- [switch]
- $Trace = $false
-)
-
-Set-StrictMode -Version Latest
-if ($Trace) { Set-PSDebug -Trace 1 }
-
-. $PSScriptRoot\modules.ps1 | out-null
-. $scriptsDirectory\Modules\Versioning.ps1 | out-null
-. $scriptsDirectory\Modules\Vsix.ps1 | out-null
-. $scriptsDirectory\Modules\SolutionInfo.ps1 | out-null
-. $scriptsDirectory\Modules\AppVeyor.ps1 | out-null
-. $scriptsDirectory\Modules\DirectoryBuildProps.ps1 | out-null
-
-if ($NewVersion -eq $null) {
- if (!$BumpMajor -and !$BumpMinor -and !$BumpPatch -and !$BumpBuild){
- Die -1 "You need to indicate which part of the version to update via -BumpMajor/-BumpMinor/-BumpPatch/-BumpBuild flags or a custom version via -NewVersion"
- }
-}
-
-if ($Push -and !$Commit) {
- Die 1 "Cannot push a version bump without -Commit"
-}
-
-if ($Commit -and !$Force){
- Require-CleanWorkTree "bump version"
-}
-
-if (!$?) {
- exit 1
-}
-
-if ($NewVersion -eq $null) {
- $currentVersion = Read-Version
- $NewVersion = Generate-Version $currentVersion $BumpMajor $BumpMinor $BumpPatch $BumpBuild $BuildNumber
-}
-
-Write-Output "Setting version to $NewVersion"
-Write-Version $NewVersion
-
-if ($Commit) {
- Write-Output "Committing version change"
- Commit-Version $NewVersion
-
- if ($Push) {
- Write-Output "Pushing version change"
- $branch = & $git rev-parse --abbrev-ref HEAD
- Push-Changes $branch
- }
-}
diff --git a/scripts/Get-CheckedOutBranch.ps1 b/scripts/Get-CheckedOutBranch.ps1
deleted file mode 100644
index 38a961c2e3..0000000000
--- a/scripts/Get-CheckedOutBranch.ps1
+++ /dev/null
@@ -1,31 +0,0 @@
-<#
-.SYNOPSIS
- Returns the name of the working directory's currently checked-out branch
-#>
-
-Set-PSDebug -Strict
-
-$scriptsDirectory = Split-Path $MyInvocation.MyCommand.Path
-$rootDirectory = Split-Path $scriptsDirectory
-
-. $scriptsDirectory\common.ps1
-
-function Die([string]$message, [object[]]$output) {
- if ($output) {
- Write-Output $output
- $message += ". See output above."
- }
- Write-Error $message
- exit 1
-}
-
-$output = & $git symbolic-ref HEAD 2>&1 | %{ "$_" }
-if (!$? -or ($LastExitCode -ne 0)) {
- Die "Failed to determine current branch" $output
-}
-
-if (!($output -match "^refs/heads/(\S+)$")) {
- Die "Failed to determine current branch. HEAD is $output" $output
-}
-
-$matches[1]
diff --git a/scripts/Require-CleanWorkTree.ps1 b/scripts/Require-CleanWorkTree.ps1
deleted file mode 100644
index 741a05ab26..0000000000
--- a/scripts/Require-CleanWorkTree.ps1
+++ /dev/null
@@ -1,57 +0,0 @@
-<#
-.SYNOPSIS
- Ensures the working tree has no uncommitted changes
-.PARAMETER Action
- The action that requires a clean work tree. This will appear in error messages.
-.PARAMETER WarnOnly
- When true, warns rather than dies when uncommitted changes are found.
-#>
-
-[CmdletBinding()]
-Param(
- [ValidateNotNullOrEmpty()]
- [string]
- $Action
- ,
- [switch]
- $WarnOnly = $false
-)
-
-Set-StrictMode -Version Latest
-$ErrorActionPreference = "Stop"
-
-. $PSScriptRoot\modules.ps1 | out-null
-
-# Based on git-sh-setup.sh:require_clean_work_tree in git.git, but changed not
-# to ignore submodules.
-
-Push-Location $rootDirectory
-
-Run-Command -Fatal { & $git rev-parse --verify HEAD | Out-Null }
-
-& $git update-index -q --refresh
-
-& $git diff-files --quiet
-$error = ""
-if ($LastExitCode -ne 0) {
- $error = "You have unstaged changes."
-}
-
-& $git diff-index --cached --quiet HEAD --
-if ($LastExitCode -ne 0) {
- if ($error) {
- $error += " Additionally, your index contains uncommitted changes."
- } else {
- $error = "Your index contains uncommitted changes."
- }
-}
-
-if ($error) {
- if ($WarnOnly) {
- Write-Warning "$error Continuing anyway."
- } else {
- Die 2 ("Cannot $Action" + ": $error")
- }
-}
-
-Pop-Location
diff --git a/scripts/Run-CodeCoverage.ps1 b/scripts/Run-CodeCoverage.ps1
deleted file mode 100644
index 752dad5ff8..0000000000
--- a/scripts/Run-CodeCoverage.ps1
+++ /dev/null
@@ -1,83 +0,0 @@
-<#
-.SYNOPSIS
- Runs NUnit
-#>
-
-[CmdletBinding()]
-Param(
- [string]
- $Configuration
- ,
- [switch]
- $AppVeyor = $false
-)
-
-$scriptsDirectory = $PSScriptRoot
-$rootDirectory = Split-Path ($scriptsDirectory)
-. $scriptsDirectory\modules.ps1 | out-null
-
-$nunitDirectory = Join-Path $rootDirectory packages\NUnit.ConsoleRunner.3.7.0\tools
-$nunitConsoleRunner = Join-Path $nunitDirectory nunit3-console.exe
-
-$testAssemblies = @(
- "test\GitHub.Api.UnitTests\bin\$Configuration\net461\GitHub.Api.UnitTests.dll",
- "test\GitHub.App.UnitTests\bin\$Configuration\net461\GitHub.App.UnitTests.dll",
- "test\GitHub.Exports.Reactive.UnitTests\bin\$Configuration\net461\GitHub.Exports.Reactive.UnitTests.dll",
- "test\GitHub.Exports.UnitTests\bin\$Configuration\net461\GitHub.Exports.UnitTests.dll",
- "test\GitHub.Extensions.UnitTests\bin\$Configuration\net461\GitHub.Extensions.UnitTests.dll",
- "test\GitHub.InlineReviews.UnitTests\bin\$Configuration\net461\GitHub.InlineReviews.UnitTests.dll",
- "test\GitHub.TeamFoundation.UnitTests\bin\$Configuration\net461\GitHub.TeamFoundation.UnitTests.dll",
- "test\GitHub.UI.UnitTests\bin\$Configuration\net461\GitHub.UI.UnitTests.dll",
- "test\GitHub.VisualStudio.UnitTests\bin\$Configuration\net461\GitHub.VisualStudio.UnitTests.dll",
- "test\MetricsTests\MetricsTests\bin\$Configuration\MetricsTests.dll",
- "test\TrackingCollectionTests\bin\$Configuration\net461\TrackingCollectionTests.dll"
-)
-
-$opencoverTargetArgs = ($testAssemblies -join " ") + " --where \`"cat!=Timings and cat!=CodeCoverageFlake\`" --inprocess --noresult"
-
-$opencoverDirectory = Join-Path $env:USERPROFILE .nuget\packages\opencover\4.6.519\tools
-$opencover = Join-Path $opencoverDirectory OpenCover.Console.exe
-$opencoverArgs = @(
- "-target:`"$nunitConsoleRunner`"",
- "-targetargs:`"$opencoverTargetArgs`"",
- "-filter:`"+[GitHub*]* -[GitHub*Unit]GitHub.*.SampleData -[GitHub*UnitTests]*`"",
- "-excludebyfile:*.xaml;*.xaml.cs",
- "-register:user -output:$rootDirectory\coverage.xml"
-) -join " "
-
-$codecovDirectory = Join-Path $env:USERPROFILE .nuget\packages\codecov\1.1.0\tools
-$codecov = Join-Path $codecovDirectory codecov.exe
-$codecovArgs = "-f $rootDirectory\coverage.xml"
-
-& {
- Trap {
- Write-Output "OpenCover trapped"
- exit 0
- }
-
- Write-Output $opencover
-
- Run-Process 600 $opencover $opencoverArgs
-
- if (!$?) {
- Write-Output "OpenCover failed"
- exit 0
- }
-}
-
-if($AppVeyor) {
- & {
- Trap {
- Write-Output "Codecov trapped"
- exit 0
- }
-
- Push-AppveyorArtifact "$rootDirectory\coverage.xml"
- Run-Process 300 $codecov $codecovArgs
-
- if (!$?) {
- Write-Output "Codecov failed"
- exit 0
- }
- }
-}
diff --git a/scripts/Run-NUnit.ps1 b/scripts/Run-NUnit.ps1
deleted file mode 100644
index ac4662198a..0000000000
--- a/scripts/Run-NUnit.ps1
+++ /dev/null
@@ -1,54 +0,0 @@
-<#
-.SYNOPSIS
- Runs NUnit
-#>
-
-[CmdletBinding()]
-Param(
- [Parameter(Mandatory=$true)]
- [ValidateNotNullOrEmpty()]
- [string]
- $BasePathToProject
- ,
- [Parameter(Mandatory=$true)]
- [ValidateNotNullOrEmpty()]
- [string]
- $Project
- ,
- [int]
- $TimeoutDuration
- ,
- [string]
- $Configuration
- ,
- [switch]
- $AppVeyor = $false
-)
-
-$scriptsDirectory = $PSScriptRoot
-$rootDirectory = Split-Path ($scriptsDirectory)
-. $scriptsDirectory\modules.ps1 | out-null
-
-$dll = "$BasePathToProject\$Project\bin\$Configuration\$Project.dll"
-$nunitDirectory = Join-Path $rootDirectory packages\NUnit.ConsoleRunner.3.7.0\tools
-$consoleRunner = Join-Path $nunitDirectory nunit3-console.exe
-$xml = Join-Path $rootDirectory "nunit-$Project.xml"
-
-& {
- Trap {
- Write-Output "$Project tests failed"
- exit -1
- }
-
- $args = @()
- if ($AppVeyor) {
- $args = $dll, "--where", "cat!=Timings", "--result=$xml;format=AppVeyor"
- } else {
- $args = $dll, "--where", "cat!=Timings", "--result=$xml"
- }
-
- Run-Process -Fatal $TimeoutDuration $consoleRunner $args
- if (!$?) {
- Die 1 "$Project tests failed"
- }
-}
diff --git a/scripts/build.ps1 b/scripts/build.ps1
deleted file mode 100644
index aa44a88934..0000000000
--- a/scripts/build.ps1
+++ /dev/null
@@ -1,84 +0,0 @@
-<#
-.SYNOPSIS
- Builds and (optionally) runs tests for GitHub for Visual Studio
-.DESCRIPTION
- Build GHfVS
-.PARAMETER Clean
- When true, all untracked (and ignored) files will be removed from the work
- tree and all submodules. Defaults to false.
-.PARAMETER Config
- Debug or Release
-.PARAMETER RunTests
- Runs the tests (defauls to false)
-#>
-[CmdletBinding()]
-
-Param(
- [switch]
- $UpdateSubmodules = $false
- ,
- [switch]
- $Clean = $false
- ,
- [ValidateSet('Debug', 'Release')]
- [string]
- $Config = "Release"
- ,
- [switch]
- $Package = $false
- ,
- [switch]
- $AppVeyor = $false
- ,
- [switch]
- $BumpVersion = $false
- ,
- [int]
- $BuildNumber = -1
- ,
- [switch]
- $Trace = $false
-)
-
-Set-StrictMode -Version Latest
-if ($Trace) {
- Set-PSDebug -Trace 1
-}
-
-. $PSScriptRoot\modules.ps1 | out-null
-$env:PATH = "$scriptsDirectory;$scriptsDirectory\Modules;$env:PATH"
-
-Import-Module $scriptsDirectory\Modules\Debugging.psm1
-Vsix | out-null
-
-Push-Location $rootDirectory
-
-if ($UpdateSubmodules) {
- Update-Submodules
-}
-
-if ($Clean) {
- Clean-WorkingTree
-}
-
-$fullBuild = Test-Path env:GHFVS_KEY
-$publishable = $fullBuild -and $AppVeyor -and ($env:APPVEYOR_PULL_REQUEST_NUMBER -or $env:APPVEYOR_REPO_BRANCH -eq "master")
-if ($publishable) { #forcing a deploy flag for CI
- $Package = $true
- $BumpVersion = $true
-}
-
-if ($BumpVersion) {
- Write-Output "Bumping the version"
- Bump-Version -BumpBuild -BuildNumber:$BuildNumber
-}
-
-if ($Package) {
- Write-Output "Building and packaging GitHub for Visual Studio"
-} else {
- Write-Output "Building GitHub for Visual Studio"
-}
-
-Build-Solution GitHubVs.sln "Build" $config -Deploy:$Package
-
-Pop-Location
diff --git a/scripts/clearerror.cmd b/scripts/clearerror.cmd
deleted file mode 100644
index 9a18480a67..0000000000
--- a/scripts/clearerror.cmd
+++ /dev/null
@@ -1 +0,0 @@
-@echo off
\ No newline at end of file
diff --git a/scripts/common.ps1 b/scripts/common.ps1
deleted file mode 100644
index 3637124792..0000000000
--- a/scripts/common.ps1
+++ /dev/null
@@ -1,69 +0,0 @@
-$scriptsDirectory = Split-Path $MyInvocation.MyCommand.Path
-$rootDirectory = Split-Path ($scriptsDirectory)
-
-function Die([string]$message, [object[]]$output) {
- if ($output) {
- Write-Output $output
- $message += ". See output above."
- }
- Throw (New-Object -TypeName ScriptException -ArgumentList $message)
-}
-
-if (Test-Path "C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe") {
- $msbuild = "C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe"
-}
-elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe") {
- $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe"
-}
-else {
- Die("No suitable msbuild.exe found.")
-}
-
-$git = (Get-Command 'git.exe').Path
-if (!$git) {
- $git = Join-Path $rootDirectory 'PortableGit\cmd\git.exe'
-}
-if (!$git) {
- throw "Couldn't find installed an git.exe"
-}
-
-$nuget = Join-Path $rootDirectory "tools\nuget\nuget.exe"
-
-function Create-TempDirectory {
- $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
- New-Item -Type Directory $path
-}
-
-function Build-Solution([string]$solution,[string]$target,[string]$configuration, [bool]$ForVSInstaller) {
- Run-Command -Fatal { & $nuget restore $solution -NonInteractive -Verbosity detailed }
- $flag1 = ""
- $flag2 = ""
- if ($ForVSInstaller) {
- $flag1 = "/p:IsProductComponent=true"
- $flag2 = "/p:TargetVsixContainer=$rootDirectory\build\vsinstaller\GitHub.VisualStudio.vsix"
- new-item -Path $rootDirectory\build\vsinstaller -ItemType Directory -Force | Out-Null
- }
-
- Write-Output "$msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=14.0 $flag1 $flag2"
- Run-Command -Fatal { & $msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=14.0 $flag1 $flag2 }
-}
-
-function Push-Changes([string]$branch) {
- Push-Location $rootDirectory
-
- Write-Output "Pushing $Branch to GitHub..."
-
- Run-Command -Fatal { & $git push origin $branch }
-
- Pop-Location
-}
-
-Add-Type -AssemblyName "System.Core"
-Add-Type -TypeDefinition @"
-public class ScriptException : System.Exception
-{
- public ScriptException(string message) : base(message)
- {
- }
-}
-"@
diff --git a/scripts/modules.ps1 b/scripts/modules.ps1
deleted file mode 100644
index 83fe9e39f7..0000000000
--- a/scripts/modules.ps1
+++ /dev/null
@@ -1,202 +0,0 @@
-Add-Type -AssemblyName "System.Core"
-Add-Type -TypeDefinition @"
-public class ScriptException : System.Exception
-{
- public int ExitCode { get; private set; }
- public ScriptException(string message, int exitCode) : base(message)
- {
- this.ExitCode = exitCode;
- }
-}
-"@
-
-New-Module -ScriptBlock {
- $rootDirectory = Split-Path ($PSScriptRoot)
- $scriptsDirectory = Join-Path $rootDirectory "scripts"
- $nuget = Join-Path $rootDirectory "tools\nuget\nuget.exe"
- Export-ModuleMember -Variable scriptsDirectory,rootDirectory,nuget
-}
-
-New-Module -ScriptBlock {
- function Die([int]$exitCode, [string]$message, [object[]]$output) {
- #$host.SetShouldExit($exitCode)
- if ($output) {
- Write-Host $output
- $message += ". See output above."
- }
- $hash = @{
- Message = $message
- ExitCode = $exitCode
- Output = $output
- }
- Throw (New-Object -TypeName ScriptException -ArgumentList $message,$exitCode)
- #throw $message
- }
-
-
- function Run-Command([scriptblock]$Command, [switch]$Fatal, [switch]$Quiet) {
- $output = ""
-
- $exitCode = 0
-
- if ($Quiet) {
- $output = & $command 2>&1 | %{ "$_" }
- } else {
- & $command
- }
-
- if (!$? -and $LastExitCode -ne 0) {
- $exitCode = $LastExitCode
- } elseif ($? -and $LastExitCode -ne 0) {
- $exitCode = $LastExitCode
- }
-
- if ($exitCode -ne 0) {
- if (!$Fatal) {
- Write-Host "``$Command`` failed" $output
- } else {
- Die $exitCode "``$Command`` failed" $output
- }
- }
- $output
- }
-
- function Run-Process([int]$Timeout, [string]$Command, [string[]]$Arguments, [switch]$Fatal = $false)
- {
- $args = ($Arguments | %{ "`"$_`"" })
- [object[]] $output = "$Command " + $args
- $exitCode = 0
- $outputPath = [System.IO.Path]::GetTempFileName()
- $process = Start-Process -PassThru -NoNewWindow -RedirectStandardOutput $outputPath $Command ($args | %{ "`"$_`"" })
- Wait-Process -InputObject $process -Timeout $Timeout -ErrorAction SilentlyContinue
- if ($process.HasExited) {
- $output += Get-Content $outputPath
- $exitCode = $process.ExitCode
- } else {
- $output += "Process timed out. Backtrace:"
- $output += Get-DotNetStack $process.Id
- $exitCode = 9999
- }
- Stop-Process -InputObject $process
- Remove-Item $outputPath
- if ($exitCode -ne 0) {
- if (!$Fatal) {
- Write-Host "``$Command`` failed" $output
- } else {
- Die $exitCode "``$Command`` failed" $output
- }
- }
- $output
- }
-
- function Create-TempDirectory {
- $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
- New-Item -Type Directory $path
- }
-
- Export-ModuleMember -Function Die,Run-Command,Run-Process,Create-TempDirectory
-}
-
-New-Module -ScriptBlock {
- function Find-MSBuild() {
- if (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\MSBuild.exe") {
- $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\MSBuild.exe"
- }
- elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe") {
- $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe"
- }
- elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe") {
- $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe"
- }
- else {
- Die("No suitable msbuild.exe found.")
- }
- $msbuild
- }
-
- function Build-Solution([string]$solution, [string]$target, [string]$configuration, [switch]$ForVSInstaller, [bool]$Deploy = $false) {
- Run-Command -Fatal { & $nuget restore $solution -NonInteractive -Verbosity detailed }
- $flag1 = ""
- $flag2 = ""
- if ($ForVSInstaller) {
- $flag1 = "/p:IsProductComponent=true"
- $flag2 = "/p:TargetVsixContainer=$rootDirectory\build\vsinstaller\GitHub.VisualStudio.vsix"
- new-item -Path $rootDirectory\build\vsinstaller -ItemType Directory -Force | Out-Null
- } elseif (!$Deploy) {
- $configuration += "WithoutVsix"
- $flag1 = "/p:Package=Skip"
- }
-
- $msbuild = Find-MSBuild
-
- Write-Host "$msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=15.0 /bl:output.binlog $flag1 $flag2"
- Run-Command -Fatal { & $msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=15.0 /bl:output.binlog $flag1 $flag2 }
- }
-
- Export-ModuleMember -Function Find-MSBuild,Build-Solution
-}
-
-New-Module -ScriptBlock {
- function Find-Git() {
- $git = (Get-Command 'git.exe').Path
- if (!$git) {
- $git = Join-Path $rootDirectory 'PortableGit\cmd\git.exe'
- }
- if (!$git) {
- Die("Couldn't find installed an git.exe")
- }
- $git
- }
-
- function Push-Changes([string]$branch) {
- Push-Location $rootDirectory
-
- Write-Host "Pushing $Branch to GitHub..."
-
- Run-Command -Fatal { & $git push origin $branch }
-
- Pop-Location
- }
-
- function Update-Submodules {
- Write-Host "Updating submodules..."
- Write-Host ""
-
- Run-Command -Fatal { git submodule init }
- Run-Command -Fatal { git submodule sync }
- Run-Command -Fatal { git submodule update --recursive --force }
- }
-
- function Clean-WorkingTree {
- Write-Host "Cleaning work tree..."
- Write-Host ""
-
- Run-Command -Fatal { git clean -xdf }
- Run-Command -Fatal { git submodule foreach git clean -xdf }
- }
-
- function Get-HeadSha {
- Run-Command -Quiet { & $git rev-parse HEAD }
- }
-
- $git = Find-Git
- Export-ModuleMember -Function Find-Git,Push-Changes,Update-Submodules,Clean-WorkingTree,Get-HeadSha
-}
-
-New-Module -ScriptBlock {
- function Write-Manifest([string]$directory) {
- Add-Type -Path (Join-Path $rootDirectory packages\Newtonsoft.Json.6.0.8\lib\net35\Newtonsoft.Json.dll)
-
- $manifest = @{
- NewestExtension = @{
- Version = [string](Read-CurrentVersionVsix)
- Commit = [string](Get-HeadSha)
- }
- }
-
- $manifestPath = Join-Path $directory manifest
- [Newtonsoft.Json.JsonConvert]::SerializeObject($manifest) | Out-File $manifestPath -Encoding UTF8
- }
-
- Export-ModuleMember -Function Write-Manifest
-}
\ No newline at end of file
diff --git a/scripts/modules/AppVeyor.ps1 b/scripts/modules/AppVeyor.ps1
deleted file mode 100644
index 49470283d0..0000000000
--- a/scripts/modules/AppVeyor.ps1
+++ /dev/null
@@ -1,41 +0,0 @@
-Set-StrictMode -Version Latest
-
-New-Module -ScriptBlock {
-
- function Get-AppVeyorPath {
- Join-Path $rootDirectory appveyor.yml
- }
-
- function Read-VersionAppVeyor {
- $file = Get-AppVeyorPath
- $currentVersion = Get-Content $file | %{
- $regex = "`^version: '(\d+\.\d+\.\d+)\.`{build`}'`$"
- if ($_ -match $regex) {
- $matches[1]
- }
- }
- [System.Version] $currentVersion
- }
-
- function Write-VersionAppVeyor([System.Version]$version) {
- $file = Get-AppVeyorPath
- $numberOfReplacements = 0
- $newContent = Get-Content $file | %{
- $newString = $_
- $regex = "version: '(\d+\.\d+\.\d+)"
- if ($newString -match $regex) {
- $numberOfReplacements++
- $newString = $newString -replace $regex, "version: '$($version.Major).$($version.Minor).$($version.Build)"
- }
- $newString
- }
-
- if ($numberOfReplacements -ne 1) {
- Die 1 "Expected to replace the version number in 1 place in appveyor.yml (version) but actually replaced it in $numberOfReplacements"
- }
-
- $newContent | Set-Content $file
- }
-
- Export-ModuleMember -Function Get-AppVeyorPath,Read-VersionAppVeyor,Write-VersionAppVeyor
-}
\ No newline at end of file
diff --git a/scripts/modules/BuildUtils.psm1 b/scripts/modules/BuildUtils.psm1
deleted file mode 100644
index f93d6eecb2..0000000000
--- a/scripts/modules/BuildUtils.psm1
+++ /dev/null
@@ -1,18 +0,0 @@
-Set-StrictMode -Version Latest
-
-function Update-Submodules {
- Write-Output "Updating submodules..."
- Write-Output ""
-
- Run-Command -Fatal { git submodule init }
- Run-Command -Fatal { git submodule sync }
- Run-Command -Fatal { git submodule update --recursive --force }
-}
-
-function Clean-WorkingTree {
- Write-Output "Cleaning work tree..."
- Write-Output ""
-
- Run-Command -Fatal { git clean -xdf }
- Run-Command -Fatal { git submodule foreach git clean -xdf }
-}
\ No newline at end of file
diff --git a/scripts/modules/Debugging.psm1 b/scripts/modules/Debugging.psm1
deleted file mode 100644
index 2ca851ec0a..0000000000
--- a/scripts/modules/Debugging.psm1
+++ /dev/null
@@ -1,26 +0,0 @@
-Set-StrictMode -Version Latest
-$ErrorActionPreference = "Stop"
-
-$rootDirectory = Split-Path (Split-Path (Split-Path $MyInvocation.MyCommand.Path))
-$cdb = Join-Path $rootDirectory "tools\Debugging Tools for Windows\cdb.exe"
-
-function Get-DotNetStack([int]$ProcessId) {
- $commands = @(
- ".cordll -ve -u -l",
- ".loadby sos clr",
- "!eestack -ee",
- ".detach",
- "q"
- )
-
- $Env:_NT_SYMBOL_PATH = "cache*${Env:PROGRAMDATA}\dbg\sym;SRV*http://msdl.microsoft.com/download/symbols;srv*http://windows-symbols.githubapp.com/symbols"
- $output = & $cdb -lines -p $ProcessId -c ($commands -join "; ")
- if ($LastExitCode -ne 0) {
- $output
- throw "Error running cdb"
- }
-
- $start = ($output | Select-String -List -Pattern "^Thread 0").LineNumber - 1
- $end = ($output | Select-String -List -Pattern "^Detached").LineNumber - 2
- $output[$start..$end]
-}
diff --git a/scripts/modules/DirectoryBuildProps.ps1 b/scripts/modules/DirectoryBuildProps.ps1
deleted file mode 100644
index 811ff3d19f..0000000000
--- a/scripts/modules/DirectoryBuildProps.ps1
+++ /dev/null
@@ -1,24 +0,0 @@
-Set-StrictMode -Version Latest
-
-New-Module -ScriptBlock {
- function Get-DirectoryBuildPropsPath {
- Join-Path $rootDirectory Directory.Build.Props
- }
-
- function Get-DirectoryBuildProps {
- $xmlLines = Get-Content (Get-DirectoryBuildPropsPath) -encoding UTF8
- [xml] $xmlLines
- }
-
- function Write-DirectoryBuildProps([System.Version]$version) {
-
- $document = Get-DirectoryBuildProps
-
- $numberOfReplacements = 0
- $document.Project.PropertyGroup.Version = $version.ToString()
-
- $document.Save((Get-DirectoryBuildPropsPath))
- }
-
- Export-ModuleMember -Function Write-DirectoryBuildProps
-}
\ No newline at end of file
diff --git a/scripts/modules/SolutionInfo.ps1 b/scripts/modules/SolutionInfo.ps1
deleted file mode 100644
index 4e1d6e1d0f..0000000000
--- a/scripts/modules/SolutionInfo.ps1
+++ /dev/null
@@ -1,41 +0,0 @@
-Set-StrictMode -Version Latest
-
-New-Module -ScriptBlock {
-
- function Get-SolutionInfoPath {
- Join-Path $rootDirectory src\common\SolutionInfo.cs
- }
-
- function Read-VersionSolutionInfo {
- $file = Get-SolutionInfoPath
- $currentVersion = Get-Content $file | %{
- $regex = "const string Version = `"(\d+\.\d+\.\d+\.\d+)`";"
- if ($_ -match $regex) {
- $matches[1]
- }
- }
- [System.Version] $currentVersion
- }
-
- function Write-VersionSolutionInfo([System.Version]$version) {
- $file = Get-SolutionInfoPath
- $numberOfReplacements = 0
- $newContent = Get-Content $file | %{
- $newString = $_
- $regex = "(string Version = `")\d+\.\d+\.\d+\.\d+"
- if ($_ -match $regex) {
- $numberOfReplacements++
- $newString = $newString -replace $regex, "string Version = `"$version"
- }
- $newString
- }
-
- if ($numberOfReplacements -ne 1) {
- Die 1 "Expected to replace the version number in 1 place in SolutionInfo.cs (Version) but actually replaced it in $numberOfReplacements"
- }
-
- $newContent | Set-Content $file
- }
-
- Export-ModuleMember -Function Get-SolutionInfoPath,Read-VersionSolutionInfo,Write-VersionSolutionInfo
-}
\ No newline at end of file
diff --git a/scripts/modules/Versioning.ps1 b/scripts/modules/Versioning.ps1
deleted file mode 100644
index e7f2efad7a..0000000000
--- a/scripts/modules/Versioning.ps1
+++ /dev/null
@@ -1,69 +0,0 @@
-Set-StrictMode -Version Latest
-
-New-Module -ScriptBlock {
-
- function Validate-Version([System.Version]$version) {
- ($version.Major -ge 0) -and ($version.Minor -ge 0) -and ($version.Build -ge 0)
- }
-
- function Generate-Version([System.Version]$currentVersion,
- [bool]$BumpMajor, [bool] $BumpMinor,
- [bool]$BumpPatch, [bool] $BumpBuild,
- [int]$BuildNumber = -1) {
-
- if (!(Validate-Version $currentVersion)) {
- Die 1 "Invalid current version $currentVersion"
- }
-
- if ($BumpMajor) {
- New-Object -TypeName System.Version -ArgumentList ($currentVersion.Major + 1), $currentVersion.Minor, $currentVersion.Build, 0
- } elseif ($BumpMinor) {
- New-Object -TypeName System.Version -ArgumentList $currentVersion.Major, ($currentVersion.Minor + 1), $currentVersion.Build, 0
- } elseif ($BumpPatch) {
- New-Object -TypeName System.Version -ArgumentList $currentVersion.Major, $currentVersion.Minor, ($currentVersion.Build + 1), 0
- } elseif ($BumpBuild) {
- if ($BuildNumber -ge 0) {
- [System.Version] "$($currentVersion.Major).$($currentVersion.Minor).$($currentVersion.Build).$BuildNumber"
- } else {
- $timestamp = [System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
- [System.Version] "$($currentVersion.Major).$($currentVersion.Minor).$($currentVersion.Build).$timestamp"
- }
- }
- else {
- $currentVersion
- }
- }
-
- function Read-Version {
- Read-VersionAppVeyor
- }
-
- function Write-Version([System.Version]$version) {
- Write-VersionVsixManifest $version
- Write-VersionSolutionInfo $version
- Write-VersionAppVeyor $version
- Write-DirectoryBuildProps $version
- Push-Location $rootDirectory
- New-Item -Type Directory -ErrorAction SilentlyContinue build | out-null
- Set-Content build\version $version
- Pop-Location
- }
-
- function Commit-Version([System.Version]$version) {
-
- Write-Host "Committing version bump..."
-
- Push-Location $rootDirectory
-
- Run-Command -Fatal { & $git commit --message "Bump version to $version" -- }
-
- $output = Start-Process $git "commit --all --message ""Bump version to $version""" -wait -NoNewWindow -ErrorAction Continue -PassThru
- if ($output.ExitCode -ne 0) {
- Die 1 "Error committing version bump"
- }
-
- Pop-Location
- }
-
- Export-ModuleMember -Function Validate-Version,Write-Version,Commit-Version,Generate-Version,Read-Version
-}
diff --git a/scripts/modules/Vsix.ps1 b/scripts/modules/Vsix.ps1
deleted file mode 100644
index 63563d3f00..0000000000
--- a/scripts/modules/Vsix.ps1
+++ /dev/null
@@ -1,35 +0,0 @@
-Set-StrictMode -Version Latest
-
-New-Module -ScriptBlock {
- $gitHubDirectory = Join-Path $rootDirectory src\GitHub.VisualStudio
-
- function Get-VsixManifestPath {
- Join-Path $gitHubDirectory source.extension.vsixmanifest
- }
-
- function Get-VsixManifestXml {
- $xmlLines = Get-Content (Get-VsixManifestPath)
- # If we don't explicitly join the lines with CRLF, comments in the XML will
- # end up with LF line-endings, which will make Git spew a warning when we
- # try to commit the version bump.
- $xmlText = $xmlLines -join [System.Environment]::NewLine
-
- [xml] $xmlText
- }
-
- function Read-CurrentVersionVsix {
- [System.Version] (Get-VsixManifestXml).PackageManifest.Metadata.Identity.Version
- }
-
- function Write-VersionVsixManifest([System.Version]$version) {
-
- $document = Get-VsixManifestXml
-
- $numberOfReplacements = 0
- $document.PackageManifest.Metadata.Identity.Version = $version.ToString()
-
- $document.Save((Get-VsixManifestPath))
- }
-
- Export-ModuleMember -Function Read-CurrentVersionVsix,Write-VersionVsixManifest
-}
\ No newline at end of file
diff --git a/scripts/test.ps1 b/scripts/test.ps1
deleted file mode 100644
index 6e64d9df63..0000000000
--- a/scripts/test.ps1
+++ /dev/null
@@ -1,103 +0,0 @@
-<#
-.SYNOPSIS
- Runs tests for GitHub for Visual Studio
-.DESCRIPTION
- Build GHfVS
-.PARAMETER Clean
- When true, all untracked (and ignored) files will be removed from the work
- tree and all submodules. Defaults to false.
-#>
-[CmdletBinding()]
-
-Param(
- [ValidateSet('Debug', 'Release')]
- [string]
- $Config = "Release"
- ,
- [int]
- $TimeoutDuration = 180
- ,
- [switch]
- $Trace = $false
-
-)
-
-Set-StrictMode -Version Latest
-if ($Trace) {
- Set-PSDebug -Trace 1
-}
-
-$env:PATH = "$PSScriptRoot;$env:PATH"
-
-$exitcode = 0
-
-Write-Output "Running Tracking Collection Tests..."
-Run-NUnit test TrackingCollectionTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 1
-}
-
-Write-Output "Running GitHub.Api.UnitTests..."
-Run-NUnit test GitHub.Api.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 2
-}
-
-Write-Output "Running GitHub.App.UnitTests..."
-Run-NUnit test GitHub.App.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 3
-}
-
-Write-Output "Running GitHub.Exports.Reactive.UnitTests..."
-Run-NUnit test GitHub.Exports.Reactive.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 4
-}
-
-Write-Output "Running GitHub.Exports.UnitTests..."
-Run-NUnit test GitHub.Exports.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 5
-}
-
-Write-Output "Running GitHub.Extensions.UnitTests..."
-Run-NUnit test GitHub.Extensions.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 6
-}
-
-Write-Output "Running GitHub.Primitives.UnitTests..."
-Run-NUnit test GitHub.Primitives.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 7
-}
-
-Write-Output "Running GitHub.TeamFoundation.UnitTests..."
-Run-NUnit test GitHub.TeamFoundation.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 8
-}
-
-Write-Output "Running GitHub.UI.UnitTests..."
-Run-NUnit test GitHub.UI.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 9
-}
-
-Write-Output "Running GitHub.VisualStudio.UnitTests..."
-Run-NUnit test GitHub.VisualStudio.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 10
-}
-
-Write-Output "Running GitHub.InlineReviews.UnitTests..."
-Run-NUnit test GitHub.InlineReviews.UnitTests $TimeoutDuration $config
-if (!$?) {
- $exitcode = 11
-}
-
-if ($exitcode -ne 0) {
- $host.SetShouldExit($exitcode)
-}
-exit $exitcode
\ No newline at end of file
diff --git a/signingkey.snk b/signingkey.snk
deleted file mode 100644
index 371008d5a6..0000000000
Binary files a/signingkey.snk and /dev/null differ
diff --git a/src/CredentialManagement/NativeMethods.cs b/src/CredentialManagement/NativeMethods.cs
index cd5dc3ef42..c88a973f60 100644
--- a/src/CredentialManagement/NativeMethods.cs
+++ b/src/CredentialManagement/NativeMethods.cs
@@ -4,6 +4,10 @@
using System.Text;
using Microsoft.Win32.SafeHandles;
+#pragma warning disable CA1034 // Nested types should not be visible
+#pragma warning disable CA1051 // Do not declare visible instance fields
+#pragma warning disable CA1707 // Identifiers should not contain underscores
+
namespace GitHub.Authentication.CredentialManagement
{
public static class NativeMethods
diff --git a/src/GitHub.Api/ApiClientConfiguration.cs b/src/GitHub.Api/ApiClientConfiguration.cs
index 937f837db7..4336ea36df 100644
--- a/src/GitHub.Api/ApiClientConfiguration.cs
+++ b/src/GitHub.Api/ApiClientConfiguration.cs
@@ -40,7 +40,7 @@ static ApiClientConfiguration()
///
/// Gets the ideal scopes requested by the application.
///
- public static IReadOnlyList RequestedScopes { get; } = new[] { "user", "repo", "gist", "write:public_key", "read:org" };
+ public static IReadOnlyList RequestedScopes { get; } = new[] { "user", "repo", "gist", "write:public_key", "read:org", "workflow" };
///
/// Gets a note that will be stored with the OAUTH token.
diff --git a/src/GitHub.Api/ApiClientConfiguration_User.cs b/src/GitHub.Api/ApiClientConfiguration_User.cs
deleted file mode 100644
index fdffb967e8..0000000000
--- a/src/GitHub.Api/ApiClientConfiguration_User.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-
-namespace GitHub.Api
-{
- static partial class ApiClientConfiguration
- {
- const string clientId = "YOUR CLIENT ID HERE";
- const string clientSecret = "YOUR CLIENT SECRET HERE";
-
- static partial void Configure()
- {
- ClientId = clientId;
- ClientSecret = clientSecret;
- }
- }
-}
diff --git a/src/GitHub.Api/Caching/FileCache.cs b/src/GitHub.Api/Caching/FileCache.cs
new file mode 100644
index 0000000000..dd5f937e61
--- /dev/null
+++ b/src/GitHub.Api/Caching/FileCache.cs
@@ -0,0 +1,1295 @@
+/*
+Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
+
+This file is part of FileCache (http://github.com/acarteas/FileCache).
+
+FileCache is distributed under the Apache License 2.0.
+Consult "LICENSE.txt" included in this package for the Apache License 2.0.
+*/
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Runtime.Caching
+{
+ public class FileCache : ObjectCache
+ {
+ private static int _nameCounter = 1;
+ private string _name = "";
+ private SerializationBinder _binder;
+ private string _cacheSubFolder = "cache";
+ private string _policySubFolder = "policy";
+ private TimeSpan _cleanInterval = new TimeSpan(7, 0, 0, 0); // default to 1 week
+ private const string LastCleanedDateFile = "cache.lcd";
+ private const string CacheSizeFile = "cache.size";
+ // this is a file used to prevent multiple processes from trying to "clean" at the same time
+ private const string SemaphoreFile = "cache.sem";
+ private long _currentCacheSize = 0;
+ private PayloadMode _readMode = PayloadMode.Serializable;
+ public string CacheDir { get; protected set; }
+
+
+ ///
+ /// Used to store the default region when accessing the cache via [] calls
+ ///
+ public string DefaultRegion { get; set; }
+
+ ///
+ /// Used to set the default policy when setting cache values via [] calls
+ ///
+ public CacheItemPolicy DefaultPolicy { get; set; }
+
+ ///
+ /// Specified how the cache payload is to be handled.
+ ///
+ public enum PayloadMode
+ {
+ ///
+ /// Treat the payload a a serializable object.
+ ///
+ Serializable,
+ ///
+ /// Treat the payload as a file name. File content will be copied on add, while get returns the file name.
+ ///
+ Filename,
+ ///
+ /// Treat the paylad as raw bytes. A byte[] and readable streams are supported on add.
+ ///
+ RawBytes
+ }
+
+ ///
+ /// Specified whether the payload is deserialized or just the file name.
+ ///
+ public PayloadMode PayloadReadMode
+ {
+ get => _readMode;
+ set
+ {
+ if (value == PayloadMode.RawBytes)
+ {
+ throw new ArgumentException("The read mode cannot be set to RawBytes. Use the file name please.");
+ }
+ _readMode = value;
+ }
+ }
+
+ ///
+ /// Specified how the payload is to be handled on add operations.
+ ///
+ public PayloadMode PayloadWriteMode { get; set; } = PayloadMode.Serializable;
+
+ ///
+ /// The amount of time before expiry that a filename will be used as a payoad. I.e.
+ /// the amount of time the cache's user can safely use the file delivered as a payload.
+ /// Default 10 minutes.
+ ///
+ public TimeSpan FilenameAsPayloadSafetyMargin = TimeSpan.FromMinutes(10);
+
+ ///
+ /// Used to determine how long the FileCache will wait for a file to become
+ /// available. Default (00:00:00) is indefinite. Should the timeout be
+ /// reached, an exception will be thrown.
+ ///
+ public TimeSpan AccessTimeout { get; set; }
+
+ ///
+ /// Used to specify the disk size, in bytes, that can be used by the File Cache
+ ///
+ public long MaxCacheSize { get; set; }
+
+ ///
+ /// Returns the approximate size of the file cache
+ ///
+ public long CurrentCacheSize
+ {
+ get
+ {
+ // if this is the first query, we need to load the cache size from somewhere
+ if (_currentCacheSize == 0)
+ {
+ // Read the system file for cache size
+ object cacheSizeObj = ReadSysFile(CacheSizeFile);
+
+ // Did we successfully get data from the file?
+ if (cacheSizeObj != null)
+ {
+ _currentCacheSize = (long)cacheSizeObj;
+ }
+ }
+
+ return _currentCacheSize;
+ }
+ private set
+ {
+ // no need to do a pointless re-store of the same value
+ if (_currentCacheSize != value || value == 0)
+ {
+ WriteSysFile(CacheSizeFile, value);
+ _currentCacheSize = value;
+ }
+ }
+ }
+
+ ///
+ /// Event that will be called when is reached.
+ ///
+ public event EventHandler MaxCacheSizeReached = delegate { };
+
+ public event EventHandler CacheResized = delegate { };
+
+ ///
+ /// The default cache path used by FC.
+ ///
+ private string DefaultCachePath
+ {
+ get
+ {
+ return Directory.GetCurrentDirectory();
+ }
+ }
+
+ #region constructors
+
+ ///
+ /// Creates a default instance of the file cache. Don't use if you plan to serialize custom objects
+ ///
+ /// If true, will calcualte the cache's current size upon new object creation.
+ /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
+ /// use case.
+ ///
+ /// If supplied, sets the interval of time that must occur between self cleans
+ public FileCache(bool calculateCacheSize = false, TimeSpan cleanInterval = new TimeSpan())
+ {
+ // CT note: I moved this code to an init method because if the user specified a cache root, that needs to
+ // be set before checking if we should clean (otherwise it will look for the file in the wrong place)
+ Init(calculateCacheSize, cleanInterval);
+ }
+
+ ///
+ /// Creates an instance of the file cache using the supplied path as the root save path.
+ ///
+ /// The cache's root file path
+ /// If true, will calcualte the cache's current size upon new object creation.
+ /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
+ /// use case.
+ ///
+ /// If supplied, sets the interval of time that must occur between self cleans
+ public FileCache(string cacheRoot, bool calculateCacheSize = false, TimeSpan cleanInterval = new TimeSpan())
+ {
+ CacheDir = cacheRoot;
+ Init(calculateCacheSize, cleanInterval, false);
+ }
+
+ ///
+ /// Creates an instance of the file cache.
+ ///
+ /// The SerializationBinder used to deserialize cached objects. Needed if you plan
+ /// to cache custom objects.
+ ///
+ /// If true, will calcualte the cache's current size upon new object creation.
+ /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
+ /// use case.
+ ///
+ /// If supplied, sets the interval of time that must occur between self cleans
+ public FileCache(SerializationBinder binder, bool calculateCacheSize = false, TimeSpan cleanInterval = new TimeSpan())
+ {
+ _binder = binder;
+ Init(calculateCacheSize, cleanInterval, true, false);
+ }
+
+ ///
+ /// Creates an instance of the file cache.
+ ///
+ /// The cache's root file path
+ /// The SerializationBinder used to deserialize cached objects. Needed if you plan
+ /// to cache custom objects.
+ /// If true, will calcualte the cache's current size upon new object creation.
+ /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
+ /// use case.
+ ///
+ /// If supplied, sets the interval of time that must occur between self cleans
+ public FileCache(string cacheRoot, SerializationBinder binder, bool calculateCacheSize = false, TimeSpan cleanInterval = new TimeSpan())
+ {
+ _binder = binder;
+ CacheDir = cacheRoot;
+ Init(calculateCacheSize, cleanInterval, false, false);
+ }
+
+ #endregion
+
+ #region custom methods
+
+ private void Init(bool calculateCacheSize = false, TimeSpan cleanInterval = new TimeSpan(), bool setCacheDirToDefault = true, bool setBinderToDefault = true)
+ {
+ _name = "FileCache_" + _nameCounter;
+ _nameCounter++;
+
+ DefaultRegion = null;
+ DefaultPolicy = new CacheItemPolicy();
+ AccessTimeout = new TimeSpan();
+ MaxCacheSize = long.MaxValue;
+
+ // set default values if not already set
+ if (setCacheDirToDefault)
+ CacheDir = DefaultCachePath;
+ if (setBinderToDefault)
+ _binder = new FileCacheBinder();
+
+ // if it doesn't exist, we need to make it
+ if (!Directory.Exists(CacheDir))
+ Directory.CreateDirectory(CacheDir);
+
+ // only set the clean interval if the user supplied it
+ if (cleanInterval > new TimeSpan())
+ {
+ _cleanInterval = cleanInterval;
+ }
+
+ //check to see if cache is in need of immediate cleaning
+ if (ShouldClean())
+ {
+ CleanCacheAsync();
+ }
+ else if (calculateCacheSize || CurrentCacheSize == 0)
+ {
+ // This is in an else if block, because CleanCacheAsync will
+ // update the cache size, so no need to do it twice.
+ UpdateCacheSizeAsync();
+ }
+
+ MaxCacheSizeReached += FileCache_MaxCacheSizeReached;
+ }
+
+ private void FileCache_MaxCacheSizeReached(object sender, FileCacheEventArgs e)
+ {
+ Task.Factory.StartNew((Action)(() =>
+ {
+ // Shrink the cache to 75% of the max size
+ // that way there's room for it to grow a bit
+ // before we have to do this again.
+ long newSize = ShrinkCacheToSize((long)(MaxCacheSize * 0.75));
+ }));
+ }
+
+
+ // Returns the cleanlock file if it can be opened, otherwise it is being used by another process so return null
+ private FileStream GetCleaningLock()
+ {
+ try
+ {
+ return File.Open(Path.Combine(CacheDir, SemaphoreFile), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ // Determines whether or not enough time has passed that the cache should clean itself
+ private bool ShouldClean()
+ {
+ try
+ {
+ // if the file can't be found, or is corrupt this will throw an exception
+ DateTime? lastClean = ReadSysFile(LastCleanedDateFile) as DateTime?;
+
+ //AC: rewrote to be safer in null cases
+ if (lastClean == null)
+ {
+ return true;
+ }
+
+ // return true if the amount of time between now and the last clean is greater than or equal to the
+ // clean interval, otherwise return false.
+ return DateTime.Now - lastClean >= _cleanInterval;
+ }
+ catch (Exception)
+ {
+ return true;
+ }
+ }
+
+ ///
+ /// Shrinks the cache until the cache size is less than
+ /// or equal to the size specified (in bytes). This is a
+ /// rather expensive operation, so use with discretion.
+ ///
+ /// The new size of the cache
+ public long ShrinkCacheToSize(long newSize, string regionName = null)
+ {
+ long originalSize = 0, amount = 0, removed = 0;
+
+ //lock down other treads from trying to shrink or clean
+ using (FileStream cLock = GetCleaningLock())
+ {
+ if (cLock == null)
+ return -1;
+
+ // if we're shrinking the whole cache, we can use the stored
+ // size if it's available. If it's not available we calculate it and store
+ // it for next time.
+ if (regionName == null)
+ {
+ if (CurrentCacheSize == 0)
+ {
+ CurrentCacheSize = GetCacheSize();
+ }
+
+ originalSize = CurrentCacheSize;
+ }
+ else
+ {
+ originalSize = GetCacheSize(regionName);
+ }
+
+ // Find out how much we need to get rid of
+ amount = originalSize - newSize;
+
+ // CT note: This will update CurrentCacheSize
+ removed = DeleteOldestFiles(amount, regionName);
+
+ // unlock the semaphore for others
+ cLock.Close();
+ }
+
+ // trigger the event
+ CacheResized(this, new FileCacheEventArgs(originalSize - removed, MaxCacheSize));
+
+ // return the final size of the cache (or region)
+ return originalSize - removed;
+ }
+
+ public void CleanCacheAsync()
+ {
+ Task.Factory.StartNew((Action)(() =>
+ {
+ CleanCache();
+ }));
+ }
+
+ ///
+ /// Loop through the cache and delete all expired files
+ ///
+ /// The amount removed (in bytes)
+ public long CleanCache(string regionName = null)
+ {
+ long removed = 0;
+
+ //lock down other treads from trying to shrink or clean
+ using (FileStream cLock = GetCleaningLock())
+ {
+ if (cLock == null)
+ return 0;
+
+ foreach (string key in GetKeys(regionName))
+ {
+ CacheItemPolicy policy = GetPolicy(key, regionName);
+ if (policy.AbsoluteExpiration < DateTime.Now)
+ {
+ try
+ {
+ string cachePath = GetCachePath(key, regionName);
+ string policyPath = GetPolicyPath(key, regionName);
+ CacheItemReference ci = new CacheItemReference(key, cachePath, policyPath);
+ Remove(key, regionName); // CT note: Remove will update CurrentCacheSize
+ removed += ci.Length;
+ }
+ catch (Exception) // skip if the file cannot be accessed
+ { }
+ }
+ }
+
+ // mark that we've cleaned the cache
+ WriteSysFile(LastCleanedDateFile, DateTime.Now);
+
+ // unlock
+ cLock.Close();
+ }
+
+ return removed;
+ }
+
+ public void ClearRegion(string regionName)
+ {
+ using (var cLock = GetCleaningLock())
+ {
+ if (cLock == null)
+ return;
+
+ foreach (var key in GetKeys(regionName))
+ {
+ Remove(key, regionName);
+ }
+
+ cLock.Close();
+ }
+ }
+
+ ///
+ /// Delete the oldest items in the cache to shrink the chache by the
+ /// specified amount (in bytes).
+ ///
+ /// The amount of data that was actually removed
+ private long DeleteOldestFiles(long amount, string regionName = null)
+ {
+ // Verify that we actually need to shrink
+ if (amount <= 0)
+ {
+ return 0;
+ }
+
+ //Heap of all CacheReferences
+ PriortyQueue cacheReferences = new PriortyQueue();
+
+ //build a heap of all files in cache region
+ foreach (string key in GetKeys(regionName))
+ {
+ try
+ {
+ //build item reference
+ string cachePath = GetCachePath(key, regionName);
+ string policyPath = GetPolicyPath(key, regionName);
+ CacheItemReference ci = new CacheItemReference(key, cachePath, policyPath);
+ cacheReferences.Enqueue(ci);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ }
+
+ //remove cache items until size requirement is met
+ long removedBytes = 0;
+ while (removedBytes < amount && cacheReferences.GetSize() > 0)
+ {
+ //remove oldest item
+ CacheItemReference oldest = cacheReferences.Dequeue();
+ removedBytes += oldest.Length;
+ Remove(oldest.Key, regionName);
+ }
+ return removedBytes;
+ }
+
+ ///
+ /// This method calls GetCacheSize on a separate thread to
+ /// calculate and then store the size of the cache.
+ ///
+ public void UpdateCacheSizeAsync()
+ {
+ Task.Factory.StartNew((Action)(() =>
+ {
+ CurrentCacheSize = GetCacheSize();
+ }));
+ }
+
+ //AC Note: From MSDN / SO (http://stackoverflow.com/questions/468119/whats-the-best-way-to-calculate-the-size-of-a-directory-in-net)
+ ///
+ /// Calculates the size, in bytes of the file cache
+ ///
+ /// The region to calculate. If NULL, will return total size.
+ ///
+ public long GetCacheSize(string regionName = null)
+ {
+ long size = 0;
+
+ //AC note: First parameter is unused, so just pass in garbage ("DummyValue")
+ string policyPath = Path.GetDirectoryName(GetPolicyPath("DummyValue", regionName));
+ string cachePath = Path.GetDirectoryName(GetCachePath("DummyValue", regionName));
+ size += CacheSizeHelper(new DirectoryInfo(policyPath));
+ size += CacheSizeHelper(new DirectoryInfo(cachePath));
+ return size;
+ }
+
+ ///
+ /// Helper method for public .
+ ///
+ ///
+ ///
+ private long CacheSizeHelper(DirectoryInfo root)
+ {
+ long size = 0;
+
+ // Add file sizes.
+ var fis = root.EnumerateFiles();
+ foreach (FileInfo fi in fis)
+ {
+ size += fi.Length;
+ }
+ // Add subdirectory sizes.
+ var dis = root.EnumerateDirectories();
+ foreach (DirectoryInfo di in dis)
+ {
+ size += CacheSizeHelper(di);
+ }
+ return size;
+ }
+
+ ///
+ /// Flushes the file cache using DateTime.Now as the minimum date
+ ///
+ ///
+ public void Flush(string regionName = null)
+ {
+ Flush(DateTime.Now, regionName);
+ }
+
+ ///
+ /// Flushes the cache based on last access date, filtered by optional region
+ ///
+ ///
+ ///
+ public void Flush(DateTime minDate, string regionName = null)
+ {
+ // prevent other threads from altering stuff while we delete junk
+ using (FileStream cLock = GetCleaningLock())
+ {
+ if (cLock == null)
+ return;
+
+ //AC note: First parameter is unused, so just pass in garbage ("DummyValue")
+ string policyPath = Path.GetDirectoryName(GetPolicyPath("DummyValue", regionName));
+ string cachePath = Path.GetDirectoryName(GetCachePath("DummyValue", regionName));
+ FlushHelper(new DirectoryInfo(policyPath), minDate);
+ FlushHelper(new DirectoryInfo(cachePath), minDate);
+
+ // Update the Cache size
+ CurrentCacheSize = GetCacheSize();
+
+ // unlock
+ cLock.Close();
+ }
+ }
+
+ ///
+ /// Helper method for public flush
+ ///
+ ///
+ ///
+ private void FlushHelper(DirectoryInfo root, DateTime minDate)
+ {
+ // check files.
+ foreach (FileInfo fi in root.EnumerateFiles())
+ {
+ //is the file stale?
+ if (minDate > File.GetLastAccessTime(fi.FullName))
+ {
+ File.Delete(fi.FullName);
+ }
+ }
+
+ // check subdirectories
+ foreach (DirectoryInfo di in root.EnumerateDirectories())
+ {
+ FlushHelper(di, minDate);
+ }
+ }
+
+ ///
+ /// Returns the policy attached to a given cache item.
+ ///
+ /// The key of the item
+ /// The region in which the key exists
+ ///
+ public CacheItemPolicy GetPolicy(string key, string regionName = null)
+ {
+ CacheItemPolicy policy = new CacheItemPolicy();
+ FileCachePayload payload = ReadFile(PayloadMode.Filename, key, regionName) as FileCachePayload;
+ if (payload != null)
+ {
+ try
+ {
+ policy.SlidingExpiration = payload.Policy.SlidingExpiration;
+ policy.AbsoluteExpiration = payload.Policy.AbsoluteExpiration;
+ }
+ catch (Exception)
+ {
+ }
+ }
+ return policy;
+ }
+
+ ///
+ /// Returns a list of keys for a given region.
+ ///
+ ///
+ ///
+ public IEnumerable GetKeys(string regionName = null)
+ {
+ string region = "";
+ if (string.IsNullOrEmpty(regionName) == false)
+ {
+ region = regionName;
+ }
+ string directory = Path.Combine(CacheDir, _cacheSubFolder, region);
+ if (Directory.Exists(directory))
+ {
+ foreach (string file in Directory.EnumerateFiles(directory))
+ {
+ yield return Path.GetFileNameWithoutExtension(file);
+ }
+ }
+ }
+
+ #endregion
+
+ #region helper methods
+
+ ///
+ /// This function servies to centralize file stream access within this class.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private FileStream GetStream(string path, FileMode mode, FileAccess access, FileShare share)
+ {
+ FileStream stream = null;
+ TimeSpan interval = new TimeSpan(0, 0, 0, 0, 50);
+ TimeSpan totalTime = new TimeSpan();
+ while (stream == null)
+ {
+ try
+ {
+ stream = File.Open(path, mode, access, share);
+ }
+ catch (IOException ex)
+ {
+ Thread.Sleep(interval);
+ totalTime += interval;
+
+ //if we've waited too long, throw the original exception.
+ if (AccessTimeout.Ticks != 0)
+ {
+ if (totalTime > AccessTimeout)
+ {
+ throw ex;
+ }
+ }
+ }
+ }
+ return stream;
+ }
+
+ ///
+ /// This function serves to centralize file reads within this class.
+ ///
+ /// the payload reading mode
+ ///
+ ///
+ ///
+ private FileCachePayload ReadFile(PayloadMode mode, string key, string regionName = null, SerializationBinder objectBinder = null)
+ {
+ object data = null;
+ SerializableCacheItemPolicy policy = new SerializableCacheItemPolicy();
+ string cachePath = GetCachePath(key, regionName);
+ string policyPath = GetPolicyPath(key, regionName);
+ FileCachePayload payload = new FileCachePayload(null);
+
+ if (File.Exists(cachePath))
+ {
+ switch (mode)
+ {
+ default:
+ case PayloadMode.Filename:
+ data = cachePath;
+ break;
+ case PayloadMode.Serializable:
+ data = DeserializePayloadData(objectBinder, cachePath);
+ break;
+ case PayloadMode.RawBytes:
+ data = LoadRawPayloadData(cachePath);
+ break;
+ }
+ }
+ if (File.Exists(policyPath))
+ {
+ using (FileStream stream = GetStream(policyPath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+ formatter.Binder = new LocalCacheBinder();
+ try
+ {
+ policy = formatter.Deserialize(stream) as SerializableCacheItemPolicy;
+ }
+ catch (SerializationException)
+ {
+ policy = new SerializableCacheItemPolicy();
+ }
+ }
+ }
+ payload.Payload = data;
+ payload.Policy = policy;
+ return payload;
+ }
+
+ private object LoadRawPayloadData(string cachePath)
+ {
+ throw new NotSupportedException("Reading raw payload is not currently supported.");
+ }
+
+ private object DeserializePayloadData(SerializationBinder objectBinder, string cachePath)
+ {
+ object data;
+ using (FileStream stream = GetStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+
+ //AC: From http://spazzarama.com//2009/06/25/binary-deserialize-unable-to-find-assembly/
+ // Needed to deserialize custom objects
+ if (objectBinder != null)
+ {
+ //take supplied binder over default binder
+ formatter.Binder = objectBinder;
+ }
+ else if (_binder != null)
+ {
+ formatter.Binder = _binder;
+ }
+
+ try
+ {
+ data = formatter.Deserialize(stream);
+ }
+ catch (SerializationException)
+ {
+ data = null;
+ }
+ }
+
+ return data;
+ }
+
+ ///
+ /// This function serves to centralize file writes within this class
+ ///
+ private void WriteFile(PayloadMode mode, string key, FileCachePayload data, string regionName = null, bool policyUpdateOnly = false)
+ {
+ string cachedPolicy = GetPolicyPath(key, regionName);
+ string cachedItemPath = GetCachePath(key, regionName);
+
+
+ if (!policyUpdateOnly)
+ {
+ long oldBlobSize = 0;
+ if (File.Exists(cachedItemPath))
+ {
+ oldBlobSize = new FileInfo(cachedItemPath).Length;
+ }
+
+ switch (mode)
+ {
+ case PayloadMode.Serializable:
+ using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+
+ BinaryFormatter formatter = new BinaryFormatter();
+ formatter.Serialize(stream, data.Payload);
+ }
+ break;
+ case PayloadMode.RawBytes:
+ using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+
+ if (data.Payload is byte[])
+ {
+ byte[] dataPayload = (byte[])data.Payload;
+ stream.Write(dataPayload, 0, dataPayload.Length);
+ }
+ else if (data.Payload is Stream)
+ {
+ Stream dataPayload = (Stream)data.Payload;
+ dataPayload.CopyTo(stream);
+ // no close or the like, we are not the owner
+ }
+ }
+ break;
+
+ case PayloadMode.Filename:
+ File.Copy((string)data.Payload, cachedItemPath, true);
+ break;
+ }
+
+ //adjust cache size (while we have the file to ourselves)
+ CurrentCacheSize += new FileInfo(cachedItemPath).Length - oldBlobSize;
+ }
+
+ //remove current policy file from cache size calculations
+ if (File.Exists(cachedPolicy))
+ {
+ CurrentCacheSize -= new FileInfo(cachedPolicy).Length;
+ }
+
+ //write the cache policy
+ using (FileStream stream = GetStream(cachedPolicy, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+ formatter.Serialize(stream, data.Policy);
+
+ // adjust cache size
+ CurrentCacheSize += new FileInfo(cachedPolicy).Length;
+
+ stream.Close();
+ }
+
+ //check to see if limit was reached
+ if (CurrentCacheSize > MaxCacheSize)
+ {
+ MaxCacheSizeReached(this, new FileCacheEventArgs(CurrentCacheSize, MaxCacheSize));
+ }
+ }
+
+ ///
+ /// Reads data in from a system file. System files are not part of the
+ /// cache itself, but serve as a way for the cache to store data it
+ /// needs to operate.
+ ///
+ /// The name of the sysfile (without directory)
+ /// The data from the file
+ private object ReadSysFile(string filename)
+ {
+ // sys files go in the root directory
+ string path = Path.Combine(CacheDir, filename);
+ object data = null;
+
+ if (File.Exists(path))
+ {
+ for (int i = 5; i > 0; i--) // try 5 times to read the file, if we can't, give up
+ {
+ try
+ {
+ using (FileStream stream = GetStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+ try
+ {
+ data = formatter.Deserialize(stream);
+ }
+ catch (Exception)
+ {
+ data = null;
+ }
+ finally
+ {
+ stream.Close();
+ }
+ }
+ break;
+ }
+ catch (IOException)
+ {
+ // we timed out... so try again
+ }
+ }
+ }
+
+ return data;
+ }
+
+ ///
+ /// Writes data to a system file that is not part of the cache itself,
+ /// but is used to help it function.
+ ///
+ /// The name of the sysfile (without directory)
+ /// The data to write to the file
+ private void WriteSysFile(string filename, object data)
+ {
+ // sys files go in the root directory
+ string path = Path.Combine(CacheDir, filename);
+
+ // write the data to the file
+ using (FileStream stream = GetStream(path, FileMode.Create, FileAccess.Write, FileShare.Write))
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+ formatter.Serialize(stream, data);
+ stream.Close();
+ }
+ }
+
+ ///
+ /// Builds a string that will place the specified file name within the appropriate
+ /// cache and workspace folder.
+ ///
+ ///
+ ///
+ ///
+ private string GetCachePath(string FileName, string regionName = null)
+ {
+ if (regionName == null)
+ {
+ regionName = "";
+ }
+ string directory = Path.Combine(CacheDir, _cacheSubFolder, regionName);
+ string filePath = Path.Combine(directory, Path.GetFileNameWithoutExtension(FileName) + ".dat");
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ return filePath;
+ }
+
+ ///
+ /// Builds a string that will get the path to the supplied file's policy file
+ ///
+ ///
+ ///
+ ///
+ private string GetPolicyPath(string FileName, string regionName = null)
+ {
+ if (regionName == null)
+ {
+ regionName = "";
+ }
+ string directory = Path.Combine(CacheDir, _policySubFolder, regionName);
+ string filePath = Path.Combine(directory, Path.GetFileNameWithoutExtension(FileName) + ".policy");
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ return filePath;
+ }
+
+ #endregion
+
+ #region ObjectCache overrides
+
+ public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null)
+ {
+ string path = GetCachePath(key, regionName);
+ object oldData = null;
+
+ //pull old value if it exists
+ if (File.Exists(path))
+ {
+ try
+ {
+ oldData = Get(key, regionName);
+ }
+ catch (Exception)
+ {
+ oldData = null;
+ }
+ }
+ SerializableCacheItemPolicy cachePolicy = new SerializableCacheItemPolicy(policy);
+ FileCachePayload newPayload = new FileCachePayload(value, cachePolicy);
+ WriteFile(PayloadWriteMode, key, newPayload, regionName);
+
+ //As documented in the spec (http://msdn.microsoft.com/en-us/library/dd780602.aspx), return the old
+ //cached value or null
+ return oldData;
+ }
+
+ public override CacheItem AddOrGetExisting(CacheItem value, CacheItemPolicy policy)
+ {
+ object oldData = AddOrGetExisting(value.Key, value.Value, policy, value.RegionName);
+ CacheItem returnItem = null;
+ if (oldData != null)
+ {
+ returnItem = new CacheItem(value.Key)
+ {
+ Value = oldData,
+ RegionName = value.RegionName
+ };
+ }
+ return returnItem;
+ }
+
+ public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
+ {
+ CacheItemPolicy policy = new CacheItemPolicy();
+ policy.AbsoluteExpiration = absoluteExpiration;
+ return AddOrGetExisting(key, value, policy, regionName);
+ }
+
+ public override bool Contains(string key, string regionName = null)
+ {
+ string path = GetCachePath(key, regionName);
+ return File.Exists(path);
+ }
+
+ public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable keys, string regionName = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override DefaultCacheCapabilities DefaultCacheCapabilities
+ {
+ get
+ {
+ //AC note: can use boolean OR "|" to set multiple flags.
+ return DefaultCacheCapabilities.CacheRegions
+ |
+ DefaultCacheCapabilities.AbsoluteExpirations
+ |
+ DefaultCacheCapabilities.SlidingExpirations
+ ;
+ }
+ }
+
+ public override object Get(string key, string regionName = null)
+ {
+ FileCachePayload payload = ReadFile(PayloadReadMode, key, regionName) as FileCachePayload;
+ string cachedItemPath = GetCachePath(key, regionName);
+
+ DateTime cutoff = DateTime.Now;
+ if (PayloadReadMode == PayloadMode.Filename)
+ {
+ cutoff += FilenameAsPayloadSafetyMargin;
+ }
+
+ //null payload?
+ if (payload != null)
+ {
+ //did the item expire?
+ if (payload.Policy.AbsoluteExpiration < cutoff)
+ {
+ //set the payload to null
+ payload.Payload = null;
+
+ //delete the file from the cache
+ try
+ {
+ // CT Note: I changed this to Remove from File.Delete so that the coresponding
+ // policy file will be deleted as well, and CurrentCacheSize will be updated.
+ Remove(key, regionName);
+ }
+ catch (Exception)
+ {
+ }
+ }
+ else
+ {
+ //does the item have a sliding expiration?
+ if (payload.Policy.SlidingExpiration > new TimeSpan())
+ {
+ payload.Policy.AbsoluteExpiration = DateTime.Now.Add(payload.Policy.SlidingExpiration);
+ WriteFile(PayloadWriteMode, cachedItemPath, payload, regionName, true);
+ }
+
+ }
+ }
+ else
+ {
+ //remove null payload
+ Remove(key, regionName);
+
+ //create dummy one for return
+ payload = new FileCachePayload(null);
+ }
+ return payload.Payload;
+ }
+
+ public override CacheItem GetCacheItem(string key, string regionName = null)
+ {
+ object value = Get(key, regionName);
+ CacheItem item = new CacheItem(key);
+ item.Value = value;
+ item.RegionName = regionName;
+ return item;
+ }
+
+ public override long GetCount(string regionName = null)
+ {
+ if (regionName == null)
+ {
+ regionName = "";
+ }
+ string path = Path.Combine(CacheDir, _cacheSubFolder, regionName);
+ if (Directory.Exists(path))
+ return Directory.GetFiles(path).Count();
+ else
+ return 0;
+ }
+
+ ///
+ /// Returns an enumerator for the specified region (defaults to base-level cache directory).
+ /// This function *WILL NOT* recursively locate files in subdirectories.
+ ///
+ ///
+ ///
+ public IEnumerator> GetEnumerator(string regionName = null)
+ {
+ string region = "";
+ if (string.IsNullOrEmpty(regionName) == false)
+ {
+ region = regionName;
+ }
+ List> enumerator = new List>();
+
+ string directory = Path.Combine(CacheDir, _cacheSubFolder, region);
+ foreach (string filePath in Directory.EnumerateFiles(directory))
+ {
+ string key = Path.GetFileNameWithoutExtension(filePath);
+ enumerator.Add(new KeyValuePair(key, this.Get(key, regionName)));
+ }
+ return enumerator.GetEnumerator();
+ }
+
+ ///
+ /// Will return an enumerator with all cache items listed in the root file path ONLY. Use the other
+ /// if you want to specify a region
+ ///
+ ///
+ protected override IEnumerator> GetEnumerator()
+ {
+ return GetEnumerator(null);
+ }
+
+ public override IDictionary GetValues(IEnumerable keys, string regionName = null)
+ {
+ Dictionary values = new Dictionary();
+ foreach (string key in keys)
+ {
+ values[key] = Get(key, regionName);
+ }
+ return values;
+ }
+
+ public override string Name
+ {
+ get { return _name; }
+ }
+
+ public override object Remove(string key, string regionName = null)
+ {
+ object valueToDelete = null;
+ if (Contains(key, regionName))
+ {
+ // Because of the possibility of multiple threads accessing this, it's possible that
+ // while we're trying to remove something, another thread has already removed it.
+ try
+ {
+ //remove cache entry
+ // CT note: calling Get from remove leads to an infinite loop and stack overflow,
+ // so I replaced it with a simple ReadFile call. None of the code here actually
+ // uses this object returned, but just in case someone else's outside code does.
+ FileCachePayload fcp = ReadFile(PayloadMode.Filename, key, regionName);
+ valueToDelete = fcp.Payload;
+ string path = GetCachePath(key, regionName);
+ CurrentCacheSize -= new FileInfo(path).Length;
+ File.Delete(path);
+
+ //remove policy file
+ string cachedPolicy = GetPolicyPath(key, regionName);
+ CurrentCacheSize -= new FileInfo(cachedPolicy).Length;
+ File.Delete(cachedPolicy);
+ }
+ catch (IOException)
+ {
+ }
+
+ }
+ return valueToDelete;
+ }
+
+ public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null)
+ {
+ Add(key, value, policy, regionName);
+ }
+
+ public override void Set(CacheItem item, CacheItemPolicy policy)
+ {
+ Add(item, policy);
+ }
+
+ public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
+ {
+ Add(key, value, absoluteExpiration, regionName);
+ }
+
+ public override object this[string key]
+ {
+ get
+ {
+ return this.Get(key, DefaultRegion);
+ }
+ set
+ {
+ this.Set(key, value, DefaultPolicy, DefaultRegion);
+ }
+ }
+
+ #endregion
+
+ private class LocalCacheBinder : System.Runtime.Serialization.SerializationBinder
+ {
+ public override Type BindToType(string assemblyName, string typeName)
+ {
+ Type typeToDeserialize = null;
+
+ String currentAssembly = Assembly.GetAssembly(typeof(LocalCacheBinder)).FullName;
+ assemblyName = currentAssembly;
+
+ // Get the type using the typeName and assemblyName
+ typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
+ typeName, assemblyName));
+
+ return typeToDeserialize;
+ }
+ }
+
+ // CT: This private class is used to help shrink the cache.
+ // It computes the total size of an entry including it's policy file.
+ // It also implements IComparable functionality to allow for sorting based on access time
+ private class CacheItemReference : IComparable
+ {
+ public readonly DateTime LastAccessTime;
+ public readonly long Length;
+ public readonly string Key;
+
+ public CacheItemReference(string key, string cachePath, string policyPath)
+ {
+ Key = key;
+ FileInfo cfi = new FileInfo(cachePath);
+ FileInfo pfi = new FileInfo(policyPath);
+ cfi.Refresh();
+ LastAccessTime = cfi.LastAccessTime;
+ Length = cfi.Length + pfi.Length;
+ }
+
+ public int CompareTo(CacheItemReference other)
+ {
+ int i = LastAccessTime.CompareTo(other.LastAccessTime);
+
+ // It's possible, although rare, that two different items will have
+ // the same LastAccessTime. So in that case, we need to check to see
+ // if they're actually the same.
+ if (i == 0)
+ {
+ // second order should be length (but from smallest to largest,
+ // that way we delete smaller files first)
+ i = -1 * Length.CompareTo(other.Length);
+ if (i == 0)
+ {
+ i = Key.CompareTo(other.Key);
+ }
+ }
+
+ return i;
+ }
+
+ public static bool operator >(CacheItemReference lhs, CacheItemReference rhs)
+ {
+ if (lhs.CompareTo(rhs) > 0)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public static bool operator <(CacheItemReference lhs, CacheItemReference rhs)
+ {
+ if (lhs.CompareTo(rhs) < 0)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/Caching/FileCacheBinder.cs b/src/GitHub.Api/Caching/FileCacheBinder.cs
new file mode 100644
index 0000000000..dd03649603
--- /dev/null
+++ b/src/GitHub.Api/Caching/FileCacheBinder.cs
@@ -0,0 +1,34 @@
+/*
+Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
+
+This file is part of FileCache (http://github.com/acarteas/FileCache).
+
+FileCache is distributed under the Apache License 2.0.
+Consult "LICENSE.txt" included in this package for the Apache License 2.0.
+*/
+using System.Reflection;
+
+namespace System.Runtime.Caching
+{
+ ///
+ /// You should be able to copy & paste this code into your local project to enable caching custom objects.
+ ///
+ public sealed class FileCacheBinder : System.Runtime.Serialization.SerializationBinder
+ {
+ public override Type BindToType(string assemblyName, string typeName)
+ {
+ Type typeToDeserialize = null;
+
+ String currentAssembly = Assembly.GetExecutingAssembly().FullName;
+
+ // In this case we are always using the current assembly
+ assemblyName = currentAssembly;
+
+ // Get the type using the typeName and assemblyName
+ typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
+ typeName, assemblyName));
+
+ return typeToDeserialize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/Caching/FileCacheEventArgs.cs b/src/GitHub.Api/Caching/FileCacheEventArgs.cs
new file mode 100644
index 0000000000..917ff89e95
--- /dev/null
+++ b/src/GitHub.Api/Caching/FileCacheEventArgs.cs
@@ -0,0 +1,22 @@
+/*
+Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
+
+This file is part of FileCache (http://github.com/acarteas/FileCache).
+
+FileCache is distributed under the Apache License 2.0.
+Consult "LICENSE.txt" included in this package for the Apache License 2.0.
+*/
+
+namespace System.Runtime.Caching
+{
+ public class FileCacheEventArgs : EventArgs
+ {
+ public long CurrentCacheSize { get; private set; }
+ public long MaxCacheSize { get; private set; }
+ public FileCacheEventArgs(long currentSize, long maxSize)
+ {
+ CurrentCacheSize = currentSize;
+ MaxCacheSize = maxSize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/Caching/FileCachePayload.cs b/src/GitHub.Api/Caching/FileCachePayload.cs
new file mode 100644
index 0000000000..1361e6f663
--- /dev/null
+++ b/src/GitHub.Api/Caching/FileCachePayload.cs
@@ -0,0 +1,33 @@
+/*
+Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
+
+This file is part of FileCache (http://github.com/acarteas/FileCache).
+
+FileCache is distributed under the Apache License 2.0.
+Consult "LICENSE.txt" included in this package for the Apache License 2.0.
+*/
+
+namespace System.Runtime.Caching
+{
+ [Serializable]
+ public class FileCachePayload
+ {
+ public object Payload { get; set; }
+ public SerializableCacheItemPolicy Policy { get; set; }
+
+ public FileCachePayload(object payload)
+ {
+ Payload = payload;
+ Policy = new SerializableCacheItemPolicy()
+ {
+ AbsoluteExpiration = DateTime.Now.AddYears(10)
+ };
+ }
+
+ public FileCachePayload(object payload, SerializableCacheItemPolicy policy)
+ {
+ Payload = payload;
+ Policy = policy;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/Caching/PriortyQueue.cs b/src/GitHub.Api/Caching/PriortyQueue.cs
new file mode 100644
index 0000000000..cda897b9d5
--- /dev/null
+++ b/src/GitHub.Api/Caching/PriortyQueue.cs
@@ -0,0 +1,207 @@
+/*
+Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
+
+This file is part of FileCache (http://github.com/acarteas/FileCache).
+
+FileCache is distributed under the Apache License 2.0.
+Consult "LICENSE.txt" included in this package for the Apache License 2.0.
+*/
+using System.Collections.Generic;
+
+namespace System.Runtime.Caching
+{
+ ///
+ /// A basic min priorty queue (min heap)
+ ///
+ /// Data type to store
+ public class PriortyQueue where T : IComparable
+ {
+
+ private List _items;
+ private IComparer _comparer;
+
+ ///
+ /// Default constructor.
+ ///
+ /// The comparer to use. The default comparer will make the smallest item the root of the heap.
+ ///
+ ///
+ public PriortyQueue(IComparer comparer = null)
+ {
+ _items = new List();
+ if (comparer == null)
+ {
+ _comparer = new GenericComparer();
+ }
+ }
+
+ ///
+ /// Constructor that will convert an existing list into a min heap
+ ///
+ /// The unsorted list of items
+ /// The comparer to use. The default comparer will make the smallest item the root of the heap.
+ public PriortyQueue(List unsorted, IComparer comparer = null)
+ : this(comparer)
+ {
+ for (int i = 0; i < unsorted.Count; i++)
+ {
+ _items.Add(unsorted[i]);
+ }
+ BuildHeap();
+ }
+
+ private void BuildHeap()
+ {
+ for (int i = _items.Count / 2; i >= 0; i--)
+ {
+ adjustHeap(i);
+ }
+ }
+
+ //Percolates the item specified at by index down into its proper location within a heap. Used
+ //for dequeue operations and array to heap conversions
+ private void adjustHeap(int index)
+ {
+ //cannot percolate empty list
+ if (_items.Count == 0)
+ {
+ return;
+ }
+
+ //GOAL: get value at index, make sure this value is less than children
+ // IF NOT: swap with smaller of two
+ // (continue to do so until we can't swap)
+ T item = _items[index];
+
+ //helps us figure out if a given index has children
+ int end_location = _items.Count;
+
+ //keeps track of smallest index
+ int smallest_index = index;
+
+ //while we're not the last thing in the heap
+ while (index < end_location)
+ {
+ //get left child index
+ int left_child_index = (2 * index) + 1;
+ int right_child_index = left_child_index + 1;
+
+ //Three cases:
+ // 1. left index is out of range
+ // 2. right index is out or range
+ // 3. both indices are valid
+ if (left_child_index < end_location)
+ {
+ //CASE 1 is FALSE
+ //remember that left index is the smallest
+ smallest_index = left_child_index;
+
+ if (right_child_index < end_location)
+ {
+ //CASE 2 is FALSE (CASE 3 is true)
+ //TODO: find value of smallest index
+ smallest_index = (_comparer.Compare(_items[left_child_index], _items[right_child_index]) < 0)
+ ? left_child_index
+ : right_child_index;
+ }
+ }
+
+ //we have two things: original index and (potentially) a child index
+ if (_comparer.Compare(_items[index], _items[smallest_index]) > 0)
+ {
+ //move parent down (it was too big)
+ T temp = _items[index];
+ _items[index] = _items[smallest_index];
+ _items[smallest_index] = temp;
+
+ //update index
+ index = smallest_index;
+ }
+ else
+ {
+ //no swap necessary
+ break;
+ }
+ }
+ }
+
+ public bool isEmpty()
+ {
+ return _items.Count == 0;
+ }
+
+ public int GetSize()
+ {
+ return _items.Count;
+ }
+
+
+ public void Enqueue(T item)
+ {
+ //calculate positions
+ int current_position = _items.Count;
+ int parent_position = (current_position - 1) / 2;
+
+ //insert element (note: may get erased if we hit the WHILE loop)
+ _items.Add(item);
+
+ //find parent, but be careful if we are an empty queue
+ T parent = default(T);
+ if (parent_position >= 0)
+ {
+ //find parent
+ parent = _items[parent_position];
+
+ //bubble up until we're done
+ while (_comparer.Compare(parent, item) > 0 && current_position > 0)
+ {
+ //move parent down
+ _items[current_position] = parent;
+
+ //recalculate position
+ current_position = parent_position;
+ parent_position = (current_position - 1) / 2;
+
+ //make sure that we have a valid index
+ if (parent_position >= 0)
+ {
+ //find parent
+ parent = _items[parent_position];
+ }
+ }
+ } //end check for nullptr
+
+ //after WHILE loop, current_position will point to the place that
+ //variable "item" needs to go
+ _items[current_position] = item;
+
+ }
+
+ public T GetFirst()
+ {
+ return _items[0];
+ }
+
+ public T Dequeue()
+ {
+ int last_position = _items.Count - 1;
+ T last_item = _items[last_position];
+ T top = _items[0];
+ _items[0] = last_item;
+ _items.RemoveAt(_items.Count - 1);
+
+ //percolate down
+ adjustHeap(0);
+ return top;
+ }
+
+
+ private class GenericComparer : IComparer where TInner : IComparable
+ {
+ public int Compare(TInner x, TInner y)
+ {
+ return x.CompareTo(y);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/Caching/SerializableCacheItemPolicy.cs b/src/GitHub.Api/Caching/SerializableCacheItemPolicy.cs
new file mode 100644
index 0000000000..a3a22f5c54
--- /dev/null
+++ b/src/GitHub.Api/Caching/SerializableCacheItemPolicy.cs
@@ -0,0 +1,44 @@
+/*
+Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
+
+This file is part of FileCache (http://github.com/acarteas/FileCache).
+
+FileCache is distributed under the Apache License 2.0.
+Consult "LICENSE.txt" included in this package for the Apache License 2.0.
+*/
+
+namespace System.Runtime.Caching
+{
+ [Serializable]
+ public class SerializableCacheItemPolicy
+ {
+ public DateTimeOffset AbsoluteExpiration { get; set; }
+
+ private TimeSpan _slidingExpiration;
+ public TimeSpan SlidingExpiration
+ {
+ get
+ {
+ return _slidingExpiration;
+ }
+ set
+ {
+ _slidingExpiration = value;
+ if (_slidingExpiration > new TimeSpan())
+ {
+ AbsoluteExpiration = DateTimeOffset.Now.Add(_slidingExpiration);
+ }
+ }
+ }
+ public SerializableCacheItemPolicy(CacheItemPolicy policy)
+ {
+ AbsoluteExpiration = policy.AbsoluteExpiration;
+ SlidingExpiration = policy.SlidingExpiration;
+ }
+
+ public SerializableCacheItemPolicy()
+ {
+ SlidingExpiration = new TimeSpan();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj
index 6ff35a4ff0..421201ab15 100644
--- a/src/GitHub.Api/GitHub.Api.csproj
+++ b/src/GitHub.Api/GitHub.Api.csproj
@@ -4,22 +4,53 @@
fulltrue
-
+
+
+
+
+ 2454a3e6102fd41cc212
+ 2157c138e970165d955d09562230afcfbcda23f2
+
+
-
-
-
- ApiClientConfiguration_User.cs
-
-
-
+
+
+
+ $(IntermediateOutputPath)ApiClientConfiguration.$(GitHubVS_ClientId).cs
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -30,6 +61,5 @@
-
diff --git a/src/GitHub.Api/GlobalSuppressions.cs b/src/GitHub.Api/GlobalSuppressions.cs
new file mode 100644
index 0000000000..3753e44dee
--- /dev/null
+++ b/src/GitHub.Api/GlobalSuppressions.cs
@@ -0,0 +1,9 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "Discouraged for VSSDK projects.")]
+
diff --git a/src/GitHub.Api/GraphQLClient.cs b/src/GitHub.Api/GraphQLClient.cs
new file mode 100644
index 0000000000..155bd2bd07
--- /dev/null
+++ b/src/GitHub.Api/GraphQLClient.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Caching;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using GitHub.Extensions;
+using Octokit.GraphQL;
+using Octokit.GraphQL.Core;
+
+namespace GitHub.Api
+{
+ public class GraphQLClient : IGraphQLClient
+ {
+ public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromHours(8);
+ readonly IConnection connection;
+ readonly FileCache cache;
+
+ public GraphQLClient(
+ IConnection connection,
+ FileCache cache)
+ {
+ this.connection = connection;
+ this.cache = cache;
+ }
+
+ public Task ClearCache(string regionName)
+ {
+ // Switch to background thread because FileCache does not provide an async API.
+ return Task.Run(() => cache.ClearRegion(GetFullRegionName(regionName)));
+ }
+
+ public Task Run(
+ IQueryableValue query,
+ Dictionary variables = null,
+ bool refresh = false,
+ TimeSpan? cacheDuration = null,
+ string regionName = null,
+ CancellationToken cancellationToken = default)
+ {
+ return Run(query.Compile(), variables, refresh, cacheDuration, regionName, cancellationToken);
+ }
+
+ public Task> Run(
+ IQueryableList query,
+ Dictionary variables = null,
+ bool refresh = false,
+ TimeSpan? cacheDuration = null,
+ string regionName = null,
+ CancellationToken cancellationToken = default)
+ {
+ return Run(query.Compile(), variables, refresh, cacheDuration, regionName, cancellationToken);
+ }
+
+ public async Task Run(
+ ICompiledQuery query,
+ Dictionary variables = null,
+ bool refresh = false,
+ TimeSpan? cacheDuration = null,
+ string regionName = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (!query.IsMutation)
+ {
+ var wrapper = new CachingWrapper(
+ this,
+ refresh,
+ cacheDuration ?? DefaultCacheDuration,
+ GetFullRegionName(regionName));
+ return await wrapper.Run(query, variables, cancellationToken);
+ }
+ else
+ {
+ return await connection.Run(query, variables, cancellationToken);
+ }
+ }
+
+ string GetFullRegionName(string regionName)
+ {
+ var result = connection.Uri.Host;
+
+ if (!string.IsNullOrWhiteSpace(regionName))
+ {
+ result += Path.DirectorySeparatorChar + regionName;
+ }
+
+ return result.EnsureValidPath();
+ }
+
+ static string GetHash(string input)
+ {
+ var sb = new StringBuilder();
+
+ using (var hash = SHA256.Create())
+ {
+ var result = hash.ComputeHash(Encoding.UTF8.GetBytes(input));
+
+ foreach (var b in result)
+ {
+ sb.Append(b.ToString("x2", CultureInfo.InvariantCulture));
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ class CachingWrapper : IConnection
+ {
+ readonly GraphQLClient owner;
+ readonly bool refresh;
+ readonly TimeSpan cacheDuration;
+ readonly string regionName;
+
+ public CachingWrapper(
+ GraphQLClient owner,
+ bool refresh,
+ TimeSpan cacheDuration,
+ string regionName)
+ {
+ this.owner = owner;
+ this.refresh = refresh;
+ this.cacheDuration = cacheDuration;
+ this.regionName = regionName;
+ }
+
+ public Uri Uri => owner.connection.Uri;
+
+ public Task Run(string query, CancellationToken cancellationToken = default)
+ {
+ // Switch to background thread because FileCache does not provide an async API.
+ return Task.Run(async () =>
+ {
+ var hash = GetHash(query);
+
+ if (refresh)
+ {
+ owner.cache.Remove(hash, regionName);
+ }
+
+ var data = (string) owner.cache.Get(hash, regionName);
+
+ if (data != null)
+ {
+ return data;
+ }
+
+ var result = await owner.connection.Run(query, cancellationToken);
+ owner.cache.Add(hash, result, DateTimeOffset.Now + cacheDuration, regionName);
+ return result;
+ }, cancellationToken);
+ }
+ }
+ }
+}
diff --git a/src/GitHub.Api/GraphQLClientFactory.cs b/src/GitHub.Api/GraphQLClientFactory.cs
index cd91295935..635467a845 100644
--- a/src/GitHub.Api/GraphQLClientFactory.cs
+++ b/src/GitHub.Api/GraphQLClientFactory.cs
@@ -1,6 +1,9 @@
using System;
using System.ComponentModel.Composition;
+using System.IO;
+using System.Runtime.Caching;
using System.Threading.Tasks;
+using GitHub.Info;
using GitHub.Models;
using GitHub.Primitives;
using Octokit.GraphQL;
@@ -17,6 +20,7 @@ public class GraphQLClientFactory : IGraphQLClientFactory
{
readonly IKeychain keychain;
readonly IProgram program;
+ readonly FileCache cache;
///
/// Initializes a new instance of the class.
@@ -28,14 +32,21 @@ public GraphQLClientFactory(IKeychain keychain, IProgram program)
{
this.keychain = keychain;
this.program = program;
+
+ var cachePath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ ApplicationInfo.ApplicationName,
+ "GraphQLCache");
+ cache = new FileCache(cachePath);
}
///
- public Task CreateConnection(HostAddress address)
+ public Task CreateConnection(HostAddress address)
{
var credentials = new GraphQLKeychainCredentialStore(keychain, address);
var header = new ProductHeaderValue(program.ProductHeader.Name, program.ProductHeader.Version);
- return Task.FromResult(new Connection(header, address.GraphQLUri, credentials));
+ var connection = new Connection(header, address.GraphQLUri, credentials);
+ return Task.FromResult(new GraphQLClient(connection, cache));
}
}
}
diff --git a/src/GitHub.Api/IGraphQLClient.cs b/src/GitHub.Api/IGraphQLClient.cs
new file mode 100644
index 0000000000..d45062c6b4
--- /dev/null
+++ b/src/GitHub.Api/IGraphQLClient.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Octokit.GraphQL;
+using Octokit.GraphQL.Core;
+
+namespace GitHub.Api
+{
+ public interface IGraphQLClient
+ {
+ Task ClearCache(string regionName);
+
+ Task Run(
+ IQueryableValue query,
+ Dictionary variables = null,
+ bool refresh = false,
+ TimeSpan? cacheDuration = null,
+ string regionName = null,
+ CancellationToken cancellationToken = default);
+
+ Task> Run(
+ IQueryableList query,
+ Dictionary variables = null,
+ bool refresh = false,
+ TimeSpan? cacheDuration = null,
+ string regionName = null,
+ CancellationToken cancellationToken = default);
+
+ Task Run(
+ ICompiledQuery query,
+ Dictionary variables = null,
+ bool refresh = false,
+ TimeSpan? cacheDuration = null,
+ string regionName = null,
+ CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.Api/IGraphQLClientFactory.cs b/src/GitHub.Api/IGraphQLClientFactory.cs
index 464fab0de8..f29fba4b7f 100644
--- a/src/GitHub.Api/IGraphQLClientFactory.cs
+++ b/src/GitHub.Api/IGraphQLClientFactory.cs
@@ -4,16 +4,15 @@
namespace GitHub.Api
{
///
- /// Creates GraphQL s for querying the
- /// GitHub GraphQL API.
+ /// Creates s for querying the GitHub GraphQL API.
///
public interface IGraphQLClientFactory
{
///
- /// Creates a new .
+ /// Creates a new .
///
/// The address of the server.
- /// A task returning the created connection.
- Task CreateConnection(HostAddress address);
+ /// A task returning the created client.
+ Task CreateConnection(HostAddress address);
}
}
\ No newline at end of file
diff --git a/src/GitHub.Api/IOAuthCallbackListener.cs b/src/GitHub.Api/IOAuthCallbackListener.cs
index 4c8d29b85c..19a7c8f428 100644
--- a/src/GitHub.Api/IOAuthCallbackListener.cs
+++ b/src/GitHub.Api/IOAuthCallbackListener.cs
@@ -16,5 +16,11 @@ public interface IOAuthCallbackListener
/// A cancellation token.
/// The temporary code included in the callback.
Task Listen(string id, CancellationToken cancel);
+
+ ///
+ /// Redirects the last context to respond with listen and stops the underlying http listener
+ ///
+ /// Url to redirect to.
+ void RedirectLastContext(Uri url);
}
}
diff --git a/src/GitHub.Api/LoginManager.cs b/src/GitHub.Api/LoginManager.cs
index 0ffd81a77d..bdfe84c2c9 100644
--- a/src/GitHub.Api/LoginManager.cs
+++ b/src/GitHub.Api/LoginManager.cs
@@ -157,6 +157,8 @@ public async Task LoginViaOAuth(
await keychain.Save("[oauth]", token.AccessToken, hostAddress).ConfigureAwait(false);
var result = await ReadUserWithRetry(client).ConfigureAwait(false);
await keychain.Save(result.User.Login, token.AccessToken, hostAddress).ConfigureAwait(false);
+ oauthListener.RedirectLastContext(hostAddress.WebUri.Append(result.User.Login));
+
return result;
}
@@ -289,7 +291,7 @@ async Task HandleTwoFactorAuthorization(
}
}
- ApplicationAuthorization EnsureNonNullAuthorization(ApplicationAuthorization auth)
+ static ApplicationAuthorization EnsureNonNullAuthorization(ApplicationAuthorization auth)
{
// If a mock IGitHubClient is not set up correctly, it can return null from
// IGutHubClient.Authorization.Create - this will cause an infinite loop in Login()
@@ -344,9 +346,14 @@ async Task GetUserAndCheckScopes(IGitHubClient client)
var response = await client.Connection.Get(
UserEndpoint, null, null).ConfigureAwait(false);
- if (response.HttpResponse.Headers.ContainsKey(ScopesHeader))
+ var scopes = response.HttpResponse.Headers
+ .Where(h => string.Equals(h.Key, ScopesHeader, StringComparison.OrdinalIgnoreCase))
+ .Select(h => h.Value)
+ .FirstOrDefault();
+
+ if (scopes != null)
{
- var returnedScopes = new ScopesCollection(response.HttpResponse.Headers[ScopesHeader]
+ var returnedScopes = new ScopesCollection(scopes
.Split(',')
.Select(x => x.Trim())
.ToArray());
diff --git a/src/GitHub.App/Api/ApiClient.cs b/src/GitHub.App/Api/ApiClient.cs
index f9fe4055ee..56257e82a3 100644
--- a/src/GitHub.App/Api/ApiClient.cs
+++ b/src/GitHub.App/Api/ApiClient.cs
@@ -18,7 +18,6 @@ namespace GitHub.Api
{
public partial class ApiClient : IApiClient
{
- const string ScopesHeader = "X-OAuth-Scopes";
const string ProductName = Info.ApplicationInfo.ApplicationDescription;
static readonly ILogger log = LogManager.ForContext();
diff --git a/src/GitHub.App/Authentication/TwoFactorChallengeHandler.cs b/src/GitHub.App/Authentication/TwoFactorChallengeHandler.cs
index 74214a1741..1c0fbb3cb9 100644
--- a/src/GitHub.App/Authentication/TwoFactorChallengeHandler.cs
+++ b/src/GitHub.App/Authentication/TwoFactorChallengeHandler.cs
@@ -6,9 +6,11 @@
using ReactiveUI;
using System.Threading.Tasks;
using GitHub.Api;
-using GitHub.Helpers;
using GitHub.Extensions;
using GitHub.ViewModels.Dialog;
+using Microsoft.VisualStudio.Threading;
+using Microsoft.VisualStudio.Shell;
+using Task = System.Threading.Tasks.Task;
namespace GitHub.Authentication
{
@@ -17,6 +19,12 @@ namespace GitHub.Authentication
[PartCreationPolicy(CreationPolicy.Shared)]
public class TwoFactorChallengeHandler : ReactiveObject, IDelegatingTwoFactorChallengeHandler
{
+ [ImportingConstructor]
+ public TwoFactorChallengeHandler([Import(AllowDefault = true)] JoinableTaskContext joinableTaskContext)
+ {
+ JoinableTaskContext = joinableTaskContext ?? ThreadHelper.JoinableTaskContext;
+ }
+
ILogin2FaViewModel twoFactorDialog;
public IViewModel CurrentViewModel
{
@@ -33,7 +41,7 @@ public async Task HandleTwoFactorException(TwoFactorAu
{
Guard.ArgumentNotNull(exception, nameof(exception));
- await ThreadingHelper.SwitchToMainThreadAsync();
+ await JoinableTaskContext.Factory.SwitchToMainThreadAsync();
var userError = new TwoFactorRequiredUserError(exception);
var result = await twoFactorDialog.Show(userError);
@@ -50,8 +58,10 @@ public async Task HandleTwoFactorException(TwoFactorAu
public async Task ChallengeFailed(Exception exception)
{
- await ThreadingHelper.SwitchToMainThreadAsync();
+ await JoinableTaskContext.Factory.SwitchToMainThreadAsync();
twoFactorDialog.Cancel();
}
+
+ JoinableTaskContext JoinableTaskContext { get; }
}
}
\ No newline at end of file
diff --git a/src/GitHub.App/Collections/VirtualizingList.cs b/src/GitHub.App/Collections/VirtualizingList.cs
index b283c3f56f..1e4f1edb4f 100644
--- a/src/GitHub.App/Collections/VirtualizingList.cs
+++ b/src/GitHub.App/Collections/VirtualizingList.cs
@@ -12,6 +12,10 @@
using GitHub.Logging;
using Serilog;
+#pragma warning disable CA1010 // Collections should implement generic interface
+#pragma warning disable CA1033 // Interface methods should be callable by child types
+#pragma warning disable CA1710 // Identifiers should have correct suffix
+
namespace GitHub.Collections
{
///
diff --git a/src/GitHub.App/Collections/VirtualizingListCollectionView.cs b/src/GitHub.App/Collections/VirtualizingListCollectionView.cs
index 9aa5900b24..1cd16b8f13 100644
--- a/src/GitHub.App/Collections/VirtualizingListCollectionView.cs
+++ b/src/GitHub.App/Collections/VirtualizingListCollectionView.cs
@@ -4,6 +4,10 @@
using System.Collections.Specialized;
using System.Windows.Data;
+#pragma warning disable CA1010 // Collections should implement generic interface
+#pragma warning disable CA1033 // Interface methods should be callable by child types
+#pragma warning disable CA1710 // Identifiers should have correct suffix
+
namespace GitHub.Collections
{
///
diff --git a/src/GitHub.App/Factories/ModelServiceFactory.cs b/src/GitHub.App/Factories/ModelServiceFactory.cs
index a5ef967749..5ff0c3b1ba 100644
--- a/src/GitHub.App/Factories/ModelServiceFactory.cs
+++ b/src/GitHub.App/Factories/ModelServiceFactory.cs
@@ -8,6 +8,7 @@
using GitHub.Models;
using GitHub.Services;
using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Threading;
namespace GitHub.Factories
{
@@ -25,11 +26,13 @@ public sealed class ModelServiceFactory : IModelServiceFactory, IDisposable
public ModelServiceFactory(
IApiClientFactory apiClientFactory,
IHostCacheFactory hostCacheFactory,
- IAvatarProvider avatarProvider)
+ IAvatarProvider avatarProvider,
+ [Import(AllowDefault = true)] JoinableTaskContext joinableTaskContext)
{
this.apiClientFactory = apiClientFactory;
this.hostCacheFactory = hostCacheFactory;
this.avatarProvider = avatarProvider;
+ JoinableTaskContext = joinableTaskContext ?? ThreadHelper.JoinableTaskContext;
}
public async Task CreateAsync(IConnection connection)
@@ -60,9 +63,11 @@ await hostCacheFactory.Create(connection.HostAddress),
public IModelService CreateBlocking(IConnection connection)
{
- return ThreadHelper.JoinableTaskFactory.Run(() => CreateAsync(connection));
+ return JoinableTaskContext.Factory.Run(() => CreateAsync(connection));
}
public void Dispose() => cacheLock.Dispose();
+
+ JoinableTaskContext JoinableTaskContext { get; }
}
}
diff --git a/src/GitHub.App/GitHub.App.csproj b/src/GitHub.App/GitHub.App.csproj
index a6f5be28c8..d9b0ef5354 100644
--- a/src/GitHub.App/GitHub.App.csproj
+++ b/src/GitHub.App/GitHub.App.csproj
@@ -25,6 +25,8 @@
+
+
@@ -40,19 +42,21 @@
-
-
-
+
+
-
-
-
-
+
-
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/src/GitHub.App/GlobalSuppressions.cs b/src/GitHub.App/GlobalSuppressions.cs
index 8e2e306555..997cb18a85 100644
--- a/src/GitHub.App/GlobalSuppressions.cs
+++ b/src/GitHub.App/GlobalSuppressions.cs
@@ -1,12 +1,3 @@
-using System.Diagnostics.CodeAnalysis;
-
-[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "GitHub.ViewModels.CreateRepoViewModel.#ResetState()")]
-[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Git", Scope = "resource", Target = "GitHub.Resources.resources")]
-[assembly: SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "GitHub.Caches.CredentialCache.#InsertObject`1(System.String,!!0,System.Nullable`1)")]
-[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Git", Scope = "resource", Target = "GitHub.App.Resources.resources")]
-[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)", Scope = "member", Target = "GitHub.Services.PullRequestService.#CreateTempFile(System.String,System.String,System.String)")]
-[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)", Scope = "member", Target = "GitHub.Services.PullRequestService.#CreateTempFile(System.String,System.String,System.String,System.Text.Encoding)")]
-
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
@@ -16,3 +7,15 @@
// Code Analysis results, point to "Suppress Message", and click
// "In Suppression File".
// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "GitHub.ViewModels.CreateRepoViewModel.#ResetState()")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Git", Scope = "resource", Target = "GitHub.Resources.resources")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "GitHub.Caches.CredentialCache.#InsertObject`1(System.String,!!0,System.Nullable`1)")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Git", Scope = "resource", Target = "GitHub.App.Resources.resources")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)", Scope = "member", Target = "GitHub.Services.PullRequestService.#CreateTempFile(System.String,System.String,System.String)")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)", Scope = "member", Target = "GitHub.Services.PullRequestService.#CreateTempFile(System.String,System.String,System.String,System.Text.Encoding)")]
+[assembly: SuppressMessage("Design", "CA1056:Uri properties should not be strings")]
+[assembly: SuppressMessage("Design", "CA1054:Uri parameters should not be strings")]
+[assembly: SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "Discouraged for VSSDK projects.")]
diff --git a/src/GitHub.App/Models/PullRequestModel.cs b/src/GitHub.App/Models/PullRequestModel.cs
index 6a560fa2ea..c1f6bd3837 100644
--- a/src/GitHub.App/Models/PullRequestModel.cs
+++ b/src/GitHub.App/Models/PullRequestModel.cs
@@ -110,8 +110,8 @@ public string Title
}
}
- PullRequestStateEnum status;
- public PullRequestStateEnum State
+ PullRequestState status;
+ public PullRequestState State
{
get { return status; }
set
@@ -126,8 +126,8 @@ public PullRequestStateEnum State
}
// TODO: Remove these property once maintainer workflow has been merged to master.
- public bool IsOpen => State == PullRequestStateEnum.Open;
- public bool Merged => State == PullRequestStateEnum.Merged;
+ public bool IsOpen => State == PullRequestState.Open;
+ public bool Merged => State == PullRequestState.Merged;
int commentCount;
public int CommentCount
diff --git a/src/GitHub.App/Models/SuggestionItem.cs b/src/GitHub.App/Models/SuggestionItem.cs
new file mode 100644
index 0000000000..713e0357e9
--- /dev/null
+++ b/src/GitHub.App/Models/SuggestionItem.cs
@@ -0,0 +1,51 @@
+using System;
+using GitHub.Extensions;
+using GitHub.Helpers;
+
+namespace GitHub.Models
+{
+ ///
+ /// Represents a single auto completion suggestion (mentions, emojis, issues) in a generic format that can be
+ /// easily cached.
+ ///
+ public class SuggestionItem
+ {
+ public SuggestionItem(string name, string description)
+ {
+ Guard.ArgumentNotEmptyString(name, "name");
+ Guard.ArgumentNotEmptyString(description, "description");
+
+ Name = name;
+ Description = description;
+ }
+
+ public SuggestionItem(string name, string description, string imageUrl)
+ {
+ Guard.ArgumentNotEmptyString(name, "name");
+
+ Name = name;
+ Description = description;
+ ImageUrl = imageUrl;
+ }
+
+ ///
+ /// The name to display for this entry
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Additional details about the entry
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// An image url for this entry
+ ///
+ public string ImageUrl { get; set; }
+
+ ///
+ /// The date this suggestion was last modified according to the API.
+ ///
+ public DateTimeOffset? LastModifiedDate { get; set; }
+ }
+}
diff --git a/src/GitHub.App/Properties/AssemblyInfo.cs b/src/GitHub.App/Properties/AssemblyInfo.cs
index 20fe17f77f..0ad9954b61 100644
--- a/src/GitHub.App/Properties/AssemblyInfo.cs
+++ b/src/GitHub.App/Properties/AssemblyInfo.cs
@@ -2,7 +2,9 @@
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Dialog.Clone")]
+[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Documents")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog.Clone")]
+[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Documents")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.GitHubPane")]
diff --git a/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs b/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs
index cd282d81c7..aaf13bafef 100644
--- a/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs
+++ b/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs
@@ -1,5 +1,7 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
+using GitHub.Models;
using GitHub.ViewModels;
using ReactiveUI;
@@ -8,6 +10,16 @@ namespace GitHub.SampleData
[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
public class CommentThreadViewModelDesigner : ViewModelBase, ICommentThreadViewModel
{
+ public CommentThreadViewModelDesigner()
+ {
+ Comments = new ReactiveList(){new CommentViewModelDesigner()
+ {
+ Author = new ActorViewModel{ Login = "shana"},
+ Body = "You can use a `CompositeDisposable` type here, it's designed to handle disposables in an optimal way (you can just call `Dispose()` on it and it will handle disposing everything it holds)."
+ }};
+
+ }
+
public IReadOnlyReactiveList Comments { get; }
= new ReactiveList();
diff --git a/src/GitHub.App/SampleData/CommentViewModelDesigner.cs b/src/GitHub.App/SampleData/CommentViewModelDesigner.cs
index 425f536ad7..132fe03568 100644
--- a/src/GitHub.App/SampleData/CommentViewModelDesigner.cs
+++ b/src/GitHub.App/SampleData/CommentViewModelDesigner.cs
@@ -1,6 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive;
+using System.Threading.Tasks;
+using GitHub.Models;
+using GitHub.Services;
using GitHub.ViewModels;
using ReactiveUI;
@@ -22,7 +25,9 @@ public CommentViewModelDesigner()
public CommentEditState EditState { get; set; }
public bool IsReadOnly { get; set; }
public bool IsSubmitting { get; set; }
+ public bool CanCancel { get; } = true;
public bool CanDelete { get; } = true;
+ public string CommitCaption { get; set; } = "Comment";
public ICommentThreadViewModel Thread { get; }
public DateTimeOffset CreatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3));
public IActorViewModel Author { get; set; }
@@ -31,7 +36,13 @@ public CommentViewModelDesigner()
public ReactiveCommand BeginEdit { get; }
public ReactiveCommand CancelEdit { get; }
public ReactiveCommand CommitEdit { get; }
- public ReactiveCommand OpenOnGitHub { get; }
+ public ReactiveCommand OpenOnGitHub { get; } = ReactiveCommand.Create(() => { });
public ReactiveCommand Delete { get; }
+ public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
+
+ public Task InitializeAsync(ICommentThreadViewModel thread, ActorModel currentUser, CommentModel comment, CommentEditState state)
+ {
+ return Task.CompletedTask;
+ }
}
}
diff --git a/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs b/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs
index 4b2c599fe9..6ce69138d9 100644
--- a/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs
+++ b/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs
@@ -2,6 +2,7 @@
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
+using GitHub.Primitives;
using GitHub.ViewModels;
using GitHub.ViewModels.Dialog.Clone;
using ReactiveUI;
@@ -17,15 +18,16 @@ public RepositoryCloneViewModelDesigner()
}
public string Path { get; set; }
+ public UriString Url { get; set; }
public string PathWarning { get; set; }
public int SelectedTabIndex { get; set; }
public string Title => null;
public IObservable