diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dc4eefb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and test + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + + - uses: microsoft/setup-msbuild@v3 + + - name: Build + run: msbuild SourceControlSwitcher.slnx -restore -p:Configuration=Release -p:DeployExtension=false -v:minimal + + - name: Test + run: dotnet test SourceControlSwitcher.Tests/SourceControlSwitcher.Tests.csproj -c Release --no-build --logger "console;verbosity=normal" + + - name: Upload VSIX + uses: actions/upload-artifact@v7 + with: + name: SourceControlSwitcher-vsix + path: target/Release/SourceControlSwitcher.vsix + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4f18d02 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,116 @@ +name: Release + +# Publishes a new version when a tag vX.Y.Z is pushed: +# 1. checks that the tag matches the version in the manifest, AssemblyInfo and package registration; +# 2. builds and tests; +# 3. publishes the VSIX to the Visual Studio Marketplace; +# 4. creates the GitHub release with the VSIX and the CHANGELOG section as notes. +# Run it manually (workflow_dispatch) for a dry run: steps 1-2 only, VSIX uploaded as an artifact. +# +# Secrets: +# VS_MARKETPLACE_PAT Azure DevOps personal access token, scope "Marketplace (Manage)", owned by an account +# that is a member of the "Ryadel" Marketplace publisher. Required to publish. +# SNK_BASE64 Optional: the strong-name key (Key.snk) encoded in base64. Without it the build +# generates a new key on every run. + +on: + push: + tags: ['v*.*.*'] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + name: Build and publish + runs-on: windows-latest + # Protect the environment in Settings > Environments to require an approval before publishing. + environment: marketplace + env: + SNK_BASE64: ${{ secrets.SNK_BASE64 }} + PUBLISH: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} + steps: + - uses: actions/checkout@v7 + + - uses: microsoft/setup-msbuild@v3 + + - name: Check version + id: version + shell: pwsh + run: | + [xml]$manifest = Get-Content 'SourceControlSwitcher/Manifest/source.extension.vsixmanifest' + $version = $manifest.PackageManifest.Metadata.Identity.Version + $assembly = (Select-String -Path 'SourceControlSwitcher/Properties/AssemblyInfo.cs' -Pattern 'AssemblyVersion\("([^"]+)"\)').Matches[0].Groups[1].Value + $registration = (Select-String -Path 'SourceControlSwitcher/SourceControlSwitcherPackage.cs' -Pattern 'InstalledProductRegistration\("#110", "#112", "([^"]+)"\)').Matches[0].Groups[1].Value + Write-Host "Manifest: $version AssemblyVersion: $assembly InstalledProductRegistration: $registration" + if ($assembly -ne "$version.0") { throw "AssemblyVersion $assembly does not match manifest version $version" } + if ($registration -ne $version) { throw "InstalledProductRegistration $registration does not match manifest version $version" } + if ($env:PUBLISH -eq 'true' -and $env:GITHUB_REF_NAME -ne "v$version") { throw "Tag $env:GITHUB_REF_NAME does not match manifest version v$version" } + if (-not (Select-String -Path 'CHANGELOG.txt' -SimpleMatch "SourceControlSwitcher - v$version" -Quiet)) { throw "CHANGELOG.txt has no section for v$version" } + "version=$version" >> $env:GITHUB_OUTPUT + + - name: Restore strong-name key + if: env.SNK_BASE64 != '' + shell: pwsh + run: '[IO.File]::WriteAllBytes("$PWD/Key.snk", [Convert]::FromBase64String($env:SNK_BASE64))' + + - name: Build + run: msbuild SourceControlSwitcher.slnx -restore -p:Configuration=Release -p:DeployExtension=false -v:minimal + + - name: Test + run: dotnet test SourceControlSwitcher.Tests/SourceControlSwitcher.Tests.csproj -c Release --no-build + + - name: Release notes from CHANGELOG + shell: pwsh + run: | + $lines = Get-Content 'CHANGELOG.txt' + $start = [Array]::FindIndex($lines, [Predicate[string]]{ param($l) $l -eq "SourceControlSwitcher - v${{ steps.version.outputs.version }}" }) + $end = [Array]::FindIndex($lines, $start + 1, [Predicate[string]]{ param($l) $l -like 'SourceControlSwitcher - v*' }) + if ($end -lt 0) { $end = $lines.Length } + $lines[($start + 2)..($end - 1)] | Set-Content -Encoding utf8 'release-notes.md' + Get-Content 'release-notes.md' + + - name: Upload VSIX + uses: actions/upload-artifact@v7 + with: + name: SourceControlSwitcher-${{ steps.version.outputs.version }}-vsix + path: target/Release/SourceControlSwitcher.vsix + if-no-files-found: error + + - name: Publish to the Visual Studio Marketplace + if: env.PUBLISH == 'true' + shell: pwsh + env: + VS_MARKETPLACE_PAT: ${{ secrets.VS_MARKETPLACE_PAT }} + run: | + if (-not $env:VS_MARKETPLACE_PAT) { throw 'Secret VS_MARKETPLACE_PAT is not set' } + # VsixPublisher ships with the Microsoft.VSSDK.BuildTools package restored by the build. + $publisher = Get-ChildItem "$env:USERPROFILE\.nuget\packages\microsoft.vssdk.buildtools" -Recurse -Filter VsixPublisher.exe | + Where-Object { $_.Directory.Name -eq 'bin' } | Sort-Object FullName -Descending | Select-Object -First 1 + if (-not $publisher) { throw 'VsixPublisher.exe not found' } + & $publisher.FullName publish ` + -payload 'target/Release/SourceControlSwitcher.vsix' ` + -publishManifest 'SourceControlSwitcher/Manifest/publishManifest.json' ` + -personalAccessToken $env:VS_MARKETPLACE_PAT + if ($LASTEXITCODE -ne 0) { throw "VsixPublisher failed with exit code $LASTEXITCODE" } + + - name: Create GitHub release + if: env.PUBLISH == 'true' + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $tag = $env:GITHUB_REF_NAME + gh release view $tag *> $null + if ($LASTEXITCODE -eq 0) { + gh release upload $tag 'target/Release/SourceControlSwitcher.vsix' --clobber + } else { + gh release create $tag 'target/Release/SourceControlSwitcher.vsix' --verify-tag ` + --title "Source Control Switcher ${{ steps.version.outputs.version }}" --notes-file 'release-notes.md' + } + if ($LASTEXITCODE -ne 0) { throw "gh release failed with exit code $LASTEXITCODE" } diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 897963a..ce31207 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -16,6 +16,8 @@ SourceControlSwitcher - v1.2.0 * Removed clients that are no longer maintained: Easy Git Integration Tools / Git Source Control Provider, VisualHG, VSHG, HgSccPackage (Mercurial support). * Solution migrated to .slnx; unit tests added. + * Icon redrawn from the vector logo; Getting Started link in the extension details. Builds, tests and releases now run + on GitHub Actions (CI on every push and pull request; tag vX.Y.Z publishes to the Marketplace). SourceControlSwitcher - v1.1.2 ------------------------------------------ diff --git a/README.md b/README.md index 592ca62..d9a0cb5 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,129 @@ -# Source Control Switcher +
+
+
+ Automatically activates the right source control provider in Visual Studio for every solution you open. +
-| Repository | Detected by | Providers | + + +--- + +## Why + +Visual Studio can have only **one** source control provider active at a time, and it does not choose one per +repository. If you work with Git in one solution and Subversion, TFVC or Perforce in another, you end up changing +*Tools > Options > Source Control > Plug-in Selection* by hand every time. + +**Source Control Switcher does it for you**: when a solution or folder opens, it detects which kind of repository +it belongs to and activates the matching provider. + +## Supported repositories + +| Repository | Detected by | Provider activated | |---|---|---| -| Git | a `.git` folder (or file, for worktrees and submodules) in the solution folder or a parent | Git (Microsoft Git provider, built into Visual Studio) | -| Subversion | a `.svn` folder in the solution folder or a parent | VisualSVN, AnkhSVN | -| Team Foundation Version Control | TFVC bindings in the `.sln` file, or a TFVC local workspace (`$tf`) | Visual Studio Team Foundation Server (built in) | -| Perforce | `p4 where` succeeds in the solution folder (only when `p4.exe` is on PATH) | P4VS | -| Anything else | a file or folder name you choose | any source control provider, by GUID | +| **Git** | a `.git` folder, or a `.git` file (worktrees, submodules), in the solution folder or a parent | Git (Microsoft Git provider, built into Visual Studio) | +| **Subversion** | a `.svn` folder in the solution folder or a parent | VisualSVN or AnkhSVN | +| **Team Foundation (TFVC)** | TFVC bindings in the `.sln`/`.slnx`, or a TFVC local workspace (`$tf`) | Visual Studio Team Foundation Server (built in) | +| **Perforce** | `p4 where` succeeds in the solution folder (only when `p4.exe` is on PATH) | P4VS | +| **Anything else** | a file or folder name you choose | any source control provider, by GUID | + +Works with solutions (`.sln`, `.slnx`) and with **Open Folder**. + +## Requirements + +- **Visual Studio 2022 or Visual Studio 2026**: Community, Professional or Enterprise, on x64 or Arm64. +- The source control client(s) you use: Git and TFVC are built into Visual Studio; for Subversion install + VisualSVN or AnkhSVN; for Perforce install P4VS. + +Still on Visual Studio 2015–2019? Use version 1.1.2. + +## Installation + +In Visual Studio: **Extensions > Manage Extensions**, search for *Source Control Switcher*, install and restart. +Alternatively, download it from the +[Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Ryadel.SourceControlSwitcher) or +from the [GitHub releases](https://github.com/Ryadel/SourceControlSwitcher/releases). -The extension supports **Visual Studio 2022 and Visual Studio 2026** (Community, Professional, Enterprise; x64 and -Arm64). Users of Visual Studio 2015–2019 can keep using version 1.1.2. +That's it: there is nothing to configure for the common cases. + +## How it works + +When a solution or folder opens, the extension looks for repository markers starting from the solution folder and +moving up to the drive root. The nearest marker wins; at each folder level the order is: + +1. your custom markers (see [Custom providers](#custom-providers)); +2. TFVC bindings in the solution file (solution folder only); +3. `.git`, then `.svn`, then `$tf`. + +If nothing is found and Perforce detection is enabled, it asks the Perforce client in the background. When a +repository is found, the provider configured for it is activated; when nothing is found, the current provider is +left unchanged. ## Options -*Tools* > *Options* > *Source Control Switcher* > *General*: +**Tools > Options > Source Control Switcher > General** -- **General** - - *Switch automatically* — switch when a solution or folder opens. When off, switch on demand with - *Tools* > *Detect Source Control Provider*. - - *Show status bar notifications* — a status bar message whenever the provider changes. - - *Logging* — detail written to the *Source Control Switcher* pane of the Output window (Off, Normal, Verbose). -- **Providers** — for Git, Subversion, TFVC and Perforce: *Automatic* (first supported provider installed), a - specific provider, or *Disabled* (leave the provider alone for that repository type). -- **Detection** - - *Search parent folders* — look for markers in the solution folder and all its parents. - - *Detect TFVC bindings in .sln files*. - - *Detect Perforce* and *Perforce timeout (seconds)* — the Perforce check runs in the background. -- **Custom providers** — `marker=providerGuid` rules separated by `;`. When the marker (a file or folder name) - is found, the provider with that GUID is activated. Custom rules are checked before the built-in ones. +| Category | Option | Default | Description | +|---|---|---|---| +| General | Switch automatically | On | Switch when a solution or folder opens. When off, use the *Detect Source Control Provider* command. | +| General | Show status bar notifications | On | Show a message in the status bar when the provider changes. | +| General | Logging | Normal | Detail written to the *Source Control Switcher* pane of the Output window: Off, Normal, Verbose. | +| Providers | Git / Subversion / Team Foundation / Perforce | Automatic | *Automatic* picks the first supported provider installed; you can force a specific one, or choose *Disabled* to leave the provider alone for that repository type. | +| Detection | Search parent folders | On | Look for markers in the solution folder and all its parents. | +| Detection | Detect TFVC bindings in .sln files | On | Turn off for repositories migrated from TFVC that still carry old bindings. | +| Detection | Detect Perforce | On | Ask `p4 where` when no other marker is found (background, only if `p4.exe` is on PATH). | +| Detection | Perforce timeout (seconds) | 5 | Maximum wait for the Perforce client (1–60). | +| Custom providers | Custom providers | *(empty)* | `marker=providerGuid` rules, separated by `;`. | -## Commands +## Command -- *Tools* > *Detect Source Control Provider* — re-runs detection for the open solution or folder and activates the - matching provider, for example after changing the provider by hand. +**Tools > Detect Source Control Provider** runs the detection again for the open solution or folder and activates +the matching provider. Use it after changing the provider by hand, or to switch on demand with automatic switching +turned off. -## Building +## Custom providers -See [BUILD.txt](BUILD.txt). +For a source control client the extension does not know, add a rule mapping a marker to the client's +**provider GUID**, for example: -## Credits +```text +.vault=01234567-89ab-cdef-0123-456789abcdef +``` + +When `.vault` (a file or a folder) is found, that provider is activated. To find a provider's GUID, look for +`SourceControlProviders\{GUID}` in the `.pkgdef` files of the client's extension folder. -This extension is based on [SccAutoSwitcher](https://github.com/ceztko/SccAutoSwitcher) by *Francesco Pretto*. -Thanks to everyone who contributed fixes and ideas, including *QianMoXi* (start-up with a solution), -*joe-kr* (Visual Studio 2022 support, AnkhSVN 2019) and *davideciarmiello* (TFVC support). +## Troubleshooting + +Set **Logging** to *Verbose* and open **View > Output**, then pick *Source Control Switcher* in the +*Show output from* list: every detection step and its outcome is logged there. Messages are also written to the +Visual Studio activity log when Visual Studio is started with `/log`. + +Found a bug or have an idea? [Open an issue](https://github.com/Ryadel/SourceControlSwitcher/issues). + +## Building from source + +Open `SourceControlSwitcher.slnx` in Visual Studio 2022 (17.14+) or 2026 and build; press F5 to debug in the +experimental instance. Details in +[BUILD.txt](https://github.com/Ryadel/SourceControlSwitcher/blob/master/BUILD.txt). Release notes are in the +[CHANGELOG](https://github.com/Ryadel/SourceControlSwitcher/blob/master/CHANGELOG.txt). + +## Credits -License is MIT. +Based on [SccAutoSwitcher](https://github.com/ceztko/SccAutoSwitcher) by **Francesco Pretto**. +Thanks to everyone who contributed fixes and ideas, including **@QianMoXi** (start-up with a solution), +**@joe-kr** (Visual Studio 2022 support, AnkhSVN 2019) and **@davideciarmiello** (TFVC support). -## Useful References +## License - * [**Source Control Switcher** on GitHub](https://github.com/Ryadel/SourceControlSwitcher/) - * [**Source Control Switcher** on Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Ryadel.SourceControlSwitcher) - * [**Source Control Switcher** official page](https://www.ryadel.com/en/portfolio/source-control-switcher/) +[MIT](https://github.com/Ryadel/SourceControlSwitcher/blob/master/LICENSE.txt) — see also the +[official page](https://www.ryadel.com/en/portfolio/source-control-switcher/) on ryadel.com. diff --git a/SourceControlSwitcher/Manifest/publishManifest.json b/SourceControlSwitcher/Manifest/publishManifest.json new file mode 100644 index 0000000..bc67d78 --- /dev/null +++ b/SourceControlSwitcher/Manifest/publishManifest.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/vsix-publish", + "categories": [ "source control", "team development" ], + "identity": { + "internalName": "SourceControlSwitcher" + }, + "overview": "../../README.md", + "priceCategory": "free", + "publisher": "Ryadel", + "private": false, + "qna": true, + "repo": "https://github.com/Ryadel/SourceControlSwitcher" +} diff --git a/SourceControlSwitcher/Manifest/source.extension.vsixmanifest b/SourceControlSwitcher/Manifest/source.extension.vsixmanifest index 8454df5..b532720 100644 --- a/SourceControlSwitcher/Manifest/source.extension.vsixmanifest +++ b/SourceControlSwitcher/Manifest/source.extension.vsixmanifest @@ -6,9 +6,9 @@