Repository: microsoft/azure-repos-vscode Branch: master Commit: 6bc90f085308 Files: 168 Total size: 1.0 MB Directory structure: gitextract_ylv2h823/ ├── .gitattributes ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEPRECATED.md ├── LICENSE.txt ├── README.md ├── TFVC_README.md ├── ThirdPartyNotices.txt ├── gulpfile.js ├── package.json ├── patches/ │ └── vso-node-api/ │ └── handlers/ │ └── ntlm.js ├── src/ │ ├── clients/ │ │ ├── baseclient.ts │ │ ├── buildclient.ts │ │ ├── coreapiclient.ts │ │ ├── feedbackclient.ts │ │ ├── gitclient.ts │ │ ├── httpclient.ts │ │ ├── repositoryinfoclient.ts │ │ ├── soapclient.ts │ │ ├── teamservicesclient.ts │ │ ├── tfscatalogsoapclient.ts │ │ └── witclient.ts │ ├── contexts/ │ │ ├── externalcontext.ts │ │ ├── gitcontext.ts │ │ ├── repocontextfactory.ts │ │ ├── repositorycontext.ts │ │ ├── servercontext.ts │ │ └── tfvccontext.ts │ ├── credentialstore/ │ │ ├── credential.ts │ │ ├── credentialstore.ts │ │ ├── interfaces/ │ │ │ └── icredentialstore.ts │ │ ├── linux/ │ │ │ ├── file-token-storage.ts │ │ │ └── linux-file-api.ts │ │ ├── osx/ │ │ │ ├── osx-keychain-api.ts │ │ │ ├── osx-keychain-parser.js │ │ │ └── osx-keychain.js │ │ └── win32/ │ │ ├── win-credstore-api.ts │ │ ├── win-credstore-parser.js │ │ └── win-credstore.js │ ├── extension.ts │ ├── extensionmanager.ts │ ├── helpers/ │ │ ├── constants.ts │ │ ├── credentialmanager.ts │ │ ├── logger.ts │ │ ├── repoutils.ts │ │ ├── settings.ts │ │ ├── strings.ts │ │ ├── urlbuilder.ts │ │ ├── useragentprovider.ts │ │ ├── utils.ts │ │ ├── vscodeutils.interfaces.ts │ │ └── vscodeutils.ts │ ├── info/ │ │ ├── credentialinfo.ts │ │ ├── extensionrequesthandler.ts │ │ ├── repositoryinfo.ts │ │ └── userinfo.ts │ ├── services/ │ │ ├── build.ts │ │ ├── coreapi.ts │ │ ├── gitvc.ts │ │ ├── telemetry.ts │ │ └── workitemtracking.ts │ ├── team-extension.ts │ └── tfvc/ │ ├── commands/ │ │ ├── add.ts │ │ ├── argumentbuilder.ts │ │ ├── checkin.ts │ │ ├── commandhelper.ts │ │ ├── delete.ts │ │ ├── findconflicts.ts │ │ ├── findworkspace.ts │ │ ├── getfilecontent.ts │ │ ├── getinfo.ts │ │ ├── getversion.ts │ │ ├── rename.ts │ │ ├── resolveconflicts.ts │ │ ├── status.ts │ │ ├── sync.ts │ │ └── undo.ts │ ├── interfaces.ts │ ├── scm/ │ │ ├── commithoverprovider.ts │ │ ├── decorationprovider.ts │ │ ├── model.ts │ │ ├── resource.ts │ │ ├── resourcegroups.ts │ │ ├── status.ts │ │ └── tfvccontentprovider.ts │ ├── tfcommandlinerunner.ts │ ├── tfvc-extension.ts │ ├── tfvcerror.ts │ ├── tfvcoutput.ts │ ├── tfvcrepository.ts │ ├── tfvcscmprovider.ts │ ├── tfvcsettings.ts │ ├── tfvcversion.ts │ ├── uihelper.ts │ └── util.ts ├── test/ │ ├── contexts/ │ │ ├── contexthelper.ts │ │ ├── externalcontext.test.ts │ │ ├── gitcontext.test.ts │ │ ├── servercontext.test.ts │ │ └── testrepos/ │ │ ├── emptyconfig/ │ │ │ └── dotgit/ │ │ │ └── config │ │ ├── githubrepo/ │ │ │ └── dotgit/ │ │ │ ├── HEAD │ │ │ └── config │ │ ├── gitrepo/ │ │ │ └── dotgit/ │ │ │ ├── HEAD │ │ │ ├── config │ │ │ └── refs/ │ │ │ └── heads/ │ │ │ └── jeyou/ │ │ │ └── approved-pr │ │ ├── gitrepo-old-ssh/ │ │ │ └── dotgit/ │ │ │ ├── HEAD │ │ │ ├── config │ │ │ └── refs/ │ │ │ └── heads/ │ │ │ └── master │ │ ├── gitrepo-ssh/ │ │ │ └── dotgit/ │ │ │ ├── HEAD │ │ │ ├── config │ │ │ └── refs/ │ │ │ └── heads/ │ │ │ └── master │ │ ├── gitrepo-ssh.v3/ │ │ │ └── dotgit/ │ │ │ ├── HEAD │ │ │ ├── config │ │ │ └── refs/ │ │ │ └── heads/ │ │ │ └── master │ │ ├── tfsrepo/ │ │ │ └── dotgit/ │ │ │ ├── HEAD │ │ │ ├── config │ │ │ └── refs/ │ │ │ └── heads/ │ │ │ └── master │ │ └── tfsrepo-ssh/ │ │ └── dotgit/ │ │ ├── HEAD │ │ ├── config │ │ └── refs/ │ │ └── heads/ │ │ └── master │ ├── helpers/ │ │ ├── logger.test.ts │ │ ├── repoutils.test.ts │ │ ├── testrepos/ │ │ │ └── gitreposubfolder/ │ │ │ ├── dotgit/ │ │ │ │ └── config │ │ │ └── folder/ │ │ │ └── subfolder/ │ │ │ └── README.md │ │ ├── urlbuilder.test.ts │ │ └── utils.test.ts │ ├── index.ts │ ├── info/ │ │ ├── credentialinfo.test.ts │ │ ├── repositoryinfo.test.ts │ │ └── userinfo.test.ts │ ├── services/ │ │ ├── build.test.ts │ │ ├── gitvc.test.ts │ │ └── workitemtracking.test.ts │ └── tfvc/ │ ├── commands/ │ │ ├── add.test.ts │ │ ├── argumentbuilder.test.ts │ │ ├── checkin.test.ts │ │ ├── commandhelper.test.ts │ │ ├── delete.test.ts │ │ ├── findconflicts.test.ts │ │ ├── findworkspace.test.ts │ │ ├── getfilecontent.test.ts │ │ ├── getinfo.test.ts │ │ ├── getversion.test.ts │ │ ├── rename.test.ts │ │ ├── resolveconflicts.test.ts │ │ ├── status.test.ts │ │ ├── sync.test.ts │ │ └── undo.test.ts │ ├── scm/ │ │ ├── resourcegroup.test.ts │ │ └── status.test.ts │ ├── tfvcerror.test.ts │ └── tfvcversion.test.ts ├── test-integration/ │ ├── clients/ │ │ └── teamservicesclient.integration.test.ts │ ├── contexts/ │ │ └── servercontext.integration.test.ts │ ├── helpers/ │ │ └── credentialmanager.test.ts │ ├── helpers-integration/ │ │ ├── mocks.ts │ │ └── testsettings.ts │ ├── index.ts │ └── services/ │ ├── build.integration.test.ts │ ├── gitvc.integration.test.ts │ └── workitemtracking.integration.test.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto ================================================ FILE: .gitignore ================================================ node_modules/ out/ *.vsix team-extension.log *.zip .taskkey ================================================ FILE: .vscode/launch.json ================================================ // A launch configuration that compiles the extension and then opens it inside a new window // "preLaunchTask": "npm" { "version": "0.1.0", "configurations": [ { "name": "Launch Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], "stopOnEntry": false, "sourceMaps": true, "outDir": "${workspaceRoot}/out/src", "preLaunchTask": "publishbuild" }, { "name": "Launch Tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], "stopOnEntry": false, "sourceMaps": true, "outDir": "${workspaceRoot}/out/test", "preLaunchTask": "publishall" }, { "name": "Launch Integration Tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test-integration" ], "stopOnEntry": false, "sourceMaps": true, "outDir": "${workspaceRoot}/out/test-integration", "preLaunchTask": "publishall" } ] } ================================================ FILE: .vscode/settings.json ================================================ // Place your settings in this file to overwrite default and user settings. { "files.exclude": { "out": false // set this to true to hide the "out" folder with the compiled JS files }, "search.exclude": { "out": true // set this to false to include "out" folder in search results }, "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version } ================================================ FILE: .vscode/tasks.json ================================================ // Available variables which can be used inside of strings. // ${workspaceRoot}: the root folder of the team // ${file}: the current opened file // ${fileBasename}: the current opened file's basename // ${fileDirname}: the current opened file's dirname // ${fileExtname}: the current opened file's extension // ${cwd}: the current working directory of the spawned process { "version": "0.1.0", "command": "gulp", "isShellCommand": true, "problemMatcher": "$gulp-tsc", "args": [ "--no-color" ], "tasks": [ { "isBuildCommand": true, "taskName": "publishall", "showOutput": "always" }, { "isTestCommand": true, "taskName": "test", "showOutput": "always" }, { "isBuildCommand": false, "taskName": "clean", "showOutput": "always" } ] } ================================================ FILE: .vscodeignore ================================================ .vscode/** typings/** out/test/** out/test-integration/** out/results/** test/** test-integration/** src/** **/*.map .gitignore tsconfig.json gulpfile.js tsd.json tslint.json team-extension.log **/*.zip .taskkey ================================================ FILE: CHANGELOG.md ================================================ ## [v1.161.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.161.1) (2020-09-04) The Azure Repos VS Code extension has been sunsetted. Learn more at in our [deprecation notice](https://aka.ms/AA9k2vv). This version of the extension contains no functional changes other than a message box directing you to that notice. ## [v1.161.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.161.0) (2019-11-06) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.149.2...v1.161.0) **Closed issues:** - Feature request: Change default for restrict workspace [\#329](https://github.com/microsoft/azure-repos-vscode/issues/329) - Source Control Panel Blank - VSCode 1.40.0-insider [\#586](https://github.com/microsoft/azure-repos-vscode/issues/586) - Octicons not Rendering in bottom task bar [\#572](https://github.com/microsoft/azure-repos-vscode/issues/572) - Source Control window \(TFVC\) is blank but icon shows changes [\#571](https://github.com/microsoft/azure-repos-vscode/issues/571) - vsts [\#568](https://github.com/microsoft/azure-repos-vscode/issues/568) - explorer in vs code gets closed [\#567](https://github.com/microsoft/azure-repos-vscode/issues/567) - I am using a local workspace, but it is saying that I am using a server workspace [\#552](https://github.com/microsoft/azure-repos-vscode/issues/552) - The setting team.pinnedQueries in VSCode doesn't return the correct result [\#549](https://github.com/microsoft/azure-repos-vscode/issues/549) - "Browse your pull requests" open incorrect url [\#534](https://github.com/microsoft/azure-repos-vscode/issues/534) - Extension causes high cpu load [\#533](https://github.com/microsoft/azure-repos-vscode/issues/533) - TFS 2017 on premise - No source control providers registered [\#531](https://github.com/microsoft/azure-repos-vscode/issues/531) - Azure Repos taskbar missing [\#530](https://github.com/microsoft/azure-repos-vscode/issues/530) - Extension causes high cpu load [\#501](https://github.com/microsoft/azure-repos-vscode/issues/501) - Extension causes high cpu load [\#495](https://github.com/microsoft/azure-repos-vscode/issues/495) - Extension causes high cpu load [\#487](https://github.com/microsoft/azure-repos-vscode/issues/487) **Merged pull requests:** - Fix undefined access error [\#590](https://github.com/microsoft/azure-repos-vscode/pull/590) ([joaomoreno](https://github.com/joaomoreno)) - Updated icon references to match vscode documentation [\#573](https://github.com/microsoft/azure-repos-vscode/pull/573) ([AVDW](https://github.com/AVDW)) - Use vscode.env.openExternal to open browser windows when available [\#498](https://github.com/microsoft/azure-repos-vscode/pull/498) ([Chuxel](https://github.com/Chuxel)) ## [v1.149.2](https://github.com/microsoft/azure-repos-vscode/tree/v1.149.2) (2019-02-26) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.144.1...v1.149.2) **Closed issues:** - Extension causes high cpu load [\#484](https://github.com/microsoft/azure-repos-vscode/issues/484) - Extension causes high cpu load [\#482](https://github.com/microsoft/azure-repos-vscode/issues/482) - Extension causes high cpu load [\#481](https://github.com/microsoft/azure-repos-vscode/issues/481) - Extension causes high cpu load [\#480](https://github.com/microsoft/azure-repos-vscode/issues/480) - Extension causes high cpu load [\#478](https://github.com/microsoft/azure-repos-vscode/issues/478) - Extension causes high cpu load [\#475](https://github.com/microsoft/azure-repos-vscode/issues/475) - Extension causes high cpu load [\#474](https://github.com/microsoft/azure-repos-vscode/issues/474) - Extension causes high cpu load [\#470](https://github.com/microsoft/azure-repos-vscode/issues/470) - Replace the user of the 'open' and 'opener' modules with vscode.open command [\#464](https://github.com/microsoft/azure-repos-vscode/issues/464) - Extension causes high cpu load [\#462](https://github.com/microsoft/azure-repos-vscode/issues/462) - Feature Request: Start pipelines from VS Code [\#453](https://github.com/microsoft/azure-repos-vscode/issues/453) - Unable to configure Azure DevOps PAT [\#452](https://github.com/microsoft/azure-repos-vscode/issues/452) **Merged pull requests:** - Don't bypass TLS verification [\#490](https://github.com/microsoft/azure-repos-vscode/pull/490) ([jrbriggs](https://github.com/jrbriggs)) ## [v1.144.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.144.1) (2018-11-05) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.142.0...v1.144.1) **Closed issues:** - "Azure Repos" extension not found in search [\#440](https://github.com/microsoft/azure-repos-vscode/issues/440) - VSTS is now Azure Repos : Finishing touches [\#437](https://github.com/microsoft/azure-repos-vscode/issues/437) - August 20 update error [\#424](https://github.com/microsoft/azure-repos-vscode/issues/424) **Merged pull requests:** - Set a minimum 10 minute polling interval [\#450](https://github.com/microsoft/azure-repos-vscode/pull/450) ([jrbriggs](https://github.com/jrbriggs)) ## [v1.142.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.142.0) (2018-10-04) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.136.0...v1.142.0) **Closed issues:** - "Associate Work Items" not getting reflected in VSTS UI [\#430](https://github.com/microsoft/azure-repos-vscode/issues/430) - TFVC only shows changes for one folder [\#426](https://github.com/microsoft/azure-repos-vscode/issues/426) - New files/folders add to the project are not reflected as pending changes [\#422](https://github.com/microsoft/azure-repos-vscode/issues/422) - Cannot link VS Code in OSX to VSTS [\#421](https://github.com/microsoft/azure-repos-vscode/issues/421) **Merged pull requests:** - Update references to Azure Repos/DevOps [\#429](https://github.com/microsoft/azure-repos-vscode/pull/429) ([kaylangan](https://github.com/kaylangan)) ## [v1.136.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.136.0) (2018-05-23) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.133.0...v1.136.0) **Closed issues:** - VSTS has stopped tracking changes [\#403](https://github.com/microsoft/azure-repos-vscode/issues/403) - New-TfsChangeset used in powershell script as a build step [\#402](https://github.com/microsoft/azure-repos-vscode/issues/402) - \(Team\)Unable to store credentials for this Host: [\#396](https://github.com/microsoft/azure-repos-vscode/issues/396) - VSTS modal dialog close event [\#390](https://github.com/microsoft/azure-repos-vscode/issues/390) - Can not "Associate work items" after updating to VScode 1.16 [\#314](https://github.com/microsoft/azure-repos-vscode/issues/314) **Merged pull requests:** - Add support for azure.com and other SSH URL formats [\#407](https://github.com/microsoft/azure-repos-vscode/pull/407) ([jeschu1](https://github.com/jeschu1)) - Consuming new version of vsts-device-flow-auth [\#406](https://github.com/microsoft/azure-repos-vscode/pull/406) ([chrispat](https://github.com/chrispat)) - Update readme to clarify TF.exe authentication [\#372](https://github.com/microsoft/azure-repos-vscode/pull/372) ([b-e-n-j](https://github.com/b-e-n-j)) ## [v1.133.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.133.0) (2018-04-03) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.122.0...v1.133.0) **Fixed bugs:** - Unable to find the TF executable [\#295](https://github.com/microsoft/azure-repos-vscode/issues/295) **Closed issues:** - What is the future of this extension? [\#370](https://github.com/microsoft/azure-repos-vscode/issues/370) - VSTS does not prompt for credentials when adding a TFS folder [\#368](https://github.com/microsoft/azure-repos-vscode/issues/368) - There's no audio with the TFVC Source Code Control in Visual Studio Code video [\#363](https://github.com/microsoft/azure-repos-vscode/issues/363) - Is there a way to enforce code review with this extension \#question [\#354](https://github.com/microsoft/azure-repos-vscode/issues/354) - Cannot sign in to TFS: Could not load type 'Microsoft.TeamFoundation.Common.TeamFoundationIdentityReference' [\#353](https://github.com/microsoft/azure-repos-vscode/issues/353) - Can't associate Work Items from another Team Project [\#351](https://github.com/microsoft/azure-repos-vscode/issues/351) - The associate work item, not populating commit message [\#341](https://github.com/microsoft/azure-repos-vscode/issues/341) - VSCode recommends this because I have git installed? [\#338](https://github.com/microsoft/azure-repos-vscode/issues/338) - @CurrentIteration doesn't seem to be working pinned queries [\#337](https://github.com/microsoft/azure-repos-vscode/issues/337) - Is this extension for Git based repos only? [\#336](https://github.com/microsoft/azure-repos-vscode/issues/336) - Git repositories cloned from VSTS/visualstudio.com using the "new" url aren't detected by the vsts extension [\#333](https://github.com/microsoft/azure-repos-vscode/issues/333) - VSTS PAT [\#327](https://github.com/microsoft/azure-repos-vscode/issues/327) - TF400324: Team Foundation services are not available from server [\#320](https://github.com/microsoft/azure-repos-vscode/issues/320) - Unable to find the TF executable [\#317](https://github.com/microsoft/azure-repos-vscode/issues/317) - Incorrectly claiming I am using a server workspace [\#315](https://github.com/microsoft/azure-repos-vscode/issues/315) - selecting them [\#307](https://github.com/microsoft/azure-repos-vscode/issues/307) - Get latest source code from TFS [\#305](https://github.com/microsoft/azure-repos-vscode/issues/305) - Soap Service error - can't connect to TFS Projects from company server [\#304](https://github.com/microsoft/azure-repos-vscode/issues/304) - Visual Studio authentication popup [\#303](https://github.com/microsoft/azure-repos-vscode/issues/303) - Team 00 关闭 错误command 'team.Signin' not found [\#301](https://github.com/microsoft/azure-repos-vscode/issues/301) - Impossible to switch to TFS [\#290](https://github.com/microsoft/azure-repos-vscode/issues/290) - \(team\) socket hang up / \(team\) undefined after switch to Azure VM [\#288](https://github.com/microsoft/azure-repos-vscode/issues/288) - Unable to connect TFVC to the existing Team foundation server 2015 update 3 [\#287](https://github.com/microsoft/azure-repos-vscode/issues/287) **Merged pull requests:** - Remove all telemetry data that could potential conflict with GDPR requirements. [\#377](https://github.com/microsoft/azure-repos-vscode/pull/377) ([ermeckle](https://github.com/ermeckle)) - Fixed "Associate work items" command for git source control \(\#314\) [\#367](https://github.com/microsoft/azure-repos-vscode/pull/367) ([dougrday](https://github.com/dougrday)) - Fix VSTS URL detection for "new-style" SSH clones [\#334](https://github.com/microsoft/azure-repos-vscode/pull/334) ([bearcage](https://github.com/bearcage)) - Visual Studio TF.exe file location [\#321](https://github.com/microsoft/azure-repos-vscode/pull/321) ([OzBob](https://github.com/OzBob)) ## [v1.122.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.122.0) (2017-08-14) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.121.1...v1.122.0) **Closed issues:** - \[Help\] How to connect to my TFS account? [\#294](https://github.com/microsoft/azure-repos-vscode/issues/294) - Error:KEY\_TF\_BAD\_EXIT\_CODE error in android studio while cloning [\#275](https://github.com/microsoft/azure-repos-vscode/issues/275) **Merged pull requests:** - Check for symlink, add telemetry [\#302](https://github.com/microsoft/azure-repos-vscode/pull/302) ([jeffyoung](https://github.com/jeffyoung)) - Two changes for Sprint 122 [\#300](https://github.com/microsoft/azure-repos-vscode/pull/300) ([jeffyoung](https://github.com/jeffyoung)) - Fix for tf.exe domain authentication [\#291](https://github.com/microsoft/azure-repos-vscode/pull/291) ([dfrencham](https://github.com/dfrencham)) ## [v1.121.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.121.1) (2017-07-13) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.121.0...v1.121.1) ## [v1.121.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.121.0) (2017-07-13) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.119.2...v1.121.0) **Closed issues:** - Continual Info bar popup [\#277](https://github.com/microsoft/azure-repos-vscode/issues/277) - TFS do not show or include files outside of currently opened project/folder [\#273](https://github.com/microsoft/azure-repos-vscode/issues/273) **Merged pull requests:** - Add 'device flow' authentication option [\#282](https://github.com/microsoft/azure-repos-vscode/pull/282) ([jeffyoung](https://github.com/jeffyoung)) - Prevent welcome message on re-initialization [\#278](https://github.com/microsoft/azure-repos-vscode/pull/278) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.119.2](https://github.com/microsoft/azure-repos-vscode/tree/v1.119.2) (2017-07-06) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.119.1...v1.119.2) **Closed issues:** - Documentation: Location of TF.exe for Visual Studio 2017 install [\#269](https://github.com/microsoft/azure-repos-vscode/issues/269) - "Logging is disabled" message in Console [\#263](https://github.com/microsoft/azure-repos-vscode/issues/263) - Associate Work Items always shown, even when SCM Provider not VSTS [\#262](https://github.com/microsoft/azure-repos-vscode/issues/262) - Non-English version of the TF executable [\#254](https://github.com/microsoft/azure-repos-vscode/issues/254) ## [v1.119.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.119.1) (2017-07-06) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.119.0...v1.119.1) **Fixed bugs:** - Pull Request URLs are incorrect for repositories with "limited refs" enabled [\#244](https://github.com/microsoft/azure-repos-vscode/issues/244) - Including an add and a delete for the same file [\#241](https://github.com/microsoft/azure-repos-vscode/issues/241) **Closed issues:** - TF10139: check-in policies have not been satisfied [\#266](https://github.com/microsoft/azure-repos-vscode/issues/266) - TFS workspace detected, but cannot switch to SCM provider [\#260](https://github.com/microsoft/azure-repos-vscode/issues/260) - EPIPE error, how to get more info? [\#257](https://github.com/microsoft/azure-repos-vscode/issues/257) - Error: command 'team.signin' not found \[mapped drive\] [\#245](https://github.com/microsoft/azure-repos-vscode/issues/245) - TFS 2017 on premise [\#65](https://github.com/microsoft/azure-repos-vscode/issues/65) **Merged pull requests:** - Add details regarding .tfignore, location of tf.exe [\#276](https://github.com/microsoft/azure-repos-vscode/pull/276) ([jeffyoung](https://github.com/jeffyoung)) - Bump vscode engine version to 1.12.0 \(use withProgress\) [\#271](https://github.com/microsoft/azure-repos-vscode/pull/271) ([jeffyoung](https://github.com/jeffyoung)) - Handle cloaked folders in TFVC workspaces [\#270](https://github.com/microsoft/azure-repos-vscode/pull/270) ([jeffyoung](https://github.com/jeffyoung)) - Several miscellaneous fixes [\#268](https://github.com/microsoft/azure-repos-vscode/pull/268) ([jeffyoung](https://github.com/jeffyoung)) - Package file updates \(updates for 'Associate Work Items' menu option\) [\#267](https://github.com/microsoft/azure-repos-vscode/pull/267) ([jeffyoung](https://github.com/jeffyoung)) - Remove console.log message [\#265](https://github.com/microsoft/azure-repos-vscode/pull/265) ([jeffyoung](https://github.com/jeffyoung)) - Fix matching workspace folders \(primarily for 'restrictWorkspace'\) [\#264](https://github.com/microsoft/azure-repos-vscode/pull/264) ([jeffyoung](https://github.com/jeffyoung)) - Doc updates, initialize Telemetry earlier [\#259](https://github.com/microsoft/azure-repos-vscode/pull/259) ([jeffyoung](https://github.com/jeffyoung)) - Add Welcome message on installation [\#256](https://github.com/microsoft/azure-repos-vscode/pull/256) ([jeffyoung](https://github.com/jeffyoung)) - Add "More Details" button in scenario when Non-Enu TF.exe is configured [\#255](https://github.com/microsoft/azure-repos-vscode/pull/255) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.119.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.119.0) (2017-06-15) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.118.2...v1.119.0) **Implemented enhancements:** - Disable Team extension by workspace settings [\#23](https://github.com/microsoft/azure-repos-vscode/issues/23) - Can we get this to work through a proxy? [\#19](https://github.com/microsoft/azure-repos-vscode/issues/19) **Fixed bugs:** - 401 Unathorized when trying to connect to on-premises TFS2015 Update 2 \(SSL\) [\#59](https://github.com/microsoft/azure-repos-vscode/issues/59) **Closed issues:** - What do the colored letter icons mean next to file names? [\#252](https://github.com/microsoft/azure-repos-vscode/issues/252) - Unable to signin [\#247](https://github.com/microsoft/azure-repos-vscode/issues/247) - Sync operation against TFS 2015 Update 2 fails unexpectedly. [\#223](https://github.com/microsoft/azure-repos-vscode/issues/223) - Unauthorized access to Team Server when signing in [\#111](https://github.com/microsoft/azure-repos-vscode/issues/111) - VSTS icon does not appear on Mac [\#58](https://github.com/microsoft/azure-repos-vscode/issues/58) - Unable to connect to VSTS [\#51](https://github.com/microsoft/azure-repos-vscode/issues/51) **Merged pull requests:** - Ensure Keep-Alive is true for HTTPS connections [\#253](https://github.com/microsoft/azure-repos-vscode/pull/253) ([jeffyoung](https://github.com/jeffyoung)) - Update Repository Url for Limited Refs [\#251](https://github.com/microsoft/azure-repos-vscode/pull/251) ([jeffyoung](https://github.com/jeffyoung)) - Add more helpful text for TFVC set up [\#250](https://github.com/microsoft/azure-repos-vscode/pull/250) ([jeffyoung](https://github.com/jeffyoung)) - Add delete menu to file explorer [\#249](https://github.com/microsoft/azure-repos-vscode/pull/249) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.118.2](https://github.com/microsoft/azure-repos-vscode/tree/v1.118.2) (2017-05-25) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.118.1...v1.118.2) **Closed issues:** - Error: \(team\) Failed to find api location for area: core id: 603fe2ac-9723-48b9-88ad-09305aa6c6e1 [\#212](https://github.com/microsoft/azure-repos-vscode/issues/212) **Merged pull requests:** - Generate consistent telemetry id [\#243](https://github.com/microsoft/azure-repos-vscode/pull/243) ([jeffyoung](https://github.com/jeffyoung)) - Replace deprecated node-uuid with uuid [\#242](https://github.com/microsoft/azure-repos-vscode/pull/242) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.118.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.118.1) (2017-05-22) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.118.0...v1.118.1) **Fixed bugs:** - Sync not working as -collection specified in the command line [\#224](https://github.com/microsoft/azure-repos-vscode/issues/224) - TFS2015 Update 3 could not find a project collection \(encoding issue\) [\#219](https://github.com/microsoft/azure-repos-vscode/issues/219) **Closed issues:** - team.Reinitialize not found [\#221](https://github.com/microsoft/azure-repos-vscode/issues/221) **Merged pull requests:** - Handle exceptions from TFVC Initialize/Reinitialize [\#239](https://github.com/microsoft/azure-repos-vscode/pull/239) ([jeffyoung](https://github.com/jeffyoung)) - formatting tweak [\#238](https://github.com/microsoft/azure-repos-vscode/pull/238) ([vtbassmatt](https://github.com/vtbassmatt)) - Check for 'core id' error \(TFS2013 RTM+\) [\#237](https://github.com/microsoft/azure-repos-vscode/pull/237) ([jeffyoung](https://github.com/jeffyoung)) - Handle error when required WIT api isn't present [\#236](https://github.com/microsoft/azure-repos-vscode/pull/236) ([jeffyoung](https://github.com/jeffyoung)) - Ensure messageOptions contains a value \(not undefined\) [\#235](https://github.com/microsoft/azure-repos-vscode/pull/235) ([jeffyoung](https://github.com/jeffyoung)) - Fix matching TF.exe \(.EXE, .exe\) [\#234](https://github.com/microsoft/azure-repos-vscode/pull/234) ([jeffyoung](https://github.com/jeffyoung)) - Add version of TFVC tooling to debug log file [\#233](https://github.com/microsoft/azure-repos-vscode/pull/233) ([jeffyoung](https://github.com/jeffyoung)) - Properly initialize MessageItems for showErrorMessage [\#232](https://github.com/microsoft/azure-repos-vscode/pull/232) ([jeffyoung](https://github.com/jeffyoung)) - TFVC Readme update \(How to acquire TF.exe\) [\#231](https://github.com/microsoft/azure-repos-vscode/pull/231) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.118.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.118.0) (2017-05-15) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.117.3...v1.118.0) **Fixed bugs:** - running the contributed command [\#204](https://github.com/microsoft/azure-repos-vscode/issues/204) **Closed issues:** - Feature Request: VSTS Build Status Notifications [\#225](https://github.com/microsoft/azure-repos-vscode/issues/225) - TF.exe missing??? [\#222](https://github.com/microsoft/azure-repos-vscode/issues/222) - Workspace not detected? Cannot select SCM Provider [\#217](https://github.com/microsoft/azure-repos-vscode/issues/217) - vsts-vscode no longer working with VS2017? [\#211](https://github.com/microsoft/azure-repos-vscode/issues/211) - duplicated icons in status bar [\#141](https://github.com/microsoft/azure-repos-vscode/issues/141) **Merged pull requests:** - Update package description to include mention of TFVC [\#230](https://github.com/microsoft/azure-repos-vscode/pull/230) ([jeffyoung](https://github.com/jeffyoung)) - Detect TEE CLC not recogizing workspace created by TF.exe [\#229](https://github.com/microsoft/azure-repos-vscode/pull/229) ([jeffyoung](https://github.com/jeffyoung)) - Handle \_JAVA\_OPTIONS env var \(used to address a Java heap error\) [\#228](https://github.com/microsoft/azure-repos-vscode/pull/228) ([jeffyoung](https://github.com/jeffyoung)) - Decode collection url and team project name [\#227](https://github.com/microsoft/azure-repos-vscode/pull/227) ([jeffyoung](https://github.com/jeffyoung)) - Ensure a folder is opened before running commands [\#220](https://github.com/microsoft/azure-repos-vscode/pull/220) ([jeffyoung](https://github.com/jeffyoung)) - Change extension category to `SCM Providers` [\#210](https://github.com/microsoft/azure-repos-vscode/pull/210) ([joaomoreno](https://github.com/joaomoreno)) ## [v1.117.3](https://github.com/microsoft/azure-repos-vscode/tree/v1.117.3) (2017-05-05) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.117.2...v1.117.3) **Fixed bugs:** - team.Reinitialize not found [\#209](https://github.com/microsoft/azure-repos-vscode/issues/209) **Merged pull requests:** - Re-fix duplicate icons [\#215](https://github.com/microsoft/azure-repos-vscode/pull/215) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.117.2](https://github.com/microsoft/azure-repos-vscode/tree/v1.117.2) (2017-05-04) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.117.1...v1.117.2) **Fixed bugs:** - arg.startsWith is not a function [\#203](https://github.com/microsoft/azure-repos-vscode/issues/203) **Merged pull requests:** - Updating README with Feedback changes [\#214](https://github.com/microsoft/azure-repos-vscode/pull/214) ([jeffyoung](https://github.com/jeffyoung)) - Telemetry tweaks [\#213](https://github.com/microsoft/azure-repos-vscode/pull/213) ([jeffyoung](https://github.com/jeffyoung)) - Ensure the feedback status item is always available [\#208](https://github.com/microsoft/azure-repos-vscode/pull/208) ([jeffyoung](https://github.com/jeffyoung)) - Credential Store: Ensure we reject promises on failure [\#207](https://github.com/microsoft/azure-repos-vscode/pull/207) ([jeffyoung](https://github.com/jeffyoung)) - Re-add Reinitialize [\#206](https://github.com/microsoft/azure-repos-vscode/pull/206) ([jeffyoung](https://github.com/jeffyoung)) - Fix 'args.startsWith is not a function' [\#205](https://github.com/microsoft/azure-repos-vscode/pull/205) ([jeffyoung](https://github.com/jeffyoung)) - Add Feedback icon to status bar [\#202](https://github.com/microsoft/azure-repos-vscode/pull/202) ([jeffyoung](https://github.com/jeffyoung)) - TFVC: Add telemetry specific to Exe and CLC [\#201](https://github.com/microsoft/azure-repos-vscode/pull/201) ([jeffyoung](https://github.com/jeffyoung)) - Miscellaneous fix-ups [\#200](https://github.com/microsoft/azure-repos-vscode/pull/200) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.117.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.117.1) (2017-05-01) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.117.0...v1.117.1) **Implemented enhancements:** - Add a Changelog file to the repo to enable native Changelog view in VS Code [\#183](https://github.com/microsoft/azure-repos-vscode/issues/183) **Closed issues:** - Non-English version of the TF Executable [\#192](https://github.com/microsoft/azure-repos-vscode/issues/192) - Wrong url for action "Team : View history" [\#189](https://github.com/microsoft/azure-repos-vscode/issues/189) **Merged pull requests:** - Add message and telemetry for getting latest VS2015 Update [\#199](https://github.com/microsoft/azure-repos-vscode/pull/199) ([jeffyoung](https://github.com/jeffyoung)) - Fix finding version in a multiline stdout [\#198](https://github.com/microsoft/azure-repos-vscode/pull/198) ([jeffyoung](https://github.com/jeffyoung)) - Adding check for server workspaces [\#197](https://github.com/microsoft/azure-repos-vscode/pull/197) ([jeffyoung](https://github.com/jeffyoung)) - README and TFVC\_README updates [\#196](https://github.com/microsoft/azure-repos-vscode/pull/196) ([jeffyoung](https://github.com/jeffyoung)) - Update RemoteUrl if we have no collection in workspace [\#195](https://github.com/microsoft/azure-repos-vscode/pull/195) ([jeffyoung](https://github.com/jeffyoung)) - Fix TFVC history url [\#194](https://github.com/microsoft/azure-repos-vscode/pull/194) ([jeffyoung](https://github.com/jeffyoung)) - Handle signout properly [\#193](https://github.com/microsoft/azure-repos-vscode/pull/193) ([jeffyoung](https://github.com/jeffyoung)) - Removing ability to store token in User settings [\#190](https://github.com/microsoft/azure-repos-vscode/pull/190) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.117.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.117.0) (2017-04-24) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.116.1...v1.117.0) **Implemented enhancements:** - TFSVC is listing all changes from the tfs workspace not the current visual studio code workspa [\#179](https://github.com/microsoft/azure-repos-vscode/issues/179) **Fixed bugs:** - guid specified for parameter projectid must not be guid.empty [\#178](https://github.com/microsoft/azure-repos-vscode/issues/178) - Not able to use TFVC; Getting Forbidden \(403\) [\#172](https://github.com/microsoft/azure-repos-vscode/issues/172) **Closed issues:** - TFVC fails with a non-English version of TF.exe [\#180](https://github.com/microsoft/azure-repos-vscode/issues/180) **Merged pull requests:** - Restrict workspace to VS Code workspace \(and others\) [\#186](https://github.com/microsoft/azure-repos-vscode/pull/186) ([jeffyoung](https://github.com/jeffyoung)) - Disable functionalithy when no team project is found [\#184](https://github.com/microsoft/azure-repos-vscode/pull/184) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.116.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.116.1) (2017-04-20) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.116.0...v1.116.1) **Closed issues:** - TFVC: Error when trying to configure location [\#177](https://github.com/microsoft/azure-repos-vscode/issues/177) - Could not find a workspace with mappings [\#174](https://github.com/microsoft/azure-repos-vscode/issues/174) **Merged pull requests:** - Add SOAP client to get project collections from TFS [\#182](https://github.com/microsoft/azure-repos-vscode/pull/182) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.116.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.116.0) (2017-04-12) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.115.0...v1.116.0) **Implemented enhancements:** - Getting Started [\#147](https://github.com/microsoft/azure-repos-vscode/issues/147) **Closed issues:** - Team: Signin Issue [\#162](https://github.com/microsoft/azure-repos-vscode/issues/162) - Signin doesn't work [\#160](https://github.com/microsoft/azure-repos-vscode/issues/160) **Merged pull requests:** - README updates for v1.116.0 [\#171](https://github.com/microsoft/azure-repos-vscode/pull/171) ([jeffyoung](https://github.com/jeffyoung)) - Fix up opening diffs and changes [\#170](https://github.com/microsoft/azure-repos-vscode/pull/170) ([jeffyoung](https://github.com/jeffyoung)) - Add 'Show Me' button on how to set up a PAT [\#169](https://github.com/microsoft/azure-repos-vscode/pull/169) ([jeffyoung](https://github.com/jeffyoung)) - Fixes for first few bug bash bugs [\#168](https://github.com/microsoft/azure-repos-vscode/pull/168) ([jeffyoung](https://github.com/jeffyoung)) - Additional changes for linter [\#167](https://github.com/microsoft/azure-repos-vscode/pull/167) ([jeffyoung](https://github.com/jeffyoung)) - Filter a couple more events [\#166](https://github.com/microsoft/azure-repos-vscode/pull/166) ([jeffyoung](https://github.com/jeffyoung)) - Multi-Select and Undo All [\#165](https://github.com/microsoft/azure-repos-vscode/pull/165) ([jeffyoung](https://github.com/jeffyoung)) - Several fixes for Mac+Linux [\#164](https://github.com/microsoft/azure-repos-vscode/pull/164) ([jeffyoung](https://github.com/jeffyoung)) - Take latest version of VS Code SCM changes [\#163](https://github.com/microsoft/azure-repos-vscode/pull/163) ([jeffyoung](https://github.com/jeffyoung)) - Add checks for non-ENU tf\(.exe\) [\#158](https://github.com/microsoft/azure-repos-vscode/pull/158) ([jeffyoung](https://github.com/jeffyoung)) - Move linting after build [\#157](https://github.com/microsoft/azure-repos-vscode/pull/157) ([jeffyoung](https://github.com/jeffyoung)) - Always show 'included' group [\#156](https://github.com/microsoft/azure-repos-vscode/pull/156) ([jeffyoung](https://github.com/jeffyoung)) - Take 1.11.0 of vscode.proposed.d.ts [\#155](https://github.com/microsoft/azure-repos-vscode/pull/155) ([jeffyoung](https://github.com/jeffyoung)) - Various linting updates [\#154](https://github.com/microsoft/azure-repos-vscode/pull/154) ([jeffyoung](https://github.com/jeffyoung)) - Get vscode version for user-agent string [\#153](https://github.com/microsoft/azure-repos-vscode/pull/153) ([jeffyoung](https://github.com/jeffyoung)) - Some refactoring [\#152](https://github.com/microsoft/azure-repos-vscode/pull/152) ([jeffyoung](https://github.com/jeffyoung)) - A couple of message updates [\#151](https://github.com/microsoft/azure-repos-vscode/pull/151) ([jeffyoung](https://github.com/jeffyoung)) - Send a custom User-Agent string in headers [\#150](https://github.com/microsoft/azure-repos-vscode/pull/150) ([jeffyoung](https://github.com/jeffyoung)) - Just a few more changes... [\#149](https://github.com/microsoft/azure-repos-vscode/pull/149) ([jeffyoung](https://github.com/jeffyoung)) - Update visibility of commands in palette [\#148](https://github.com/microsoft/azure-repos-vscode/pull/148) ([jeffyoung](https://github.com/jeffyoung)) - Some fixes related to the Undo menu option and command [\#145](https://github.com/microsoft/azure-repos-vscode/pull/145) ([jeffyoung](https://github.com/jeffyoung)) - Several fixes after doing some Tfvc conflicts testing [\#144](https://github.com/microsoft/azure-repos-vscode/pull/144) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.115.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.115.0) (2017-03-08) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.113.0...v1.115.0) **Implemented enhancements:** - Support projects where the git repository is not on VSTS [\#46](https://github.com/microsoft/azure-repos-vscode/issues/46) **Fixed bugs:** - Icons are duplicated [\#63](https://github.com/microsoft/azure-repos-vscode/issues/63) **Closed issues:** - Pinned queries: Unknown configuration setting [\#98](https://github.com/microsoft/azure-repos-vscode/issues/98) **Merged pull requests:** - Bump version to 115 [\#143](https://github.com/microsoft/azure-repos-vscode/pull/143) ([jeffyoung](https://github.com/jeffyoung)) - Fix up FindConflicts and showing of the nag message [\#142](https://github.com/microsoft/azure-repos-vscode/pull/142) ([jeffyoung](https://github.com/jeffyoung)) - Updating vsts icon [\#140](https://github.com/microsoft/azure-repos-vscode/pull/140) ([jpricket](https://github.com/jpricket)) - Use pinned query text, show PAT nag message, stop signout message [\#139](https://github.com/microsoft/azure-repos-vscode/pull/139) ([jeffyoung](https://github.com/jeffyoung)) - Fix polling in relation to re-initialization [\#138](https://github.com/microsoft/azure-repos-vscode/pull/138) ([jeffyoung](https://github.com/jeffyoung)) - renaming classes to make code more understandable [\#137](https://github.com/microsoft/azure-repos-vscode/pull/137) ([jpricket](https://github.com/jpricket)) - Fixing some dependency issues [\#136](https://github.com/microsoft/azure-repos-vscode/pull/136) ([jpricket](https://github.com/jpricket)) - Ensure we fail the build on errors [\#135](https://github.com/microsoft/azure-repos-vscode/pull/135) ([jeffyoung](https://github.com/jeffyoung)) - Passing arguments more securly via stdin [\#134](https://github.com/microsoft/azure-repos-vscode/pull/134) ([jpricket](https://github.com/jpricket)) - Fixing the problem with Include/Exclude [\#133](https://github.com/microsoft/azure-repos-vscode/pull/133) ([jpricket](https://github.com/jpricket)) - Adding tests for resourcegroup [\#132](https://github.com/microsoft/azure-repos-vscode/pull/132) ([jpricket](https://github.com/jpricket)) - Adding tests for commandhelper and find workspace [\#131](https://github.com/microsoft/azure-repos-vscode/pull/131) ([jpricket](https://github.com/jpricket)) - adding tests for sync, resolve, info, and rename [\#130](https://github.com/microsoft/azure-repos-vscode/pull/130) ([jpricket](https://github.com/jpricket)) - Remove collection option from checkin \(tf.exe\) [\#129](https://github.com/microsoft/azure-repos-vscode/pull/129) ([jeffyoung](https://github.com/jeffyoung)) - Boost FindConflicts code coverage [\#128](https://github.com/microsoft/azure-repos-vscode/pull/128) ([jeffyoung](https://github.com/jeffyoung)) - Adding tests for several commands [\#127](https://github.com/microsoft/azure-repos-vscode/pull/127) ([jpricket](https://github.com/jpricket)) - Track whether CLC or EXE is being used for TFVC [\#126](https://github.com/microsoft/azure-repos-vscode/pull/126) ([jeffyoung](https://github.com/jeffyoung)) - Additional unit tests [\#125](https://github.com/microsoft/azure-repos-vscode/pull/125) ([jeffyoung](https://github.com/jeffyoung)) - Making collection optional in argumentbuilder [\#124](https://github.com/microsoft/azure-repos-vscode/pull/124) ([jpricket](https://github.com/jpricket)) - Fix up status command for tf.exe support [\#123](https://github.com/microsoft/azure-repos-vscode/pull/123) ([jeffyoung](https://github.com/jeffyoung)) - Fixing the TODO EXEs in the code [\#122](https://github.com/microsoft/azure-repos-vscode/pull/122) ([jpricket](https://github.com/jpricket)) - Adding simple fixes to get the EXE working [\#121](https://github.com/microsoft/azure-repos-vscode/pull/121) ([jpricket](https://github.com/jpricket)) - Initial tf.exe framework [\#120](https://github.com/microsoft/azure-repos-vscode/pull/120) ([jeffyoung](https://github.com/jeffyoung)) - Ensure we have a context before resolve and delete [\#119](https://github.com/microsoft/azure-repos-vscode/pull/119) ([jeffyoung](https://github.com/jeffyoung)) - Fixing an issue where we called tf resolve without credentials [\#118](https://github.com/microsoft/azure-repos-vscode/pull/118) ([jpricket](https://github.com/jpricket)) - Adding tests for Logger [\#117](https://github.com/microsoft/azure-repos-vscode/pull/117) ([jeffyoung](https://github.com/jeffyoung)) - Getting the TFVC folder to 100% [\#116](https://github.com/microsoft/azure-repos-vscode/pull/116) ([jpricket](https://github.com/jpricket)) - Mo Tests, Mo Tests, Mo Tests [\#115](https://github.com/microsoft/azure-repos-vscode/pull/115) ([jpricket](https://github.com/jpricket)) - Improve our code coverage [\#114](https://github.com/microsoft/azure-repos-vscode/pull/114) ([jeffyoung](https://github.com/jeffyoung)) - Added TFS Proxy settings [\#113](https://github.com/microsoft/azure-repos-vscode/pull/113) ([jpricket](https://github.com/jpricket)) - Add rename context menu \(and experience\) [\#112](https://github.com/microsoft/azure-repos-vscode/pull/112) ([jeffyoung](https://github.com/jeffyoung)) - Add delete \(when files deleted in Explorer\) [\#110](https://github.com/microsoft/azure-repos-vscode/pull/110) ([jeffyoung](https://github.com/jeffyoung)) - Moving AssociateWorkItems to the team extension [\#109](https://github.com/microsoft/azure-repos-vscode/pull/109) ([jpricket](https://github.com/jpricket)) - Added associate work items command [\#108](https://github.com/microsoft/azure-repos-vscode/pull/108) ([jpricket](https://github.com/jpricket)) - Some refactoring and parsing of WIT ids from commit message [\#107](https://github.com/microsoft/azure-repos-vscode/pull/107) ([jpricket](https://github.com/jpricket)) - Expose Telemetry service globally [\#106](https://github.com/microsoft/azure-repos-vscode/pull/106) ([jeffyoung](https://github.com/jeffyoung)) - Adding commands and icons for Resolve actions [\#105](https://github.com/microsoft/azure-repos-vscode/pull/105) ([jpricket](https://github.com/jpricket)) - Creating the Conflicts section in the viewlet [\#104](https://github.com/microsoft/azure-repos-vscode/pull/104) ([jpricket](https://github.com/jpricket)) - Adding OpenDiff, OpenFile commands and menu items [\#103](https://github.com/microsoft/azure-repos-vscode/pull/103) ([jeffyoung](https://github.com/jeffyoung)) - Added Sync command and tests [\#102](https://github.com/microsoft/azure-repos-vscode/pull/102) ([jpricket](https://github.com/jpricket)) - Add ability to include/exclude changes [\#101](https://github.com/microsoft/azure-repos-vscode/pull/101) ([jeffyoung](https://github.com/jeffyoung)) - Adding Checkin and hooking it up to the viewlet [\#100](https://github.com/microsoft/azure-repos-vscode/pull/100) ([jpricket](https://github.com/jpricket)) - Support undo from viewlet \(inline\) [\#99](https://github.com/microsoft/azure-repos-vscode/pull/99) ([jeffyoung](https://github.com/jeffyoung)) - Skeleton menu items [\#97](https://github.com/microsoft/azure-repos-vscode/pull/97) ([jeffyoung](https://github.com/jeffyoung)) - Move team commands into Team category [\#96](https://github.com/microsoft/azure-repos-vscode/pull/96) ([jeffyoung](https://github.com/jeffyoung)) - Adding TFVC Undo command [\#95](https://github.com/microsoft/azure-repos-vscode/pull/95) ([jeffyoung](https://github.com/jeffyoung)) - Cleaning up the code and fixing some bugs [\#94](https://github.com/microsoft/azure-repos-vscode/pull/94) ([jpricket](https://github.com/jpricket)) - Fixing a few bugs/issues [\#93](https://github.com/microsoft/azure-repos-vscode/pull/93) ([jpricket](https://github.com/jpricket)) - Adding SCMContentProvider implementation for TFVC [\#92](https://github.com/microsoft/azure-repos-vscode/pull/92) ([jpricket](https://github.com/jpricket)) ## [v1.113.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.113.0) (2017-02-07) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.108.0...v1.113.0) **Closed issues:** - Ability to select a 'Team' under a 'Project' [\#60](https://github.com/microsoft/azure-repos-vscode/issues/60) - Work items from visualstudio.com should not be visiblewhen they are in a resolved or fixed state. [\#57](https://github.com/microsoft/azure-repos-vscode/issues/57) **Merged pull requests:** - Add signin/signout, version to 1.113.0 [\#91](https://github.com/microsoft/azure-repos-vscode/pull/91) ([jeffyoung](https://github.com/jeffyoung)) - Adding xml2js to ThirdPartyNotices file \(TPN\) [\#90](https://github.com/microsoft/azure-repos-vscode/pull/90) ([jeffyoung](https://github.com/jeffyoung)) - Viewlet shows list of files [\#89](https://github.com/microsoft/azure-repos-vscode/pull/89) ([jpricket](https://github.com/jpricket)) - Support external contexts [\#88](https://github.com/microsoft/azure-repos-vscode/pull/88) ([jeffyoung](https://github.com/jeffyoung)) - Show history for TFVC repositories [\#87](https://github.com/microsoft/azure-repos-vscode/pull/87) ([jeffyoung](https://github.com/jeffyoung)) - Updating mocha \(to 3\) and vscode components to 1.7 [\#86](https://github.com/microsoft/azure-repos-vscode/pull/86) ([jpricket](https://github.com/jpricket)) - Added GetInfo command to TFVC commands [\#85](https://github.com/microsoft/azure-repos-vscode/pull/85) ([jpricket](https://github.com/jpricket)) - Additional error cases, ensure intialization by repo type [\#84](https://github.com/microsoft/azure-repos-vscode/pull/84) ([jeffyoung](https://github.com/jeffyoung)) - Adding some debug logging [\#83](https://github.com/microsoft/azure-repos-vscode/pull/83) ([jpricket](https://github.com/jpricket)) - Fixing the version output and adding tests [\#82](https://github.com/microsoft/azure-repos-vscode/pull/82) ([jpricket](https://github.com/jpricket)) - Add telemetry for TFVC support [\#81](https://github.com/microsoft/azure-repos-vscode/pull/81) ([jeffyoung](https://github.com/jeffyoung)) - Added simple logging of commands to output window [\#80](https://github.com/microsoft/azure-repos-vscode/pull/80) ([jpricket](https://github.com/jpricket)) - Refactored extension classes [\#79](https://github.com/microsoft/azure-repos-vscode/pull/79) ([jpricket](https://github.com/jpricket)) - Several changes [\#78](https://github.com/microsoft/azure-repos-vscode/pull/78) ([jeffyoung](https://github.com/jeffyoung)) - Restrict Tfvc messages we display in VSC ui [\#77](https://github.com/microsoft/azure-repos-vscode/pull/77) ([jeffyoung](https://github.com/jeffyoung)) - Properly handle Team Services collections [\#76](https://github.com/microsoft/azure-repos-vscode/pull/76) ([jeffyoung](https://github.com/jeffyoung)) - Add add'l error handling and logging [\#75](https://github.com/microsoft/azure-repos-vscode/pull/75) ([jeffyoung](https://github.com/jeffyoung)) - Adding env vars to speed up CLC and force English [\#74](https://github.com/microsoft/azure-repos-vscode/pull/74) ([jpricket](https://github.com/jpricket)) - Add support for TFS on-prem \(TFVC\) [\#73](https://github.com/microsoft/azure-repos-vscode/pull/73) ([jeffyoung](https://github.com/jeffyoung)) - Adding CLC version checks as well as localizing [\#72](https://github.com/microsoft/azure-repos-vscode/pull/72) ([jpricket](https://github.com/jpricket)) - Updated FindWorkspace to get mappings [\#71](https://github.com/microsoft/azure-repos-vscode/pull/71) ([jpricket](https://github.com/jpricket)) - Updating chai references to imports [\#70](https://github.com/microsoft/azure-repos-vscode/pull/70) ([jeffyoung](https://github.com/jeffyoung)) - Initial extension integration with Tfvc support [\#69](https://github.com/microsoft/azure-repos-vscode/pull/69) ([jeffyoung](https://github.com/jeffyoung)) - Status cmd is working - still have a few TODOs [\#68](https://github.com/microsoft/azure-repos-vscode/pull/68) ([jpricket](https://github.com/jpricket)) - Update build and wit integration tests [\#67](https://github.com/microsoft/azure-repos-vscode/pull/67) ([jeffyoung](https://github.com/jeffyoung)) - TFVC command framework in place [\#66](https://github.com/microsoft/azure-repos-vscode/pull/66) ([jpricket](https://github.com/jpricket)) - Disable collection of all unhandled exceptions [\#61](https://github.com/microsoft/azure-repos-vscode/pull/61) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.108.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.108.0) (2016-10-24) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.104.1...v1.108.0) **Fixed bugs:** - Can't connect to TFS 2015 OnPremise [\#41](https://github.com/microsoft/azure-repos-vscode/issues/41) **Closed issues:** - Do not see in documentation how to connect to existing TFS [\#44](https://github.com/microsoft/azure-repos-vscode/issues/44) - Associate work items with Git commit/push [\#43](https://github.com/microsoft/azure-repos-vscode/issues/43) **Merged pull requests:** - Updating vscode extension to vso-node-api v5.1.1 [\#56](https://github.com/microsoft/azure-repos-vscode/pull/56) ([jeffyoung](https://github.com/jeffyoung)) - Allow markdown in Marketplace to render as GitHub [\#53](https://github.com/microsoft/azure-repos-vscode/pull/53) ([jeffyoung](https://github.com/jeffyoung)) - Fix the third-party notices link [\#52](https://github.com/microsoft/azure-repos-vscode/pull/52) ([mortonfox](https://github.com/mortonfox)) - Add task to upload code coverage report files [\#50](https://github.com/microsoft/azure-repos-vscode/pull/50) ([jeffyoung](https://github.com/jeffyoung)) - Adding more tests for several objects [\#49](https://github.com/microsoft/azure-repos-vscode/pull/49) ([jeffyoung](https://github.com/jeffyoung)) - Use gulp-istanbul for code coverage on cmd line [\#48](https://github.com/microsoft/azure-repos-vscode/pull/48) ([jeffyoung](https://github.com/jeffyoung)) - Adding additional unit tests and new integration tests [\#47](https://github.com/microsoft/azure-repos-vscode/pull/47) ([jeffyoung](https://github.com/jeffyoung)) - Update README.md, add link for more information [\#45](https://github.com/microsoft/azure-repos-vscode/pull/45) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.104.1](https://github.com/microsoft/azure-repos-vscode/tree/v1.104.1) (2016-08-03) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.104.0...v1.104.1) **Fixed bugs:** - error trying to perform any command [\#24](https://github.com/microsoft/azure-repos-vscode/issues/24) **Closed issues:** - Recognize when local folder is pushed to team project [\#27](https://github.com/microsoft/azure-repos-vscode/issues/27) **Merged pull requests:** - Improve checking of Team Foundation Server server name formats [\#42](https://github.com/microsoft/azure-repos-vscode/pull/42) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.104.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.104.0) (2016-08-02) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.103.0...v1.104.0) **Fixed bugs:** - 'create new bug' results in title that is encoded \(OS X Safari only\) [\#37](https://github.com/microsoft/azure-repos-vscode/issues/37) - Cannot read property 'SendException' of undefined [\#35](https://github.com/microsoft/azure-repos-vscode/issues/35) - v15.17 of ApplicationInsights bloats extension VSIX [\#31](https://github.com/microsoft/azure-repos-vscode/issues/31) **Merged pull requests:** - README updates for 104 release [\#39](https://github.com/microsoft/azure-repos-vscode/pull/39) ([jeffyoung](https://github.com/jeffyoung)) - Remove call to encodeURIComponent [\#38](https://github.com/microsoft/azure-repos-vscode/pull/38) ([jeffyoung](https://github.com/jeffyoung)) - Report no requestHandler via feedbackClient [\#36](https://github.com/microsoft/azure-repos-vscode/pull/36) ([jeffyoung](https://github.com/jeffyoung)) - Add 'Team Foundation Server' keyword [\#34](https://github.com/microsoft/azure-repos-vscode/pull/34) ([jeffyoung](https://github.com/jeffyoung)) - Add filewatcher to config \(remote origin change\) [\#33](https://github.com/microsoft/azure-repos-vscode/pull/33) ([jeffyoung](https://github.com/jeffyoung)) - Pin version of ApplicationInsights to 15.16 [\#32](https://github.com/microsoft/azure-repos-vscode/pull/32) ([jeffyoung](https://github.com/jeffyoung)) - Improve Team Services login experience [\#30](https://github.com/microsoft/azure-repos-vscode/pull/30) ([jeffyoung](https://github.com/jeffyoung)) - Show 'No Git repo' message when appropriate [\#29](https://github.com/microsoft/azure-repos-vscode/pull/29) ([jeffyoung](https://github.com/jeffyoung)) - Fix 'collection in the domain' issue \(\#24\) [\#28](https://github.com/microsoft/azure-repos-vscode/pull/28) ([jeffyoung](https://github.com/jeffyoung)) - Add VSTS to search keyword list [\#26](https://github.com/microsoft/azure-repos-vscode/pull/26) ([chrisdias](https://github.com/chrisdias)) ## [v1.103.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.103.0) (2016-07-08) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.100.0...v1.103.0) **Fixed bugs:** - "create pull request" doesn't populate source branch [\#18](https://github.com/microsoft/azure-repos-vscode/issues/18) **Merged pull requests:** - Add Team Foundation Server 2015 support [\#22](https://github.com/microsoft/azure-repos-vscode/pull/22) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.100.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.100.0) (2016-05-02) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.99.0...v1.100.0) **Closed issues:** - Question: Will this work with VSTS and git-tfs? [\#13](https://github.com/microsoft/azure-repos-vscode/issues/13) **Merged pull requests:** - Pinned Query Status Bar Item [\#17](https://github.com/microsoft/azure-repos-vscode/pull/17) ([mmanela](https://github.com/mmanela)) - Refactoring original Settings into Account Settings and Settings [\#16](https://github.com/microsoft/azure-repos-vscode/pull/16) ([jeffyoung](https://github.com/jeffyoung)) - Refactoring into Info objects [\#15](https://github.com/microsoft/azure-repos-vscode/pull/15) ([jeffyoung](https://github.com/jeffyoung)) - Limiting keywords to five \(latest vsce requirement\) [\#14](https://github.com/microsoft/azure-repos-vscode/pull/14) ([jeffyoung](https://github.com/jeffyoung)) ## [v1.99.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.99.0) (2016-04-14) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/1.99.0...v1.99.0) ## [1.99.0](https://github.com/microsoft/azure-repos-vscode/tree/1.99.0) (2016-04-14) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/v1.98.0...1.99.0) **Implemented enhancements:** - Could this work with my own Team Foundation Server ? [\#11](https://github.com/microsoft/azure-repos-vscode/issues/11) **Merged pull requests:** - Adding additional telemetry for WIT [\#12](https://github.com/microsoft/azure-repos-vscode/pull/12) ([jeffyoung](https://github.com/jeffyoung)) - Update README with information on the maximum number of returned work… [\#9](https://github.com/microsoft/azure-repos-vscode/pull/9) ([jeffyoung](https://github.com/jeffyoung)) - Return a maximum of 200 work items with an option to open browser for… [\#8](https://github.com/microsoft/azure-repos-vscode/pull/8) ([jeffyoung](https://github.com/jeffyoung)) - Check for accounts ending with ".visualstudio.com" in accessTokens [\#7](https://github.com/microsoft/azure-repos-vscode/pull/7) ([jeffyoung](https://github.com/jeffyoung)) - Remove confusing statement about basic authentication [\#6](https://github.com/microsoft/azure-repos-vscode/pull/6) ([jeffyoung](https://github.com/jeffyoung)) - Add telemetry to track 'startup' event [\#5](https://github.com/microsoft/azure-repos-vscode/pull/5) ([jeffyoung](https://github.com/jeffyoung)) - Update README.md with links to videos and a section on Support [\#4](https://github.com/microsoft/azure-repos-vscode/pull/4) ([jeffyoung](https://github.com/jeffyoung)) - team.appInsights.enabled default value should be bool [\#3](https://github.com/microsoft/azure-repos-vscode/pull/3) ([gaessaki](https://github.com/gaessaki)) - Fixing the fomatting of the first numbered list in the readme [\#2](https://github.com/microsoft/azure-repos-vscode/pull/2) ([buckh](https://github.com/buckh)) - CONTRIBUTING: Fix link to Node packages. [\#1](https://github.com/microsoft/azure-repos-vscode/pull/1) ([joshgav](https://github.com/joshgav)) ## [v1.98.0](https://github.com/microsoft/azure-repos-vscode/tree/v1.98.0) (2016-03-25) [Full Changelog](https://github.com/microsoft/azure-repos-vscode/compare/83ec5c4274e1a2ab89d3520fdfb4b74457009e26...v1.98.0) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ================================================ FILE: CONTRIBUTING.md ================================================ # Azure Repos Extension Contributor Guide The instructions below will help you set up your development environment to contribute to this repository. Make sure you've already cloned the repo. :smile: ## Ways to Contribute Interested in contributing to the azure-repos-vscode project? There are plenty of ways to contribute, all of which help make the project better. * Submit a [bug report](https://github.com/Microsoft/azure-repos-vscode/issues/new) or [feature request](https://github.com/Microsoft/azure-repos-vscode/issues/new) through the Issue Tracker * Review the [source code changes](https://github.com/Microsoft/azure-repos-vscode/pulls) * Submit a code fix for a bug (see `Submitting Pull Requests` below) * Participate in [discussions](https://github.com/Microsoft/azure-repos-vscode/issues) ## Set up Node, npm and gulp ### Node and npm **Windows and Mac OSX**: Download and install node from [nodejs.org](http://nodejs.org/) **Linux**: Install [using package manager](https://nodejs.org/en/download/package-manager/) From a terminal ensure at least node 5.4.1 and npm 3: ```bash $ node -v && npm -v v5.9.0 3.8.2 ``` **Note**: To get npm version 3.8.2, you may need to update npm after installing node. To do that: ```bash [sudo] npm install npm -g ``` **Note2**: Our CI service uses Node 11. Node 14 appears to be incompatible. Your mileage may vary. ### Gulp Install gulp ```bash [sudo] npm install gulp -g ``` From the root of the repo, install all of the build dependencies: ```bash [sudo] npm install --greedy ``` ### Install the Visual Studio Code Extension Manager (VSCE) Before packaging via gulp, ensure that you have the "vsce" tool installed globally. Otherwise, the package step will fail. From the root of the repo, run: ```bash [sudo] npm install vsce -g ``` ## Build To build the extension, run the following from the root of the repo: ```bash gulp ``` This command will create the out\src and out\test folders at the root of the repository. ## Tests Tests should be run with changes. Before you run tests, make sure you have built the extension. Run the following from the root of the repo: ```bash gulp test ``` To run the tests within Visual Studio Code, change the debug profile to "Launch Tests" and press `F5`. ## Package The package command will package the extension into a Visual Studio extension installer (.vsix file). It will also transpile the TypeScript into the out\src and out\test folders. From the root of the repo: ```bash gulp package ``` The VSIX package will be created in the root of the repository. ## Code Structure The code is structured between the Visual Studio Code extension file, the Azure Repos extension object, and the clients, contexts, helpers and services. ### Visual Studio Code Extension file This is the file with the code called by Visual Studio Code to bootstrap the extension. **extension.ts** should be thin and delegate to the Azure Repos Extension object. ### Azure Repos Extension object This is the object intended to have small methods that call to the feature-specific clients that manipulate the UI and make calls to Azure DevOps via the service objects. When adding new commands, the functions that are called should be defined here. ### Clients These are the clients used to talk to the services (see Services below). The clients can manipulate the UI but should be the only objects calling the feature-specific services. ### Contexts * Git - This context is meant to contain the client-side Git configuration information * Server - This context is meant to contain the server-side information needed when making calls to Azure DevOps ### Helpers These are classes used to define constants, a logger, settings (configuration), strings and various utility functions. ### Info These are classes used to hold data about particular objects (credentials, repository and user). ### Services All of the communication to Azure DevOps should be done via services found in this folder. These services should not know anything about the client-side types used to manipulate the Visual Studio Code UI. The Q Promise APIs found in the vso-node-api package is the model used in this extension. ## Debugging To debug the extension, make sure you've installed all of the npm packages as instructed earlier. Then, open the root of the repository in Visual Studio Code and press F5. If you have the extension already installed, you'll need to uninstall it via the Command Palette and try again. During debugging, you may want to control how often polling occurs for build status and pull request updates. Or you may want to turn on debug console and `winston` logging. The [README.md](README.md) file has instructions on how to change those settings. ## Code Styles 1. The various gulp commands will run `tslint` and flag any errors. Please ensure that the code stays clean. 2. All source files must have the following lines at the top: ``` /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ ``` 3. We keep LF line-endings on the server. Please set the `core.safecrlf` git config property to true. ``` git config core.safecrlf true ``` ## Contribution License Agreement In order to contribute, you will need to sign a [Contributor License Agreement](https://cla.microsoft.com/). ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Submitting Pull Requests We welcome pull requests! Fork this repo and send us your contributions. Go [here](https://help.github.com/articles/using-pull-requests/) to get familiar with GitHub pull requests. Before submitting your request, ensure that both `gulp` and `gulp test` succeed. **UPDATE**: With a recent commit, integration tests were added under the *test-integration* folder. These tests are run by the CI build and the results are reported back to any pull request as a "build check". The integration tests are not runnable outside of the CI build without setting up additional infrastructure. As such, it isn't required that a contributor run these tests before submitting the pull request. However, if an issue arises that breaks the integration tests, please file an issue and I'll follow up as quickly as possible. Note that the build for this repo is set to build every night and runs unit and integration tests at that time. ================================================ FILE: DEPRECATED.md ================================================ # Sunsetting the Azure Repos VS Code extension We're sunsetting this Azure Repos VS Code extension. On 2020-11-06 (Nov 6, 2020), we'll remove it from the VS Code Marketplace and archive the repository. If you still have it installed, you may continue to use it, but it will not receive any further investment or updates. ## Why are we doing this? Since we launched the extension four and a half years ago, Visual Studio Code has seen incredible adoption. Azure DevOps and Azure Repos have similarly continued to see amazing growth. However, use of TFVC, the centralized source control system, with VS Code has declined. The majority of VS Code users prefer Git, and therefore use of the extension has declined dramatically in the last 1-2 years. VS Code has great native Git support. Therefore we have taken the decision to discontinue support of this extension. Developers still using TFVC with VS Code will need to use an external version control client such as [the `tf` command line](https://docs.microsoft.com/azure/devops/repos/tfvc/use-team-foundation-version-control-commands). ## What happened? 1. We shipped a final update which contains this notice but contains no other functional changes, bug fixes, etc. 2. On 2020-11-06 we unpublished the extension from the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/). Those who already have it installed can continue to use it, but without support from Microsoft. The extension won't receive any updates, bug fixes, or security fixes, so you use it at your own risk. 3. We archived the [GitHub repository](https://github.com/microsoft/azure-repos-vscode) putting it into a read-only state. This will not delete the code or historical issues (though all open issues and PRs were closed). The repository is still readable and forkable. ## Is there any action for me? If you want to keep using the extension, no, there's no action. It will remain installed in your copy of VS Code. If you want to stop using the extension, you can uninstall it on VS Code's extensions page. If you do, you won't be able to reinstall it from the Marketplace. ## Thank you To everyone who used the extension, provided feedback, or contributed bug fixes, THANK YOU! ================================================ FILE: LICENSE.txt ================================================ Azure Repos Extension for Visual Studio Code Copyright (c) Microsoft Corporation All rights reserved.  MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Azure Repos Extension for Visual Studio Code **DEPRECATION NOTICE**: This extension is no longer receiving updates. [Learn more here](https://aka.ms/AA9k2vv). --- This extension allows you to connect to Azure DevOps Services and Team Foundation Server and provides support for [Team Foundation Version Control (TFVC)](TFVC_README.md#quick-start). It allows you to monitor your builds and manage your pull requests and work items for your TFVC or Git source repositories. The extension uses your local repository information to connect to either Azure DevOps Services or Team Foundation Server 2015 Update 2 (and later). ![Azure Repos extension](assets/vscode.png) Here is the [Walkthrough of the Azure Repos extension for Visual Studio Code](https://youtu.be/sk6LrzQX4P8) video that shows many of the features of the extension. ## Prerequisites ### Azure DevOps Services If you are using the extension with Azure DevOps Services, ensure you have an Azure DevOps Services organization. If you do not have one, [sign up for Azure DevOps Services](https://aka.ms/SignupAzureDevOps/?campaign=azure~repos~vscode~readme). ### Team Foundation Server If you are planning on using the extension with Team Foundation Server, you **must** be running Team Foundation Server 2015 Update 2 or later. Earlier versions of Team Foundation Server are not supported. ### Clone your Git repository With Git, the extension uses the remote origin of your repository to determine how to connect to Azure DevOps Services (or your Team Foundation Server), in most cases you will need to have a Git repository already cloned locally. If you intend on cloning an existing repository, do so before proceeding. If you do not have a Git repository cloned locally but already have an Azure DevOps Services organization (or a Team Foundation Server instance), you may create a local repository (via `git init`) and once you set the "origin" remote for that local repository, the extension will detect the change to the remote and attempt to contact Azure DevOps Services (or Team Foundation Server). ### Create your TFVC workspace With TFVC, the extension uses information about the current workspace to determine how to connect to Azure DevOps Services (or your Team Foundation Server). Workspaces can be created using the Visual Studio IDE, Eclipse or with the JetBrains IDEs (e.g, Android Studio, IntelliJ). **Note:** At this time, you will need to have a ***local*** TFVC workspace already available on your local machine. More information about the difference between the two types (and how to determine which one you're using) can be found [here](TFVC_README.md#what-is-the-difference-between-a-local-and-server-workspace-how-can-i-tell-which-one-im-working-with). The issue tracking support for Server workspaces is [here](https://github.com/Microsoft/azure-repos-vscode/issues/176). ## Installation First, you will need to install [Visual Studio Code](https://code.visualstudio.com/download) `1.12.0` or later. To install the extension with the latest version of Visual Studio Code (version 1.13.1 is the latest as of this writing), bring up the Visual Studio Code Command Palette (`F1`), type `install` and choose `Extensions: Install Extensions`. In the `Search Extensions in Marketplace` text box, type `team`. Find the `Azure Repos` extension published by *Microsoft* and click the `Install` button. Restart Visual Studio Code. ## Authentication ### Azure DevOps Services If you are connecting to Azure DevOps Services, you will need a personal access token (PAT). With the release of v1.121.0 of the extension, you have a choice of whether you would like to create a token yourself manually and provide it when prompted, or use a new experience in which you are authenticated to Azure DevOps Services using your web browser. In the new experience, a personal access token is still created on your behalf but only after you are authenticated. The created token has *All Scopes* permissions but can be updated in your profile settings. Both tokens (manual or the new experience) are stored securely on your machine. #### Manual Token Creation Should you wish to create a personal access token yourself, go [here](https://aka.ms/gtgzt4) to read how. You can also [view our video](https://youtu.be/t6gGfj8WOgg) on how to do the same. * Git repositories require that you create your token with the **Build (read)**, **Code (read)** and **Work items (read)** scopes to ensure full functionality. You can also use *All Scopes*, but the minimum required scopes are those listed above. * TFVC repositories require tokens with *All Scopes*. Anything less will cause the extension to fail. #### Browser-based Authentication When using the new authentication experience, you will be prompted to copy a *device code* used to identify yourself to the authentication system. Once you accept the prompt to begin authentication, your default web browser will be opened to a login page. After supplying that device code and having it verified, you will then be prompted to authenticate with Azure DevOps Services normally (e.g., username and password, multi-factor authentication, etc.). Once you are authenticated to Azure DevOps Services, a personal access token will be created for you and the extension will be initialized normally. To see what this experience is like, [view this video](https://youtu.be/HnDNdm1WCIo). ### Team Foundation Server If you are connecting to Team Foundation Server, you will only need your NTLM credentials (domain name, server name and password). It is assumed that you have the proper permissions on the TFS Server. Details on how to connect to either Azure DevOps Services or Team Foundation Server are found in the next section. ## TFVC Support Once you have a local TFVC workspace available, you must configure the TFVC support in Visual Studio Code. You can find that information (including demo videos) in our [TFVC documentation](TFVC_README.md). ## Open a local Repository folder Once you have installed the extension, open either the root folder or a sub-folder of the repository. Once an Azure DevOps Services or Team Foundation Server 2015 Update 2 (or later) repository is detected by the extension, you will need to provide your credentials (if credentials weren't already found). If you are required to provide your credentials, there will be an indicator in the status bar whose message will indicate that you need to sign in. The indicator looks like this: ![Team Error indicator](assets/team-error.png) To sign in, run the `team signin` command or simply click on that indicator. If your repository is hosted on Azure Repos, you will be prompted to enter your personal access token. When you do, it will be stored securely on your computer and used to connect. If your repository is on Team Foundation Server 2015 Update 2 or later, you will be prompted to enter your username and password. After both are provided, they will be stored securely on your computer and used to connect to your TFS server. Once your credentials are verified, the status bar indicators will be active and the remaining commands will be ready to use. The stored credentials will be used for each connection to the server until they are either removed by the `team signout` command or overwritten by a subsequent `team signin` command. **Note:** In order for the extension to be activated, a repository *folder* must be opened. The extension won't be activated if only a single *file* in the repository is opened. ## Status Bar Indicators * ![Team Project indicator](assets/project-indicator.png) – This status bar item is populated with the name of the team project to which the repository belongs. Clicking on the item will open your browser to the team website. * ![Pull Requests indicator](assets/pullrequest-indicator.png) – ***Git only*** This status bar item is a count of active pull requests that you either requested yourself or were added to explicitly as a reviewer. Clicking the item will display that list of pull requests in the quick pick list. Choosing one will take you to that pull request in your browser. This indicator will update its status every 5 minutes. * ![Build Status indicator](assets/buildstatus-indicator.png) – This status bar item shows the status of the build for this particular repository and branch. Hovering over the item will provide additional information about which build was referenced (if any). Clicking on the item will take you to that build’s summary page in your browser. This indicator will update its status every 5 minutes. * ![Pinned Work Item Query Status indicator](assets/pinnedquery-indicator.png) – This status bar item shows the number of items returned by your pinned work item query. If you have not configured a pinned query it defaults to the work items assigned to you. Clicking the item will show you the work items the query returns. This indicator will update its status every 5 minutes. * ![Feedback indicator](assets/feedback-indicator.png) – Clicking this status bar item allows you to quickly send feedback about the Azure Repos extension. ## Commands In addition to the status bar integrations, the extension also provides several commands for interacting with Azure DevOps Services and Team Foundation Server. In the Command Palette (`F1`), type `team` and choose a command. * `team associate work items` – Prompts you to choose a work item that is assigned to you (or from the results of your custom query). Choosing a work item will add it to the current commit/check-in message. * `team create bug` – Opens your browser to the webpage used to create a new bug. If a single line of text is highlighted in Visual Studio Code, it will be used as the title of the bug. The bug will be assigned to you. You can then choose to update the fields, save, cancel, etc. * `team create pull request` – ***Git only*** Opens your browser for a new pull request based on the current repository and branch. Before creating the pull request, ensure that you save, commit and push any changes you have before running the command. Doing so will ensure that all of your latest changes are part of the pull request. * `team create task` – Opens your browser to the webpage used to create a new task. If a single line of text is highlighted in Visual Studio Code, it will be used as the title of the task. The task will be assigned to you. You can then choose to update the fields, save, cancel, etc. * `team create work item` – Prompts you to choose a work item type from the list available in your team project. Once you make a selection, your browser is opened to the webpage used to create the work item. If a single line f text is highlighted in Visual Studio Code, it will be used as the title of the task. The work item will be assigned to you. You can then choose to update the fields, save, cancel, etc. * `team send feedback` – Prompts you to either send a smile or a frown. After choosing, you can provide us feedback of up to 1000 characters. Optionally, provide your email address so we can contact if you needed. If you do not want to provide your email address, just leave it empty (we'll still get your feedback). *Note:* Feedback can be sent even if telemetry reporting is disabled. * `team signin` – Use this command to sign in to an Azure DevOps Services organization or Team Foundation Server 2015 Update 2 (and later) server. When your credentials are provided, they will be stored securely on your computer. The saved credentials will be used for that organization until they are removed by the `team signout` command or overwritten by a subsequent `team signin` command. See the "Secure Credential Storage" topic below for more details. * `team signout` – Use this command to sign out from an Azure DevOps Services organization or Team Foundation Server 2015 Update 2 (and later) server. Signing out will remove your credentials from your local computer. To sign back in, you will need to run the `team signin` command again. * `team view blame` – ***Git only*** If a file in the repository is opened in the editor, it will open your browser to the blame page for that file in the current branch in the server repository. * `team view build summary` – Same behavior as clicking on the Build Status status bar item. * `team view history` – If a file in the repository is opened in the editor, it will open your browser to the history page for that file in the current branch in the server repository. Otherwise, the history of the current branch in the server repository will be opened. This command does support TFVC repositories. * `team view pull requests` – ***Git only*** Same behavior as clicking on the Pull Requests status bar item. * `team view website` – Same behavior as clicking on the team project status bar item. * `team view work items` – Prompts you to choose a work item that is assigned to you, sorted by ChangedDate descending. Choosing a work item will open it in your browser. This command will return a maximum of 200 results with an option to "Browse additional work items...". Choosing that option will open your browser to show all of the results of your query. * `team view work item queries` – Prompts you to choose a query stored in your “My Queries” folder in your team project. Choosing a query will run it and display the results in the Quick Pick list. Choosing one of the results will open that work item in your browser. This command will return a maximum of 200 results with an option to "Browse additional work items...". Choosing that option will open your browser to show all of the results of your query. ## Secure Credential Storage When you run the `team signin` command, the credentials that you provide will be stored securely on your computer. On Windows, your credentials wil be stored by Windows Credential Manager. On macOS, your credentials will be stored in the Keychain. On Linux, your credentials will be stored in a file on your local file system in a subdirectory of your home folder. That file is created only with RW rights for the user running Visual Studio Code. It is **not encrypted** on disk. ## How to disable telemetry reporting The Azure Repos extension collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](http://go.microsoft.com/fwlink/?LinkId=528096&clcid=0x409) to learn more. If you don’t wish to send usage data to Microsoft, add the following entry to Settings (**File > Preferences > Settings**): ```javascript "team.appInsights.enabled": "false" ``` ## Polling interval The polling interval for the pull request and build status bar indicators defaults to ten minutes. You can change this value in the Visual Studio Code Settings by adding an entry like the one below. The minimum value is 10. ```javascript "team.pollingInterval": 12 ``` ## Logging There may be times when you need to enable file logging to troubleshoot an issue. There are five levels of logging (`error`, `warn`, `info`, `verbose` and `debug`). Since logging is disabled by default, you can add an entry like the one shown below to Visual Studio Code's Settings. Once you are finished logging, either remove the setting or set it to an empty string. ```javascript "team.logging.level": "debug" ``` The log file will be placed at the root of your workspace and will be named `team-extension.log`. ### Private builds In order to facilitate more debugging, you may be provided with a "private build" of the extension. The private build will likely come in the form of a .ZIP file named similarly to the VSIX that gets deployed to the Marketplace (e.g., `team-0.117.0.vsix.zip`). To install the private build, you must uninstall the previous version and then _side load_ the new one. First, remove the .ZIP extension from the file and then follow [these instructions](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix) to install the VSIX. ## Pinned Work Item Queries You can customize the pinned work item query by adding the following in the Visual Studio Code Settings. You need to provide the following: * `account`: For Azure DevOps Services, set `account` to either `organization.visualstudio.com` or `dev.azure.com/organization`. For Team Foundation Server, if your server URL is `http://servername:8080/tfs` then set `account` to `servername:8080`. * `queryText` or `queryPath` **Using Query Text** ```javascript "team.pinnedQueries": [ { "account": "dev.azure.com/organization", "queryText": "SELECT * FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.ChangedDate] > @Today - 14" } ] ``` **Using Query Path** ```javascript "team.pinnedQueries": [ { "account": "dev.azure.com/organization", "queryPath": "Shared Queries/My Folder/My Query" } ] ``` You can also create a *global* pinned query which will be the default if you have not configured one by replacing *dev.azure.com/organization* with *global* in the previous examples. ## Using External (non-Microsoft) Source Repositories Starting with version 1.113.0, you can now use the extension with repositories that are **not** hosted with either Azure DevOps Services or Team Foundation Server. You will be able to monitor your builds (for a specific build definition) and work items that *are* hosted with either Azure DevOps Services or Team Foundation Server by specifying your server information. To do this, set the following settings in VS Code. It is recommended that you set these in your Workspace Settings (and not User Settings). You will, of course, still need to authenticate (provide credentials). **Note:** If you're using a Team Foundation Version Control repository, you should *not* use these settings. Have a look at the [TFVC Quick Start](TFVC_README.md#quick-start). ```javascript "team.remoteUrl": "https://organization.visualstudio.com", "team.teamProject": "myTeamProject", "team.buildDefinitionId": 42, ``` To determine your build definition id, open the build summary for the build you'd like to monitor and grab the value of the _buildId=_ parameter in the url. ## Support Support for this extension is provided on our [GitHub Issue Tracker](https://github.com/Microsoft/azure-repos-vscode/issues). You can submit a [bug report](https://github.com/Microsoft/azure-repos-vscode/issues/new), a [feature request](https://github.com/Microsoft/azure-repos-vscode/issues/new) or participate in [discussions](https://github.com/Microsoft/azure-repos-vscode/issues). ## Contributing to the Extension See the [developer documentation](CONTRIBUTING.md) for details on how to contribute to this extension. ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Privacy Statement The [Microsoft Visual Studio Product Family Privacy Statement](http://go.microsoft.com/fwlink/?LinkId=528096&clcid=0x409) describes the privacy statement of this software. ## License This extension is [licensed under the MIT License](LICENSE.txt). Please see the [third-party notices](ThirdPartyNotices.txt) file for additional copyright notices and license terms applicable to portions of the software. ================================================ FILE: TFVC_README.md ================================================ # Team Foundation Version Control (TFVC) Support This extension provides TFVC support by way of the Source Control Viewlet in Visual Studio Code. Here are the currently supported features provided by the extension: - Execute all basic version control actions such as add, delete, rename, move, etc. - View local changes and history for your files - Include and Exclude changes (and move files between the two states) - Merge conflicts from updates - Check-in and update local files - Integrated TFVC Output window - Support for a TFS proxy - Associate work items to check-ins *(TEE CLC only)* - Support for **Local** workspaces created with either Visual Studio, the JetBrains IDEs or Eclipse ([details](#what-is-the-difference-between-a-local-and-server-workspace-how-can-i-tell-which-one-im-working-with)) ![Team Foundation Version Control Viewlet](assets/tfvc-viewlet.png) ## Quick Start Below is a short list of steps to get up-and-running with TFVC support. Be sure to check out the other TFVC documentation on this page. - [Install the Azure Repos extension](#the-visual-studio-code-extension) for Visual Studio Code. - For Azure DevOps Services, ensure you have a Personal Access Token (PAT) with All Scopes available. Team Foundation Server requires your domain credentials. [More info...](#authentication) - Ensure you have a [TF command line client installed](#tfvc-command-line-client) (either TF.exe or the TEE CLC). - Set the [`tfvc.location`](#configure-tfvc-support) VS Code setting to the full path of your TF command line client. - Open a folder containing a *Local* TFVC Workspace and sign in when prompted. [More info...](#open-a-local-tfvc-repository-folder) - Set the SCM Provider to `TFVC`. [Read how...](https://code.visualstudio.com/updates/v1_13#_install-additional-scm-providers) ## Getting Started Videos Below are a few videos to help get you started using TFVC quickly. - [TFVC Source Code Control for Visual Studio Code](https://youtu.be/6IzJ2UPGmoQ) - This video shows you how to set up the TFVC support on Windows and demonstrates much of the functionality available for Team Foundation Version Control. The features shown in this video apply equally well to the TFVC support on macOS and Linux. - [Set up and Configure the TEE CLC on Linux (and macOS)](https://youtu.be/VPNaEIVZfr0) - This video demonstrates how to set up the Team Explorer Everywhere Command Line Client (TEE CLC) on Ubuntu. On macOS and Linux, the CLC provides the TFVC capability to the extension. - [Set up the Azure Repos extension for Visual Studio Code](https://youtu.be/t6gGfj8WOgg) - If you haven't used the extension before, this video will show you how to set it up, create a personal access token and get up and running. - [Walkthrough of the Azure Repos extension for Visual Studio Code](https://youtu.be/sk6LrzQX4P8) - This is a walkthrough of most of the features of the Azure Repos extension. ## Prerequisites ### Azure DevOps Services If you are using the extension with Azure DevOps Services, ensure you have an Azure DevOps Services organization. If you do not have one, [sign up for Azure DevOps Services](https://aka.ms/SignupAzureDevOps/?campaign=azure~repos~vscode~tfvcreadme). ### Team Foundation Server If you are planning on using the extension with Team Foundation Server, you **must** be running Team Foundation Server 2015 Update 2 or later. Earlier versions of Team Foundation Server are not supported. ## Installation First, you will need to install [Visual Studio Code](https://code.visualstudio.com/download) `1.11.1` or later. ### The Visual Studio Code Extension To install the extension with the latest version of Visual Studio Code (version 1.11.1 is the latest as of this writing), bring up the Visual Studio Code Command Palette (`F1`), type `install` and choose `Extensions: Install Extensions`. In the `Search Extensions in Marketplace` text box, type `team`. Find the `Azure Repos` extension published by *Microsoft* and click the `Install` button. Restart Visual Studio Code. ### TFVC Command Line Client In order to provide TFVC support in Visual Studio Code, the extension relies on the use of a TF command line client. Therefore, you will need to have one already installed and configured. TFVC support will not work without an available command line client. #### Visual Studio IDE (Windows) With a typical installation of Visual Studio, the Windows version of the TFVC command line client (tf.exe) is available under the `Program Files (x86)` folder. It will typically be placed in a location similar to `C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\TF.exe`. On the 2017 version of Visual Studio Community, it can be found in a location similar to `C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\TF.exe`. Visual Studio Community can be downloaded from [here](https://www.visualstudio.com/free-developer-offers/). #### Eclipse, JetBrains IDEs (Windows, macOS, Linux) If you typically use Eclipse or one of the JetBrains IDEs (e.g., Android Studio, IntelliJ), you will need to download and extract a version of the Team Explorer Everywhere Command Line Client (TEE CLC). As of this update, the latest version can be downloaded from [TEE-CLC-14.114.0.zip](https://github.com/Microsoft/team-explorer-everywhere/releases/download/v14.114.0/TEE-CLC-14.114.0.zip). After downloading, extract it to a folder and open a Terminal window. From that Terminal window, ensure that at least Java 8 is installed and available (run `java -version` to check the version). Once Java is configured properly, you will need to accept the TEE End User License Agreement by running `tf eula`. Make sure to read the EULA and accept it by entering `y`. The extension will not function properly until this EULA is accepted. If you are using the TEE CLC, see how to set it up by viewing [this video](https://youtu.be/VPNaEIVZfr0). ## Authentication ### Azure DevOps Services If you are connecting to Azure DevOps Services, you will need a personal access token (PAT). The latest version of the extension will prompt for your token and store it securely. In previous versions of the extension, you needed to create a token and store it in your Visual Studio Code settings. If you do not have a personal access token yet, you will need to create one on your Azure DevOps Services organization. To create the token, go [here](https://aka.ms/gtgzt4) to read how. You can also [view our video](https://youtu.be/t6gGfj8WOgg) on how to do the same. * TFVC repositories require tokens with *All Scopes*. Anything less will cause the extension to fail. In addition to connecting to Azure DevOps Services using a personal access token (PAT), the TF.exe command line client needs its own access to the remote repository, otherwise TFVC will fail: * Open a command prompt with the correct version of TF.exe in its PATH * Enter "tf workspace" * If this opens a login prompt, enter your credentials * You can then close the Workspace window ### Team Foundation Server If you are connecting to Team Foundation Server, you will only need your NTLM credentials (domain name, server name and password). It is assumed that you have the proper permissions on the TFS Server. ## Configure TFVC Support To configure TFVC support, you must provide the location to the TF command line client used by the extension to perform TFVC actions. To set this location, add the following entry to Settings (**File > Preferences > Settings**): ```javascript "tfvc.location": "" ``` If you plan to use the TFVC command line provided by the Visual Studio IDE, the value to provide will be similar to `C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\Common7\\IDE\\TF.exe`. If you plan to use the TEE CLC, the value to provide will be similar to `/home/username/TEE-CLC-14.114.0/tf`. On Windows, the entry should point to `tf.cmd`. On macOS and Linux, it should point to the script `tf`. If you are using the TEE CLC, see how to do this by viewing [this video](https://youtu.be/VPNaEIVZfr0). ## Team Foundation Version Control To get an overview of the TFVC support provided by the extension, view [this video](https://youtu.be/6IzJ2UPGmoQ). Although it demonstrates the extension running on Windows, the TFVC functionality will work the same on macOS and Linux. Further information is detailed below. ### Open a local TFVC Repository folder Once you have installed the extension, open either the root folder or a sub-folder of the TFVC repository. Once a Team Services or Team Foundation Server 2015 Update 2 (or later) repository is detected by the extension, you will need to provide your credentials (if credentials weren't already found). If you are required to provide your credentials, there will be an indicator in the status bar whose message will indicate that you need to sign in. The indicator looks like this: ![Team Error indicator](assets/team-error.png) To sign in, run the `team signin` command or simply click on that indicator. If your repository is hosted on Azure Repos, you will be prompted to enter your personal access token. When you do, it will be stored securely on your computer and used to connect. If your repository is on Team Foundation Server 2015 Update 2 or later, you will be prompted to enter your username and password. After both are provided, they will be stored securely on your computer and used to connect to your TFS server. Once your credentials are verified, the status bar indicators will be active and the remaining commands will be ready to use. The stored credentials will be used for each connection to the server until they are either removed by the `team signout` command or overwritten by a subsequent `team signin` command. **Note:** In order for the extension to be activated, a repository *folder* must be opened. The extension won't be activated if only a single *file* in the repository is opened. ### The TFVC Source Control Viewlet ![Team Foundation Version Control Viewlet](assets/tfvc-viewlet.png) This is the Team Foundation Version Control Viewlet. It displays lists of any conflicting changes (to be resolved before the next check-in), included changes (changes to be included in the next check-in), excluded changes (changes to keep but excluded from check-in). The viewlet also provides right-click context menus to allow additional functionality like Undo, Include and Exclude. #### Check In and Refresh ![TFVC Check In Refresh](assets/tfvc-checkin-refresh.png) * `Check In` – This `check mark` button is used to check in your changes. You will not be prompted for a comment so make sure you provide one before clicking. * `Refresh` – This `circular arrow` button runs the TFVC `Status` command and processes the results. #### The '...' Menu This menu provides access to additional commands provided by the TFVC source provider. ![TFVC ... Menu](assets/tfvc-more-menu.png) * `Sync` – This option runs the `Get` command and handles any conflicts or errors. * `Associate Work Items` – In order to associate work items to your check-in, select this option. The list of work items assigned to you (or returned from your custom query) will be displayed for you to choose. Choosing one adds it to your check-in comment. * `Check In` – Use this option to check in your changes. * `Undo All` - If there are file modifications, this command will prompt for confirmation and then undo all of the changes. * `Show TFVC Output` – Use this option to display the `TFVC Output` window which shows the TFVC commands run during this session. * `Switch SCM Provider... >` – This option is provided by VS Code and allows you to change between the available source control providers (e.g, Git, TFVC, etc.). #### Resolving Conflicts When conflicts need to be resolved, you can use these commands to do so. There are inline icons which also provide this functionality. ![TFVC Resolve Conflicts](assets/tfvc-resolve-conflicts.png) * `Open Diff` – Opens the diff view between the modified file and the versioned file. * `Open File` – Opens the modified file in the editor. * `Resolve: Take Theirs` – Resolves the conflict as `Take Theirs`. * `Resolve: Keep Yours` – Resolves the conflict as `Keep Yours`. * `Undo` – Reverts the changes made to the file. ***Note:*** Resolving conflicts between branches is not supported in the extension. At this time, these conflicts are best addressed in an IDE or at the command line. ### File Explorer Access to renaming a file is provided on Visual Studio Code's File Explorer menu. ![TFVC Rename](assets/tfvc-rename.png) * `Delete (TFVC)` – To properly delete a file in TFVC using the `Delete` command, use this option on Visual Studio Code's File Explorer. Since 1.119.0, the extension will no longer automatically promote candidate deletions to a TFVC delete. * `Rename (TFVC)` – To properly rename a file in TFVC using the `Rename` command (instead of `Delete` and `Add`), use this option on Visual Studio Code's File Explorer. You will prompted for a new file name. ## Additional Information ### Specifying a TFS Proxy To configure a TFS proxy server, you must provide the URL to that server in the Visual Studio Code Settings (**File > Preferences > Settings**): ```javascript "tfvc.proxy": "http://servername:9999/proxy" ``` ### Restricting Status changes to the current VS Code Workspace By default, TFVC support shows status across the entire mapped workspace regardless of the folder you may have open in Visual Studio Code. If you prefer to see just the status of changes for the currently opened VS Code workspace, you can do so by setting the following flag in the Visual Studio Code Settings (**File > Preferences > Settings**): ```javascript "tfvc.restrictWorkspace": true ``` ## Frequently Asked Questions ### *I received the "It appears you have configured a non-English version of the TF executable. Please ensure an English version is properly configured." error message after configuring TF.exe. How can I get the extension to work properly?* When TF.exe is configured as the tool to communicate with your TFS server (the TEE CLC does not have this problem), it will output status messages in the language of the configured operating system. So if the operating system is configured for German, the status messages will be output in German. The extension parses the output of TF.exe to show status in the SCM Viewlet. Today, the extension expects the messages to be output in English (thus the error message you received). This message is an attempt to identify the issue. While there isn't a fix (e.g, an update to TF.exe that allows it to always output English status messages), one workaround is to rename the folder on disk from where the localized resources are being loaded by TF.exe. If the folder containing the localized resources cannot be located, the .NET Framework will fallback to the resources stored in TF.exe itself (the English ones). To locate the folder that needs to be renamed, open the folder that contains the TF.exe you have configured. In that folder are folders such as `de`, `fr`, `it`, `es`. Simply rename that folder with another name and restart Visual Studio Code. If you would rather not update that particular installation of Visual Studio (as such a change would affect Visual Studio as well), you can install the [standalone Team Explorer 2017 version of Visual Studio](#how-can-i-acquire-tfexe-do-i-need-a-version-of-visual-studio), configure the TF.exe that ships with it and rename the folder in that instance. ### *Is it required that I have a TFVC workspace already on my local machine to use the TFVC support?* Since release 1.116.0, yes, it is a requirement that you have an existing workspace on your local machine. ### *Can I use the Team Explorer Everywhere Command Line Client (TEE CLC) to provide the TFVC functionality on Windows?* Yes. If you use Eclipse or one of JetBrain's IDEs (e.g, Android Studio, IntelliJ) on Windows, then you will want to use the TEE CLC to provide the TFVC support. ### *How do I set up the CLC on Windows?* While the TEE CLC is primarily for macOS and Linux users it can also be used on Windows. In order to use the CLC on Windows, you need to do the following: - Download and install a version of [Java 8 for Windows](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html). - Make sure to install the version (i.e., x86 or x64) that matches your processor architecture - Ensure the path to `java.exe` is a part of your `PATH` environment variable. - Check that you can successully run `java -version` - Download and unzip the [latest version](https://github.com/Microsoft/team-explorer-everywhere/releases) of the TEE CLC to a local folder of your choice. - The file you need to download is of the format `TEE-CLC-14.114.0.zip` - Open a command prompt and run `{path-to-tf.cmd} eula` and accept the End User License Agreement. - From within Visual Studio Code, update your `tfvc.location` setting to the full path to `tf.cmd` (e.g., `C:\TEE-CLC-14.111.1\tf.cmd`). - The last thing that you **must** do is run the `tf workspaces` command as detailed [here](#i-have-workspaces-created-with-visual-studio-can-i-use-the-tee-clc-to-work-with-them) so that the CLC is aware of the workspaces in the specified collection. (Each tool, tf.cmd and tf.exe, keeps its own local cache of workspaces.) Finally, there's also a [video](https://youtu.be/VPNaEIVZfr0) that shows how to do this on Linux (the same high-level steps apply on Windows). ### *Which instructions do I follow to set up TFVC functionality on the macOS?* You will follow the same instructions for setting up the TEE CLC as is shown on Ubuntu in [this video](https://youtu.be/VPNaEIVZfr0). ### *I have workspaces created with Visual Studio. Can I use the TEE CLC to work with them?* This should be possible. However, you will need to make the TEE CLC aware of those workspaces by running the `tf workspaces -collection:` command. ### *Using the TEE CLC, I am unable to access an existing local workspace. What can I do?* This error may mean you are attempting to access a workspace created by TF.exe from the TEE CLC. First, using the CLC, run the `tf workspaces` command as detailed [here](#i-have-workspaces-created-with-visual-studio-can-i-use-the-tee-clc-to-work-with-them) to help the CLC be aware of the workspaces in the specified collection. You may also need to run the `tf workfold` command from the local folder being accessed from Visual Studio Code. Running both commands should make the TEE CLC aware of the workspace and as well as verify that access to it is possible. ***Note:*** You will also have to do this if you move between versions of TF.exe (e.g., workspaces created with VS2015 will not be able to be detected with the TF.exe that ships with VS2017 until `tf workspaces` is run with the 2017 TF.exe; and vice-versa). Each major version of TF.exe stores its workspace information in a different folder on disk. This behavior is "by-design". ### *My TFS server requires associating work items to a check-in via check-in policies but I can't check in with TF.exe. What can I do?* Unfortunately, TF.exe doesn't provide the ability to associate work items on check in. The most TF.exe can do is submit a check in *comment* with a reference to the work item (which will not actually associate the work item). In order to enable checking in to servers that have check-in policies enabled, you must use the TEE CLC (which does provide support for associating work items on check-in). Follow [these instructions](#how-do-i-set-up-the-clc-on-windows) to set up the TEE CLC on Windows. ### *Many files are showing up in the TFVC Viewlet that don't show up in the Visual Studio IDE. How can I ignore them?* The Visual Studio IDE shows these files as 'Detected Changes' and simply displays the number of them. The TFVC Viewlet will display each file individually. See [this issue](https://github.com/Microsoft/azure-repos-vscode/issues/248) for an example of what you may see. To properly ignore these files, create a `.tfignore` file and add it to the root folder of your TFVC repository. (You will also want to check this file in.) You can find the official documentation on how to do this [here](https://www.visualstudio.com/en-us/docs/tfvc/add-files-server#customize-which-files-are-ignored-by-version-control). To get started easily, copy this [example file](https://www.visualstudio.com/en-us/docs/tfvc/add-files-server#tfignore-file-example), place it in the root of your repository, update it as necessary and check it in. For example, if you want to ignore all files under the `node_modules` folder, you would add a `\node_modules` entry to the `.tfignore` file. ### *I already have Visual Studio installed. How can I determine the location of TF.exe?* Here's a tip from **@dsolodow** ([original comment](https://github.com/Microsoft/azure-repos-vscode/issues/269#issuecomment-311837077)): _When you install Visual Studio, it creates a Start Menu shortcut called "Developer Command Prompt for VS YEAR" where YEAR is 2015, 2017, etc. If you launch that, and then at that prompt run `where tf.exe` it will give you the full path to it that you can then put into the settings.json._ ### *How can I acquire TF.exe? Do I need a version of Visual Studio?* Yes, you do need a version of Visual Studio. While TF.exe comes with the Community, Enterprise and Professional versions of Visual Studio 2017, there is also a free, standalone "Visual Studio Team Explorer 2017" version that contains TF.exe. You can find all of the versions of Visual Studio (including Team Explorer) on the [Visual Studio 2017 Downloads](https://www.visualstudio.com/downloads/) page (which needs to be expanded first). It is listed under the "Visual Studio 2017" section on that page. The release notes for Team Explorer 2017 can be found [here](https://www.visualstudio.com/en-us/news/releasenotes/vs2017-relnotes-v15.1#te). ### *Where is the support for Server workspaces?* At this time, it's still on the backlog. The issue tracking support for Server workspaces is [here](https://github.com/Microsoft/azure-repos-vscode/issues/176). ### *What is the difference between a Local and Server workspace? How can I tell which one I'm working with?* You can read about the differences between the two in [our documentation](https://www.visualstudio.com/en-us/docs/tfvc/decide-between-using-local-server-workspace). Using `tf.exe` on Windows, you can determine which type of workspace you have by running `tf workspace` from the folder where your workspace resides. When you do, a dialog box similar to the one below will be displayed and you can see the type of workspace in the `Location` field (you may need to click the `Advanced >>` button). To change the type of workspace, update the `Location` field and click `OK`. ![Azure Repos extension](assets/tf-workspace-dialog.png) ## Further Information For information on other features of the extension, support, licensing, privacy, or contributing code, please review the main [README](README.md) file. ================================================ FILE: ThirdPartyNotices.txt ================================================ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION Do Not Translate or Localize Azure Repos Extension for Visual Studio Code incorporates third party material (and other Microsoft material) from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. 1. Application Insights for Node.js (https://github.com/Microsoft/ApplicationInsights-node.js) 2. event-stream (https://github.com/dominictarr/event-stream) 3. git-repo-info (https://github.com/rwjblue/git-repo-info) 4. node-open (https://github.com/pwnall/node-open) 5. node-url (https://github.com/defunctzombie/node-url) 6. opener (https://github.com/domenic/opener) 7. parse-git-config (https://github.com/jonschlinkert/parse-git-config) 8. path (https://github.com/jinder/path) 9. readable-stream (https://github.com/nodejs/readable-stream) 10. request-promise-native (https://github.com/request/request-promise-native) 11. underscore (https://github.com/jashkenas/underscore) 12. uuid (https://github.com/kelektiv/node-uuid) 13. vso-node-api (https://github.com/Microsoft/vso-node-api) 14. vsts-device-flow-auth (https://github.com/Microsoft/vsts-device-flow-auth) 15. winston (https://github.com/winstonjs/winston) 16. xml2js (https://github.com/Leonidas-from-XIV/node-xml2js) 17. xmldoc (https://github.com/nfarina/xmldoc) %% Application Insights for Node.js NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) Microsoft Corporation All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF Application Insights for Node.js NOTICES, INFORMATION, AND LICENSE %% event-stream NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright (c) 2011 Dominic Tarr Provided for Informational Purposes Only   MIT License   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF event-stream NOTICES, INFORMATION, AND LICENSE %% git-repo-info NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) Robert Jackson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF git-repo-info NOTICES, INFORMATION, AND LICENSE %% node-open NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) 2012 Jay Jordan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF node-open NOTICES, INFORMATION, AND LICENSE %% node-url NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright Joyent, Inc. and other Node contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF node-url NOTICES, INFORMATION, AND LICENSE %% opener NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright � 2012�2015 Domenic Denicola Licensed under the WTFPL Version 2 (http://www.wtfpl.net/about/) ========================================= END OF opener NOTICES, INFORMATION, AND LICENSE %% parse-git-config NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) 2015 Jon Schlinkert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF parse-git-config NOTICES, INFORMATION, AND LICENSE %% path NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright Joyent, Inc. and other Node contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF path NOTICES, INFORMATION, AND LICENSE %% readable-stream NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright Joyent, Inc. and other Node contributors. All rights reserved. Provided for Informational Purposes Only   MIT License   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF readable-stream NOTICES, INFORMATION, AND LICENSE %% request-promise-native NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= ISC License Copyright (c) 2017, Nicolai Kamenzky and contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF request-promise-native NOTICES, INFORMATION, AND LICENSE %% underscore NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright (c) 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors   Provided for Informational Purposes Only   MIT License   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF underscore NOTICES, INFORMATION, AND LICENSE %% uuid NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) 2010-2016 Robert Kieffer and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF uuid NOTICES, INFORMATION, AND LICENSE %% vso-node-api NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) 2015 Microsoft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF vso-node-api NOTICES, INFORMATION, AND LICENSE %% vsts-device-flow-auth NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= MIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ========================================= END OF vsts-device-flow-auth NOTICES, INFORMATION, AND LICENSE %% winston NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= The MIT License (MIT) Copyright (c) 2010 Charlie Robbins Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF winston NOTICES, INFORMATION, AND LICENSE %% xml2js NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright 2010, 2011, 2012, 2013. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF xml2js NOTICES, INFORMATION, AND LICENSE %% xmldoc NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright 2012 Nick Farina. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF xmldoc NOTICES, INFORMATION, AND LICENSE ================================================ FILE: gulpfile.js ================================================ 'use strict'; var gulp = require('gulp'), mocha = require('gulp-mocha'), gutil = require('gulp-util'); var exec = require('child_process').exec; var tslint = require('gulp-tslint'); var typescript = require('gulp-typescript'); var sourcemaps = require('gulp-sourcemaps'); var del = require('del'); var argv = require('yargs').argv; var istanbul = require('gulp-istanbul'); var tl = require('vsts-task-lib'); var path = require('path'); // Default to list reporter when run directly. // CI build can pass 'reporter=junit' to create JUnit results files var reporterUnitTest = { reporter: 'list' }; var reporterIntegrationTest = { reporter: 'list' }; if (argv.reporter === "junit") { reporterUnitTest = { reporter: 'mocha-junit-reporter', reporterOptions: { mochaFile: 'out/results/tests/test-unittestresults.xml'} } ; reporterIntegrationTest = { reporter: 'mocha-junit-reporter', reporterOptions: { mochaFile: 'out/results/tests/test-integrationtestresults.xml'} } ; } function errorHandler(err) { console.error(err.message); process.exit(1); } gulp.task('clean', function (done) { return del(['out/**', '!out', '!out/src/credentialstore/linux', '!out/src/credentialstore/osx', '!out/src/credentialstore/win32'], done); }); gulp.task('copyresources', ['clean'], function() { return gulp.src('resources/**/*') .pipe(gulp.dest('out/resources')); }); gulp.task('build', ['copyresources'], function () { let tsProject = typescript.createProject('./tsconfig.json'); let tsResult = tsProject.src() .pipe(sourcemaps.init()) .pipe(tsProject()) .on('error', errorHandler); return tsResult.js .pipe(sourcemaps.write('.', { sourceRoot: function (file) { // This override is needed because of a bug in sourcemaps base logic. // "file.base"" is the out dir where all the js and map files are located. return file.base; } })) .pipe(gulp.dest('./out')); }); gulp.task('tslint', ['build'], function () { return gulp.src(['./src/**/*.ts', './test/**/*.ts', './test-integration/**/*.ts']) .pipe(tslint({ configuration: "tslint.json", formatter: "verbose" })) .pipe(tslint.report({ emitError: true, summarizeFailureOutput: true })) .on('error', errorHandler); }); gulp.task('publishbuild', ['tslint'], function () { gulp.src(['./src/credentialstore/**/*.js']) .pipe(gulp.dest('./out/src/credentialstore')); gulp.src(['./src/credentialstore/bin/win32/*']) .pipe(gulp.dest('./out/src/credentialstore/bin/win32')); }); gulp.task('publishall', ['publishbuild'], function () { gulp.src(['./test/contexts/testrepos/**/*']) .pipe(gulp.dest('./out/test/contexts/testrepos')); gulp.src(['./test/helpers/testrepos/**/*']) .pipe(gulp.dest('./out/test/helpers/testrepos')); gulp.src(['./patches/vso-node-api/handlers/ntlm.js']) .pipe(gulp.dest('./node_modules/vso-node-api/handlers')); }); //Tests will fail with MODULE_NOT_FOUND if I try to run 'publishBuild' before test target //gulp.task('test', ['publishBuild'], function() { gulp.task('test', function() { return gulp.src(['out/test/**/*.js'], {read: false}) .pipe(mocha(reporterUnitTest)) .on('error', errorHandler); }); gulp.task('test-integration', function() { return gulp.src(['out/test-integration/**/*.js'], {read: false}) .pipe(mocha(reporterIntegrationTest)) .on('error', errorHandler); }); gulp.task('test-coverage', function() { //credentialstore is brought in from separate repository, exclude it here //exclude the files we know we can't get coverage on (e.g., vscode, etc.) return gulp.src(['out/src/**/*.js', '!out/src/credentialstore/**' ,'!out/src/extension.js' ,'!out/src/extensionmanager.js' ,'!out/src/team-extension.js' ,'!out/src/clients/baseclient.js' ,'!out/src/clients/buildclient.js' ,'!out/src/clients/coreapiclient.js' ,'!out/src/clients/feedbackclient.js' ,'!out/src/clients/gitclient.js' ,'!out/src/clients/httpclient.js' ,'!out/src/clients/repositoryinfoclient.js' ,'!out/src/clients/soapclient.js' ,'!out/src/clients/tfscatalogsoapclient.js' ,'!out/src/clients/witclient.js' ,'!out/src/contexts/repocontextfactory.js' ,'!out/src/contexts/tfvccontext.js' ,'!out/src/helpers/settings.js' ,'!out/src/helpers/vscodeutils.interfaces.js' ,'!out/src/helpers/vscodeutils.js' ,'!out/src/services/telemetry.js' ,'!out/src/services/coreapi.js' ,'!out/src/tfvc/tfvcrepository.js' ,'!out/src/tfvc/tfvc-extension.js' ,'!out/src/tfvc/tfcommandlinerunner.js' ,'!out/src/tfvc/tfvcoutput.js' ,'!out/src/tfvc/tfvcscmprovider.js' ,'!out/src/tfvc/tfvcsettings.js' ,'!out/src/tfvc/uihelper.js' ,'!out/src/tfvc/util.js' ,'!out/src/tfvc/scm/commithoverprovider.js' ,'!out/src/tfvc/scm/decorationprovider.js' ,'!out/src/tfvc/scm/model.js' ,'!out/src/tfvc/scm/resource.js' ,'!out/src/tfvc/scm/tfvccontentprovider.js' ]) .pipe(istanbul({includeUntested: true})) //.pipe(istanbul()) .pipe(istanbul.hookRequire()) //for using node.js .on('finish', function() { gulp.src('out/test*/**/*.js') .pipe(mocha(reporterUnitTest)) .pipe(istanbul.writeReports( { dir: 'out/results/coverage', reporters: ['cobertura','html'], reportOpts: { dir: 'out/results/coverage' } } )); }) .on('error', errorHandler); }); //The following task is used by the CI build to upload code coverage files //Added due to race condition between writeReports and ccPublisher.publish //It's OK for this to fail if the coverage file doesn't exist gulp.task('upload-coverage-file', function() { var ccPublisher = new tl.CodeCoveragePublisher(); ccPublisher.publish('cobertura', path.join(__dirname, 'out/results/coverage/cobertura-coverage.xml'), path.join(__dirname, 'out/results/coverage'), ""); }); gulp.task('test-all', ['test', 'test-integration'], function() { }); gulp.task('packageonly', function (cb) { exec('vsce package', function (err, stdout, stderr) { console.log(stdout); console.log(stderr); cb(err); }); }); gulp.task('package', ['publishall'], function (cb) { exec('vsce package', function (err, stdout, stderr) { console.log(stdout); console.log(stderr); cb(err); }); }); gulp.task('vsce-version', function (cb) { exec('vsce -Version', function (err, stdout, stderr) { console.log(stdout); console.log(stderr); cb(err); }); }); gulp.task('default', ['publishall']); ================================================ FILE: package.json ================================================ { "name": "team", "displayName": "Azure Repos", "description": "Connect to Azure Repos and work with Git and Team Foundation Version Control (TFVC) repositories. Manage your pull requests, work items, and more.", "version": "1.161.1", "publisher": "ms-vsts", "icon": "assets/team.png", "markdown": "standard", "galleryBanner": { "color": "#313131", "theme": "dark" }, "keywords": [ "VSTS", "Team Foundation Server", "Team Services", "Visual Studio Team Services", "TFVC", "Azure Repos" ], "categories": [ "SCM Providers", "Other" ], "repository": { "type": "git", "url": "https://github.com/Microsoft/azure-repos-vscode.git" }, "bugs": "https://github.com/Microsoft/azure-repos-vscode/issues", "license": "SEE LICENSE IN LICENSE.txt", "homepage": "https://github.com/Microsoft/azure-repos-vscode/blob/master/README.md", "engines": { "vscode": "^1.12.0" }, "enableProposedApi": false, "activationEvents": [ "*" ], "main": "./out/src/extension", "contributes": { "menus": { "commandPalette": [ { "command": "team.OpenBlamePage", "when": "scmProvider != tfvc" }, { "command": "tfvc.Undo", "when": "false" }, { "command": "tfvc.UndoAll", "when": "false" }, { "command": "tfvc.ResolveTakeTheirs", "when": "false" }, { "command": "tfvc.ResolveKeepYours", "when": "false" }, { "command": "tfvc.Include", "when": "false" }, { "command": "tfvc.Exclude", "when": "false" }, { "command": "tfvc.Checkin", "when": "false" }, { "command": "tfvc.Delete", "when": "false" }, { "command": "tfvc.Rename", "when": "false" }, { "command": "tfvc.Open", "when": "false" }, { "command": "tfvc.OpenFile", "when": "false" }, { "command": "tfvc.OpenDiff", "when": "false" }, { "command": "tfvc.Refresh", "when": "scmProvider == tfvc" }, { "command": "tfvc.ShowOutput", "when": "scmProvider == tfvc" }, { "command": "tfvc.Sync", "when": "scmProvider == tfvc" } ], "explorer/context": [ { "command": "tfvc.Delete", "group": "9_cutcopypaste", "when": "scmProvider == tfvc" }, { "command": "tfvc.Rename", "group": "9_cutcopypaste", "when": "scmProvider == tfvc" } ], "scm/title": [ { "command": "tfvc.Checkin", "group": "navigation@1", "when": "scmProvider == tfvc" }, { "command": "tfvc.Refresh", "group": "navigation@2", "when": "scmProvider == tfvc" }, { "command": "tfvc.Sync", "group": "1_sync@1", "when": "scmProvider == tfvc" }, { "command": "team.AssociateWorkItems", "group": "3_commit@3", "when": "scmProvider == tfvc" }, { "command": "team.AssociateWorkItems", "group": "3_commit@5", "when": "scmProvider == git" }, { "command": "tfvc.Checkin", "group": "3_commit@4", "when": "scmProvider == tfvc" }, { "command": "tfvc.UndoAll", "group": "3_commit@5", "when": "scmProvider == tfvc" }, { "command": "tfvc.ShowOutput", "group": "5_output", "when": "scmProvider == tfvc" } ], "scm/resourceState/context": [ { "command": "tfvc.OpenDiff", "when": "scmProvider == tfvc", "group": "navigation@1" }, { "command": "tfvc.OpenFile", "when": "scmProvider == tfvc", "group": "navigation@2" }, { "command": "tfvc.Undo", "when": "scmProvider == tfvc && scmResourceGroup != conflicts", "group": "3_commit@1" }, { "command": "tfvc.Include", "when": "scmProvider == tfvc && scmResourceGroup == excluded", "group": "1_modification@1" }, { "command": "tfvc.Exclude", "when": "scmProvider == tfvc && scmResourceGroup == included", "group": "1_modification@1" }, { "command": "tfvc.ResolveTakeTheirs", "when": "scmProvider == tfvc && scmResourceGroup == conflicts", "group": "1_modification@1" }, { "command": "tfvc.ResolveKeepYours", "when": "scmProvider == tfvc && scmResourceGroup == conflicts", "group": "1_modification@2" }, { "command": "tfvc.Undo", "when": "scmProvider == tfvc && scmResourceGroup == conflicts", "group": "9_cutcopypaste" }, { "command": "tfvc.Undo", "when": "scmProvider == tfvc", "group": "inline@1" }, { "command": "tfvc.Include", "when": "scmProvider == tfvc && scmResourceGroup == excluded", "group": "inline@2" }, { "command": "tfvc.Exclude", "when": "scmProvider == tfvc && scmResourceGroup == included", "group": "inline@2" }, { "command": "tfvc.ResolveTakeTheirs", "when": "scmProvider == tfvc && scmResourceGroup == conflicts", "group": "inline@2" }, { "command": "tfvc.ResolveKeepYours", "when": "scmProvider == tfvc && scmResourceGroup == conflicts", "group": "inline@3" } ] }, "configuration": { "type": "object", "title": "Azure Repos", "properties": { "team.appInsights.enabled": { "type": "boolean", "default": true, "description": "Enables Application Insights telemetry collection for the Azure Repos extension." }, "team.logging.level": { "type": "string", "default": "", "description": "Set the logging level for the extension (error, warn, info, verbose, debug)." }, "team.pinnedQueries": { "type": "array", "default": [ { "account": "", "queryText": "", "queryPath": "" } ], "description": "Specify the account and either the queryText or queryPath of the query you'd like to monitor. If specified, queryText is preferred over queryPath." }, "team.pollingInterval": { "type": "number", "default": 5, "description": "Specify the number of minutes to wait when polling for new builds and pull requests." }, "team.remoteUrl": { "type": "string", "default": "", "description": "[Not for TFVC] Specify the url to a project collection to use when your source code repository is not hosted with Microsoft. Requires team.teamProject." }, "team.teamProject": { "type": "string", "default": "", "description": "[Not for TFVC] Specify the team project to use when your source code repository is not hosted with Microsoft. Requires team.remoteUrl." }, "team.buildDefinitionId": { "type": "number", "default": 0, "description": "[Not for TFVC] Specify the team project's build definition Id to monitor when your source code repository is not hosted with Microsoft. Requires both team.remoteUrl and team.teamProject." }, "team.showWelcomeMessage": { "type": "boolean", "default": true, "description": "Tracks whether the extension should display the Welcome message after the initial installation." }, "team.showFarewellMessage": { "type": "boolean", "default": true, "description": "Show the deprecation message again?" }, "tfvc.location": { "type": "string", "default": "", "description": "[Required for TFVC] Specify the full path to the TF executable or script to use for TFVC functionality." }, "tfvc.proxy": { "type": "string", "default": "", "description": "[Optional for TFVC] Specify the full URL (ex. http://servername:9999/proxy) to the TFS proxy to use for TFVC functionality." }, "tfvc.restrictWorkspace": { "type": "boolean", "default": false, "description": "[Optional for TFVC] Restricts the TFVC workspace to the currently open VS Code workspace." } } }, "commands": [ { "command": "team.AssociateWorkItems", "title": "Associate Work Items", "category": "Team" }, { "command": "team.OpenNewTask", "title": "Create Task", "category": "Team" }, { "command": "team.OpenNewBug", "title": "Create Bug", "category": "Team" }, { "command": "team.OpenFileHistory", "title": "View History", "category": "Team" }, { "command": "team.OpenBlamePage", "title": "View Blame", "category": "Team" }, { "command": "team.OpenNewPullRequest", "title": "Create Pull Request", "category": "Team" }, { "command": "team.OpenNewWorkItem", "title": "Create Work Item", "category": "Team" }, { "command": "team.GetPullRequests", "title": "View Pull Requests", "category": "Team" }, { "command": "team.OpenBuildSummaryPage", "title": "View Build Summary", "category": "Team" }, { "command": "team.OpenTeamSite", "title": "View Website", "category": "Team" }, { "command": "team.ViewWorkItems", "title": "View Work Items", "category": "Team" }, { "command": "team.ViewWorkItemQueries", "title": "View Work Item Queries", "category": "Team" }, { "command": "team.SendFeedback", "title": "Send Feedback", "category": "Team" }, { "command": "team.Signin", "title": "Signin", "category": "Team" }, { "command": "team.Signout", "title": "Signout", "category": "Team" }, { "command": "tfvc.Checkin", "title": "Check In", "category": "TFVC", "icon": { "light": "resources/icons/light/check.svg", "dark": "resources/icons/dark/check.svg" } }, { "command": "tfvc.Exclude", "title": "Exclude", "category": "TFVC", "icon": { "light": "resources/icons/light/unstage.svg", "dark": "resources/icons/dark/unstage.svg" } }, { "command": "tfvc.Include", "title": "Include", "category": "TFVC", "icon": { "light": "resources/icons/light/stage.svg", "dark": "resources/icons/dark/stage.svg" } }, { "command": "tfvc.Open", "title": "Open", "category": "TFVC" }, { "command": "tfvc.OpenDiff", "title": "Open Diff", "category": "TFVC" }, { "command": "tfvc.OpenFile", "title": "Open File", "category": "TFVC" }, { "command": "tfvc.Delete", "title": "Delete (TFVC)", "category": "TFVC" }, { "command": "tfvc.Rename", "title": "Rename (TFVC)", "category": "TFVC" }, { "command": "tfvc.ShowOutput", "title": "Show TFVC Output", "category": "TFVC" }, { "command": "tfvc.Refresh", "title": "Refresh", "category": "TFVC", "icon": { "light": "resources/icons/light/refresh.svg", "dark": "resources/icons/dark/refresh.svg" } }, { "command": "tfvc.ResolveKeepYours", "title": "Resolve: Keep Yours", "category": "TFVC", "icon": { "light": "resources/icons/light/resolve-keepyours.svg", "dark": "resources/icons/dark/resolve-keepyours.svg" } }, { "command": "tfvc.ResolveTakeTheirs", "title": "Resolve: Take Theirs", "category": "TFVC", "icon": { "light": "resources/icons/light/resolve-taketheirs.svg", "dark": "resources/icons/dark/resolve-taketheirs.svg" } }, { "command": "tfvc.Sync", "title": "Sync", "category": "TFVC" }, { "command": "tfvc.Undo", "title": "Undo", "category": "TFVC", "icon": { "light": "resources/icons/light/clean.svg", "dark": "resources/icons/dark/clean.svg" } }, { "command": "tfvc.UndoAll", "title": "Undo All", "category": "TFVC" } ] }, "scripts": { "compile": "tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install" }, "devDependencies": { "@types/applicationinsights": "0.15.33", "@types/chai": "^3.4.1", "@types/mocha": "^2.2.32", "@types/node": "^6.0.40", "@types/q": "^0.0.32", "@types/uuid": "^2.0.29", "@types/winston": "2.3.1", "@types/xml2js": "^0.0.32", "@types/xmldoc": "^0.5.0", "chai": "^3.4.1", "del": "^2.2.0", "gulp-istanbul": "^1.1.1", "gulp-mocha": "^3.0.1", "gulp-sourcemaps": "^2.4.0", "gulp-tslint": "^7.1.0", "gulp-typescript": "^3.1.4", "gulp-util": "^3.0.7", "mocha-junit-reporter": "^1.12.0", "should": "^8.1.1", "tslint": "^4.0.0", "typescript": "2.3.4", "vscode": "^1.0.0", "vsts-task-lib": "^0.9.18", "yargs": "^5.0.0" }, "dependencies": { "applicationinsights": "0.15.16", "event-stream": "^3.3.2", "fs": "0.0.2", "git-repo-info": "^1.1.2", "gulp": "^3.9.1", "open": "0.0.5", "opener": "^1.4.1", "parse-git-config": "^0.3.1", "path": "^0.12.7", "readable-stream": "^2.1.4", "request-promise-native": "^1.0.4", "underscore": "^1.8.3", "url": "^0.11.0", "uuid": "^3.0.1", "vso-node-api": "^5.1.1", "vsts-device-flow-auth": "^1.136.0", "winston": "2.3.1", "xml2js": "^0.4.17", "xmldoc": "^0.5.1" } } ================================================ FILE: patches/vso-node-api/handlers/ntlm.js ================================================ "use strict"; // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. var http = require("http"); var https = require("https"); var _ = require("underscore"); var ntlm = require("../opensource/node-http-ntlm/ntlm"); var NtlmCredentialHandler = (function () { function NtlmCredentialHandler(username, password, domain, workstation) { this.username = username; this.password = password; if (domain !== undefined) { this.domain = domain; } if (workstation !== undefined) { this.workstation = workstation; } } NtlmCredentialHandler.prototype.prepareRequest = function (options) { // No headers or options need to be set. We keep the credentials on the handler itself. // If a (proxy) agent is set, remove it as we don't support proxy for NTLM at this time if (options.agent) { delete options.agent; } }; NtlmCredentialHandler.prototype.canHandleAuthentication = function (res) { if (res && res.statusCode === 401) { // Ensure that we're talking NTLM here // Once we have the www-authenticate header, split it so we can ensure we can talk NTLM var wwwAuthenticate = res.headers['www-authenticate']; if (wwwAuthenticate !== undefined) { var mechanisms = wwwAuthenticate.split(', '); var idx = mechanisms.indexOf("NTLM"); if (idx >= 0) { // Check specifically for 'NTLM' since www-authenticate header can also contain // the Authorization value to use in the form of 'NTLM TlRMTVNT....AAAADw==' if (mechanisms[idx].length == 4) { return true; } } } } return false; }; // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js NtlmCredentialHandler.prototype.handleAuthentication = function (httpClient, protocol, options, objs, finalCallback) { // Set up the headers for NTLM authentication var ntlmOptions = _.extend(options, { username: this.username, password: this.password, domain: this.domain || '', workstation: this.workstation || '' }); var keepaliveAgent; if (httpClient.isSsl === true) { keepaliveAgent = new https.Agent({ keepAlive: true }); } else { keepaliveAgent = new http.Agent({ keepAlive: true }); } var self = this; // The following pattern of sending the type1 message following immediately (in a setImmediate) is // critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner) // the NTLM exchange will always fail with a 401. this.sendType1Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) { if (err) { return finalCallback(err, null, null); } setImmediate(function () { self.sendType3Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, res, finalCallback); }); }); }; // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js NtlmCredentialHandler.prototype.sendType1Message = function (httpClient, protocol, options, objs, keepaliveAgent, callback) { var type1msg = ntlm.createType1Message(options); var type1options = { headers: { 'Connection': 'keep-alive', 'Authorization': type1msg }, timeout: options.timeout || 0, agent: keepaliveAgent, // don't redirect because http could change to https which means we need to change the keepaliveAgent allowRedirects: false }; type1options = _.extend(type1options, _.omit(options, 'headers')); httpClient.requestInternal(protocol, type1options, objs, callback); }; // The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js NtlmCredentialHandler.prototype.sendType3Message = function (httpClient, protocol, options, objs, keepaliveAgent, res, callback) { if (!res.headers['www-authenticate']) { return callback(new Error('www-authenticate not found on response of second request')); } // parse type2 message from server: var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']); // create type3 message: var type3msg = ntlm.createType3Message(type2msg, options); // build type3 request: var type3options = { headers: { 'Authorization': type3msg }, allowRedirects: false, agent: keepaliveAgent }; // pass along other options: type3options.headers = _.extend(type3options.headers, options.headers); type3options = _.extend(type3options, _.omit(options, 'headers')); // send type3 message to server: httpClient.requestInternal(protocol, type3options, objs, callback); }; return NtlmCredentialHandler; }()); exports.NtlmCredentialHandler = NtlmCredentialHandler; ================================================ FILE: src/clients/baseclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { StatusBarItem } from "vscode"; import { Logger } from "../helpers/logger"; import { Telemetry } from "../services/telemetry"; import { TeamServerContext} from "../contexts/servercontext"; import { CommandNames } from "../helpers/constants"; import { Strings } from "../helpers/strings"; import { Utils } from "../helpers/utils"; import { VsCodeUtils } from "../helpers/vscodeutils"; export abstract class BaseClient { protected _serverContext: TeamServerContext; protected _statusBarItem: StatusBarItem; constructor(context: TeamServerContext, statusBarItem: StatusBarItem) { this._serverContext = context; this._statusBarItem = statusBarItem; } protected handleError(err: Error, offlineText: string, polling: boolean, infoMessage?: string): void { const offline: boolean = Utils.IsOffline(err); const msg: string = Utils.GetMessageForStatusCode(err, err.message); const logPrefix: string = (infoMessage === undefined) ? "" : infoMessage + " "; //When polling, we never display an error, we only log it (no telemetry either) if (polling === true) { Logger.LogError(logPrefix + msg); if (offline === true) { if (this._statusBarItem !== undefined) { this._statusBarItem.text = offlineText; this._statusBarItem.tooltip = Strings.StatusCodeOffline + " " + Strings.ClickToRetryConnection; this._statusBarItem.command = CommandNames.RefreshPollingStatus; } } else { //Could happen if PAT doesn't have proper permissions if (this._statusBarItem !== undefined) { this._statusBarItem.text = offlineText; this._statusBarItem.tooltip = msg; } } //If we aren't polling, we always log an error and, optionally, send telemetry } else { const logMessage: string = logPrefix + msg; if (offline === true) { Logger.LogError(logMessage); } else { Logger.LogError(logMessage); Telemetry.SendException(err); } VsCodeUtils.ShowErrorMessage(msg); } } } ================================================ FILE: src/clients/buildclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { StatusBarItem } from "vscode"; import { Build, BuildBadge, BuildResult, BuildStatus } from "vso-node-api/interfaces/BuildInterfaces"; import { Logger } from "../helpers/logger"; import { BuildService } from "../services/build"; import { Telemetry } from "../services/telemetry"; import { TeamServerContext} from "../contexts/servercontext"; import { CommandNames, TelemetryEvents, WellKnownRepositoryTypes } from "../helpers/constants"; import { Strings } from "../helpers/strings"; import { Utils } from "../helpers/utils"; import { IRepositoryContext, RepositoryType } from "../contexts/repositorycontext"; import { BaseClient } from "./baseclient"; export class BuildClient extends BaseClient { private _buildSummaryUrl: string; constructor(context: TeamServerContext, statusBarItem: StatusBarItem) { super(context, statusBarItem); } //Gets any available build status information and adds it to the status bar public async DisplayCurrentBuildStatus(context: IRepositoryContext, polling: boolean, definitionId?: number): Promise { try { const svc: BuildService = new BuildService(this._serverContext); Logger.LogInfo("Getting current build from badge..."); let buildBadge: BuildBadge; if (context.Type === RepositoryType.GIT) { buildBadge = await svc.GetBuildBadge(this._serverContext.RepoInfo.TeamProject, WellKnownRepositoryTypes.TfsGit, this._serverContext.RepoInfo.RepositoryId, context.CurrentRef); } else if (context.Type === RepositoryType.TFVC || context.Type === RepositoryType.EXTERNAL && !definitionId) { //If either TFVC or External and no definition Id, show default builds page buildBadge = await this.getTfvcBuildBadge(svc, this._serverContext.RepoInfo.TeamProject); } else if (definitionId) { //TODO: Allow definitionId to override Git and TFVC defaults (above)? const builds: Build[] = await svc.GetBuildsByDefinitionId(this._serverContext.RepoInfo.TeamProject, definitionId); if (builds.length > 0) { buildBadge = { buildId: builds[0].id, imageUrl: undefined }; } else { Logger.LogInfo(`Found zero builds for definition id ${definitionId}`); } } if (buildBadge && buildBadge.buildId !== undefined) { Logger.LogInfo("Found build id " + buildBadge.buildId.toString() + ". Getting build details..."); const build: Build = await svc.GetBuildById(buildBadge.buildId); this._buildSummaryUrl = BuildService.GetBuildSummaryUrl(this._serverContext.RepoInfo.TeamProjectUrl, build.id.toString()); Logger.LogInfo("Build summary info: " + build.id.toString() + " " + BuildStatus[build.status] + " " + BuildResult[build.result] + " " + this._buildSummaryUrl); if (this._statusBarItem !== undefined) { const icon: string = Utils.GetBuildResultIcon(build.result); this._statusBarItem.command = CommandNames.OpenBuildSummaryPage; this._statusBarItem.text = `$(package) ` + `$(${icon})`; this._statusBarItem.tooltip = "(" + BuildResult[build.result] + ") " + Strings.NavigateToBuildSummary + " " + build.buildNumber; } } else { Logger.LogInfo("No builds were found for team " + this._serverContext.RepoInfo.TeamProject.toString() + ", repo id " + this._serverContext.RepoInfo.RepositoryId.toString() + ", + branch " + (!context.CurrentBranch ? "UNKNOWN" : context.CurrentBranch.toString())); if (this._statusBarItem !== undefined) { this._statusBarItem.command = CommandNames.OpenBuildSummaryPage; this._statusBarItem.text = `$(package) ` + `$(dash)`; this._statusBarItem.tooltip = context.Type === RepositoryType.GIT ? Strings.NoBuildsFound : Strings.NoTfvcBuildsFound; } } } catch (err) { this.handleError(err, BuildClient.GetOfflineBuildStatusText(), polling, "Failed to get current build status"); } } //Gets the appropriate build for TFVC repositories and returns a 'BuildBadge' for it private async getTfvcBuildBadge(svc: BuildService, teamProjectId: string): Promise { //Create an build that doesn't exist and use as the default const emptyBuild: BuildBadge = { buildId: undefined, imageUrl: undefined }; const builds: Build[] = await svc.GetBuilds(teamProjectId); if (builds.length === 0) { return emptyBuild; } let matchingBuild: Build; for (let idx: number = 0; idx < builds.length; idx++) { const b: Build = builds[idx]; // Ignore canceled builds if (b.result === BuildResult.Canceled) { continue; } if (b.repository && b.repository.type.toLowerCase() === "tfsversioncontrol") { matchingBuild = b; break; } } if (matchingBuild) { //We dont' use imageUrl (which is a SVG) since we don't actually render the badge. return { buildId: matchingBuild.id, imageUrl: undefined }; } return emptyBuild; } public OpenBuildSummaryPage(): void { Telemetry.SendEvent(TelemetryEvents.OpenBuildSummaryPage); let url: string = this._buildSummaryUrl; if (url === undefined) { Logger.LogInfo("No build summary available, using build definitions url."); url = BuildService.GetBuildDefinitionsUrl(this._serverContext.RepoInfo.TeamProjectUrl); } Logger.LogInfo("OpenBuildSummaryPage: " + url); Utils.OpenUrl(url); } public static GetOfflineBuildStatusText() : string { return `$(package) ` + `???`; } } ================================================ FILE: src/clients/coreapiclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamProject, TeamProjectCollection } from "vso-node-api/interfaces/CoreInterfaces"; import { CoreApiService } from "../services/coreapi"; export class CoreApiClient { /* tslint:disable:no-empty */ constructor() { } /* tslint:enable:no-empty */ public async GetTeamProject(remoteUrl: string, teamProjectName: string): Promise { const svc: CoreApiService = new CoreApiService(remoteUrl); const teamProject:TeamProject = await svc.GetTeamProject(teamProjectName); return teamProject; } public async GetProjectCollection(remoteUrl: string, collectionName: string): Promise { const svc: CoreApiService = new CoreApiService(remoteUrl); const collection:TeamProjectCollection = await svc.GetProjectCollection(collectionName); return collection; } } ================================================ FILE: src/clients/feedbackclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Disposable, window } from "vscode"; import { Logger } from "../helpers/logger"; import { TelemetryEvents } from "../helpers/constants"; import { Strings } from "../helpers/strings"; import { Utils } from "../helpers/utils"; import { BaseQuickPickItem } from "../helpers/vscodeutils"; import { Telemetry } from "../services/telemetry"; export class FeedbackClient { //This feedback will go no matter whether Application Insights is enabled or not. public static async SendFeedback(): Promise { try { const choices: BaseQuickPickItem[] = []; choices.push({ label: Strings.SendASmile, description: undefined, id: TelemetryEvents.SendASmile }); choices.push({ label: Strings.SendAFrown, description: undefined, id: TelemetryEvents.SendAFrown }); const choice: BaseQuickPickItem = await window.showQuickPick(choices, { matchOnDescription: false, placeHolder: Strings.SendFeedback }); if (choice) { const value: string = await window.showInputBox({ value: undefined, prompt: Strings.SendFeedbackPrompt, placeHolder: undefined, password: false }); if (value === undefined) { const disposable = window.setStatusBarMessage(Strings.NoFeedbackSent); setTimeout(() => disposable.dispose(), 1000 * 5); return; } //This feedback will go no matter whether Application Insights is enabled or not. let trimmedValue: string = value.trim(); if (trimmedValue.length > 1000) { trimmedValue = trimmedValue.substring(0, 1000); } Telemetry.SendFeedback(choice.id, { "VSCode.Feedback.Comment" : trimmedValue } ); const disposable: Disposable = window.setStatusBarMessage(Strings.ThanksForFeedback); setTimeout(() => disposable.dispose(), 1000 * 5); } } catch (err) { const message: string = Utils.GetMessageForStatusCode(0, err.message, "Failed getting SendFeedback selection"); Logger.LogError(message); Telemetry.SendException(err); } } } ================================================ FILE: src/clients/gitclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { StatusBarItem, window } from "vscode"; import { GitPullRequest, PullRequestStatus} from "vso-node-api/interfaces/GitInterfaces"; import { BaseQuickPickItem, VsCodeUtils } from "../helpers/vscodeutils"; import { CommandNames, TelemetryEvents } from "../helpers/constants"; import { Logger } from "../helpers/logger"; import { Strings } from "../helpers/strings"; import { Utils } from "../helpers/utils"; import { IRepositoryContext, RepositoryType } from "../contexts/repositorycontext"; import { TeamServerContext} from "../contexts/servercontext"; import { GitVcService, PullRequestScore } from "../services/gitvc"; import { Telemetry } from "../services/telemetry"; import { BaseClient } from "./baseclient"; import * as path from "path"; export class GitClient extends BaseClient { constructor(context: TeamServerContext, statusBarItem: StatusBarItem) { super(context, statusBarItem); } //Initial method to display, select and navigate to my pull requests public async GetMyPullRequests(): Promise { Telemetry.SendEvent(TelemetryEvents.ViewPullRequests); try { const request: BaseQuickPickItem = await window.showQuickPick(this.getMyPullRequests(), { matchOnDescription: true, placeHolder: Strings.ChoosePullRequest }); if (request) { Telemetry.SendEvent(TelemetryEvents.ViewPullRequest); let discUrl: string = undefined; if (request.id !== undefined) { discUrl = GitVcService.GetPullRequestDiscussionUrl(this._serverContext.RepoInfo.RepositoryUrl, request.id); } else { discUrl = GitVcService.GetPullRequestsUrl(this._serverContext.RepoInfo.RepositoryUrl); } Logger.LogInfo("Pull Request Url: " + discUrl); Utils.OpenUrl(discUrl); } } catch (err) { this.handleError(err, GitClient.GetOfflinePullRequestStatusText(), false, "Error selecting pull request from QuickPick"); } } //Opens the blame page for the currently active file public OpenBlamePage(context: IRepositoryContext): void { this.ensureGitContext(context); let url: string = undefined; const editor = window.activeTextEditor; if (editor) { Telemetry.SendEvent(TelemetryEvents.OpenBlamePage); //Get the relative file path we can use to create the url let relativePath: string = "\\" + path.relative(context.RepositoryParentFolder, editor.document.fileName); relativePath = relativePath.split("\\").join("/"); //Replace all url = GitVcService.GetFileBlameUrl(context.RemoteUrl, relativePath, context.CurrentBranch); //Note: if file hasn't been pushed yet, blame link we generate won't point to anything valid (basically a 404) Logger.LogInfo("OpenBlame: " + url); Utils.OpenUrl(url); } else { const msg: string = Utils.GetMessageForStatusCode(0, Strings.NoSourceFileForBlame); Logger.LogError(msg); VsCodeUtils.ShowErrorMessage(msg); } } //Opens the file history page for the currently active file public OpenFileHistory(context: IRepositoryContext): void { this.ensureGitContext(context); let historyUrl: string = undefined; const editor = window.activeTextEditor; if (!editor) { Telemetry.SendEvent(TelemetryEvents.OpenRepositoryHistory); historyUrl = GitVcService.GetRepositoryHistoryUrl(context.RemoteUrl, context.CurrentBranch); Logger.LogInfo("OpenRepoHistory: " + historyUrl); } else { Telemetry.SendEvent(TelemetryEvents.OpenFileHistory); //Get the relative file path we can use to create the history url let relativePath: string = "\\" + path.relative(context.RepositoryParentFolder, editor.document.fileName); relativePath = relativePath.split("\\").join("/"); //Replace all historyUrl = GitVcService.GetFileHistoryUrl(context.RemoteUrl, relativePath, context.CurrentBranch); //Note: if file hasn't been pushed yet, history link we generate won't point to anything valid (basically a 404) Logger.LogInfo("OpenFileHistory: " + historyUrl); } Utils.OpenUrl(historyUrl); } public OpenNewPullRequest(remoteUrl: string, currentBranch: string): void { Telemetry.SendEvent(TelemetryEvents.OpenNewPullRequest); const url: string = GitVcService.GetCreatePullRequestUrl(remoteUrl, currentBranch); Logger.LogInfo("CreatePullRequestPage: " + url); Utils.OpenUrl(url); } public async PollMyPullRequests(): Promise { try { const requests: BaseQuickPickItem[] = await this.getMyPullRequests(); this._statusBarItem.tooltip = Strings.BrowseYourPullRequests; //Remove the default Strings.BrowseYourPullRequests item from the calculation this._statusBarItem.text = GitClient.GetPullRequestStatusText((requests.length - 1).toString()); } catch (err) { this.handleError(err, GitClient.GetOfflinePullRequestStatusText(), true, "Attempting to poll my pull requests"); } } private async getMyPullRequests(): Promise { const requestItems: BaseQuickPickItem[] = []; const requestIds: number[] = []; Logger.LogInfo("Getting pull requests that I requested..."); const svc: GitVcService = new GitVcService(this._serverContext); const myPullRequests: GitPullRequest[] = await svc.GetPullRequests(this._serverContext.RepoInfo.RepositoryId, this._serverContext.UserInfo.Id, undefined, PullRequestStatus.Active); const icon: string = "search"; const label: string = `$(${icon}) `; requestItems.push({ label: label + Strings.BrowseYourPullRequests, description: undefined, id: undefined }); myPullRequests.forEach((pr) => { const score: PullRequestScore = GitVcService.GetPullRequestScore(pr); requestItems.push(this.getPullRequestLabel(pr.createdBy.displayName, pr.title, pr.description, pr.pullRequestId.toString(), score)); requestIds.push(pr.pullRequestId); }); Logger.LogInfo("Retrieved " + myPullRequests.length + " pull requests that I requested"); Logger.LogInfo("Getting pull requests for which I'm a reviewer..."); //Go get the active pull requests that I'm a reviewer for const myReviewPullRequests: GitPullRequest[] = await svc.GetPullRequests(this._serverContext.RepoInfo.RepositoryId, undefined, this._serverContext.UserInfo.Id, PullRequestStatus.Active); myReviewPullRequests.forEach((pr) => { const score: PullRequestScore = GitVcService.GetPullRequestScore(pr); if (requestIds.indexOf(pr.pullRequestId) < 0) { requestItems.push(this.getPullRequestLabel(pr.createdBy.displayName, pr.title, pr.description, pr.pullRequestId.toString(), score)); } }); Logger.LogInfo("Retrieved " + myReviewPullRequests.length + " pull requests that I'm the reviewer"); //Remove the default Strings.BrowseYourPullRequests item from the calculation this._statusBarItem.text = GitClient.GetPullRequestStatusText((requestItems.length - 1).toString()); this._statusBarItem.tooltip = Strings.BrowseYourPullRequests; this._statusBarItem.command = CommandNames.GetPullRequests; return requestItems; } private getPullRequestLabel(displayName: string, title: string, description: string, id: string, score: PullRequestScore): BaseQuickPickItem { let scoreIcon: string = ""; if (score === PullRequestScore.Succeeded) { scoreIcon = "check"; } else if (score === PullRequestScore.Failed) { scoreIcon = "stop"; } else if (score === PullRequestScore.Waiting) { scoreIcon = "watch"; } else if (score === PullRequestScore.NoResponse) { scoreIcon = "git-pull-request"; } const scoreLabel: string = `$(${scoreIcon}) `; return { label: scoreLabel + " (" + displayName + ") " + title, description: description, id: id }; } public static GetOfflinePullRequestStatusText() : string { return `$(git-pull-request) ???`; } //Sets the text on the pull request status bar public static GetPullRequestStatusText(total?: string) : string { if (!total) { return `$(git-pull-request) $(dash)`; } return `$(git-pull-request) ${total.toString()}`; } //Ensure that we don't accidentally send non-Git (e.g., TFVC) contexts to the Git client private ensureGitContext(context: IRepositoryContext): void { if (context.Type !== RepositoryType.GIT) { throw new Error("context sent to GitClient is not a Git context object."); } } } ================================================ FILE: src/clients/httpclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; //jeyou: Brought over from vso-node-api (v5.1.2), added support for sending SOAP and handling gzip compression /* tslint:disable */ import url = require("url"); import http = require("http"); import zlib = require("zlib"); import https = require("https"); import tunnel = require("tunnel"); import ifm = require("vso-node-api/interfaces/common/VsoBaseInterfaces"); http.globalAgent.maxSockets = 100; export class HttpClient { userAgent: string; handlers: ifm.IRequestHandler[]; socketTimeout: number; isSsl: boolean; constructor(userAgent: string, handlers?: ifm.IRequestHandler[], socketTimeout?: number) { this.userAgent = userAgent; this.handlers = handlers; if (socketTimeout) { this.socketTimeout = socketTimeout; } else { // Default 3 minutes this.socketTimeout = 3 * 60000; } } // POST, PATCH, PUT send(verb: string, requestUrl: string, objs: any, headers: ifm.IHeaders, onResult: (err: any, res: http.ClientResponse, contents: string) => void): void { var options = this._getOptions(verb, requestUrl, headers); this.request(options.protocol, options.options, objs, onResult); } _getOptions(method: string, requestUrl: string, headers: any): any { var parsedUrl: url.Url = url.parse(requestUrl); var usingSsl = parsedUrl.protocol === 'https:'; var prot: any = usingSsl ? https : http; var defaultPort = usingSsl ? 443 : 80; this.isSsl = usingSsl; var proxyUrl: url.Url; if (process.env.HTTPS_PROXY && usingSsl) { proxyUrl = url.parse(process.env.HTTPS_PROXY); } else if (process.env.HTTP_PROXY) { proxyUrl = url.parse(process.env.HTTP_PROXY); } var options: any = { host: parsedUrl.hostname, port: parsedUrl.port || defaultPort, path: (parsedUrl.pathname || '') + (parsedUrl.search || ''), method: method, headers: headers || {} }; //options.headers["Accept"] = contentType; options.headers["User-Agent"] = this.userAgent; var useProxy = proxyUrl && proxyUrl.hostname; if (useProxy) { var agentOptions: tunnel.TunnelOptions = { maxSockets: http.globalAgent.maxSockets, proxy: { // TODO: support proxy-authorization //proxyAuth: "user:password", host: proxyUrl.hostname, port: proxyUrl.port } }; var tunnelAgent: Function; var overHttps = proxyUrl.protocol === 'https:'; if (usingSsl) { tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; } else { tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; } options.agent = tunnelAgent(agentOptions); } if (this.handlers) { this.handlers.forEach((handler) => { handler.prepareRequest(options); }); } return { protocol: prot, options: options, }; } request(protocol: any, options: any, objs: any, onResult: (err: any, res: http.ClientResponse, contents: string) => void): void { // Set up a callback to pass off 401s to an authentication handler that can deal with it var callback = (err: any, res: http.ClientResponse, contents: string) => { var authHandler; if (this.handlers) { this.handlers.some(function (handler /*, index, handlers*/) { // Find the first one that can handle the auth based on the response if (handler.canHandleAuthentication(res)) { authHandler = handler; return true; } return false; }); } if (authHandler !== undefined) { authHandler.handleAuthentication(this, protocol, options, objs, onResult); } else { // No auth handler found, call onResult normally onResult(err, res, contents); } }; this.requestInternal(protocol, options, objs, callback); } requestInternal(protocol: any, options: any, objs: any, onResult: (err: any, res: http.ClientResponse, contents: string) => void): void { var reqData; var socket; if (objs) { reqData = objs; } var callbackCalled: boolean = false; var handleResult = (err: any, res: http.ClientResponse, contents: string) => { if (!callbackCalled) { callbackCalled = true; onResult(err, res, contents); } }; var req = protocol.request(options, function (res) { var buffer = []; var output = ''; // If we're handling gzip compression, don't set the encoding to utf8 if (res.headers["content-encoding"] && res.headers["content-encoding"] === "gzip") { var gunzip = zlib.createGunzip(); res.pipe(gunzip); gunzip.on('data', function(data) { buffer.push(data.toString()); }).on('end', function() { handleResult(null, res, buffer.join("")); }).on('error', function(err) { handleResult(err, null, null); }); } else { res.setEncoding('utf8'); //Do this only if we expect we're getting a string back res.on('data', function (chunk) { output += chunk; }); res.on('end', function () { // res has statusCode and headers handleResult(null, res, output); }); } }); req.on('socket', function(sock) { socket = sock; }); // If we ever get disconnected, we want the socket to timeout eventually req.setTimeout(this.socketTimeout, function() { if (socket) { socket.end(); } handleResult(new Error('Request timeout: ' + options.path), null, null); }); req.on('error', function (err) { // err has statusCode property // res should have headers handleResult(err, null, null); }); if (reqData) { req.write(reqData, 'utf8'); } req.end(); } } /* tslint:enable */ ================================================ FILE: src/clients/repositoryinfoclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import VsoBaseInterfaces = require("vso-node-api/interfaces/common/VsoBaseInterfaces"); import { TeamProject, TeamProjectCollection } from "vso-node-api/interfaces/CoreInterfaces"; import { CoreApiClient } from "./coreapiclient"; import { Logger } from "../helpers/logger"; import { RepoUtils } from "../helpers/repoutils"; import { Strings } from "../helpers/strings"; import { IRepositoryContext, RepositoryType } from "../contexts/repositorycontext"; import { TeamServicesApi } from "./teamservicesclient"; import { TfsCatalogSoapClient } from "./tfscatalogsoapclient"; import { RepositoryInfo } from "../info/repositoryinfo"; import { TfvcContext } from "../contexts/tfvccontext"; import { Telemetry } from "../services/telemetry"; import * as url from "url"; export class RepositoryInfoClient { private _handler: VsoBaseInterfaces.IRequestHandler; private _repoContext: IRepositoryContext; constructor(context: IRepositoryContext, handler: VsoBaseInterfaces.IRequestHandler) { this._repoContext = context; this._handler = handler; } public async GetRepositoryInfo(): Promise { let repoInfo: any; let repositoryInfo: RepositoryInfo; let repositoryClient: TeamServicesApi; if (this._repoContext.Type === RepositoryType.GIT) { Logger.LogDebug(`Getting repository information for a Git repository at ${this._repoContext.RemoteUrl}`); repositoryClient = new TeamServicesApi(this._repoContext.RemoteUrl, [this._handler]); repoInfo = await repositoryClient.getVstsInfo(); Logger.LogDebug(`Repository information blob:`); Logger.LogObject(repoInfo); this.verifyRepoInfo(repoInfo, `RepoInfo was undefined for a ${RepositoryType[this._repoContext.Type]} repo`); repositoryInfo = new RepositoryInfo(repoInfo); Logger.LogDebug(`Finished getting repository information for a Git repository at ${this._repoContext.RemoteUrl}`); return repositoryInfo; } else if (this._repoContext.Type === RepositoryType.TFVC || this._repoContext.Type === RepositoryType.EXTERNAL) { Logger.LogDebug(`Getting repository information for a TFVC repository at ${this._repoContext.RemoteUrl}`); //For TFVC, the teamProjectName is retrieved by tf.cmd and set on the context const teamProjectName: string = this._repoContext.TeamProjectName; this.verifyRepoInfo(this._repoContext.RemoteUrl, `RemoteUrl was undefined for a ${RepositoryType[this._repoContext.Type]} repo`); repositoryInfo = new RepositoryInfo(this._repoContext.RemoteUrl); let serverUrl: string; let collectionName: string; const isTeamServices: boolean = RepoUtils.IsTeamFoundationServicesRepo(this._repoContext.RemoteUrl); if (isTeamServices) { // The Team Services collection is ALWAYS defaultCollection, and both the url with defaultcollection // and the url without defaultCollection will validate just fine. However, it expects you to refer to // the collection by the account name. So, we just need to grab the account name and use that to // recreate the url. // If validation fails, we return false. collectionName = repositoryInfo.Account; if (RepoUtils.IsTeamFoundationServicesAzureRepo(this._repoContext.RemoteUrl)) { serverUrl = `https://${repositoryInfo.Host}/${repositoryInfo.Account}/`; } else { serverUrl = `https://${repositoryInfo.Account}.visualstudio.com/`; } const valid: boolean = await this.validateTfvcCollectionUrl(serverUrl); if (!valid) { const errorMsg: string = `${Strings.UnableToValidateTeamServicesCollection} Collection name: '${collectionName}', Url: '${serverUrl}'`; Logger.LogDebug(errorMsg); throw new Error(errorMsg); } Logger.LogDebug(`Successfully validated the hosted TFVC repository. Collection name: '${collectionName}', 'Url: ${serverUrl}'`); } else { //This could be either a TFVC context or an External context serverUrl = this._repoContext.RemoteUrl; // A full Team Foundation Server collection url is required for the validate call to succeed. // So we try the url given. If that fails, we assume it is a server Url and the collection is // the defaultCollection. If that assumption fails we return false. Logger.LogDebug(`Starting the validation of the collection. Url: '${serverUrl}'`); let valid: boolean = await this.validateTfvcCollectionUrl(serverUrl); if (valid) { const parts: string[] = this.splitTfvcCollectionUrl(serverUrl); serverUrl = parts[0]; collectionName = parts[1]; Logger.LogDebug(`Validated the collection and splitting Url and Collection name. Collection name: '${collectionName}', Url: '${serverUrl}'`); } else { Logger.LogDebug(`Unable to validate the collection. Url: '${serverUrl}' Attempting validation assuming 'DefaultCollection'...`); collectionName = "DefaultCollection"; const remoteUrl: string = url.resolve(serverUrl, collectionName); valid = await this.validateTfvcCollectionUrl(remoteUrl); if (!valid) { Logger.LogDebug(Strings.UnableToValidateCollectionAssumingDefaultCollection); throw new Error(Strings.UnableToValidateCollectionAssumingDefaultCollection); } //Since we validated with the default collection, we need to update the repo context's RemoteUrl if (this._repoContext.Type === RepositoryType.TFVC) { const tfvcContext: TfvcContext = this._repoContext; tfvcContext.RemoteUrl = remoteUrl; } Logger.LogDebug(`Validated the collection assuming 'DefaultCollection'.`); } } const coreApiClient: CoreApiClient = new CoreApiClient(); let collection: TeamProjectCollection; Logger.LogDebug(`Getting project collection... url: '${serverUrl}', and collection name: '${collectionName}'`); if (isTeamServices) { //The following call works for VSTS, TFS 2017 and TFS 2015U3 (multiple collections, spaces in the name), just not for non-admins on-prem (!) Logger.LogDebug(`Using REST to get the project collection information`); collection = await coreApiClient.GetProjectCollection(serverUrl, collectionName); } else { Logger.LogDebug(`Using SOAP to get the project collection information`); // When called on-prem without admin privileges: Error: Failed Request: Forbidden(403) - Access Denied: Jeff Young (TFS) needs the following permission(s) to perform this action: Edit instance-level information const tfsClient: TfsCatalogSoapClient = new TfsCatalogSoapClient(serverUrl, [this._handler]); collection = await tfsClient.GetProjectCollection(collectionName); if (!collection) { const error: string = `Using SOAP, could not find a project collection object for ${collectionName} at ${serverUrl}`; Logger.LogDebug(error); throw new Error(error); } } Logger.LogDebug(`Found a project collection for url: '${serverUrl}' and collection name: '${collection.name}'.`); Logger.LogDebug(`Getting team project... Url: '${serverUrl}', collection name: '${collection.name}', and project: '${teamProjectName}'`); //For a Team Services collection, ignore the collectionName const resolvedRemoteUrl: string = url.resolve(serverUrl, isTeamServices ? "" : collection.name); //Delay the check for a teamProjectName (don't fail here). If we don't have one, that's OK for TFVC //functionality. We need to disable Team Services functionality if we can't find a team project later. const project: TeamProject = await this.getProjectFromServer(coreApiClient, resolvedRemoteUrl, teamProjectName); Logger.LogDebug(`Found a team project for url: '${serverUrl}', collection name: '${collection.name}', and project id: '${project.id}'`); //Now, create the JSON blob to send to new RepositoryInfo(repoInfo); repoInfo = this.getTfvcRepoInfoBlob(serverUrl, collection.id, collection.name, collection.url, project.id, project.name, project.description, project.url); Logger.LogDebug(`Repository information blob:`); Logger.LogObject(repoInfo); this.verifyRepoInfo(repoInfo, `RepoInfo was undefined for a ${RepositoryType[this._repoContext.Type]} repo`); repositoryInfo = new RepositoryInfo(repoInfo); Logger.LogDebug(`Finished getting repository information for the repository at ${this._repoContext.RemoteUrl}`); return repositoryInfo; } return repositoryInfo; } //Using to try and track down users in the scenario where repoInfo is undefined private verifyRepoInfo(repoInfo: any, message: string) { if (!repoInfo) { Telemetry.SendException(new Error(message)); } } private splitTfvcCollectionUrl(collectionUrl: string): string[] { const result: string[] = [ , ]; if (!collectionUrl) { return result; } // Now find the TRUE last separator (before the collection name) const trimmedUrl: string = this.trimTrailingSeparators(collectionUrl); const index: number = trimmedUrl.lastIndexOf("/"); if (index >= 0) { // result0 is the server url without the collection name result[0] = trimmedUrl.substring(0, index + 1); // result1 is just the collection name (no separators) result[1] = trimmedUrl.substring(index + 1); } else { // We can't determine the collection name so leave it empty result[0] = collectionUrl; result[1] = ""; } return result; } private trimTrailingSeparators(uri: string): string { if (uri) { let lastIndex: number = uri.length; while (lastIndex > 0 && uri.charAt(lastIndex - 1) === "/".charAt(0)) { lastIndex--; } if (lastIndex >= 0) { return uri.substring(0, lastIndex); } } return uri; } //RepositoryInfo uses repository.remoteUrl to set up accountUrl private getTfvcRepoInfoBlob(serverUrl: string, collectionId: string, collectionName: string, collectionUrl: string, projectId: string, projectName: string, projectDesc: string, projectUrl: string): any { return { serverUrl: serverUrl, collection: { id: collectionId, name: collectionName, url: collectionUrl }, repository: { id: "00000000-0000-0000-0000-000000000000", name: "NoNameTfvcRepository", url: serverUrl, project: { id: projectId, name: projectName, description: projectDesc, url: projectUrl, state: 1, revision: 15 }, remoteUrl: serverUrl } }; } private async getProjectFromServer(coreApiClient: CoreApiClient, remoteUrl: string, teamProjectName: string): Promise { return coreApiClient.GetTeamProject(remoteUrl, teamProjectName); } private async validateTfvcCollectionUrl(serverUrl: string): Promise { try { const repositoryClient: TeamServicesApi = new TeamServicesApi(serverUrl, [this._handler]); await repositoryClient.validateTfvcCollectionUrl(); return true; } catch (err) { if (err.statusCode === 404) { return false; } else { throw err; } } } } ================================================ FILE: src/clients/soapclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; //jeyou: Based on RestClient from vso-node-api (v5.1.2) /* tslint:disable */ import { env } from "vscode"; import ifm = require("vso-node-api/interfaces/common/VsoBaseInterfaces"); import { HttpClient } from "./httpclient"; var httpCodes = { 300: "Multiple Choices", 301: "Moved Permanantly", 302: "Resource Moved", 304: "Not Modified", 305: "Use Proxy", 306: "Switch Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect", 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout" } export function processResponse(url, res, contents, onResult) { if (res.statusCode > 299) { // not success var msg = httpCodes[res.statusCode] ? "Failed Request: " + httpCodes[res.statusCode] : "Failed Request"; msg += '(' + res.statusCode + ') - '; if (contents && contents.length > 0) { var soapObj = contents; if (soapObj && soapObj.message) { msg += soapObj.message; } else { msg += url; } } onResult(new Error(msg), res.statusCode, null); } else { try { var soapObj = null; if (contents && contents.length > 0) { soapObj = contents; } } catch (e) { onResult(new Error('Invalid Resource'), res.statusCode, null); return; } onResult(null, res.statusCode, soapObj); } }; export class SoapClient { baseUrl: string; basePath: string; httpClient: HttpClient; constructor(userAgent: string, handlers?: ifm.IRequestHandler[]) { this.httpClient = new HttpClient(userAgent, handlers); } post(url: string, requestEnvelope: string, onResult: (err: any, statusCode: number, obj: any) => void): void { this._sendSoap('POST', url, requestEnvelope, onResult); } _sendSoap(verb: string, url: string, requestEnvelope: string, onResult: (err: any, statusCode: number, obj: any) => void): void { let headers: ifm.IHeaders = {}; headers["Accept-Encoding"] = "gzip"; //Tell the server we'd like to receive a gzip compressed response headers["Accept-Language"] = env.language; //"en-US"; headers["Content-Type"] = "application/soap+xml; charset=utf-8"; headers["Chunked"] = "false"; headers["Content-Length"] = requestEnvelope.length; this.httpClient.send(verb, url, requestEnvelope, headers, (err: any, res: ifm.IHttpResponse, responseEnvelope: string) => { if (err) { onResult(err, err.statusCode, null); return; } processResponse(url, res, responseEnvelope, onResult); }); } } /* tslint:enable */ ================================================ FILE: src/clients/teamservicesclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import basem = require("vso-node-api/ClientApiBases"); import VsoBaseInterfaces = require("vso-node-api/interfaces/common/VsoBaseInterfaces"); export class TeamServicesApi extends basem.ClientApiBase { constructor(baseUrl: string, handlers: VsoBaseInterfaces.IRequestHandler[]) { super(baseUrl, handlers, "node-vsts-vscode-api"); } //This calls the vsts/info endpoint (which only exists for Git) public async getVstsInfo(): Promise { //Create an instance of Promise since we're calling a function with the callback pattern but want to return a Promise const promise: Promise = new Promise((resolve, reject) => { /* tslint:disable:no-null-keyword */ this.restClient.getJson(this.vsoClient.resolveUrl("/vsts/info"), "", null, null, (err: any, statusCode: number, obj: any) => { /* tslint:enable:no-null-keyword */ if (err) { err.statusCode = statusCode; reject(err); } else { resolve(obj); } }); }); return promise; } //Used to determine if the baseUrl points to a valid TFVC repository public async validateTfvcCollectionUrl(): Promise { //Create an instance of Promise since we're calling a function with the callback pattern but want to return a Promise const promise: Promise = new Promise((resolve, reject) => { /* tslint:disable:no-null-keyword */ this.restClient.getJson(this.vsoClient.resolveUrl("_apis/tfvc/branches"), "", null, null, (err: any, statusCode: number, obj: any) => { /* tslint:enable:no-null-keyword */ if (err) { err.statusCode = statusCode; reject(err); } else { resolve(obj); } }); }); return promise; } } ================================================ FILE: src/clients/tfscatalogsoapclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import Q = require("q"); import { IRequestHandler } from "vso-node-api/interfaces/common/VsoBaseInterfaces"; import { SoapClient } from "./soapclient"; import { UserAgentProvider } from "../helpers/useragentprovider"; import * as xmldoc from "xmldoc"; import * as url from "url"; // This class is the 'bridge' between the calling RepositoryInfoClient (which uses the // async/await pattern) and the SoapClient which (has to) implement the callback pattern export class TfsCatalogSoapClient { private soapClient: SoapClient; private serverUrl: string; private endpointUrl: string; /* tslint:disable:variable-name */ private static readonly SingleRecurseStar: string = "*"; private static readonly QueryOptionsNone: string = "0"; private static readonly QueryOptionsExpandDependencies: string = "1"; // These guids brought over from our friends at vso-intellij... // https://github.com/Microsoft/vso-intellij/blob/master/plugin/src/com/microsoft/alm/plugin/context/soap/CatalogServiceImpl.java#L56-L58 // Ensure that they rename lower-case private static readonly OrganizationalRoot: string = "69a51c5e-c093-447e-a177-a09e47a60974"; private static readonly TeamFoundationServerInstance: string = "b36f1bda-df2d-482b-993a-f194a31a1fa2"; private static readonly ProjectCollection: string = "26338d9e-d437-44aa-91f2-55880a328b54"; // Xml nodes in SOAP envelopes are case-sensitive (so don't change the values below) private static readonly XmlSoapBody: string = "soap:Body"; private static readonly XmlQueryNodesResponse: string = "QueryNodesResponse"; private static readonly XmlQueryNodesResult: string = "QueryNodesResult"; private static readonly XmlCatalogResources: string = "CatalogResources"; private static readonly XmlNodeReferencesPaths: string = "NodeReferencePaths"; /* tslint:enable:variable-name */ constructor(serverUrl: string, handlers: IRequestHandler[]) { this.serverUrl = serverUrl; this.endpointUrl = url.resolve(serverUrl, "TeamFoundation/Administration/v3.0/CatalogService.asmx"); this.soapClient = new SoapClient(UserAgentProvider.UserAgent, handlers); } /* Sample value of the parameter sent to this function: The root of the catalog tree that describes the organizational makeup of the TFS deployment. The root of the catalog tree that describes the physical makeup of the TFS deployment. The root of the catalog tree that describes the organizational makeup of the TFS deployment. 3eYRYkJOok6GHrKam0AcAA== The root of the catalog tree that describes the physical makeup of the TFS deployment. Vc1S6XwnTEe/isOiPfhmxw== 4006 */ private parseOrganizationRootPath(envelopeXml: any): string { if (!envelopeXml) { throw new Error(`No SOAP envelope was received for OrganizationRoot from ${this.endpointUrl}`); } const organizationDocument: xmldoc.XmlDocument = new xmldoc.XmlDocument(envelopeXml); const soapBody: xmldoc.XmlElement = organizationDocument.childNamed(TfsCatalogSoapClient.XmlSoapBody); const nodesResponse: xmldoc.XmlElement = soapBody.childNamed(TfsCatalogSoapClient.XmlQueryNodesResponse); const nodesResult: xmldoc.XmlElement = nodesResponse.childNamed(TfsCatalogSoapClient.XmlQueryNodesResult); const catalogResources: any = nodesResult.childNamed(TfsCatalogSoapClient.XmlCatalogResources); if (!catalogResources) { throw new Error(`No CatalogResources were received for OrganizationRoot from ${this.endpointUrl}`); } //Spin through children doing insensitive check let orgRoot: any; for (let idx: number = 0; idx < catalogResources.children.length; idx++) { if (catalogResources.children[idx].attr.ResourceTypeIdentifier.toLowerCase() === TfsCatalogSoapClient.OrganizationalRoot) { orgRoot = catalogResources.children[idx]; break; } } if (!orgRoot) { throw new Error(`No organizationRoot was found in SOAP envelope from ${this.endpointUrl}`); } const nodeRefPaths: any = orgRoot.childNamed(TfsCatalogSoapClient.XmlNodeReferencesPaths); const nodeRefPath: string = nodeRefPaths.children[0].val; return nodeRefPath; } /* Sample value of the parameter sent to this function: A deployed instance of Team Foundation Server. The web application that hosts a Team Foundation Server Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None 3eYRYkJOok6GHrKam0AcAA==GJQSi7i010yMVKSDvyLgHQ== Vc1S6XwnTEe/isOiPfhmxw==TKuP+6nWJkWp5U9GA3ovcA==Dw5+eHh8ykK/e8nLXC8QyA== 4006 */ private parseFoundationServerRootPath(envelopeXml: any): string { if (!envelopeXml) { throw new Error(`No SOAP envelope was received for FoundationServer from ${this.endpointUrl}`); } const foundationServerDocument: xmldoc.XmlDocument = new xmldoc.XmlDocument(envelopeXml); const soapBody: xmldoc.XmlElement = foundationServerDocument.childNamed(TfsCatalogSoapClient.XmlSoapBody); const nodesResponse: xmldoc.XmlElement = soapBody.childNamed(TfsCatalogSoapClient.XmlQueryNodesResponse); const nodesResult: xmldoc.XmlElement = nodesResponse.childNamed(TfsCatalogSoapClient.XmlQueryNodesResult); const catalogResources: any = nodesResult.childNamed(TfsCatalogSoapClient.XmlCatalogResources); if (!catalogResources) { throw new Error(`No CatalogResources were received for FoundationServer from ${this.endpointUrl}`); } let serverInstance: xmldoc.XmlElement; //Spin through children doing insensitive check for (let idx: number = 0; idx < catalogResources.children.length; idx++) { if (catalogResources.children[idx].attr.ResourceTypeIdentifier.toLowerCase() === TfsCatalogSoapClient.TeamFoundationServerInstance) { serverInstance = catalogResources.children[idx]; break; } } if (!serverInstance) { throw new Error(`No serverInstance was found in SOAP envelope from ${this.endpointUrl}`); } const nodeRefPaths: any = serverInstance.childNamed(TfsCatalogSoapClient.XmlNodeReferencesPaths); const nodeRefPath: string = nodeRefPaths.children[0].val; return nodeRefPath; } /* Sample value of the parameter sent to this function: Team Web Access Location A Team Project Collection that exists within the TFS deployment. Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None Context 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None 3eYRYkJOok6GHrKam0AcAA==GJQSi7i010yMVKSDvyLgHQ==5WM1lP72kkiwOcTd6ZWclw== WebApplication 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 None InstanceId 4a3d32f1-f8f4-42bc-9fea-57e547e7463d 3eYRYkJOok6GHrKam0AcAA==GJQSi7i010yMVKSDvyLgHQ==pH3F9yLMlUOsZc43M2a04A== 4006 */ private parseProjectCollections(envelopeXml: any): any[] { if (!envelopeXml) { throw new Error(`No SOAP envelope was received for ProjectCollections from ${this.endpointUrl}`); } const projectCollectionsDocument: xmldoc.XmlDocument = new xmldoc.XmlDocument(envelopeXml); const soapBody: xmldoc.XmlElement = projectCollectionsDocument.childNamed(TfsCatalogSoapClient.XmlSoapBody); const nodesResponse: xmldoc.XmlElement = soapBody.childNamed(TfsCatalogSoapClient.XmlQueryNodesResponse); const nodesResult: xmldoc.XmlElement = nodesResponse.childNamed(TfsCatalogSoapClient.XmlQueryNodesResult); const catalogResources: any = nodesResult.childNamed(TfsCatalogSoapClient.XmlCatalogResources); if (!catalogResources) { throw new Error(`No CatalogResources were received for ProjectCollections from ${this.endpointUrl}`); } const collectionNodes: any[] = []; catalogResources.eachChild(function(catalogResource) { if (catalogResource.attr.ResourceTypeIdentifier.toLowerCase() === TfsCatalogSoapClient.ProjectCollection) { collectionNodes.push(catalogResource); } }); return collectionNodes; } // Based on the passed in collectionName, it queries the TFS Catalog Service to find // the collection's display name, id (guid), and API URL (_apis/projectCollections/) // This method returns 'any' (of the _shape_ TeamProjectCollectionReference) which will // match "good enough" to the type expected in repositoryinfoclient. public GetProjectCollection(collectionName: string): Q.Promise { const deferred: Q.Deferred = Q.defer(); //Get the organizational root this.getCatalogDataFromServer(TfsCatalogSoapClient.SingleRecurseStar, TfsCatalogSoapClient.QueryOptionsNone).then((catalogDataXml: any) => { const orgRootPath: string = this.parseOrganizationRootPath(catalogDataXml); //Get the foundationServer, orgRootPath looks something like 3eYRYkJOok6GHrKam0AcAA== this.getCatalogDataFromServer(orgRootPath + TfsCatalogSoapClient.SingleRecurseStar, TfsCatalogSoapClient.QueryOptionsExpandDependencies).then((catalogDataXml:any) => { const foundationServerRootPath: string = this.parseFoundationServerRootPath(catalogDataXml); //Get the project collections, foundationServerRootPath looks something like 3eYRYkJOok6GHrKam0AcAA==GJQSi7i010yMVKSDvyLgHQ== this.getCatalogDataFromServer(foundationServerRootPath + TfsCatalogSoapClient.SingleRecurseStar, TfsCatalogSoapClient.QueryOptionsExpandDependencies).then((catalogDataXml:any) => { const collectionNodes: any[] = this.parseProjectCollections(catalogDataXml); //Now go and find the project collection we're looking for let foundTeamProject: any; for (let idx: number = 0; idx < collectionNodes.length; idx++) { if (collectionNodes[idx].attr.DisplayName.toLowerCase() === collectionName.toLowerCase()) { foundTeamProject = collectionNodes[idx]; break; } } if (foundTeamProject) { const props: any = foundTeamProject.childNamed("Properties"); const strstr: any = props.childNamed("KeyValueOfStringString"); const id: any = strstr.childNamed("Value"); //Resolve an object that +looks_ like a TeamProjectCollectionReference object deferred.resolve({ name: foundTeamProject.attr.DisplayName, id: id.val, url: url.resolve(this.serverUrl, "_apis/projectCollections/" + id.val)}); } else { deferred.resolve(undefined); } }); }); }).fail((err) => { //Apparently, we will fail if auth fails when getting organizational root deferred.reject(err); }); return deferred.promise; } private getCatalogDataFromServer(pathSpecs: string, queryOptions: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); const onResult = (err: any, statusCode: number, responseEnvelope: any) => { if (err) { err.statusCode = statusCode; deferred.reject(err); } else { deferred.resolve(responseEnvelope); } }; const envelope: string = "" + "" + "" + "" + "" + "" + pathSpecs + "" + "" + "" + queryOptions + "" + "" + "" + ""; this.soapClient.post(this.endpointUrl, envelope, onResult); return deferred.promise; } } ================================================ FILE: src/clients/witclient.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { StatusBarItem, window } from "vscode"; import { QueryHierarchyItem, WorkItemType } from "vso-node-api/interfaces/WorkItemTrackingInterfaces"; import { Logger } from "../helpers/logger"; import { SimpleWorkItem, WorkItemTrackingService } from "../services/workitemtracking"; import { Telemetry } from "../services/telemetry"; import { TeamServerContext} from "../contexts/servercontext"; import { BaseQuickPickItem, VsCodeUtils, WorkItemQueryQuickPickItem } from "../helpers/vscodeutils"; import { TelemetryEvents, WitQueries, WitTypes } from "../helpers/constants"; import { Strings } from "../helpers/strings"; import { Utils } from "../helpers/utils"; import { IPinnedQuery } from "../helpers/settings"; import { BaseClient } from "./baseclient"; export class WitClient extends BaseClient { private _pinnedQuery: IPinnedQuery; private _myQueriesFolder: string; constructor(context: TeamServerContext, pinnedQuery: IPinnedQuery, statusBarItem: StatusBarItem) { super(context, statusBarItem); this._pinnedQuery = pinnedQuery; } //Opens a browser to a new work item given the item type, title and assigned to public CreateNewItem(itemType: string, taskTitle: string): void { this.logTelemetryForWorkItem(itemType); Logger.LogInfo("Work item type is " + itemType); const newItemUrl: string = WorkItemTrackingService.GetNewWorkItemUrl(this._serverContext.RepoInfo.TeamProjectUrl, itemType, taskTitle, this.getUserName(this._serverContext)); Logger.LogInfo("New Work Item Url: " + newItemUrl); Utils.OpenUrl(newItemUrl); } //Creates a new work item based on a single line of selected text public async CreateNewWorkItem(taskTitle: string): Promise { try { Telemetry.SendEvent(TelemetryEvents.OpenNewWorkItem); const selectedType: BaseQuickPickItem = await window.showQuickPick(this.getWorkItemTypes(), { matchOnDescription: true, placeHolder: Strings.ChooseWorkItemType }); if (selectedType) { Telemetry.SendEvent(TelemetryEvents.OpenNewWorkItem); Logger.LogInfo("Selected work item type is " + selectedType.label); const newItemUrl: string = WorkItemTrackingService.GetNewWorkItemUrl(this._serverContext.RepoInfo.TeamProjectUrl, selectedType.label, taskTitle, this.getUserName(this._serverContext)); Logger.LogInfo("New Work Item Url: " + newItemUrl); Utils.OpenUrl(newItemUrl); } } catch (err) { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), false, "Error creating new work item"); } } //Navigates to a work item chosen from the results of a user-selected "My Queries" work item query //This method first displays the queries under "My Queries" and, when one is chosen, displays the associated work items. //If a work item is chosen, it is opened in the web browser. public async ShowMyWorkItemQueries(): Promise { try { Telemetry.SendEvent(TelemetryEvents.ShowMyWorkItemQueries); const query: WorkItemQueryQuickPickItem = await window.showQuickPick(this.getMyWorkItemQueries(), { matchOnDescription: false, placeHolder: Strings.ChooseWorkItemQuery }); if (query) { Telemetry.SendEvent(TelemetryEvents.ViewWorkItems); Logger.LogInfo("Selected query is " + query.label); Logger.LogInfo("Getting work items for query..."); const workItem: BaseQuickPickItem = await window.showQuickPick(this.getMyWorkItems(this._serverContext.RepoInfo.TeamProject, query.wiql), { matchOnDescription: true, placeHolder: Strings.ChooseWorkItem }); if (workItem) { let url: string = undefined; if (workItem.id === undefined) { Telemetry.SendEvent(TelemetryEvents.OpenAdditionalQueryResults); url = WorkItemTrackingService.GetMyQueryResultsUrl(this._serverContext.RepoInfo.TeamProjectUrl, this._myQueriesFolder, query.label); } else { Telemetry.SendEvent(TelemetryEvents.ViewWorkItem); url = WorkItemTrackingService.GetEditWorkItemUrl(this._serverContext.RepoInfo.TeamProjectUrl, workItem.id); } Logger.LogInfo("Work Item Url: " + url); Utils.OpenUrl(url); } } } catch (err) { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), false, "Error showing work item queries"); } } public async ShowPinnedQueryWorkItems(): Promise { Telemetry.SendEvent(TelemetryEvents.ViewPinnedQueryWorkItems); try { const queryText: string = await this.getPinnedQueryText(); await this.showWorkItems(queryText); } catch (err) { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), false, "Error showing pinned query work items"); } } public async ShowMyWorkItems(): Promise { Telemetry.SendEvent(TelemetryEvents.ViewMyWorkItems); try { await this.showWorkItems(WitQueries.MyWorkItems); } catch (err) { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), false, "Error showing my work items"); } } public async ChooseWorkItems(): Promise { Logger.LogInfo("Getting work items to choose from..."); try { const query: string = await this.getPinnedQueryText(); //gets either MyWorkItems, queryText or wiql of queryPath of PinnedQuery // TODO: There isn't a way to do a multi select pick list right now, but when there is we should change this to use it. const workItem: BaseQuickPickItem = await window.showQuickPick(this.getMyWorkItems(this._serverContext.RepoInfo.TeamProject, query), { matchOnDescription: true, placeHolder: Strings.ChooseWorkItem }); if (workItem) { return ["#" + workItem.id + " - " + workItem.description]; } else { return []; } } catch (err) { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), false, "Error showing my work items in order to choose (associate)"); return []; } } private async showWorkItems(wiql: string): Promise { Logger.LogInfo("Getting work items..."); const workItem: BaseQuickPickItem = await window.showQuickPick(this.getMyWorkItems(this._serverContext.RepoInfo.TeamProject, wiql), { matchOnDescription: true, placeHolder: Strings.ChooseWorkItem }); if (workItem) { let url: string = undefined; if (workItem.id === undefined) { Telemetry.SendEvent(TelemetryEvents.OpenAdditionalQueryResults); url = WorkItemTrackingService.GetWorkItemsBaseUrl(this._serverContext.RepoInfo.TeamProjectUrl); } else { Telemetry.SendEvent(TelemetryEvents.ViewWorkItem); url = WorkItemTrackingService.GetEditWorkItemUrl(this._serverContext.RepoInfo.TeamProjectUrl, workItem.id); } Logger.LogInfo("Work Item Url: " + url); Utils.OpenUrl(url); } } public async GetPinnedQueryResultCount(): Promise { try { Logger.LogInfo("Running pinned work item query to get count (" + this._serverContext.RepoInfo.TeamProject + ")..."); const queryText: string = await this.getPinnedQueryText(); const svc: WorkItemTrackingService = new WorkItemTrackingService(this._serverContext); return svc.GetQueryResultCount(this._serverContext.RepoInfo.TeamProject, queryText); } catch (err) { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), false, "Error getting pinned query result count"); } } private async getPinnedQueryText(): Promise { const promise: Promise = new Promise(async (resolve, reject) => { try { if (this._pinnedQuery.queryText && this._pinnedQuery.queryText.length > 0) { resolve(this._pinnedQuery.queryText); } else if (this._pinnedQuery.queryPath && this._pinnedQuery.queryPath.length > 0) { Logger.LogInfo("Getting my work item query (" + this._serverContext.RepoInfo.TeamProject + ")..."); Logger.LogInfo("QueryPath: " + this._pinnedQuery.queryPath); const svc: WorkItemTrackingService = new WorkItemTrackingService(this._serverContext); const queryItem: QueryHierarchyItem = await svc.GetWorkItemQuery(this._serverContext.RepoInfo.TeamProject, this._pinnedQuery.queryPath); resolve(queryItem.wiql); } } catch (err) { reject(err); } }); return promise; } private async getMyWorkItemQueries(): Promise { const queries: WorkItemQueryQuickPickItem[] = []; const svc: WorkItemTrackingService = new WorkItemTrackingService(this._serverContext); Logger.LogInfo("Getting my work item queries (" + this._serverContext.RepoInfo.TeamProject + ")..."); const hierarchyItems: QueryHierarchyItem[] = await svc.GetWorkItemHierarchyItems(this._serverContext.RepoInfo.TeamProject); Logger.LogInfo("Retrieved " + hierarchyItems.length + " hierarchyItems"); hierarchyItems.forEach((folder) => { if (folder && folder.isFolder === true && folder.isPublic === false) { // Because "My Queries" is localized and there is no API to get the name of the localized // folder, we need to save off the localized name when constructing URLs. this._myQueriesFolder = folder.name; if (folder.hasChildren === true) { //Gets all of the queries under "My Queries" and gets their name and wiql for (let index: number = 0; index < folder.children.length; index++) { queries.push({ id: folder.children[index].id, label: folder.children[index].name, description: "", wiql: folder.children[index].wiql }); } } } }); return queries; } private async getMyWorkItems(teamProject: string, wiql: string): Promise { const workItems: BaseQuickPickItem[] = []; const svc: WorkItemTrackingService = new WorkItemTrackingService(this._serverContext); Logger.LogInfo("Getting my work items (" + this._serverContext.RepoInfo.TeamProject + ")..."); const simpleWorkItems: SimpleWorkItem[] = await svc.GetWorkItems(teamProject, wiql); Logger.LogInfo("Retrieved " + simpleWorkItems.length + " work items"); simpleWorkItems.forEach((wi) => { workItems.push({ label: wi.label, description: wi.description, id: wi.id}); }); if (simpleWorkItems.length === WorkItemTrackingService.MaxResults) { workItems.push({ id: undefined, label: Strings.BrowseAdditionalWorkItems, description: Strings.BrowseAdditionalWorkItemsDescription }); } return workItems; } private getUserName(context: TeamServerContext): string { let userName: string = undefined; Logger.LogDebug("UserCustomDisplayName: " + context.UserInfo.CustomDisplayName); Logger.LogDebug("UserProviderDisplayName: " + context.UserInfo.ProviderDisplayName); if (context.UserInfo.CustomDisplayName !== undefined) { userName = context.UserInfo.CustomDisplayName; } else { userName = context.UserInfo.ProviderDisplayName; } Logger.LogDebug("User is " + userName); return userName; } private async getWorkItemTypes(): Promise { const svc: WorkItemTrackingService = new WorkItemTrackingService(this._serverContext); const types: WorkItemType[] = await svc.GetWorkItemTypes(this._serverContext.RepoInfo.TeamProject); const workItemTypes: BaseQuickPickItem[] = []; types.forEach((type) => { workItemTypes.push({ label: type.name, description: type.description, id: undefined }); }); workItemTypes.sort((t1, t2) => { return (t1.label.localeCompare(t2.label)); }); return workItemTypes; } private handleWitError(err: Error, offlineText: string, polling: boolean, infoMessage?: string): void { if (err.message.includes("Failed to find api location for area: wit id:")) { Telemetry.SendEvent(TelemetryEvents.UnsupportedWitServerVersion); const msg: string = Strings.UnsupportedWitServerVersion; Logger.LogError(msg); if (this._statusBarItem !== undefined) { this._statusBarItem.text = `$(bug) $(x)`; this._statusBarItem.tooltip = msg; this._statusBarItem.command = undefined; //Clear the existing command } if (!polling) { VsCodeUtils.ShowErrorMessage(msg); } } else { this.handleError(err, offlineText, polling, infoMessage); } } private logTelemetryForWorkItem(wit: string): void { switch (wit) { case WitTypes.Bug: Telemetry.SendEvent(TelemetryEvents.OpenNewBug); break; case WitTypes.Task: Telemetry.SendEvent(TelemetryEvents.OpenNewTask); break; default: break; } } public PollPinnedQuery(): void { this.GetPinnedQueryResultCount().then((numberOfItems) => { this._statusBarItem.tooltip = Strings.ViewYourPinnedQuery; this._statusBarItem.text = WitClient.GetPinnedQueryStatusText(numberOfItems.toString()); }).catch((err) => { this.handleWitError(err, WitClient.GetOfflinePinnedQueryStatusText(), true, "Failed to get pinned query count during polling"); }); } public static GetOfflinePinnedQueryStatusText() : string { return `$(bug) ???`; } public static GetPinnedQueryStatusText(total?: string) : string { if (!total) { return `$(bug) $(dash)`; } return `$(bug) ${total.toString()}`; } } ================================================ FILE: src/contexts/externalcontext.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IRepositoryContext, RepositoryType } from "./repositorycontext"; import { RepoUtils } from "../helpers/repoutils"; import { Logger } from "../helpers/logger"; import { ISettings } from "../helpers/settings"; export class ExternalContext implements IRepositoryContext { private _folder: string; private _remoteUrl: string; private _isSsh: boolean = false; private _isTeamServicesUrl: boolean = false; private _isTeamFoundationServer: boolean = false; private _teamProjectName: string; constructor(rootPath: string) { //The passed in path is the workspace.rootPath (which could be a sub-folder) this._folder = rootPath; } public dispose() { //nothing to do } //Need to call tf.cmd to get TFVC information (and constructors can't be async) public async Initialize(settings: ISettings): Promise { Logger.LogDebug(`Looking for an External Context at ${this._folder}`); if (!settings.RemoteUrl || !settings.TeamProject) { Logger.LogDebug(`No External Context at ${this._folder}`); return false; } this._remoteUrl = settings.RemoteUrl; this._isTeamServicesUrl = RepoUtils.IsTeamFoundationServicesRepo(this._remoteUrl); this._isTeamFoundationServer = RepoUtils.IsTeamFoundationServerRepo(this._remoteUrl); this._teamProjectName = settings.TeamProject; Logger.LogDebug(`Found an External Context at ${this._folder}`); return true; } // Tfvc implementation public get TeamProjectName(): string { return this._teamProjectName; } // Git implementation public get CurrentBranch(): string { return undefined; } public get CurrentRef(): string { return undefined; } // IRepositoryContext implementation public get RepoFolder(): string { return this._folder; } public get IsSsh(): boolean { return this._isSsh; } public get IsTeamFoundation(): boolean { return this._isTeamServicesUrl || this._isTeamFoundationServer; } public get IsTeamServices(): boolean { return this._isTeamServicesUrl; } public get RemoteUrl(): string { return this._remoteUrl; } public get RepositoryParentFolder(): string { return undefined; } public get Type(): RepositoryType { return RepositoryType.EXTERNAL; } } ================================================ FILE: src/contexts/gitcontext.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Utils } from "../helpers/utils"; import { RepoUtils } from "../helpers/repoutils"; import { IRepositoryContext, RepositoryType } from "./repositorycontext"; import * as pgc from "parse-git-config"; import * as gri from "git-repo-info"; import * as path from "path"; import * as url from "url"; //Gets as much information as it can regarding the Git repository without calling the server (vsts/info) export class GitContext implements IRepositoryContext { private _gitConfig: any; private _gitRepoInfo: any; private _gitFolder: string; private _gitParentFolder: string; private _gitOriginalRemoteUrl: string; private _gitRemoteUrl: string; private _gitCurrentBranch: string; private _gitCurrentRef: string; private _isSsh: boolean = false; private _isTeamServicesUrl: boolean = false; private _isTeamFoundationServer: boolean = false; //When gitDir is provided, rootPath is the path to the Git repo constructor(rootPath: string, gitDir?: string) { if (rootPath) { //If gitDir, use rootPath as the .git folder if (gitDir) { this._gitFolder = rootPath; gri._changeGitDir(gitDir); } else { this._gitFolder = Utils.FindGitFolder(rootPath); } if (this._gitFolder !== undefined) { // With parse-git-config, cwd is the directory containing the path, .git/config, you want to sync this._gitParentFolder = path.dirname(this._gitFolder); let syncObj: any = { cwd: this._gitParentFolder }; //If gitDir, send pgc the exact path to the config file to use if (gitDir) { syncObj = { path: path.join(this._gitFolder, "config") }; } this._gitConfig = pgc.sync(syncObj); /* tslint:disable:quotemark */ const remote: any = this._gitConfig['remote "origin"']; /* tslint:enable:quotemark */ if (remote === undefined) { return; } this._gitOriginalRemoteUrl = remote.url; if (gitDir) { this._gitRepoInfo = gri(this._gitParentFolder); } else { this._gitRepoInfo = gri(this._gitFolder); } this._gitCurrentBranch = this._gitRepoInfo.branch; this._gitCurrentRef = "refs/heads/" + this._gitCurrentBranch; //Check if any heuristics for TFS/VSTS URLs match if (RepoUtils.IsTeamFoundationGitRepo(this._gitOriginalRemoteUrl)) { const purl = url.parse(this._gitOriginalRemoteUrl); if (purl) { if (RepoUtils.IsTeamFoundationServicesRepo(this._gitOriginalRemoteUrl)) { this._isTeamServicesUrl = true; const splitHref = purl.href.split("@"); if (splitHref.length === 2) { //RemoteUrl is SSH this._isSsh = true; // VSTS now has three URL modes v3, _git, and _ssh. if (purl.pathname.indexOf("/_git/") >= 0) { // For Team Services, default to https:// as the protocol this._gitRemoteUrl = "https://" + purl.hostname + purl.pathname; } else if (RepoUtils.IsTeamFoundationServicesV3SshRepo(purl.href)) { this._gitRemoteUrl = RepoUtils.ConvertSshV3ToUrl(purl.href); } else { // Do a few substitutions to get the correct url: // * ssh:// -> https:// // * vs-ssh -> accountname // * _git -> _ssh // so ssh://account@vsts-ssh.visualstudio.com/DefaultCollection/_ssh/foo // becomes https://account.visualstudio.com/DefaultCollection/_git/foo const scheme = "https://"; const hostname = purl.auth + ".visualstudio.com"; const path = purl.pathname.replace("_ssh", "_git"); this._gitRemoteUrl = scheme + hostname + path; } } else { this._gitRemoteUrl = this._gitOriginalRemoteUrl; } } else if (RepoUtils.IsTeamFoundationServerRepo(this._gitOriginalRemoteUrl)) { this._isTeamFoundationServer = true; this._gitRemoteUrl = this._gitOriginalRemoteUrl; if (purl.protocol.toLowerCase() === "ssh:") { this._isSsh = true; // TODO: No support yet for SSH on-premises (no-op the extension) this._isTeamFoundationServer = false; } } } } } } } public dispose() { //nothing to do } //constructor already initializes the GitContext public async Initialize(): Promise { return true; } //Git implementation public get CurrentBranch(): string { return this._gitCurrentBranch; } public get CurrentRef(): string { return this._gitCurrentRef; } //TFVC implementation //For Git, TeamProjectName is set after the call to vsts/info public get TeamProjectName(): string { return undefined; } //IRepositoryContext implementation public get RepoFolder(): string { return this._gitFolder; } public get IsSsh(): boolean { return this._isSsh; } public get IsTeamFoundation(): boolean { return this._isTeamServicesUrl || this._isTeamFoundationServer; } public get IsTeamServices(): boolean { return this._isTeamServicesUrl; } public get RemoteUrl(): string { return this._gitRemoteUrl; } public get RepositoryParentFolder(): string { return this._gitParentFolder; } public get Type(): RepositoryType { return RepositoryType.GIT; } } ================================================ FILE: src/contexts/repocontextfactory.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IRepositoryContext } from "../contexts/repositorycontext"; import { GitContext } from "../contexts/gitcontext"; import { TfvcContext } from "../contexts/tfvccontext"; import { TeamServerContext } from "../contexts/servercontext"; import { ExternalContext } from "../contexts/externalcontext"; import { Settings } from "../helpers/settings"; import { TfCommandLineRunner } from "../tfvc/tfcommandlinerunner"; export class RepositoryContextFactory { //Returns an IRepositoryContext if the repository is either TFS or Team Services public static async CreateRepositoryContext(path: string, settings: Settings) : Promise { let repoContext: IRepositoryContext; let initialized: boolean = false; //Check for remoteUrl and teamProject in settings first repoContext = new ExternalContext(path); initialized = await repoContext.Initialize(settings); if (!initialized) { //Check for Git next since it should be faster to determine and this code will //be called on Reinitialize (when config changes, for example) repoContext = new GitContext(path); initialized = await repoContext.Initialize(); if (!repoContext || repoContext.IsTeamFoundation === false || !initialized) { //Check if we have a TFVC repository repoContext = new TfvcContext(path); initialized = await repoContext.Initialize(); if (!initialized) { return undefined; } if (repoContext.IsTeamFoundation === false) { //We don't have any Team Services repository return undefined; } } } return repoContext; } /** * This method allows the ExtensionManager the ability to update the repository context it obtained with the server context information * it has. This provides one source for TFVC classes like Tfvc and Repository. * This method doesn't do anything for other types of repository contexts. */ public static UpdateRepositoryContext(currentRepo: IRepositoryContext, serverContext: TeamServerContext): IRepositoryContext { if (currentRepo && currentRepo instanceof TfvcContext) { const context: TfvcContext = currentRepo; context.TfvcRepository = TfCommandLineRunner.CreateRepository(serverContext, context.RepoFolder); } return currentRepo; } } ================================================ FILE: src/contexts/repositorycontext.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { ISettings } from "../helpers/settings"; export enum RepositoryType { GIT, TFVC, ANY, EXTERNAL } export interface IRepositoryContext { Type: RepositoryType; //Added Initialize() so TFVC could call tf.cmd async Initialize(settings?: ISettings): Promise; IsSsh: boolean; IsTeamFoundation: boolean; IsTeamServices: boolean; RemoteUrl: string; RepoFolder: string; RepositoryParentFolder: string; //Git-specific values CurrentBranch: string; CurrentRef: string; //TFVC-specific values TeamProjectName: string; } ================================================ FILE: src/contexts/servercontext.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IRequestHandler } from "vso-node-api/interfaces/common/VsoBaseInterfaces"; import { CredentialInfo } from "../info/credentialinfo"; import { RepositoryInfo } from "../info/repositoryinfo"; import { UserInfo } from "../info/userinfo"; export class TeamServerContext { private _userInfo: UserInfo; private _repositoryInfo: RepositoryInfo; private _credentialHandler: IRequestHandler; private _credentialInfo: CredentialInfo; //The constructor simply parses the remoteUrl to determine if we're Team Services or Team Foundation Server. //Any additional information we can get from the url is also parsed. Once we call the vsts/info api, we can //get the rest of the information that we need. constructor(remoteUrl: string) { if (remoteUrl === undefined) { return; } this._repositoryInfo = new RepositoryInfo(remoteUrl); } public get CredentialHandler(): IRequestHandler { return this._credentialHandler; } public set CredentialHandler(handler: IRequestHandler) { this._credentialHandler = handler; } public get RepoInfo(): RepositoryInfo { return this._repositoryInfo; } public set RepoInfo(info: RepositoryInfo) { this._repositoryInfo = info; } public get UserInfo(): UserInfo { return this._userInfo; } public set UserInfo(info: UserInfo) { this._userInfo = info; } public get CredentialInfo(): CredentialInfo { return this._credentialInfo; } public set CredentialInfo(info: CredentialInfo) { this._credentialInfo = info; } } ================================================ FILE: src/contexts/tfvccontext.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IRepositoryContext, RepositoryType } from "./repositorycontext"; import { TfCommandLineRunner } from "../tfvc/tfcommandlinerunner"; import { TfvcRepository } from "../tfvc/tfvcrepository"; import { IWorkspace } from "../tfvc/interfaces"; import { RepoUtils } from "../helpers/repoutils"; import { Logger } from "../helpers/logger"; export class TfvcContext implements IRepositoryContext { private _tfvcFolder: string; private _gitParentFolder: string; private _tfvcRemoteUrl: string; private _isSsh: boolean = false; private _isTeamServicesUrl: boolean = false; private _isTeamFoundationServer: boolean = false; private _teamProjectName: string; private _repo: TfvcRepository; private _tfvcWorkspace: IWorkspace; constructor(rootPath: string) { this._tfvcFolder = rootPath; } //Need to call tf.cmd to get TFVC information (and constructors can't be async) public async Initialize(): Promise { Logger.LogDebug(`Looking for TFVC repository at ${this._tfvcFolder}`); this._repo = TfCommandLineRunner.CreateRepository(undefined, this._tfvcFolder); //Ensure we have an appropriate ENU version of tf executable //The call will throw if we have tf configured properly but it isn't ENU await this._repo.CheckVersion(); this._tfvcWorkspace = await this._repo.FindWorkspace(this._tfvcFolder); this._tfvcRemoteUrl = this._tfvcWorkspace.server; this._isTeamServicesUrl = RepoUtils.IsTeamFoundationServicesRepo(this._tfvcRemoteUrl); this._isTeamFoundationServer = RepoUtils.IsTeamFoundationServerRepo(this._tfvcRemoteUrl); this._teamProjectName = this._tfvcWorkspace.defaultTeamProject; Logger.LogDebug(`Found a TFVC repository for url: '${this._tfvcRemoteUrl}' and team project: '${this._teamProjectName}'.`); return true; } // Tfvc implementation public get TeamProjectName(): string { return this._teamProjectName; } public get TfvcRepository(): TfvcRepository { return this._repo; } public set TfvcRepository(newRepository: TfvcRepository) { // Don't let the repository be undefined if (newRepository) { this._repo = newRepository; } } public get TfvcWorkspace(): IWorkspace { return this._tfvcWorkspace; } // Git implementation public get CurrentBranch(): string { return undefined; } public get CurrentRef(): string { return undefined; } // IRepositoryContext implementation public get RepoFolder(): string { return this._tfvcFolder; } public get IsSsh(): boolean { return this._isSsh; } public get IsTeamFoundation(): boolean { return this._isTeamServicesUrl || this._isTeamFoundationServer; } public get IsTeamServices(): boolean { return this._isTeamServicesUrl; } public get RemoteUrl(): string { return this._tfvcRemoteUrl; } public get RepositoryParentFolder(): string { return this._gitParentFolder; } public get Type(): RepositoryType { return RepositoryType.TFVC; } //This is used if we need to update the RemoteUrl after validating the TFVC collection with the repositoryinfoclient public set RemoteUrl(remoteUrl: string) { this._tfvcRemoteUrl = remoteUrl; } } ================================================ FILE: src/credentialstore/credential.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; //TODO: Add an interface that represents a Credential? export class Credential { private _service: string; private _username: string; private _password: string; constructor(service: string, username: string, password: string) { this._service = service; this._username = username; this._password = password; } public get Service() : string { return this._service; } public get Username() : string { return this._username; } public get Password() : string { return this._password; } } ================================================ FILE: src/credentialstore/credentialstore.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as os from "os"; import * as Q from "q"; import { LinuxFileApi } from "./linux/linux-file-api"; import { OsxKeychainApi } from "./osx/osx-keychain-api"; import { WindowsCredentialStoreApi } from "./win32/win-credstore-api"; import { ICredentialStore } from "./interfaces/icredentialstore"; import { Credential } from "./credential"; /** * Implements a credential storage for Windows, Mac (darwin), or Linux. * * Allows a single credential to be stored per service (that is, one username per service); */ export class CredentialStore implements ICredentialStore { private _credentialStore: ICredentialStore; private _filename: string; private _folder: string; private _prefix: string; private _defaultPrefix: string = "secret:"; private _defaultFilename: string = "secrets.json"; private _defaultFolder: string = ".secrets"; constructor(prefix?: string, folder?: string, filename?: string) { if (prefix !== undefined) { this._prefix = prefix; } if (folder !== undefined) { this._folder = folder; } if (filename !== undefined) { this._filename = filename; } // In the case of win32 or darwin, this._folder will contain the prefix. switch (os.platform()) { case "win32": if (prefix === undefined) { this._prefix = this._defaultPrefix; } this._credentialStore = new WindowsCredentialStoreApi(this._prefix); break; case "darwin": if (prefix === undefined) { this._prefix = this._defaultPrefix; } this._credentialStore = new OsxKeychainApi(this._prefix); break; /* tslint:disable:no-switch-case-fall-through */ case "linux": default: /* tslint:enable:no-switch-case-fall-through */ if (folder === undefined) { this._folder = this._defaultFolder; } if (filename === undefined) { this._filename = this._defaultFilename; } this._credentialStore = new LinuxFileApi(this._folder, this._filename); break; } } public GetCredential(service: string) : Q.Promise { return this._credentialStore.GetCredential(service); } public SetCredential(service: string, username: string, password: any) : Q.Promise { const deferred: Q.Deferred = Q.defer(); // First, look to see if we have a credential for this service already. If so, remove it // since we don't know if the user is changing the username or the password (or both) for // the particular service. this.GetCredential(service).then((cred) => { if (cred !== undefined) { // On Windows, "*" will delete all matching credentials in one go // On Linux, we use 'underscore' to remove the ones we want to remove and save the leftovers // On Mac, "*" will find all matches and delete each individually this.RemoveCredential(service).then(() => { this._credentialStore.SetCredential(service, username, password).then(() => { deferred.resolve(undefined); }).catch((reason) => { deferred.reject(reason); }); }); } else { this._credentialStore.SetCredential(service, username, password).then(() => { deferred.resolve(undefined); }).catch((reason) => { deferred.reject(reason); }); } }).catch((reason) => { deferred.reject(reason); }); return deferred.promise; } public RemoveCredential(service: string) : Q.Promise { return this._credentialStore.RemoveCredential(service); } // Used by tests to ensure certain credentials we create don't exist public getCredentialByName(service: string, username: string) : Q.Promise { return this._credentialStore.getCredentialByName(service, username); } // Used by tests to remove certain credentials public removeCredentialByName(service: string, username: string) : Q.Promise { return this._credentialStore.removeCredentialByName(service, username); } } ================================================ FILE: src/credentialstore/interfaces/icredentialstore.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Credential } from "../credential"; /* tslint:disable:no-unused-variable */ import Q = require("q"); /* tslint:enable:no-unused-variable */ export interface ICredentialStore { GetCredential(service: string) : Q.Promise; SetCredential(service: string, username: string, password: any) : Q.Promise; RemoveCredential(service: string) : Q.Promise; getCredentialByName(service: string, username: string) : Q.Promise; removeCredentialByName(service: string, username: string) : Q.Promise; } ================================================ FILE: src/credentialstore/linux/file-token-storage.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as Q from "q"; import * as fs from "fs"; import * as path from "path"; /* Provides storage of credentials in a file on the local file system. Does not support any kind of 'prefix' of the credential (since this storage mechanism is not shared with either Windows or OSX). The file is secured as RW for the owner of the process. */ export class FileTokenStorage { private _filename: string; constructor(filename: string) { this._filename = filename; } public AddEntries(newEntries: Array, existingEntries: Array) : Q.Promise { const entries: Array = existingEntries.concat(newEntries); return this.saveEntries(entries); } public Clear() : Q.Promise { return this.saveEntries([]); } public LoadEntries() : Q.Promise { const deferred: Q.Deferred = Q.defer(); let entries: Array = []; let err: any; try { const content: string = fs.readFileSync(this._filename, {encoding: "utf8", flag: "r"}); entries = JSON.parse(content); deferred.resolve(entries); } catch (ex) { if (ex.code !== "ENOENT") { err = ex; deferred.reject(err); } else { // If it is ENOENT (the file doesn't exist or can't be found) // Return an empty array (no items yet) deferred.resolve([]); } } return deferred.promise; } public RemoveEntries(entriesToKeep: Array /*, entriesToRemove?: Array*/) : Q.Promise { return this.saveEntries(entriesToKeep); } private saveEntries(entries: Array) : Q.Promise { const defer: Q.Deferred = Q.defer(); const writeOptions = { encoding: "utf8", mode: 384, // Permission 0600 - owner read/write, nobody else has access flag: "w" }; // If the path we want to store in doesn't exist, create it const folder: string = path.dirname(this._filename); if (!fs.existsSync(folder)) { fs.mkdirSync(folder); } fs.writeFile(this._filename, JSON.stringify(entries), writeOptions, (err) => { if (err) { defer.reject(err); } else { defer.resolve(undefined); } }); return defer.promise; } } ================================================ FILE: src/credentialstore/linux/linux-file-api.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { FileTokenStorage } from "./file-token-storage"; import { Credential } from "../credential"; import { ICredentialStore } from "../interfaces/icredentialstore"; import * as Q from "q"; import * as os from "os"; import * as path from "path"; import * as _ from "underscore"; /* Provides the ICredentialStore API on top of file-based storage. Does not support any kind of 'prefix' of the credential (since its storage mechanism is not shared with either Windows or OSX). User must provide a custom folder and custom file name for storage. */ export class LinuxFileApi implements ICredentialStore { private _folder: string; private _filename: string; private _fts: FileTokenStorage; constructor(folder: string, filename: string) { this._folder = folder; this._filename = filename; this._fts = new FileTokenStorage(path.join(path.join(os.homedir(), this._folder, this._filename))); } public GetCredential(service: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.loadCredentials().then((entries) => { // Find the entry I want based on service const entryArray: Array = _.where(entries, { service: service }); if (entryArray !== undefined && entryArray.length > 0) { const credential: Credential = this.createCredential(entryArray[0]); deferred.resolve(credential); } else { deferred.resolve(undefined); } }) .catch((err) => { deferred.reject(err); }); return deferred.promise; } public SetCredential(service: string, username: string, password: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.loadCredentials().then((entries) => { // Remove any entries that are the same as the one I'm about to add const existingEntries = _.reject(entries, function(elem) { return elem.username === username && elem.service === service; }); const newEntry = { username: username, password: password, service: service }; this._fts.AddEntries([ newEntry ], existingEntries).then(() => { deferred.resolve(undefined); }).catch((err) => { deferred.reject(err); }); }) .catch((err) => { deferred.reject(err); }); return deferred.promise; } public RemoveCredential(service: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.loadCredentials().then((entries) => { // Find the entry being asked to be removed; if found, remove it, save the remaining list const existingEntries = _.reject(entries, function(elem) { return elem.service === service; }); // TODO: RemoveEntries doesn't do anything with second arg. For now, do nothing to // the api as I'm wrapping it in all its glory. Could consider later. this._fts.RemoveEntries(existingEntries /*, undefined*/).then(() => { deferred.resolve(undefined); }).catch((err) => { deferred.reject(err); }); }) .catch((err) => { deferred.reject(err); }); return deferred.promise; } public getCredentialByName(service: string, username: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.loadCredentials().then((entries) => { // Find the entry I want based on service and username const entryArray: Array = _.where(entries, { service: service, username: username }); if (entryArray !== undefined && entryArray.length > 0) { const credential: Credential = this.createCredential(entryArray[0]); deferred.resolve(credential); } else { deferred.resolve(undefined); } }) .catch((err) => { deferred.reject(err); }); return deferred.promise; } public removeCredentialByName(service: string, username: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.loadCredentials().then((entries) => { // Find the entry being asked to be removed; if found, remove it, save the remaining list const existingEntries = _.reject(entries, function(elem) { if (username === "*") { return elem.service === service; } else { return elem.username === username && elem.service === service; } }); // TODO: RemoveEntries doesn't do anything with second arg. For now, do nothing to // the api as I'm wrapping it in all its glory. Could consider later. this._fts.RemoveEntries(existingEntries /*, undefined*/).then(() => { deferred.resolve(undefined); }).catch((err) => { deferred.reject(err); }); }) .catch((err) => { deferred.reject(err); }); return deferred.promise; } private createCredential(cred: any) : Credential { return new Credential(cred.service, cred.username, cred.password); } private loadCredentials() : Q.Promise { const deferred: Q.Deferred = Q.defer(); this._fts.LoadEntries().then((entries) => { deferred.resolve(entries); }) .catch((err) => { deferred.reject(err); }); return deferred.promise; } } ================================================ FILE: src/credentialstore/osx/osx-keychain-api.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Credential } from "../credential"; import { ICredentialStore } from "../interfaces/icredentialstore"; import * as Q from "q"; /* tslint:disable:no-var-keyword */ var osxkeychain = require("./osx-keychain"); /* tslint:enable:no-var-keyword */ /* Provides the ICredentialStore API on top of OSX keychain-based storage. User can provide a custom prefix for the credential. */ export class OsxKeychainApi implements ICredentialStore { private _prefix: string; constructor(credentialPrefix: string) { if (credentialPrefix !== undefined) { this._prefix = credentialPrefix; osxkeychain.setPrefix(credentialPrefix); } } public GetCredential(service: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); let credential: Credential; // To get the credential, I must first list all of the credentials we previously // stored there. Find the one we want, then go and ask for the secret. this.listCredentials().then((credentials) => { // Spin through the returned credentials to ensure I got the one I want // based on passed in 'service' for (let index: number = 0; index < credentials.length; index++) { if (credentials[index].Service === service) { credential = credentials[index]; break; } } if (credential !== undefined) { //Go get the password osxkeychain.get(credential.Username, credential.Service, function(err, cred) { if (err) { deferred.reject(err); } if (cred !== undefined) { credential = new Credential(credential.Service, credential.Username, cred); deferred.resolve(credential); } }); } else { deferred.resolve(undefined); } }).fail((reason) => { deferred.reject(reason); }); return deferred.promise; } public SetCredential(service: string, username: string, password: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); // I'm not supporting a description so pass "" for that parameter osxkeychain.set(username, service, "" /*description*/, password, function(err) { if (err) { deferred.reject(err); } else { deferred.resolve(undefined); } }); return deferred.promise; } public RemoveCredential(service: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.removeCredentials(service).then(() => { deferred.resolve(undefined); }) .fail((reason) => { deferred.reject(reason); }); return deferred.promise; } public getCredentialByName(service: string, username: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); let credential: Credential; // To get the credential, I must first list all of the credentials we previously // stored there. Find the one we want, then go and ask for the secret. this.listCredentials().then((credentials) => { // Spin through the returned credentials to ensure I got the one I want // based on passed in 'service' for (let index: number = 0; index < credentials.length; index++) { if (credentials[index].Service === service && credentials[index].Username === username) { credential = credentials[index]; break; } } if (credential !== undefined) { //Go get the password osxkeychain.get(credential.Username, credential.Service, function(err, cred) { if (err) { deferred.reject(err); } if (cred !== undefined) { credential = new Credential(credential.Service, credential.Username, cred); deferred.resolve(credential); } }); } else { deferred.resolve(undefined); } }).fail((reason) => { deferred.reject(reason); }); return deferred.promise; } public removeCredentialByName(service: string, username: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); // if username === "*", we need to remove all credentials for this service. if (username === "*") { this.removeCredentials(service).then(() => { deferred.resolve(undefined); }) .fail((reason) => { deferred.reject(reason); }); } else { osxkeychain.remove(username, service, "" /*description*/, function(err) { if (err) { if (err.code !== undefined && err.code === 44) { // If credential is not found, don't fail. deferred.resolve(undefined); } else { deferred.reject(err); } } else { deferred.resolve(undefined); } }); } return deferred.promise; } private removeCredentials(service: string): Q.Promise { const deferred: Q.Deferred = Q.defer(); // listCredentials will return all of the credentials for this prefix and service this.listCredentials(service).then((creds) => { if (creds !== undefined && creds.length > 0) { // Remove all of these credentials const promises: Q.Promise[] = []; creds.forEach((cred) => { promises.push(this.removeCredentialByName(cred.Service, cred.Username)); }); Q.all(promises).then(() => { deferred.resolve(undefined); }); } else { deferred.resolve(undefined); } }); return deferred.promise; } private listCredentials(service? : string) : Q.Promise> { const deferred: Q.Deferred> = Q.defer>(); const credentials: Array = []; const stream = osxkeychain.list(); stream.on("data", (cred) => { // Don't return all credentials, just ones that start // with our prefix and optional service if (cred.svce !== undefined) { if (cred.svce.indexOf(this._prefix) === 0) { const svc: string = cred.svce.substring(this._prefix.length); const username: string = cred.acct; //password is undefined because we don't have it yet const credential: Credential = new Credential(svc, username, undefined); // Only add the credential if we want them all or it's a match on service if (service === undefined || service === svc) { credentials.push(credential); } } } }); stream.on("end", () => { deferred.resolve(credentials); }); stream.on("error", (error) => { console.log(error); deferred.reject(error); }); return deferred.promise; } } ================================================ FILE: src/credentialstore/osx/osx-keychain-parser.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; // // Parser for the output of the security(1) command line. // var _ = require('underscore'); var es = require('event-stream'); var stream = require('readable-stream'); var util = require('util'); // // Regular expressions that match the various fields in the input // // Fields at the root - not attributes var rootFieldRe = /^([^:]+):(?: (?:"([^"]+)")|(.*))?$/; // Attribute values, this gets a little more complicated var attrRe = /^ (?:(0x[0-9a-fA-F]+) |"([a-z]{4})")<[^>]+>=(?:()|"([^"]+)"|(0x[0-9a-fA-F]+)(?: "([^"]+)")|(.*)?)/; // // Stream based parser for the OSX security(1) program output. // Implements a simple state machine. States are: // // 0 - Waiting for the initial "keychain" string. // 1 - Waiting for the "attributes" string. adds any properties to the // current entry object being parsed while waiting. // 2 - reading attributes. Continues adding the attributes to the // current entry object until we hit either a non-indented line // or end. At which point we emit. // var Transform = stream.Transform; function OsxSecurityParsingStream() { Transform.call(this, { objectMode: true }); this.currentEntry = null; this.state = 0; } util.inherits(OsxSecurityParsingStream, Transform); _.extend(OsxSecurityParsingStream.prototype, { _transform: function (chunk, encoding, callback) { var match; var value; var line = chunk.toString(); var count = 0; while (line !== null && line !== '') { ++count; if (count > 2) { return callback(new Error('Multiple passes attempting to parse line [' + line + ']. Possible bug in parser and infinite loop')); } switch(this.state) { case 0: match = rootFieldRe.exec(line); if (match !== null) { if (match[1] === 'keychain') { this.currentEntry = { keychain: match[2] }; this.state = 1; line = null; } else { this.currentEntry[match[1]] = match[2] || match[3]; } } break; case 1: match = rootFieldRe.exec(line); if (match !== null) { if (match[1] !== 'attributes') { this.currentEntry[match[1]] = match[2]; line = null; } else { this.state = 2; line = null; } } break; case 2: match = attrRe.exec(line); if (match !== null) { // Did we match a four-char named field? We don't care about hex fields if (match[2]) { // We skip nulls, and grab text rather than hex encoded versions of value value = match[6] || match[4]; if (value) { this.currentEntry[match[2]] = value; } } line = null; } else { // Didn't match, so emit current entry, then // reset to state zero and start processing for the // next entry. this.push(this.currentEntry); this.currentEntry = null; this.state = 0; } break; } } callback(); }, _flush: function (callback) { if (this.currentEntry) { this.push(this.currentEntry); } callback(); } }); function createParsingStream() { return es.pipeline(es.split(), new OsxSecurityParsingStream()); } createParsingStream.ParsingStream = OsxSecurityParsingStream; module.exports = createParsingStream; ================================================ FILE: src/credentialstore/osx/osx-keychain.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; // // Access to the OSX keychain - list, add, get password, remove // var _ = require('underscore'); var childProcess = require('child_process'); var es = require('event-stream'); var parser = require('./osx-keychain-parser'); var securityPath = '/usr/bin/security'; var targetNamePrefix = ''; //Allow callers to set their own prefix function setPrefix(prefix) { targetNamePrefix = prefix; } function ensurePrefix(targetName) { if (targetName.slice(targetNamePrefix.length) !== targetNamePrefix) { targetName = targetNamePrefix + targetName; } return targetName; } function removePrefix(targetName) { return targetName.slice(targetNamePrefix.length); } /** * List contents of default keychain, no passwords. * * @return {Stream} object mode stream of parsed results. */ function list() { var securityProcess = childProcess.spawn(securityPath, ['dump-keychain']); return securityProcess.stdout .pipe(es.split()) .pipe(es.mapSync(function (line) { return line.replace(/\\134/g, '\\'); })) .pipe(new parser.ParsingStream()); } /** * Get the password for a given key from the keychain * Assumes it's a generic credential. * * @param {string} userName user name to look up * @param {string} service service identifier * @param {Function(err, string)} callback callback receiving * returned result. */ function get(userName, service, callback) { var args = [ 'find-generic-password', '-a', userName, '-s', ensurePrefix(service), '-g' ]; childProcess.execFile(securityPath, args, function (err, stdout, stderr) { if (err) { return callback(err); } var match = /^password: (?:0x[0-9A-F]+ )?"(.*)"$/m.exec(stderr); if (match) { var password = match[1].replace(/\\134/g, '\\'); return callback(null, password); } return callback(new Error('Password in invalid format')); }); } /** * Set the password for a given key in the keychain. * Will overwrite password if the key already exists. * * @param {string} userName * @param {string} service * @param {string} description * @param {string} password * @param {function(err)} callback called on completion. */ function set(userName, service, description, password, callback) { var args = [ 'add-generic-password', '-a', userName, '-D', description, '-s', ensurePrefix(service), '-w', password, '-U' ]; childProcess.execFile(securityPath, args, function (err, stdout, stderr) { if (err) { return callback(new Error('Could not add password to keychain: ' + stderr)); } return callback(); }); } /** * Remove the given account from the keychain * * @param {string} userName * @param {string} service * @param {string} description * @param {function (err)} callback called on completion */ function remove(userName, service, description, callback) { var args = ['delete-generic-password']; if (userName) { args = args.concat(['-a', userName]); } if (service) { args = args.concat(['-s', ensurePrefix(service)]); } if (description) { args = args.concat(['-D', description]); } childProcess.execFile(securityPath, args, function (err, stdout, stderr) { if (err) { return callback(err); } return callback(); }); } _.extend(exports, { list: list, set: set, get: get, remove: remove, setPrefix: setPrefix }); ================================================ FILE: src/credentialstore/win32/win-credstore-api.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Credential } from "../credential"; import { ICredentialStore } from "../interfaces/icredentialstore"; import * as Q from "q"; /* tslint:disable:no-var-keyword */ var wincredstore = require("./win-credstore"); /* tslint:enable:no-var-keyword */ /* Provides the ICredentialStore API on top of Windows Credential Store-based storage. User can provide a custom prefix for the credential. */ export class WindowsCredentialStoreApi implements ICredentialStore { private static separator: string = "|"; constructor(credentialPrefix: string) { if (credentialPrefix !== undefined) { wincredstore.setPrefix(credentialPrefix); } } public GetCredential(service: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); let credential: Credential; //TODO: Why not just have listCredentials send back the ones I want based on (optional) service? this.listCredentials().then((credentials) => { //Spin through the returned credentials to ensure I got the one I want based on passed in 'service' for (let index: number = 0; index < credentials.length; index++) { credential = this.createCredential(credentials[index]); if (credential.Service === service) { break; } else { // The current credential isn't the one we're looking for credential = undefined; } } deferred.resolve(credential); }).fail((reason) => { deferred.reject(reason); }); return deferred.promise; } public SetCredential(service: string, username: string, password: any) : Q.Promise { const deferred: Q.Deferred = Q.defer(); const targetName: string = this.createTargetName(service, username); // Here, `password` is either the password or pat wincredstore.set(targetName, password, function(err) { if (err) { deferred.reject(err); } else { deferred.resolve(undefined); } }); return deferred.promise; } public RemoveCredential(service: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); const targetName: string = this.createTargetName(service, "*"); wincredstore.remove(targetName, function(err) { if (err) { if (err.code !== undefined && err.code === 1168) { //code 1168: not found // If credential isn't found, don't fail. deferred.resolve(undefined); } else { deferred.reject(err); } } else { deferred.resolve(undefined); } }); return deferred.promise; } // Adding for test purposes (to ensure a particular credential doesn't exist) public getCredentialByName(service: string, username: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); let credential: Credential; this.listCredentials().then((credentials) => { //Spin through the returned credentials to ensure I got the one I want based on passed in 'service' for (let index: number = 0; index < credentials.length; index++) { credential = this.createCredential(credentials[index]); if (credential.Service === service && credential.Username === username) { break; } else { // The current credential isn't the one we're looking for credential = undefined; } } deferred.resolve(credential); }).fail((reason) => { deferred.reject(reason); }); return deferred.promise; } public removeCredentialByName(service: string, username: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); const targetName: string = this.createTargetName(service, username); wincredstore.remove(targetName, function(err) { if (err) { if (err.code !== undefined && err.code === 1168) { //code 1168: not found // If credential isn't found, don't fail. deferred.resolve(undefined); } else { deferred.reject(err); } } else { deferred.resolve(undefined); } }); return deferred.promise; } private createCredential(cred: any) : Credential { const password: string = new Buffer(cred.credential, "hex").toString("utf8"); // http://servername:port|\\domain\username const segments: Array = cred.targetName.split(WindowsCredentialStoreApi.separator); const username: string = segments[segments.length - 1]; const service: string = segments[0]; return new Credential(service, username, password); } private createTargetName(service: string, username: string) : string { return service + WindowsCredentialStoreApi.separator + username; } private listCredentials() : Q.Promise> { const deferred: Q.Deferred> = Q.defer>(); const credentials: Array = []; const stream = wincredstore.list(); stream.on("data", (cred) => { credentials.push(cred); }); stream.on("end", () => { deferred.resolve(credentials); }); stream.on("error", (error) => { console.log(error); deferred.reject(error); }); return deferred.promise; } } ================================================ FILE: src/credentialstore/win32/win-credstore-parser.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; // // Parser for the output of the creds.exe helper program. // var _ = require('underscore'); var es = require('event-stream'); var stream = require('readable-stream'); var util = require('util'); var Transform = stream.Transform; // // Regular expression to match the various fields in the input. // var fieldRe = /^([^:]+):\s(.*)$/; // // Convert space separated pascal caps ("Target Type") // to camel case no spaces ("targetType"). Used to Convert // field names to property names. // function fieldNameToPropertyName(fieldName) { var parts = fieldName.split(' '); parts[0] = parts[0].toLowerCase(); return parts.join(''); } // // Simple streaming parser. It's in one of two states: // 0 - Waiting for an entry // 1 - in an entry // // At the ending blank line (each entry has one) we output // the accumulated object. // function WinCredStoreParsingStream() { Transform.call(this, { objectMode: true }); this.currentEntry = null; } util.inherits(WinCredStoreParsingStream, Transform); _.extend(WinCredStoreParsingStream.prototype, { _transform: function (chunk, encoding, callback) { var match; var line = chunk.toString(); var count = 0; while (line !== null) { ++count; if (count > 2) { return callback(new Error(util.format('Multiple passes attempting to parse line [%s]. Possible bug in parser and infinite loop', line))); } if (this.currentEntry === null) { if (line !== '') { this.currentEntry = {}; // Loop back around to process this line. continue; } // Skip blank lines between items. line = null; } if (this.currentEntry) { if (line !== '') { match = fieldRe.exec(line); var key = fieldNameToPropertyName(match[1]); var value = match[2]; this.currentEntry[key] = value; line = null; } else { // Blank line ends an entry this.push(this.currentEntry); this.currentEntry = null; line = null; } } } callback(); }, _flush: function (callback) { if (this.currentEntry) { this.push(this.currentEntry); this.currentEntry = null; } callback(); } }); function createParsingStream() { return es.pipeline(es.split(), new WinCredStoreParsingStream()); } createParsingStream.ParsingStream = WinCredStoreParsingStream; module.exports = createParsingStream; ================================================ FILE: src/credentialstore/win32/win-credstore.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; // // Wrapper module around Windows credential store. // Uses the creds.exe program. // var _ = require('underscore'); var childProcess = require('child_process'); var es = require('event-stream'); var path = require('path'); var parser = require('./win-credstore-parser'); var credExePath = path.join(__dirname, '../bin/win32/creds.exe'); var targetNamePrefix = ''; // Allow callers to set their own prefix function setPrefix(prefix) { targetNamePrefix = prefix; } function ensurePrefix(targetName) { if (targetName.slice(targetNamePrefix.length) !== targetNamePrefix) { targetName = targetNamePrefix + targetName; } return targetName; } function removePrefix(targetName) { return targetName.slice(targetNamePrefix.length); } /** * list the contents of the credential store, parsing each value. * * We ignore everything that wasn't put there by us, we look * for target names starting with the target name prefix. * * * @return {Stream} object mode stream of credentials. */ function list() { var credsProcess = childProcess.spawn(credExePath,['-s', '-g', '-t', targetNamePrefix + '*']); return credsProcess.stdout .pipe(parser()) .pipe(es.mapSync(function (cred) { cred.targetName = removePrefix(cred.targetName); return cred; })); } /** * Get details for a specific credential. Assumes generic credential. * * @param {string} targetName target name for credential * @param {function (err, credential)} callback callback function that receives * returned credential. */ function get(targetName, callback) { var args = [ '-s', '-t', ensurePrefix(targetName) ]; var credsProcess = childProcess.spawn(credExePath, args); var result = null; var errors = []; credsProcess.stdout.pipe(parser()) .on('data', function (credential) { result = credential; result.targetName = removePrefix(result.targetName); }); credsProcess.stderr.pipe(es.split()) .on('data', function (line) { errors.push(line); }); credsProcess.on('exit', function (code) { if (code === 0) { callback(null, result); } else { callback(new Error('Getting credential failed, exit code ' + code + ': ' + errors.join(', '))); } }); } /** * Set the credential for a given key in the credential store. * Creates or updates, assumes generic credential. * If credential is buffer, stores buffer contents as binary directly. * If credential is string, stores UTF-8 encoded binary. * * @param {String} targetName target name for entry * @param {Buffer|String} credential the credential * @param {Function(err)} callback completion callback */ function set(targetName, credential, callback) { if (_.isString(credential)) { credential = new Buffer(credential, 'utf8'); } var args = [ '-a', '-t', ensurePrefix(targetName), '-p', credential.toString('hex') ]; childProcess.execFile(credExePath, args, function (err) { callback(err); }); } /** * Remove the given key from the credential store. * * @param {string} targetName target name to remove. * if ends with "*" character, * will delete all targets * starting with that prefix * @param {Function(err)} callback completion callback */ function remove(targetName, callback) { var args = [ '-d', '-t', ensurePrefix(targetName) ]; if (targetName.slice(-1) === '*') { args.push('-g'); } childProcess.execFile(credExePath, args, function (err) { callback(err); }); } _.extend(exports, { list: list, set: set, get: get, remove: remove, setPrefix: setPrefix }); ================================================ FILE: src/extension.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { commands, ExtensionContext } from "vscode"; import { CommandNames, TfvcCommandNames } from "./helpers/constants"; import { ExtensionManager } from "./extensionmanager"; import { AutoResolveType } from "./tfvc/interfaces"; let _extensionManager: ExtensionManager; export async function activate(context: ExtensionContext) { // Construct the extension manager that handles Team and Tfvc commands _extensionManager = new ExtensionManager(); await _extensionManager.Initialize(); // Register the ext manager for disposal context.subscriptions.push(_extensionManager); context.subscriptions.push(commands.registerCommand(CommandNames.AssociateWorkItems, () => _extensionManager.RunCommand(() => _extensionManager.Team.AssociateWorkItems()))); context.subscriptions.push(commands.registerCommand(CommandNames.GetPullRequests, () => _extensionManager.RunCommand(() => _extensionManager.Team.GetMyPullRequests()))); context.subscriptions.push(commands.registerCommand(CommandNames.Signin, () => _extensionManager.RunCommand(() => _extensionManager.Team.Signin()))); context.subscriptions.push(commands.registerCommand(CommandNames.Signout, () => _extensionManager.RunCommand(() => _extensionManager.Team.Signout()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenBlamePage, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenBlamePage()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenBuildSummaryPage, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenBuildSummaryPage()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenFileHistory, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenFileHistory()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenNewBug, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenNewBug()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenNewPullRequest, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenNewPullRequest()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenNewTask, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenNewTask()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenNewWorkItem, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenNewWorkItem()))); context.subscriptions.push(commands.registerCommand(CommandNames.OpenTeamSite, () => _extensionManager.RunCommand(() => _extensionManager.Team.OpenTeamProjectWebSite()))); context.subscriptions.push(commands.registerCommand(CommandNames.ViewWorkItems, () => _extensionManager.RunCommand(() => _extensionManager.Team.ViewMyWorkItems()))); context.subscriptions.push(commands.registerCommand(CommandNames.ViewPinnedQueryWorkItems, () => _extensionManager.RunCommand(() => _extensionManager.Team.ViewPinnedQueryWorkItems()))); context.subscriptions.push(commands.registerCommand(CommandNames.ViewWorkItemQueries, () => _extensionManager.RunCommand(() => _extensionManager.Team.ViewWorkItems()))); context.subscriptions.push(commands.registerCommand(CommandNames.SendFeedback, () => _extensionManager.RunCommand(() => _extensionManager.SendFeedback()))); context.subscriptions.push(commands.registerCommand(CommandNames.RefreshPollingStatus, () => _extensionManager.RunCommand(() => _extensionManager.Team.RefreshPollingStatus()))); context.subscriptions.push(commands.registerCommand(CommandNames.Reinitialize, () => _extensionManager.Reinitialize())); // TFVC Commands context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Delete, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Delete(args ? args[0] : undefined)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.UndoAll, () => _extensionManager.RunCommand(() => _extensionManager.Tfvc.UndoAll()))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Undo, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Undo(args)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Exclude, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Exclude(args)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Include, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Include(args)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Rename, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Rename(args ? args[0] : undefined)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Open, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Open(args ? args[0] : undefined)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.OpenDiff, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.OpenDiff(args ? args[0] : undefined)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.OpenFile, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.OpenFile(args ? args[0] : undefined)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.ResolveKeepYours, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Resolve(args ? args[0] : undefined, AutoResolveType.KeepYours)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.ResolveTakeTheirs, (...args) => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Resolve(args ? args[0] : undefined, AutoResolveType.TakeTheirs)))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Refresh, () => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Refresh()))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.ShowOutput, () => _extensionManager.RunCommand(() => _extensionManager.Tfvc.ShowOutput()))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Checkin, () => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Checkin()))); context.subscriptions.push(commands.registerCommand(TfvcCommandNames.Sync, () => _extensionManager.RunCommand(() => _extensionManager.Tfvc.Sync()))); } ================================================ FILE: src/extensionmanager.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Disposable, FileSystemWatcher, StatusBarAlignment, StatusBarItem, version, window, workspace } from "vscode"; import { Settings } from "./helpers/settings"; import { CommandNames, Constants, TelemetryEvents, TfvcTelemetryEvents } from "./helpers/constants"; import { CredentialManager } from "./helpers/credentialmanager"; import { Logger } from "./helpers/logger"; import { Strings } from "./helpers/strings"; import { UserAgentProvider } from "./helpers/useragentprovider"; import { Utils } from "./helpers/utils"; import { VsCodeUtils } from "./helpers/vscodeutils"; import { IButtonMessageItem } from "./helpers/vscodeutils.interfaces"; import { RepositoryContextFactory } from "./contexts/repocontextfactory"; import { IRepositoryContext, RepositoryType } from "./contexts/repositorycontext"; import { TeamServerContext } from "./contexts/servercontext"; import { TfvcContext } from "./contexts/tfvccontext"; import { Telemetry } from "./services/telemetry"; import { TeamServicesApi } from "./clients/teamservicesclient"; import { FeedbackClient } from "./clients/feedbackclient"; import { RepositoryInfoClient } from "./clients/repositoryinfoclient"; import { UserInfo } from "./info/userinfo"; import { CredentialInfo } from "./info/credentialinfo"; import { TeamExtension } from "./team-extension"; import { TfCommandLineRunner } from "./tfvc/tfcommandlinerunner"; import { TfvcExtension } from "./tfvc/tfvc-extension"; import { TfvcErrorCodes } from "./tfvc/tfvcerror"; import { TfvcSCMProvider } from "./tfvc/tfvcscmprovider"; import { TfvcRepository } from "./tfvc/tfvcrepository"; import * as path from "path"; import * as util from "util"; export class ExtensionManager implements Disposable { private _teamServicesStatusBarItem: StatusBarItem; private _feedbackStatusBarItem: StatusBarItem; private _errorMessage: string; private _feedbackClient: FeedbackClient; private _serverContext: TeamServerContext; private _repoContext: IRepositoryContext; private _settings: Settings; private _credentialManager : CredentialManager; private _teamExtension: TeamExtension; private _tfvcExtension: TfvcExtension; private _scmProvider: TfvcSCMProvider; public async Initialize(): Promise { await this.setupFileSystemWatcherOnConfig(); await this.initializeExtension(false /*reinitializing*/); // Add the event listener for settings changes, then re-initialized the extension if (workspace) { workspace.onDidChangeConfiguration(() => { Logger.LogDebug("Reinitializing due to onDidChangeConfiguration"); //FUTURE: Check to see if we really need to do the re-initialization this.Reinitialize(); }); } } public get RepoContext(): IRepositoryContext { return this._repoContext; } public get ServerContext(): TeamServerContext { return this._serverContext; } public get CredentialManager(): CredentialManager { return this._credentialManager; } public get FeedbackClient(): FeedbackClient { return this._feedbackClient; } public get Settings(): Settings { return this._settings; } public get Team(): TeamExtension { return this._teamExtension; } public get Tfvc(): TfvcExtension { return this._tfvcExtension; } //Meant to reinitialize the extension when coming back online public Reinitialize(): void { this.cleanup(true); this.initializeExtension(true /*reinitializing*/); } public SendFeedback(): void { //SendFeedback doesn't need to ensure the extension is initialized FeedbackClient.SendFeedback(); } //Ensure we have a TFS or Team Services-based repository. Otherwise, return false. private ensureMinimalInitialization(): boolean { if (!this._repoContext || !this._serverContext || !this._serverContext.RepoInfo.IsTeamFoundation) { //If the user previously signed out (in this session of VS Code), show a message to that effect if (this._teamExtension.IsSignedOut) { this.setErrorStatus(Strings.UserMustSignIn, CommandNames.Signin); } else { this.setErrorStatus(Strings.NoRepoInformation); } return false; } return true; } //Checks to ensure we're good to go for running TFVC commands public EnsureInitializedForTFVC(): boolean { return this.ensureMinimalInitialization(); } //Checks to ensure that Team Services functionality is ready to go. public EnsureInitialized(expectedType: RepositoryType): boolean { //Ensure we have a TFS or Team Services-based repository. Otherwise, return false. if (!this.ensureMinimalInitialization()) { return false; } //If we aren't the expected type and we also aren't ANY, determine which error to show. //If we aren't ANY, then this If will handle Git and TFVC. So if we get past the first //if, we're returning false either for Git or for TFVC (there's no other option) if (expectedType !== this._repoContext.Type && expectedType !== RepositoryType.ANY) { //If we already have an error message set, we're in an error state and use that message if (this._errorMessage) { return false; } //Display the message straightaway in this case (instead of using status bar) if (expectedType === RepositoryType.GIT) { VsCodeUtils.ShowErrorMessage(Strings.NotAGitRepository); return false; } if (expectedType === RepositoryType.TFVC) { VsCodeUtils.ShowErrorMessage(Strings.NotATfvcRepository); return false; } } //For TFVC, without a TeamProjectName, we can't initialize the Team Services functionality if ((expectedType === RepositoryType.TFVC || expectedType === RepositoryType.ANY) && this._repoContext.Type === RepositoryType.TFVC && !this._repoContext.TeamProjectName) { this.setErrorStatus(Strings.NoTeamProjectFound); return false; } //Finally, if we set a global error message, there's an issue so we can't initialize. if (this._errorMessage !== undefined) { return false; } return true; } //Return value indicates whether a message was displayed public DisplayErrorMessage(message?: string): boolean { const msg: string = message ? message : this._errorMessage; if (msg) { VsCodeUtils.ShowErrorMessage(msg); return true; } return false; } public DisplayWarningMessage(message: string): void { VsCodeUtils.ShowWarningMessage(message); } //Logs an error to the logger and sends an exception to telemetry service public ReportError(err: Error, message: string, showToUser: boolean = false): void { const fullMessage = err ? message + " " + err : message; // Log the message Logger.LogError(fullMessage); if (err && err.message) { // Log additional information for debugging purposes Logger.LogDebug(err.message); } // Show just the message to the user if needed if (showToUser) { this.DisplayErrorMessage(message); } // Send it to telemetry if (err !== undefined && (Utils.IsUnauthorized(err) || Utils.IsOffline(err) || Utils.IsProxyIssue(err))) { //Don't log exceptions for Unauthorized, Offline or Proxy scenarios return; } Telemetry.SendException(err); } //Ensures a folder is open before attempting to run any command already shown in //the Command Palette (and defined in package.json). public RunCommand(funcToTry: (args) => void, ...args: string[]): void { if (!workspace || !workspace.rootPath) { this.DisplayErrorMessage(Strings.FolderNotOpened); return; } funcToTry(args); } private displayNoCredentialsMessage(): void { let error: string = Strings.NoTeamServerCredentialsRunSignin; let displayError: string = Strings.NoTeamServerCredentialsRunSignin; const messageItems: IButtonMessageItem[] = []; if (this._serverContext.RepoInfo.IsTeamServices === true) { messageItems.push({ title : Strings.LearnMore, url : Constants.TokenLearnMoreUrl, telemetryId: TelemetryEvents.TokenLearnMoreClick }); messageItems.push({ title : Strings.ShowMe, url : Constants.TokenShowMeUrl, telemetryId: TelemetryEvents.TokenShowMeClick }); //Need different messages for popup message and status bar //Add the account name to the message to help the user error = util.format(Strings.NoAccessTokenRunSignin, this._serverContext.RepoInfo.Account); displayError = util.format(Strings.NoAccessTokenLearnMoreRunSignin, this._serverContext.RepoInfo.Account); } Logger.LogError(error); this.setErrorStatus(error, CommandNames.Signin); VsCodeUtils.ShowErrorMessage(displayError, ...messageItems); } private formatErrorLogMessage(err): string { let logMsg: string = err.message; if (err.stderr) { //Add stderr to logged message if we have it logMsg = Utils.FormatMessage(`${logMsg} ${err.stderr}`); } return logMsg; } private async initializeExtension(reinitializing: boolean): Promise { //Set version of VSCode on the UserAgentProvider UserAgentProvider.VSCodeVersion = version; //Users could install without having a folder (workspace) open this._settings = new Settings(); //We need settings before showing the Welcome message Telemetry.Initialize(this._settings); //Need to initialize telemetry for showing welcome message if (!reinitializing) { await this.showFarewellMessage(); await this.showWelcomeMessage(); //Ensure we show the message before hooking workspace.onDidChangeConfiguration } //Don't initialize if we don't have a workspace if (!workspace || !workspace.rootPath) { return; } // Create the extensions this._teamExtension = new TeamExtension(this); this._tfvcExtension = new TfvcExtension(this); //If Logging is enabled, the user must have used the extension before so we can enable //it here. This will allow us to log errors when we begin processing TFVC commands. Telemetry.SendEvent(TelemetryEvents.Installed); //Send event that the extension is installed (even if not used) this.logStart(this._settings.LoggingLevel, workspace.rootPath); this._teamServicesStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 100); this._feedbackStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 96); try { //RepositoryContext has some initial information about the repository (what we can get without authenticating with server) this._repoContext = await RepositoryContextFactory.CreateRepositoryContext(workspace.rootPath, this._settings); if (this._repoContext) { this.showFeedbackItem(); this.setupFileSystemWatcherOnHead(); this._serverContext = new TeamServerContext(this._repoContext.RemoteUrl); //Now that we have a server context, we can update it on the repository context RepositoryContextFactory.UpdateRepositoryContext(this._repoContext, this._serverContext); this._feedbackClient = new FeedbackClient(); this._credentialManager = new CredentialManager(); this._credentialManager.GetCredentials(this._serverContext).then(async (creds: CredentialInfo) => { if (!creds || !creds.CredentialHandler) { this.displayNoCredentialsMessage(); return; } else { this._serverContext.CredentialInfo = creds; Telemetry.Initialize(this._settings, this._serverContext); //Re-initialize the telemetry with the server context information Logger.LogDebug("Started ApplicationInsights telemetry"); //Set up the client we need to talk to the server for more repository information const repositoryInfoClient: RepositoryInfoClient = new RepositoryInfoClient(this._repoContext, CredentialManager.GetCredentialHandler()); Logger.LogInfo("Getting repository information with repositoryInfoClient"); Logger.LogDebug("RemoteUrl = " + this._repoContext.RemoteUrl); try { //At this point, we have either successfully called git.exe or tf.cmd (we just need to verify the remote urls) //For Git repositories, we call vsts/info and get collection ids, etc. //For TFVC, we have to (potentially) make multiple other calls to get collection ids, etc. this._serverContext.RepoInfo = await repositoryInfoClient.GetRepositoryInfo(); //Now we need to go and get the authorized user information const connectionUrl: string = (this._serverContext.RepoInfo.IsTeamServices === true ? this._serverContext.RepoInfo.AccountUrl : this._serverContext.RepoInfo.CollectionUrl); const accountClient: TeamServicesApi = new TeamServicesApi(connectionUrl, [CredentialManager.GetCredentialHandler()]); Logger.LogInfo("Getting connectionData with accountClient"); Logger.LogDebug("connectionUrl = " + connectionUrl); try { const settings: any = await accountClient.connect(); Logger.LogInfo("Retrieved connectionData with accountClient"); this.resetErrorStatus(); this._serverContext.UserInfo = new UserInfo(settings.authenticatedUser.id, settings.authenticatedUser.providerDisplayName, settings.authenticatedUser.customDisplayName); this.initializeStatusBars(); await this.initializeClients(this._repoContext.Type); this.sendStartupTelemetry(); Logger.LogInfo(`Sent extension start up telemetry`); Logger.LogObject(settings); this.logDebugInformation(); } catch (err) { this.setErrorStatus(Utils.GetMessageForStatusCode(err, err.message), (err.statusCode === 401 ? CommandNames.Signin : undefined)); //Wrap err here to get a useful call stack this.ReportError(new Error(err), Utils.GetMessageForStatusCode(err, err.message, "Failed to get results with accountClient: ")); } } catch (err) { //TODO: With TFVC, creating a RepositoryInfo can throw (can't get project collection, can't get team project, etc.) // We get a 404 on-prem if we aren't TFS 2015 Update 2 or later and 'core id' error with TFS 2013 RTM (and likely later) if (this._serverContext.RepoInfo.IsTeamFoundationServer === true && (err.statusCode === 404 || (err.message && err.message.indexOf("Failed to find api location for area: core id:") === 0))) { this.setErrorStatus(Strings.UnsupportedServerVersion); Logger.LogError(Strings.UnsupportedServerVersion); Telemetry.SendEvent(TelemetryEvents.UnsupportedServerVersion); } else { this.setErrorStatus(Utils.GetMessageForStatusCode(err, err.message), (err.statusCode === 401 ? CommandNames.Signin : undefined)); //Wrap err here to get a useful call stack this.ReportError(new Error(err), Utils.GetMessageForStatusCode(err, err.message, "Failed call with repositoryClient: ")); } } } // Now that everything else is ready, create the SCM provider try { if (this._repoContext.Type === RepositoryType.TFVC) { const tfvcContext: TfvcContext = this._repoContext; this.sendTfvcConfiguredTelemetry(tfvcContext.TfvcRepository); Logger.LogInfo(`Sent TFVC tooling telemetry`); if (!this._scmProvider) { Logger.LogDebug(`Initializing the TfvcSCMProvider`); this._scmProvider = new TfvcSCMProvider(this); await this._scmProvider.Initialize(); Logger.LogDebug(`Initialized the TfvcSCMProvider`); } else { Logger.LogDebug(`Re-initializing the TfvcSCMProvider`); await this._scmProvider.Reinitialize(); Logger.LogDebug(`Re-initialized the TfvcSCMProvider`); } this.sendTfvcConnectedTelemetry(tfvcContext.TfvcRepository); } } catch (err) { Logger.LogError(`Caught an exception during Tfvc SCM Provider initialization`); const logMsg: string = this.formatErrorLogMessage(err); Logger.LogError(logMsg); if (err.tfvcErrorCode) { this.setErrorStatus(err.message); //Dispose of the Build and WIT status bar items so they don't show up (they should be re-created once a new folder is opened) this._teamExtension.cleanup(); if (this.shouldDisplayTfvcError(err.tfvcErrorCode)) { VsCodeUtils.ShowErrorMessage(err.message, ...err.messageOptions); } } } }).fail((err) => { this.setErrorStatus(Utils.GetMessageForStatusCode(err, err.message), (err.statusCode === 401 ? CommandNames.Signin : undefined)); //If we can't get a requestHandler, report the error via the feedbackclient const message: string = Utils.GetMessageForStatusCode(err, err.message, "Failed to get a credential handler"); Logger.LogError(message); Telemetry.SendException(err); }); } } catch (err) { const logMsg: string = this.formatErrorLogMessage(err); Logger.LogError(logMsg); //For now, don't report these errors via the FeedbackClient (TFVC errors could result from TfvcContext creation failing) if (!err.tfvcErrorCode || this.shouldDisplayTfvcError(err.tfvcErrorCode)) { this.setErrorStatus(err.message); VsCodeUtils.ShowErrorMessage(err.message, ...err.messageOptions); } } } //Sends the "StartUp" event based on repository type private sendStartupTelemetry(): void { let event: string = TelemetryEvents.StartUp; if (this._repoContext.Type === RepositoryType.TFVC) { event = TfvcTelemetryEvents.StartUp; } else if (this._repoContext.Type === RepositoryType.EXTERNAL) { event = TelemetryEvents.ExternalRepository; } Telemetry.SendEvent(event); } //Sends telemetry based on values of the TfvcRepository (which TF tooling (Exe or CLC) is configured) private sendTfvcConfiguredTelemetry(repository: TfvcRepository): void { let event: string = TfvcTelemetryEvents.ExeConfigured; if (!repository.IsExe) { event = TfvcTelemetryEvents.ClcConfigured; } Telemetry.SendEvent(event); //For now, this is simply an indication that users have configured that feature if (repository.RestrictWorkspace) { Telemetry.SendEvent(TfvcTelemetryEvents.RestrictWorkspace); } } //Sends telemetry based on values of the TfvcRepository (which TF tooling (Exe or CLC) was connected) private sendTfvcConnectedTelemetry(repository: TfvcRepository): void { let event: string = TfvcTelemetryEvents.ExeConnected; if (!repository.IsExe) { event = TfvcTelemetryEvents.ClcConnected; } Telemetry.SendEvent(event); } //Determines which Tfvc errors to display in the status bar ui private shouldDisplayTfvcError(errorCode: string): boolean { if (TfvcErrorCodes.MinVersionWarning === errorCode || TfvcErrorCodes.NotFound === errorCode || TfvcErrorCodes.NotAuthorizedToAccess === errorCode || TfvcErrorCodes.NotAnEnuTfCommandLine === errorCode || TfvcErrorCodes.WorkspaceNotKnownToClc === errorCode) { return true; } return false; } //Ensure this is async (and is awaited on) so that the extension doesn't continue until user deals with message private async showWelcomeMessage(): Promise { if (this._settings.ShowWelcomeMessage) { const welcomeMessage: string = `This is version ${Constants.ExtensionVersion} of the Azure Repos extension.`; const messageItems: IButtonMessageItem[] = []; messageItems.push({ title : Strings.LearnMore, url : Constants.ReadmeLearnMoreUrl, telemetryId : TelemetryEvents.WelcomeLearnMoreClick }); messageItems.push({ title : Strings.SetupTfvcSupport, url : Constants.TfvcLearnMoreUrl, telemetryId : TfvcTelemetryEvents.SetupTfvcSupportClick }); messageItems.push({ title : Strings.DontShowAgain }); const chosenItem: IButtonMessageItem = await VsCodeUtils.ShowInfoMessage(welcomeMessage, ...messageItems); if (chosenItem && chosenItem.title === Strings.DontShowAgain) { this._settings.ShowWelcomeMessage = false; } } } private async showFarewellMessage(): Promise { if (this._settings.ShowFarewellMessage) { const farewellMessage: string = `The Azure Repos extension has been sunsetted.`; const messageItems: IButtonMessageItem[] = []; messageItems.push({ title : Strings.LearnMore, url : Constants.FarewellLearnMoreUrl, telemetryId : TelemetryEvents.FarewellLearnMoreClick }); messageItems.push({ title : Strings.DontShowAgain }); const chosenItem: IButtonMessageItem = await VsCodeUtils.ShowInfoMessage(farewellMessage, ...messageItems); if (chosenItem && chosenItem.title === Strings.DontShowAgain) { this._settings.ShowFarewellMessage = false; } } } //Set up the initial status bars private initializeStatusBars(): void { if (this.ensureMinimalInitialization()) { this._teamServicesStatusBarItem.command = CommandNames.OpenTeamSite; this._teamServicesStatusBarItem.text = this._serverContext.RepoInfo.TeamProject ? this._serverContext.RepoInfo.TeamProject : ""; this._teamServicesStatusBarItem.tooltip = Strings.NavigateToTeamServicesWebSite; this._teamServicesStatusBarItem.show(); if (this.EnsureInitialized(RepositoryType.ANY)) { // Update the extensions this._teamExtension.InitializeStatusBars(); //this._tfvcExtension.InitializeStatusBars(); } } } //Set up the initial status bars private async initializeClients(repoType: RepositoryType): Promise { await this._teamExtension.InitializeClients(repoType); await this._tfvcExtension.InitializeClients(repoType); } private logDebugInformation(): void { Logger.LogDebug("Account: " + this._serverContext.RepoInfo.Account + " " + "Team Project: " + this._serverContext.RepoInfo.TeamProject + " " + "Collection: " + this._serverContext.RepoInfo.CollectionName + " " + "Repository: " + this._serverContext.RepoInfo.RepositoryName + " " + "UserCustomDisplayName: " + this._serverContext.UserInfo.CustomDisplayName + " " + "UserProviderDisplayName: " + this._serverContext.UserInfo.ProviderDisplayName + " " + "UserId: " + this._serverContext.UserInfo.Id + " "); Logger.LogDebug("repositoryFolder: " + this._repoContext.RepoFolder); Logger.LogDebug("repositoryRemoteUrl: " + this._repoContext.RemoteUrl); if (this._repoContext.Type === RepositoryType.GIT) { Logger.LogDebug("gitRepositoryParentFolder: " + this._repoContext.RepositoryParentFolder); Logger.LogDebug("gitCurrentBranch: " + this._repoContext.CurrentBranch); Logger.LogDebug("gitCurrentRef: " + this._repoContext.CurrentRef); } Logger.LogDebug("IsSsh: " + this._repoContext.IsSsh); Logger.LogDebug("proxy: " + (Utils.IsProxyEnabled() ? "enabled" : "not enabled") + ", azure devops services: " + this._serverContext.RepoInfo.IsTeamServices.toString()); } private logStart(loggingLevel: string, rootPath: string): void { if (loggingLevel === undefined) { return; } Logger.SetLoggingLevel(loggingLevel); if (rootPath !== undefined) { Logger.LogPath = rootPath; Logger.LogInfo(`*** FOLDER: ${rootPath} ***`); Logger.LogInfo(`${UserAgentProvider.UserAgent}`); } else { Logger.LogInfo(`*** Folder not opened ***`); } } private resetErrorStatus(): void { this._errorMessage = undefined; } private setErrorStatus(message: string, commandOnClick?: string): void { this._errorMessage = message; if (this._teamServicesStatusBarItem !== undefined) { //TODO: Should the default command be to display the message? this._teamServicesStatusBarItem.command = commandOnClick; // undefined clears the command this._teamServicesStatusBarItem.text = `Team $(stop)`; this._teamServicesStatusBarItem.tooltip = message; this._teamServicesStatusBarItem.show(); } } //Sets up a file system watcher on HEAD so we can know when the current branch has changed private async setupFileSystemWatcherOnHead(): Promise { if (this._repoContext && this._repoContext.Type === RepositoryType.GIT) { const pattern: string = this._repoContext.RepoFolder + "/HEAD"; const fsw:FileSystemWatcher = workspace.createFileSystemWatcher(pattern, true, false, true); fsw.onDidChange(async (/*uri*/) => { Logger.LogInfo("HEAD has changed, re-parsing RepoContext object"); this._repoContext = await RepositoryContextFactory.CreateRepositoryContext(workspace.rootPath, this._settings); Logger.LogInfo("CurrentBranch is: " + this._repoContext.CurrentBranch); this.notifyBranchChanged(/*this._repoContext.CurrentBranch*/); }); } } private notifyBranchChanged(/*TODO: currentBranch: string*/): void { this._teamExtension.NotifyBranchChanged(); //this._tfvcExtension.NotifyBranchChanged(currentBranch); } //Sets up a file system watcher on config so we can know when the remote origin has changed private async setupFileSystemWatcherOnConfig(): Promise { //If we don't have a workspace, don't set up the file watcher if (!workspace || !workspace.rootPath) { return; } if (this._repoContext && this._repoContext.Type === RepositoryType.GIT) { const pattern: string = path.join(workspace.rootPath, ".git", "config"); //We want to listen to file creation, change and delete events const fsw:FileSystemWatcher = workspace.createFileSystemWatcher(pattern, false, false, false); fsw.onDidCreate((/*uri*/) => { //When a new local repo is initialized (e.g., git init), re-initialize the extension Logger.LogInfo("config has been created, re-initializing the extension"); this.Reinitialize(); }); fsw.onDidChange(async (uri) => { Logger.LogInfo("config has changed, checking if 'remote origin' changed"); const context: IRepositoryContext = await RepositoryContextFactory.CreateRepositoryContext(uri.fsPath, this._settings); const remote: string = context.RemoteUrl; if (remote === undefined) { //There is either no remote defined yet or it isn't a Team Services repo if (this._repoContext.RemoteUrl !== undefined) { //We previously had a Team Services repo and now we don't, reinitialize Logger.LogInfo("remote was removed, previously had an Azure Repos remote, re-initializing the extension"); this.Reinitialize(); return; } //There was no previous remote, so do nothing Logger.LogInfo("remote does not exist, no previous Azure Repos remote, nothing to do"); } else if (this._repoContext !== undefined) { //We have a valid gitContext already, check to see what changed if (this._repoContext.RemoteUrl !== undefined) { //The config has changed, and we had a Team Services remote already if (remote.toLowerCase() !== this._repoContext.RemoteUrl.toLowerCase()) { //And they're different, reinitialize Logger.LogInfo("remote changed to a different Azure Repos remote, re-initializing the extension"); this.Reinitialize(); } } else { //The remote was initialized to a Team Services remote, reinitialize Logger.LogInfo("remote initialized to an Azure Repos remote, re-initializing the extension"); this.Reinitialize(); } } }); fsw.onDidDelete((/*uri*/) => { Logger.LogInfo("config has been deleted, re-initializing the extension"); this.Reinitialize(); }); } } private showFeedbackItem(): void { this._feedbackStatusBarItem.command = CommandNames.SendFeedback; this._feedbackStatusBarItem.text = `$(megaphone)`; this._feedbackStatusBarItem.tooltip = Strings.SendFeedback; this._feedbackStatusBarItem.show(); } private cleanup(preserveTeamExtension: boolean = false): void { if (this._teamServicesStatusBarItem) { this._teamServicesStatusBarItem.dispose(); this._teamServicesStatusBarItem = undefined; } if (this._feedbackStatusBarItem !== undefined) { this._feedbackStatusBarItem.dispose(); this._feedbackStatusBarItem = undefined; } //No matter if we're signing out or re-initializing, we need the team extension's //status bars and timers to be disposed but not the entire object this._teamExtension.cleanup(); //If we are signing out, we need to keep some of the objects around if (!preserveTeamExtension && this._teamExtension) { this._teamExtension.dispose(); this._teamExtension = undefined; this._serverContext = undefined; this._credentialManager = undefined; if (this._tfvcExtension) { this._tfvcExtension.dispose(); this._tfvcExtension = undefined; } if (this._scmProvider) { this._scmProvider.dispose(); this._scmProvider = undefined; } //Make sure we clean up any running instances of TF TfCommandLineRunner.DisposeStatics(); } //The following will be reset during a re-initialization this._repoContext = undefined; this._settings = undefined; this._errorMessage = undefined; } public dispose() { this.cleanup(); } //If we're signing out, we don't want to dispose of everything. public SignOut(): void { this.cleanup(true); } } ================================================ FILE: src/helpers/constants.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; /* tslint:disable:variable-name */ export class Constants { static ExtensionName: string = "team"; static ExtensionUserAgentName: string = "AzureReposVSCode"; static ExtensionVersion: string = "1.161.1"; static OAuth: string = "OAuth"; static TokenLearnMoreUrl: string = "https://aka.ms/gtgzt4"; static TokenShowMeUrl: string = "https://aka.ms/o2wkmo"; static ReadmeLearnMoreUrl: string = "https://aka.ms/jkapah"; static FarewellLearnMoreUrl: string = "https://aka.ms/AA9k2vv"; static TfvcLearnMoreUrl: string = "https://github.com/Microsoft/azure-repos-vscode/blob/master/TFVC_README.md#quick-start"; static ServerWorkspaceUrl: string = "https://github.com/Microsoft/azure-repos-vscode/blob/master/TFVC_README.md#what-is-the-difference-between-a-local-and-server-workspace-how-can-i-tell-which-one-im-working-with"; static VS2015U3CSRUrl: string = "https://msdn.microsoft.com/en-us/library/mt752379.aspx"; static WorkspaceNotDetectedByClcUrl: string = "https://github.com/Microsoft/azure-repos-vscode/blob/master/TFVC_README.md#using-the-tee-clc-i-am-unable-to-access-an-existing-local-workspace-what-can-i-do"; static NonEnuTfExeConfiguredUrl: string = "https://github.com/Microsoft/azure-repos-vscode/blob/master/TFVC_README.md#i-received-the-it-appears-you-have-configured-a-non-english-version-of-the-tf-executable-please-ensure-an-english-version-is-properly-configured-error-message-after-configuring-tfexe-how-can-i-get-the-extension-to-work-properly"; } export class CommandNames { static CommandPrefix: string = Constants.ExtensionName + "."; static AssociateWorkItems: string = CommandNames.CommandPrefix + "AssociateWorkItems"; static GetPullRequests: string = CommandNames.CommandPrefix + "GetPullRequests"; static OpenBlamePage: string = CommandNames.CommandPrefix + "OpenBlamePage"; static OpenBuildSummaryPage: string = CommandNames.CommandPrefix + "OpenBuildSummaryPage"; static OpenFileHistory: string = CommandNames.CommandPrefix + "OpenFileHistory"; static OpenNewBug: string = CommandNames.CommandPrefix + "OpenNewBug"; static OpenNewTask: string = CommandNames.CommandPrefix + "OpenNewTask"; static OpenNewPullRequest: string = CommandNames.CommandPrefix + "OpenNewPullRequest"; static OpenNewWorkItem: string = CommandNames.CommandPrefix + "OpenNewWorkItem"; static OpenTeamSite: string = CommandNames.CommandPrefix + "OpenTeamSite"; static RefreshPollingStatus: string = CommandNames.CommandPrefix + "RefreshPollingStatus"; static Reinitialize: string = CommandNames.CommandPrefix + "Reinitialize"; static SendFeedback: string = CommandNames.CommandPrefix + "SendFeedback"; static Signin: string = CommandNames.CommandPrefix + "Signin"; static Signout: string = CommandNames.CommandPrefix + "Signout"; static ViewWorkItemQueries: string = CommandNames.CommandPrefix + "ViewWorkItemQueries"; static ViewWorkItems: string = CommandNames.CommandPrefix + "ViewWorkItems"; static ViewPinnedQueryWorkItems: string = CommandNames.CommandPrefix + "ViewPinnedQueryWorkItems"; } export class DeviceFlowConstants { static ManualOption: string = "manual"; static DeviceFlowOption: string = "deviceflow"; static ClientId: string = "97877f11-0fc6-4aee-b1ff-febb0519dd00"; static RedirectUri: string = "https://java.visualstudio.com"; } export class TfvcCommandNames { static CommandPrefix: string = "tfvc."; static Checkin: string = TfvcCommandNames.CommandPrefix + "Checkin"; static Delete: string = TfvcCommandNames.CommandPrefix + "Delete"; static Exclude: string = TfvcCommandNames.CommandPrefix + "Exclude"; static ExcludeAll: string = TfvcCommandNames.CommandPrefix + "ExcludeAll"; static Include: string = TfvcCommandNames.CommandPrefix + "Include"; static IncludeAll: string = TfvcCommandNames.CommandPrefix + "IncludeAll"; static Open: string = TfvcCommandNames.CommandPrefix + "Open"; static OpenDiff: string = TfvcCommandNames.CommandPrefix + "OpenDiff"; static OpenFile: string = TfvcCommandNames.CommandPrefix + "OpenFile"; static Refresh: string = TfvcCommandNames.CommandPrefix + "Refresh"; static Rename: string = TfvcCommandNames.CommandPrefix + "Rename"; static ResolveKeepYours: string = TfvcCommandNames.CommandPrefix + "ResolveKeepYours"; static ResolveTakeTheirs: string = TfvcCommandNames.CommandPrefix + "ResolveTakeTheirs"; static ShowOutput: string = TfvcCommandNames.CommandPrefix + "ShowOutput"; static Sync: string = TfvcCommandNames.CommandPrefix + "Sync"; static Undo: string = TfvcCommandNames.CommandPrefix + "Undo"; static UndoAll: string = TfvcCommandNames.CommandPrefix + "UndoAll"; } export class SettingNames { static SettingsPrefix: string = Constants.ExtensionName + "."; static PinnedQueries: string = SettingNames.SettingsPrefix + "pinnedQueries"; static AccessTokens: string = SettingNames.SettingsPrefix + "accessTokens"; static LoggingPrefix: string = SettingNames.SettingsPrefix + "logging."; static LoggingLevel: string = SettingNames.LoggingPrefix + "level"; static PollingInterval: string = SettingNames.SettingsPrefix + "pollingInterval"; static AppInsights: string = SettingNames.SettingsPrefix + "appInsights."; static AppInsightsEnabled: string = SettingNames.AppInsights + "enabled"; static AppInsightsKey: string = SettingNames.AppInsights + "key"; static RemoteUrl: string = SettingNames.SettingsPrefix + "remoteUrl"; static TeamProject: string = SettingNames.SettingsPrefix + "teamProject"; static BuildDefinitionId: string = SettingNames.SettingsPrefix + "buildDefinitionId"; static ShowWelcomeMessage: string = SettingNames.SettingsPrefix + "showWelcomeMessage"; static ShowFarewellMessage: string = SettingNames.SettingsPrefix + "showFarewellMessage"; } export class TelemetryEvents { static TelemetryPrefix: string = Constants.ExtensionName + "/"; static AssociateWorkItems: string = TelemetryEvents.TelemetryPrefix + "associateworkitems"; static DeviceFlowCanceled: string = TelemetryEvents.TelemetryPrefix + "deviceflowcanceled"; static DeviceFlowFailed: string = TelemetryEvents.TelemetryPrefix + "deviceflowfailed"; static DeviceFlowPat: string = TelemetryEvents.TelemetryPrefix + "deviceflowpat"; static ExternalRepository: string = TelemetryEvents.TelemetryPrefix + "externalrepo"; static FarewellLearnMoreClick: string = TelemetryEvents.TelemetryPrefix + "farewelllearnmoreclick"; static Installed: string = TelemetryEvents.TelemetryPrefix + "installed"; static ManualPat: string = TelemetryEvents.TelemetryPrefix + "manualpat"; static OpenAdditionalQueryResults: string = TelemetryEvents.TelemetryPrefix + "openaddlqueryresults"; static OpenBlamePage: string = TelemetryEvents.TelemetryPrefix + "openblame"; static OpenBuildSummaryPage: string = TelemetryEvents.TelemetryPrefix + "openbuildsummary"; static OpenFileHistory: string = TelemetryEvents.TelemetryPrefix + "openfilehistory"; static OpenNewTask: string = TelemetryEvents.TelemetryPrefix + "opennewtask"; static OpenNewBug: string = TelemetryEvents.TelemetryPrefix + "opennewbug"; static OpenNewPullRequest: string = TelemetryEvents.TelemetryPrefix + "opennewpullrequest"; static OpenNewWorkItem: string = TelemetryEvents.TelemetryPrefix + "opennewworkitem"; static OpenRepositoryHistory: string = TelemetryEvents.TelemetryPrefix + "openrepohistory"; static OpenTeamSite: string = TelemetryEvents.TelemetryPrefix + "openteamprojectweb"; static ReadmeLearnMoreClick: string = TelemetryEvents.TelemetryPrefix + "readmelearnmoreclick"; static SendAFrown: string = TelemetryEvents.TelemetryPrefix + "sendafrown"; static SendASmile: string = TelemetryEvents.TelemetryPrefix + "sendasmile"; static ShowMyWorkItemQueries: string = TelemetryEvents.TelemetryPrefix + "showmyworkitemqueries"; static StartUp: string = TelemetryEvents.TelemetryPrefix + "startup"; static TokenLearnMoreClick: string = TelemetryEvents.TelemetryPrefix + "tokenlearnmoreclick"; static TokenShowMeClick: string = TelemetryEvents.TelemetryPrefix + "tokenshowmeclick"; static UnsupportedServerVersion: string = TelemetryEvents.TelemetryPrefix + "unsupportedversion"; static UnsupportedWitServerVersion: string = TelemetryEvents.TelemetryPrefix + "unsupportedwitversion"; static ViewPullRequest: string = TelemetryEvents.TelemetryPrefix + "viewpullrequest"; static ViewPullRequests: string = TelemetryEvents.TelemetryPrefix + "viewpullrequests"; static ViewMyWorkItems: string = TelemetryEvents.TelemetryPrefix + "viewmyworkitems"; static ViewPinnedQueryWorkItems: string = TelemetryEvents.TelemetryPrefix + "viewpinnedqueryworkitems"; static ViewWorkItem: string = TelemetryEvents.TelemetryPrefix + "viewworkitem"; static ViewWorkItems: string = TelemetryEvents.TelemetryPrefix + "viewworkitems"; static VS2015U3CSR: string = TelemetryEvents.TelemetryPrefix + "vs2015u3csr"; static WelcomeLearnMoreClick: string = TelemetryEvents.TelemetryPrefix + "welcomelearnmoreclick"; } //Don't export this class. TfvcTelemetryEvents is the only one which should be used when sending telemetry class TfvcBaseTelemetryEvents { static TelemetryPrefix: string = "tfvc/"; static Clc: string = TfvcBaseTelemetryEvents.TelemetryPrefix + "clc"; static Exe: string = TfvcBaseTelemetryEvents.TelemetryPrefix + "exe"; static Add: string = "add"; static Checkin: string = "checkin"; static Configured: string = "configured"; static Connected: string = "connected"; static Delete: string = "delete"; static GetFileContent: string = "getfilecontent"; static LearnMoreClick: string = "learnmoreclick"; static NameAndContentConflict: string = "nameandcontentconflict"; static NonEnuConfiguredMoreDetails: string = "nonenuconfiguredmoredetails"; static OpenFileHistory: string = "openfilehistory"; static OpenRepositoryHistory: string = "openrepohistory"; static RenameConflict: string = "renameconflict"; static Rename: string = "rename"; static ResolveConflicts: string = "resolveconflicts"; static RestrictWorkspace: string = "restrictworkspace"; static StartUp: string = "startup"; static Sync: string = "sync"; static Undo: string = "undo"; static UndoAll: string = "undoall"; static WorkspaceAccessError: string = "workspaceaccesserror"; } export class TfvcTelemetryEvents { static UsingClc: string = TfvcBaseTelemetryEvents.Clc; static UsingExe: string = TfvcBaseTelemetryEvents.Exe; static LearnMoreClick: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.LearnMoreClick; static NameAndContentConflict: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.NameAndContentConflict; static OpenFileHistory: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.OpenFileHistory; static OpenRepositoryHistory: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.OpenRepositoryHistory; static RenameConflict: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.RenameConflict; static RestrictWorkspace: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.RestrictWorkspace; static StartUp: string = TfvcBaseTelemetryEvents.TelemetryPrefix + TfvcBaseTelemetryEvents.StartUp; static SetupTfvcSupportClick: string = TfvcBaseTelemetryEvents.TelemetryPrefix + "setuptfvcsupportclick"; //Begin tooling-specific telemetry (tf.exe or CLC) static ClcConfigured: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Configured; static ExeConfigured: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Configured; static ClcConnected: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Connected; static ExeConnected: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Connected; static AddExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Add; static AddClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Add; static CheckinExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Checkin; static CheckinClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Checkin; static DeleteExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Delete; static DeleteClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Delete; static GetFileContentExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.GetFileContent; static GetFileContentClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.GetFileContent; static RenameExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Rename; static RenameClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Rename; static ResolveConflictsExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.ResolveConflicts; static ResolveConflictsClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.ResolveConflicts; static SyncExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Sync; static SyncClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Sync; static UndoExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.Undo; static UndoClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.Undo; static UndoAllExe: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.UndoAll; static UndoAllClc: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.UndoAll; static ClcCannotAccessWorkspace: string = TfvcTelemetryEvents.UsingClc + "-" + TfvcBaseTelemetryEvents.WorkspaceAccessError; static ExeNonEnuConfiguredMoreDetails: string = TfvcTelemetryEvents.UsingExe + "-" + TfvcBaseTelemetryEvents.NonEnuConfiguredMoreDetails; } export class WellKnownRepositoryTypes { static TfsGit: string = "TfsGit"; } export class WitQueries { static MyWorkItems: string = "select [System.Id], [System.WorkItemType], [System.Title], [System.State] " + "from WorkItems where [System.TeamProject] = @project and " + "[System.WorkItemType] <> '' and [System.AssignedTo] = @Me order by [System.ChangedDate] desc"; } export class WitTypes { static Bug: string = "Bug"; static Task: string = "Task"; } export enum MessageTypes { Error = 0, Warn = 1, Info = 2 } /* tslint:enable:variable-name */ ================================================ FILE: src/helpers/credentialmanager.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IRequestHandler } from "vso-node-api/interfaces/common/VsoBaseInterfaces"; import { CredentialInfo } from "../info/credentialinfo"; import { TeamServerContext } from "../contexts/servercontext"; import { CredentialStore } from "../credentialstore/credentialstore"; import { RepoUtils } from "./repoutils"; import * as Q from "q"; export class CredentialManager { private static _credentialHandler: IRequestHandler; private _credentialStore: CredentialStore; constructor() { // Specify the prefix for use on Windows and Mac. // On Linux, create a custom folder and file. this._credentialStore = new CredentialStore("team:", ".team", "team-secrets.json"); } public static GetCredentialHandler() : IRequestHandler { return CredentialManager._credentialHandler; } public GetCredentials(context: TeamServerContext) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this.getCredentials(context).then((credInfo: CredentialInfo) => { if (credInfo !== undefined) { CredentialManager._credentialHandler = credInfo.CredentialHandler; deferred.resolve(credInfo); } else { deferred.resolve(undefined); } }).catch((reason) => { deferred.reject(reason); }); return deferred.promise; } public RemoveCredentials(context:TeamServerContext) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this._credentialStore.RemoveCredential(CredentialManager.getKeyFromContext(context)).then(() => { deferred.resolve(undefined); }).catch((reason) => { deferred.reject(reason); }); return deferred.promise; } public StoreCredentials(context:TeamServerContext, username: string, password: string) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this._credentialStore.SetCredential(CredentialManager.getKeyFromContext(context), username, password).then(() => { deferred.resolve(undefined); }).catch((reason) => { deferred.reject(reason); }); return deferred.promise; } private getCredentials(context:TeamServerContext) : Q.Promise { const deferred: Q.Deferred = Q.defer(); this._credentialStore.GetCredential(CredentialManager.getKeyFromContext(context)).then((cred) => { if (cred !== undefined) { if (context.RepoInfo.IsTeamServices) { deferred.resolve(new CredentialInfo(cred.Password)); } else if (context.RepoInfo.IsTeamFoundationServer) { let domain: string; let user: string = cred.Username; const pair: string[] = user.split("\\"); if (pair.length > 1) { domain = pair[0]; user = pair[pair.length - 1]; } deferred.resolve(new CredentialInfo(user, cred.Password, domain, /*workstation*/ undefined)); } } else { deferred.resolve(undefined); } }).catch((reason) => { deferred.reject(reason); }); return deferred.promise; } private static getKeyFromContext(context:TeamServerContext): string { if (RepoUtils.IsTeamFoundationServicesAzureRepo(context.RepoInfo.AccountUrl)) { return context.RepoInfo.Host + "/" + context.RepoInfo.Account; } return context.RepoInfo.Host; } } ================================================ FILE: src/helpers/logger.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Constants } from "./constants"; import * as winston from "winston"; import * as path from "path"; export class Logger { private static initialized: boolean = false; private static loggingLevel: LoggingLevel; private static logPath: string = ""; private static initalize() { //Only initialize the logger if a logging level is set (in settings) and we haven't initialized it yet if (Logger.loggingLevel !== undefined && Logger.initialized === false) { const fileOpt:winston.FileTransportOptions = { json: false, filename: path.join(Logger.logPath, Constants.ExtensionName + "-extension.log"), level: LoggingLevel[Logger.loggingLevel].toLowerCase(), maxsize: 4000000, maxFiles: 5, tailable: false }; winston.add(winston.transports.File, fileOpt); winston.remove(winston.transports.Console); Logger.initialized = true; } } private static addPid(message: string): string { return " [" + Logger.addZero(process.pid, 10000) + "] " + message; } public static LogDebug(message: string) : void { Logger.initalize(); if (Logger.initialized === true && this.loggingLevel === LoggingLevel.Debug) { winston.log("debug", this.addPid(message)); console.log(Logger.getNow() + message); } } //Logs message to console and winston logger public static LogError(message: string) : void { Logger.initalize(); if (Logger.initialized === true && this.loggingLevel >= LoggingLevel.Error) { winston.log("error", this.addPid(message)); console.log(Logger.getNow() + "ERROR: " + message); } //When displaying messages, don't add timestamp or our severity level prefix } //Logs message only to console public static LogInfo(message: string) : void { Logger.initalize(); if (Logger.initialized === true && this.loggingLevel >= LoggingLevel.Info) { winston.log("info", " " + this.addPid(message)); //five-wide console.log(Logger.getNow() + message); } } public static LogObject(object: any) : void { Logger.initalize(); if (Logger.initialized === true && this.loggingLevel === LoggingLevel.Debug) { winston.log("debug", object); console.log(object); } } //Logs message to console and displays Warning message public static LogWarning(message: string) : void { Logger.initalize(); if (Logger.initialized === true && this.loggingLevel >= LoggingLevel.Warn) { winston.log("warn", " " + this.addPid(message)); //five-wide console.log(Logger.getNow() + "WARNING: " + message); } //When displaying messages, don't add timestamp or our severity level prefix } public static get LogPath(): string { return Logger.logPath; } public static set LogPath(path: string) { if (path !== undefined) { Logger.logPath = path; } } public static get LoggingLevel(): LoggingLevel { return Logger.loggingLevel; } public static SetLoggingLevel(level: string): void { if (level === undefined) { Logger.loggingLevel = undefined; return; } switch (level.toLowerCase()) { case "error": Logger.loggingLevel = LoggingLevel.Error; break; case "warn": Logger.loggingLevel = LoggingLevel.Warn; break; case "info": Logger.loggingLevel = LoggingLevel.Info; break; case "verbose": Logger.loggingLevel = LoggingLevel.Verbose; break; case "debug": Logger.loggingLevel = LoggingLevel.Debug; break; default: Logger.loggingLevel = undefined; break; } } //Returns string representation of now() public static get Now() : string { return Logger.getNow(); } private static getNow(): string { const now: Date = new Date(); const strDateTime: string = [[Logger.addZero(now.getHours()), Logger.addZero(now.getMinutes()), Logger.addZero(now.getSeconds())].join(":"), Logger.addZero(now.getMilliseconds(), 100)].join("."); return strDateTime + " "; } //Adds a preceding zero if num is less than base (or the default of 10) private static addZero(num: number, base?: number): string { let val: number = base; if (val === undefined) { val = 10; } return (num >= 0 && num < val) ? "0" + num.toString() : num.toString() + ""; } } export enum LoggingLevel { Error = 0, Warn = 1, Info = 2, Verbose = 3, Debug = 4 } ================================================ FILE: src/helpers/repoutils.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Logger } from "../helpers/logger"; import * as url from "url"; //This class is responsible for determining information about a string-based url (not a uri) //A Team Services Git repository is determined by: // "_git" AND ".visualstudio.com" / "azure.com" in the url //A TFS Git repository is determined by: // "_git" in the url and ".visualstudio.com" / "azure.com" NOT in the url //A Team Services TFVC repository is determined by: // "_git" NOT in the URL and ".visualstudio.com" / "azure.com" in the url //A Team Services azure repository is determined by: // "azure.com" in the hostname //A TFS TFVC repository is determined by: // "_git" NOT in the URL and ".visualstudio.com" / "azure.com" NOT in the url // //NOTE: the detection of a TFS TFVC repository is basically a "catch all". Any url could match //it and we'd think it's a TFVC repo. For that to be true, this class now assumes that it is //always detecting either a Team Services or TFS repository. It no longer tries to determine //that a url is NOT either a TFS or Team Services repository. So it is up to the caller to only //instantiate this class if we know we should have either a TFS or Team Services repository. export class RepoUtils { private static sshV3 = new RegExp("git@(?:vs-)?ssh\.(.+):v3\/(.+)\/(.+)\/(.+)"); //Checks a handful of heuristics to see if the url provided is a TFS or VSTS repo public static IsTeamFoundationGitRepo(url: string): boolean { if (!url) { return false; } const asLower = url.toLowerCase(); // TFS uses /_git/ in all repository paths const containsUnderGit = asLower.indexOf("/_git/") >= 0; if (containsUnderGit) { return true; } // VSTS uses /_ssh/ after visualstudio.com in all repository paths const underSSHIndex = asLower.indexOf("/_ssh/"); const visualstudioDotComIndex = asLower.indexOf(".visualstudio.com"); if (visualstudioDotComIndex >= 0 && underSSHIndex >= visualstudioDotComIndex) { return true; } // Check for v3 url format if (RepoUtils.IsTeamFoundationServicesV3SshRepo(url)) { return true; } return false; } //Checks to ensure it's a Team Foundation Git repo, then ensures it's hosted on visualstudio.com public static IsTeamFoundationServicesRepo(url: string): boolean { if ((url.toLowerCase().indexOf(".visualstudio.com") >= 0) || RepoUtils.IsTeamFoundationServicesAzureRepo(url)) { return true; } return false; } //Checks to ensure it's a Team Foundation Git repo, then ensures it's hosted on azure.com public static IsTeamFoundationServicesAzureRepo(respositoryUrl: string): boolean { try { // check for ssh based url that will not parse as a standard url if (RepoUtils.IsTeamFoundationServicesV3SshRepo(respositoryUrl)) { return true; } const purl: url.Url = url.parse(respositoryUrl); if (purl.hostname.toLowerCase().indexOf("azure.com") >= 0) { return true; } } catch (err) { Logger.LogDebug("Could not parse repository url: " + respositoryUrl); } return false; } public static IsTeamFoundationServicesV3SshRepo(respositoryUrl: string): boolean { return RepoUtils.sshV3.test(respositoryUrl.toLowerCase()); } public static ConvertSshV3ToUrl(respositoryUrl: string): string { const scheme = "https://"; const match = RepoUtils.sshV3.exec(respositoryUrl.toLowerCase()); if (match.length === 5) { if (match[1] === "visualstudio.com") { return scheme + match[2] + "." + match[1] + "/" + match[3] + "/_git/" + match[4]; } return scheme + match[1] + "/" + match[2] + "/" + match[3] + "/_git/" + match[4]; } Logger.LogDebug("Could not parse as v3 repository url: " + respositoryUrl); return undefined; } //This is the "catch all" method. A repository hosted on TFS is defined by not being on "visualstudio.com" public static IsTeamFoundationServerRepo(url: string): boolean { if (!RepoUtils.IsTeamFoundationServicesRepo(url)) { return true; } return false; } } ================================================ FILE: src/helpers/settings.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { workspace } from "vscode"; import { SettingNames, WitQueries } from "./constants"; import { Logger } from "../helpers/logger"; export abstract class BaseSettings { protected readSetting(name: string, defaultValue:T): T { const configuration = workspace.getConfiguration(); const value = configuration.get(name, undefined); // If user specified a value, use it if (value !== undefined) { return value; } return defaultValue; } protected writeSetting(name: string, value: any, global?: boolean): void { const configuration = workspace.getConfiguration(); configuration.update(name, value, global); } } export interface IPinnedQuery { queryText?: string; queryPath?: string; account: string; } export class PinnedQuerySettings extends BaseSettings { private _pinnedQuery: IPinnedQuery; private _account: string; constructor(account: string) { super(); this._account = account; this._pinnedQuery = this.getPinnedQuery(account); } private getPinnedQuery(account: string) : IPinnedQuery { const pinnedQueries = this.readSetting(SettingNames.PinnedQueries, undefined); if (pinnedQueries !== undefined) { Logger.LogDebug("Found pinned queries in user configuration settings."); let global: IPinnedQuery = undefined; for (let index: number = 0; index < pinnedQueries.length; index++) { const element = pinnedQueries[index]; if (element.account === account || element.account === account + ".visualstudio.com") { return element; } else if (element.account === "global") { global = element; } } if (global !== undefined) { Logger.LogDebug("No account-specific pinned query found, using global pinned query."); return global; } } Logger.LogDebug("No account-specific pinned query or global pinned query found. Using default."); return undefined; } public get PinnedQuery() : IPinnedQuery { return this._pinnedQuery || { account: this._account, queryText: WitQueries.MyWorkItems }; } } export interface ISettings { AppInsightsEnabled: boolean; AppInsightsKey: string; LoggingLevel: string; PollingInterval: number; RemoteUrl: string; TeamProject: string; BuildDefinitionId: number; ShowWelcomeMessage: boolean; ShowFarewellMessage: boolean; } export class Settings extends BaseSettings implements ISettings { private _appInsightsEnabled: boolean; private _appInsightsKey: string; private _loggingLevel: string; private _pollingInterval: number; private _remoteUrl: string; private _teamProject: string; private _buildDefinitionId: number; private _showWelcomeMessage: boolean; private _showFarewellMessage: boolean; constructor() { super(); const loggingLevel = SettingNames.LoggingLevel; this._loggingLevel = this.readSetting(loggingLevel, undefined); const pollingInterval = SettingNames.PollingInterval; this._pollingInterval = this.readSetting(pollingInterval, 10); Logger.LogDebug("Polling interval value (minutes): " + this._pollingInterval.toString()); // Ensure a minimum value when an invalid value is set if (this._pollingInterval < 10) { Logger.LogDebug("Polling interval must be greater than 10 minutes."); this._pollingInterval = 10; } this._appInsightsEnabled = this.readSetting(SettingNames.AppInsightsEnabled, true); this._appInsightsKey = this.readSetting(SettingNames.AppInsightsKey, undefined); this._remoteUrl = this.readSetting(SettingNames.RemoteUrl, undefined); this._teamProject = this.readSetting(SettingNames.TeamProject, undefined); this._buildDefinitionId = this.readSetting(SettingNames.BuildDefinitionId, 0); this._showWelcomeMessage = this.readSetting(SettingNames.ShowWelcomeMessage, true); this._showFarewellMessage = this.readSetting(SettingNames.ShowFarewellMessage, true); } public get AppInsightsEnabled(): boolean { return this._appInsightsEnabled; } public get AppInsightsKey(): string { return this._appInsightsKey; } public get LoggingLevel(): string { return this._loggingLevel; } public get PollingInterval(): number { return this._pollingInterval; } public get RemoteUrl(): string { return this._remoteUrl; } public get TeamProject(): string { return this._teamProject; } public get BuildDefinitionId(): number { return this._buildDefinitionId; } public get ShowWelcomeMessage(): boolean { return this._showWelcomeMessage; } public set ShowWelcomeMessage(value: boolean) { this.writeSetting(SettingNames.ShowWelcomeMessage, value, true /*global*/); } public get ShowFarewellMessage(): boolean { return this._showFarewellMessage; } public set ShowFarewellMessage(value: boolean) { this.writeSetting(SettingNames.ShowFarewellMessage, value, true /*global*/); } } ================================================ FILE: src/helpers/strings.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; /* tslint:disable:variable-name */ export class Strings { static ViewYourPinnedQuery: string = "View your pinned work item query results."; static BrowseYourPullRequests: string = "Browse your pull requests."; static BrowseAdditionalWorkItems: string = "Browse additional work items..."; static BrowseAdditionalWorkItemsDescription: string = "Choose this item to see all query results in your web browser"; static FolderNotOpened: string = "You must open a repository folder in order to use the Azure Repos extension."; static NavigateToBuildSummary: string = "Click to view build"; static NavigateToTeamServicesWebSite: string = "Click to view your team project website."; static NoAccessTokenFound: string = "A personal access token for this repository hosted on Azure DevOps Services was not found in your local user settings."; static NoAccessTokenLearnMoreRunSignin: string = "You are not connected to Azure DevOps Services (%s). Select 'Learn more...' and then run the 'team signin' command."; static NoAccessTokenRunSignin: string = "You are not connected to Azure DevOps Services (%s). Please run the 'team signin' command."; static NoTeamServerCredentialsRunSignin: string = "You are not connected to a Team Foundation Server. Please run the 'team signin' command."; static NoBuildsFound: string = "No builds were found for this repository and branch. Click to view your team project's build definitions page."; static NoTfvcBuildsFound: string = "No builds were found for this repository. Click to view your team project's build definitions page."; static NoRepoInformation: string = "No Azure DevOps Services or Team Foundation Server repository configuration was found. Ensure you've opened a folder that contains a repository."; static NoSourceFileForBlame: string = "A source file must be opened to show blame information."; static UserMustSignIn: string = "You are signed out. Please run the 'team signin' command."; static DeviceFlowAuthenticatingToTeamServices: string = "Authenticating to Azure DevOps Services (%s)..."; static DeviceFlowCopyCode: string = "Copy this code and then press Enter to start the authentication process"; static DeviceFlowManualPrompt: string = "Provide an access token manually (current experience)"; static DeviceFlowPrompt: string = "Authenticate and get an access token automatically (new experience)"; static DeviceFlowPlaceholder: string = "Choose your method of authenticating to Azure DevOps Services..."; static ErrorRequestingToken: string = "An error occurred requesting a personal access token for %s."; static SendAFrown: string = "Send a Frown"; static SendASmile: string = "Send a Smile"; static SendFeedback: string = "Send us feedback about the Azure Repos extension!"; static SendFeedbackPrompt: string = "Enter your feedback here (1000 char limit)"; static NoFeedbackSent: string = "No feedback was sent."; static ThanksForFeedback: string = "Thanks for sending feedback!"; static LearnMore: string = "Learn More..."; static LearnMoreAboutTfvc: string = "TFVC Support..."; static MoreDetails: string = "More Details..."; static SetupTfvcSupport: string = "Set Up TFVC Support..."; static ShowMe: string = "Show Me!"; static VS2015Update3CSR: string = "Get Latest VS 2015 Update"; static DontShowAgain: string = "Don't Show Again"; static ChoosePullRequest: string = "Choose a pull request"; static ChooseWorkItem: string = "Choose a work item"; static ChooseWorkItemQuery: string = "Choose a work item query"; static ChooseWorkItemType: string = "Choose a work item type"; static ClickToRetryConnection: string = "Click to retry."; static ProvideAccessToken: string = "Provide the personal access token for your organization"; static ProvidePassword: string = "Provide the password for username"; static ProvideUsername: string = "Provide the username for server"; static UnsupportedWitServerVersion: string = "Work Item Tracking (WIT) functionality is disabled. WIT functionality requires TFS version 2015 Update 2 or later."; static UnsupportedServerVersion: string = "The Azure Repos extension only supports TFS version 2015 Update 2 or later. Please verify your TFS server version."; static UnableToRemoveCredentials: string = "Unable to remove credentials for this host. You may need to remove them manually. Host: "; static UnableToStoreCredentials: string = "Unable to store credentials for this host. Host: "; static UnableToValidateTeamServicesCollection: string = "Unable to validate the Azure DevOps Services collection."; static UnableToValidateCollectionAssumingDefaultCollection: string = "Unable to validate the collection assuming 'DefaultCollection'."; //Status codes static StatusCode401: string = "Unauthorized. Check your authentication credentials and try again."; static StatusCodeOffline: string = "It appears Visual Studio Code is offline. Please connect and try again."; static ProxyUnreachable: string = "It appears the configured proxy is not reachable. Please check your connection and try again."; // TFVC messages/errors static ChooseItemQuickPickPlaceHolder: string = "Choose a file to open it."; static NotAGitRepository: string = "The open folder is not a Git repository. Please check the folder location and try again."; static NotATfvcRepository: string = "The open folder is not a TFVC repository. Please check the folder location and try again."; static NotAnEnuTfCommandLine: string = "It appears you have configured a non-English version of the TF executable. Please ensure an English version is properly configured."; static TokenNotAllScopes: string = "The personal access token provided does not have All Scopes. All Scopes is required for TFVC support."; static TfvcLocationMissingError: string = "The path to the TFVC command line (including filename) was not found in the user settings. Please set this value (tfvc.location) and try again."; static TfMissingError: string = "Unable to find the TF executable. Please ensure TF is installed and the path specified contains the filename."; static TfInitializeFailureError: string = "Unable to initialize the TF executable. Please verify the installation of Java and ensure it is in the PATH."; static TfExecFailedError: string = "Execution of the TFVC command line failed unexpectedly."; static TfVersionWarning: string = "The configured version of TF does not meet the minimum version. You may run into errors or limitations with certain commands until you upgrade. Minimum version: "; static TfNoPendingChanges: string = "There are no matching pending changes."; static TfServerWorkspace: string = "It appears you are using a Server workspace. Currently, TFVC support is limited to Local workspaces."; static ClcCannotAccessWorkspace: string = "It appears you are using the TEE CLC and are unable to access an existing workspace. The TFVC SCM Provider cannot be initialized. Click 'More details...' to learn more."; static UndoChanges: string = "Undo Changes"; static DeleteFile: string = "Delete File"; static NoChangesToCheckin: string = "There are no changes to check in. Changes must be added to the 'Included' section to be checked in."; static NoChangesToUndo: string = "There are no changes to undo."; static AllFilesUpToDate: string = "All files are up to date."; static CommandRequiresFileContext: string = "This command requires a file context and can only be executed from the TFVC viewlet window."; static CommandRequiresExplorerContext: string = "This command requires a file context and can only be executed from the Explorer window."; static RenamePrompt: string = "Provide the new name for the file."; static NoMatchesFound: string = "No items match any of the file paths provided."; static NoTeamProjectFound: string = "No team project found for this repository. Build and Work Item functionality has been disabled."; static NoWorkspaceMappings: string = "Could not find a workspace with mappings (e.g., not a TFVC repository, wrong version of TF is being used)."; static ShowTfvcOutput: string = "Show TFVC Output"; // TFVC viewlet Strings static ExcludedGroupName: string = "Excluded changes"; static IncludedGroupName: string = "Included changes"; static ConflictsGroupName: string = "Conflicting changes"; // TFVC Sync Types static SyncTypeConflict: string = "Conflict"; static SyncTypeDeleted: string = "Deleted"; static SyncTypeError: string = "Error"; static SyncTypeNew: string = "New"; static SyncTypeUpdated: string = "Updated"; static SyncTypeWarning: string = "Warning"; // TFVC Conflict Titles static ConflictAlreadyDeleted: string = "ALREADY DELETED"; static ConflictAlreadyExists: string = "ALREADY EXISTS"; static ConflictDeletedLocally: string = "DELETED LOCALLY"; // TFVC AutoResolveType Strings static AutoResolveTypeAutoMerge: string = "Auto Merge"; static AutoResolveTypeDeleteConflict: string = "Delete Conflict"; static AutoResolveTypeKeepYours: string = "Keep Yours"; static AutoResolveTypeKeepYoursRenameTheirs: string = "Keep Yours Rename Theirs"; static AutoResolveTypeOverwriteLocal: string = "Overwrite Local"; static AutoResolveTypeTakeTheirs: string = "Take Theirs"; } /* tslint:enable:variable-name */ ================================================ FILE: src/helpers/urlbuilder.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; export class UrlBuilder { //Joins multiple paths with '/'. Not intended to use with query params or hashes public static Join(baseUrl: string, ...args: string[]): string { if (!baseUrl || !args || !args[0]) { return baseUrl; } let finalUrl: string = baseUrl; //If we're going to build up a url, pull off final '/', if present //If we don't have any args, we don't want to change the finalUrl we'll return if (args && args.length > 0 && finalUrl.endsWith("/")) { finalUrl = finalUrl.substring(0, finalUrl.length - 1); } for (let idx: number = 0; idx < args.length; idx++) { let arg: string = args[idx]; //Ensure each arg doesn't start with a '/', we'll be adding those if (arg.startsWith("/")) { arg = arg.substring(1, arg.length); } finalUrl = `${finalUrl}/${arg}`; } return finalUrl; } public static AddQueryParams(baseUrl: string, ...args: string[]) : string { if (!baseUrl || !args || !args[0]) { return baseUrl; } let finalUrl: string = baseUrl; //If we're going to build up a url, pull off final '/', if present //If we don't have any args, we don't want to change the finalUrl we'll return if (args && args.length > 0 && finalUrl.endsWith("/")) { finalUrl = finalUrl.substring(0, finalUrl.length - 1); } for (let idx: number = 0; idx < args.length; idx++) { const prefix: string = (idx === 0 ? "?" : "&"); let arg: string = args[idx]; if (arg.startsWith("?") || arg.startsWith("&")) { arg = arg.substring(1, arg.length); } finalUrl = `${finalUrl}${prefix}${arg}`; } return finalUrl; } public static AddHashes(baseUrl: string, ...args: string[]) : string { if (!baseUrl || !args || !args[0]) { return baseUrl; } let finalUrl: string = baseUrl; //If we're going to build up a url, pull off final '/', if present //If we don't have any args, we don't want to change the finalUrl we'll return if (args && args.length > 0 && finalUrl.endsWith("/")) { finalUrl = finalUrl.substring(0, finalUrl.length - 1); } for (let idx: number = 0; idx < args.length; idx++) { const prefix: string = (idx === 0 ? "#" : "&"); let arg: string = args[idx]; if (arg.startsWith("#") || arg.startsWith("&")) { arg = arg.substring(1, arg.length); } finalUrl = `${finalUrl}${prefix}${arg}`; } return finalUrl; } } ================================================ FILE: src/helpers/useragentprovider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Constants } from "../helpers/constants"; import * as os from "os"; export class UserAgentProvider { private static _vsCodeVersion: string = "0.0.0"; public static get UserAgent() : string { // Example: VSTSVSCode/1.115.1 (VSCode/10.1.0; Windows_NT/10.0.10586; Node/6.5.0) const userAgent: string = `${Constants.ExtensionUserAgentName}/${Constants.ExtensionVersion} (VSCode ${UserAgentProvider._vsCodeVersion}; ${os.type()} ${os.release()}; Node ${process.versions["node"]})`; return userAgent; } //Allow the VS Code version to be set (but only retrieved via UserAgent string) public static set VSCodeVersion(vsCodeVersion: string) { UserAgentProvider._vsCodeVersion = vsCodeVersion; } } ================================================ FILE: src/helpers/utils.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { BuildResult } from "vso-node-api/interfaces/BuildInterfaces"; import { Strings } from "./strings"; import * as vscode from "vscode"; import * as fs from "fs"; import * as path from "path"; import * as open from "open"; import * as opener from "opener"; export class Utils { public static FormatMessage(message: string): string { if (message) { //Replace newlines with spaces return message.replace(/\r\n/g, " ").replace(/\n/g, " ").trim(); } return message; } //gitDir provided for unit testing purposes public static FindGitFolder(startingPath: string, gitDir?: string): string { if (!fs.existsSync(startingPath)) { return undefined; } let gitPath: string; let lastPath: string; let currentPath: string = startingPath; do { gitPath = path.join(currentPath, gitDir || ".git"); if (fs.existsSync(gitPath)) { return gitPath; } lastPath = currentPath; currentPath = path.resolve(currentPath, ".."); } while (lastPath !== currentPath); return undefined; } //Returns the icon string to use for a particular BuildResult public static GetBuildResultIcon(result: BuildResult) : string { switch (result) { case BuildResult.Succeeded: return "check"; case BuildResult.Canceled: return "alert"; case BuildResult.Failed: return "stop"; case BuildResult.PartiallySucceeded: return "alert"; case BuildResult.None: return "question"; default: return "question"; } } //Returns a particular message for a particular reason. Otherwise, returns the optional prefix + message public static GetMessageForStatusCode(reason: any, message?: string, prefix?: string) : string { let msg: string = undefined; if (prefix === undefined) { msg = ""; } else { msg = prefix + " "; } let statusCode: string = "0"; if (reason.statusCode !== undefined) { statusCode = reason.statusCode.toString(); } else if (reason.code !== undefined) { statusCode = reason.code; } switch (statusCode) { case "401": msg = msg + Strings.StatusCode401; break; case "ENOENT": case "ENOTFOUND": case "EAI_AGAIN": msg = msg + Strings.StatusCodeOffline; break; case "ECONNRESET": case "ECONNREFUSED": if (this.IsProxyEnabled()) { msg = msg + Strings.ProxyUnreachable; break; } return message; default: return message; } return msg; } //Use some common error codes to indicate offline status public static IsProxyEnabled(): boolean { if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) { return true; } return false; } public static IsProxyIssue(reason: any): boolean { // If the proxy isn't enabled/set, it can't be a proxy issue if (!this.IsProxyEnabled()) { return false; } // If proxy is set, check for error codes if (reason !== undefined) { if (reason.code === "ECONNRESET" || reason.code === "ECONNREFUSED") { return true; } if (reason.statusCode === "ECONNRESET" || reason.statusCode === "ECONNREFUSED") { return true; } } return false; } //Use some common error codes to indicate offline status public static IsOffline(reason: any): boolean { if (reason !== undefined) { if (reason.code === "ENOENT" || reason.code === "ENOTFOUND" || reason.code === "EAI_AGAIN") { return true; } if (reason.statusCode === "ENOENT" || reason.statusCode === "ENOTFOUND" || reason.statusCode === "EAI_AGAIN") { return true; } } return false; } //Use some common error codes to indicate unauthorized status public static IsUnauthorized(reason: any): boolean { if (reason !== undefined) { if (reason.code === 401 || reason.statusCode === 401) { return true; } } return false; } //Use open for Windows and Mac, opener for Linux public static OpenUrl(url: string) : void { // Use the built in VS Code openExternal function if present. if ((vscode.env).openExternal) { (vscode.env).openExternal(vscode.Uri.parse(url)); return; } // Fallback to other node modules for old versions of VS Code switch (process.platform) { case "win32": case "darwin": open(url); break; default: opener(url); break; } } } ================================================ FILE: src/helpers/vscodeutils.interfaces.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; //The shape of this interface must always match vscodeutils.ButtonMessageItem since //we are trying to limit the proliferation of references to "vscode" via imports export interface IButtonMessageItem { title: string; url?: string; command?: string; telemetryId?: string; } ================================================ FILE: src/helpers/vscodeutils.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { commands, MessageItem, QuickPickItem, Range, window } from "vscode"; import { MessageTypes } from "./constants"; import { IButtonMessageItem } from "./vscodeutils.interfaces"; import { Utils } from "./utils"; import { Telemetry } from "../services/telemetry"; export class BaseQuickPickItem implements QuickPickItem { label: string; description: string; id: string; } export class WorkItemQueryQuickPickItem extends BaseQuickPickItem { wiql: string; } //Any changes to ButtonMessageItem must be reflected in IButtonMessageItem export class ButtonMessageItem implements MessageItem, IButtonMessageItem { title: string; url?: string; command?: string; telemetryId?: string; } export class VsCodeUtils { //Returns the trimmed value if there's an activeTextEditor and a selection public static GetActiveSelection(): string { const editor = window.activeTextEditor; if (!editor) { return undefined; } // Make sure that the selection is not empty and it is a single line const selection = editor.selection; if (selection.isEmpty || !selection.isSingleLine) { return undefined; } const range = new Range(selection.start.line, selection.start.character, selection.end.line, selection.end.character); const value = editor.document.getText(range).trim(); return value; } public static async ShowErrorMessage(message: string, ...urlMessageItem: IButtonMessageItem[]): Promise { return this.showMessage(message, MessageTypes.Error, ...urlMessageItem); } public static async ShowInfoMessage(message: string, ...urlMessageItem: IButtonMessageItem[]): Promise { return this.showMessage(message, MessageTypes.Info, ...urlMessageItem); } public static async ShowWarningMessage(message: string): Promise { return this.showMessage(message, MessageTypes.Warn); } //We have a single method to display either simple messages (with no options) or messages //that have multiple buttons that can run commands, open URLs, send telemetry, etc. private static async showMessage(message: string, type: MessageTypes, ...urlMessageItem: IButtonMessageItem[]): Promise { //The following "cast" allows us to pass our own type around (and not reference "vscode" via an import) const messageItems: ButtonMessageItem[] = urlMessageItem; const messageToDisplay: string = `${Utils.FormatMessage(message)}`; //Use the typescript spread operator to pass the rest parameter to showErrorMessage let chosenItem: IButtonMessageItem; switch (type) { case MessageTypes.Error: chosenItem = await window.showErrorMessage(messageToDisplay, ...messageItems); break; case MessageTypes.Info: chosenItem = await window.showInformationMessage(messageToDisplay, ...messageItems); break; case MessageTypes.Warn: chosenItem = await window.showWarningMessage(messageToDisplay, ...messageItems); break; default: break; } if (chosenItem) { if (chosenItem.url) { Utils.OpenUrl(chosenItem.url); } if (chosenItem.telemetryId) { Telemetry.SendEvent(chosenItem.telemetryId); } if (chosenItem.command) { commands.executeCommand(chosenItem.command); } } return chosenItem; } } ================================================ FILE: src/info/credentialinfo.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IRequestHandler } from "vso-node-api/interfaces/common/VsoBaseInterfaces"; import { ExtensionRequestHandler } from "./extensionrequesthandler"; export class CredentialInfo { private _credentialHandler: ExtensionRequestHandler; constructor(accessToken: string); constructor(username: string, password?: string); constructor(username: string, password?: string, domain?: string, workstation?: string); constructor(username: string, password?: string, domain?: string, workstation?: string) { if (username !== undefined && password !== undefined) { // NTLM (we don't support Basic auth) this._credentialHandler = new ExtensionRequestHandler(username, password, domain, workstation); } else { // Personal Access Token // Use username (really, accessToken) since it is first argument to constructor this._credentialHandler = new ExtensionRequestHandler(username); } } public get CredentialHandler() : IRequestHandler { return this._credentialHandler; } public set CredentialHandler(handler : IRequestHandler) { this._credentialHandler = handler; } public get Domain(): string { return this._credentialHandler.Domain; } public get Username(): string { return this._credentialHandler.Username; } public get Password(): string { return this._credentialHandler.Password; } public get Workstation(): string { return this._credentialHandler.Workstation; } } ================================================ FILE: src/info/extensionrequesthandler.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IHttpResponse, IRequestHandler } from "vso-node-api/interfaces/common/VsoBaseInterfaces"; import { getBasicHandler } from "vso-node-api/WebApi"; import { getNtlmHandler } from "vso-node-api/WebApi"; import { Constants } from "../helpers/constants"; import { UserAgentProvider } from "../helpers/useragentprovider"; // This class creates an IRequestHandler so we can send our own custom user-agent string export class ExtensionRequestHandler implements IRequestHandler { private _domain: string; private _username: string; private _password: string; private _workstation: string; private _credentialHandler: IRequestHandler; constructor(accessToken: string); constructor(username: string, password?: string, domain?: string, workstation?: string); constructor(username: string, password?: string, domain?: string, workstation?: string) { if (username !== undefined && password !== undefined) { // NTLM (we don't support Basic auth) this._username = username; this._password = password; this._domain = domain; this._workstation = workstation; this._credentialHandler = getNtlmHandler(this._username, this._password, this._domain, this._workstation); } else { // Personal Access Token this._username = Constants.OAuth; this._password = username; //use username since it is first argument to constructor this._credentialHandler = getBasicHandler(this._username, this._password); } } public get Domain(): string { return this._domain; } public get Username(): string { return this._username; } public get Password(): string { return this._password; } public get Workstation(): string { return this._workstation; } // Below are the IRequestHandler implementation/overrides public prepareRequest(options: any): void { this._credentialHandler.prepareRequest(options); // Get user agent string from the UserAgentProvider (Example: VSTSVSCode/1.115.1 (VSCode/10.1.0; Windows_NT/10.0.10586; Node/6.5.0)) const userAgent: string = UserAgentProvider.UserAgent; options.headers["User-Agent"] = userAgent; } public canHandleAuthentication(res: IHttpResponse) : boolean { return this._credentialHandler.canHandleAuthentication(res); } public handleAuthentication(httpClient: any, protocol: any, options: any, objs: any, finalCallback: any): void { return this._credentialHandler.handleAuthentication(httpClient, protocol, options, objs, finalCallback); } } ================================================ FILE: src/info/repositoryinfo.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Logger } from "../helpers/logger"; import { RepoUtils } from "../helpers/repoutils"; import { UrlBuilder } from "../helpers/urlbuilder"; import * as url from "url"; //When a RepositoryInfo object is created, we have already verified whether or not it //is either a Team Services or Team Foundation Server repository. With the introduction //of TFVC support, we cannot determine if it is TFVC based on url alone. Therfore, //we have to assume that are creating a RepositoryInfo for an existing TF repo. export class RepositoryInfo { private _host: string; private _hostName: string; private _path: string; private _pathName: string; private _port: string; private _protocol: string; private _query: string; private _account: string; private _collection: string; private _collectionId: string; private _teamProject: string; private _repositoryName: string; private _repositoryUrl: string; private _serverUrl: string; // Indicates whether the repository is Team Services private _isTeamServicesUrl: boolean = false; // Indicates whether the repository is an on-premises server private _isTeamFoundationServer: boolean = false; private _repositoryId: string; constructor(repositoryUrl: string); constructor(repositoryInfo: any); constructor (repositoryInfo: any) { if (!repositoryInfo) { throw new Error(`repositoryInfo is undefined`); } let repositoryUrl: string = undefined; if (typeof repositoryInfo === "object") { repositoryUrl = repositoryInfo.repository.remoteUrl; } else { repositoryUrl = repositoryInfo; } //Clean up repository URLs for repos that have "limited refs" enabled repositoryUrl = repositoryUrl.replace("/_git/_full/", "/_git/").replace("/_git/_optimized/", "/_git/"); const purl: url.Url = url.parse(repositoryUrl); if (purl) { this._host = purl.host; this._hostName = purl.hostname; this._path = purl.path; this._pathName = purl.pathname; this._port = purl.port; this._protocol = purl.protocol; this._query = purl.query; this._repositoryUrl = repositoryUrl; if (RepoUtils.IsTeamFoundationServicesRepo(repositoryUrl)) { if (RepoUtils.IsTeamFoundationServicesAzureRepo(this._repositoryUrl)) { const splitPath = this._path.split("/"); if (splitPath.length >= 1) { this._account = splitPath[1]; } else { throw new Error(`Could not parse account from ${this._path}`); } } else { const splitHost = this._host.split("."); this._account = splitHost[0]; } this._isTeamServicesUrl = true; Logger.LogDebug("_isTeamServicesUrl: true"); } else if (RepoUtils.IsTeamFoundationServerRepo(repositoryUrl)) { this._account = purl.host; this._isTeamFoundationServer = true; } if (typeof repositoryInfo === "object") { Logger.LogDebug("Parsing values from repositoryInfo object as any"); //The following properties are returned from the vsts/info api //If you add additional properties to the server context, they need to be set here this._collection = repositoryInfo.collection.name; Logger.LogDebug("_collection: " + this._collection); this._collectionId = repositoryInfo.collection.id; Logger.LogDebug("_collectionId: " + this._collectionId); this._repositoryId = repositoryInfo.repository.id; Logger.LogDebug("_repositoryId: " + this._repositoryId); this._repositoryName = repositoryInfo.repository.name; Logger.LogDebug("_repositoryName: " + this._repositoryName); this._teamProject = repositoryInfo.repository.project.name; Logger.LogDebug("_teamProject: " + this._teamProject); if (this._isTeamFoundationServer === true) { Logger.LogDebug("_isTeamFoundationServer: true"); //_serverUrl is only set for TeamFoundationServer repositories this._serverUrl = repositoryInfo.serverUrl; } } else { Logger.LogDebug("Parsing values from repositoryInfo as string url"); } } } public get Account(): string { return this._account; } public get AccountUrl(): string { if (this._isTeamServicesUrl) { if (RepoUtils.IsTeamFoundationServicesAzureRepo(this._repositoryUrl)) { return this._protocol + "//" + this._host + "/" + this._account; } return this._protocol + "//" + this._host; } else if (this._isTeamFoundationServer) { return this._serverUrl; } } public get CollectionId(): string { return this._collectionId; } public get CollectionName(): string { return this._collection; } public get CollectionUrl(): string { if (this._collection === undefined) { return undefined; } // While leaving the actual data alone, check for 'collection in the domain' // If an Azure repo the "DefaultCollection" should never be part of the URL. if (this._account.toLowerCase() !== this._collection.toLowerCase() && !RepoUtils.IsTeamFoundationServicesAzureRepo(this.RepositoryUrl)) { return UrlBuilder.Join(this.AccountUrl, this._collection); } else { return this.AccountUrl; } } public get Host(): string { return this._host; } public get IsTeamFoundation(): boolean { return this._isTeamServicesUrl || this._isTeamFoundationServer; } public get IsTeamFoundationServer(): boolean { return this._isTeamFoundationServer; } public get IsTeamServices(): boolean { return this._isTeamServicesUrl; } public get Protocol(): string { return this._protocol; } public get RepositoryId(): string { return this._repositoryId; } public get RepositoryName(): string { return this._repositoryName; } public get RepositoryUrl(): string { return this._repositoryUrl; } public get TeamProjectUrl(): string { if (this._teamProject === undefined) { return undefined; } return UrlBuilder.Join(this.CollectionUrl, this._teamProject); } public get TeamProject(): string { return this._teamProject; } } ================================================ FILE: src/info/userinfo.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; export class UserInfo { private _id: string; private _providerDisplayName: string; private _customDisplayName: string; constructor(id: string, providerDisplayName: string, customDisplayName: string) { this._id = id; this._providerDisplayName = providerDisplayName; this._customDisplayName = customDisplayName; } public get Id(): string { return this._id; } public get ProviderDisplayName(): string { return this._providerDisplayName; } public get CustomDisplayName(): string { return this._customDisplayName; } } ================================================ FILE: src/services/build.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Build, BuildBadge, BuildQueryOrder, BuildStatus, DefinitionReference, QueryDeletedOption } from "vso-node-api/interfaces/BuildInterfaces"; import { IBuildApi } from "vso-node-api/BuildApi"; import { WebApi } from "vso-node-api/WebApi"; import { TeamServerContext } from "../contexts/servercontext"; import { CredentialManager } from "../helpers/credentialmanager"; import { UrlBuilder } from "../helpers/urlbuilder"; export class BuildService { private _buildApi: IBuildApi; constructor(context: TeamServerContext) { this._buildApi = new WebApi(context.RepoInfo.CollectionUrl, CredentialManager.GetCredentialHandler()).getBuildApi(); } //Get the latest build id and badge of a build definition based on current project, repo and branch public async GetBuildBadge(project: string, repoType: string, repoId: string, branchName: string): Promise { return await this._buildApi.getBuildBadge(project, repoType, repoId, branchName); } //Get extra details of a build based on the build id public async GetBuildById(buildId: number): Promise { return await this._buildApi.getBuild(buildId); }; //Returns the build definitions (regardless of type) for the team project public async GetBuildDefinitions(teamProject: string): Promise { return await this._buildApi.getDefinitions(teamProject); } //Returns the most recent 100 completed builds public async GetBuilds(teamProject: string): Promise { /* tslint:disable:no-null-keyword */ return await this._buildApi.getBuilds(teamProject, null, null, null, null, null, null, null, BuildStatus.Completed, null, null, null, 100, null, 1, QueryDeletedOption.ExcludeDeleted, BuildQueryOrder.FinishTimeDescending); /* tslint:enable:no-null-keyword */ } //Returns the "latest" build for this definition public async GetBuildsByDefinitionId(teamProject: string, definitionId: number): Promise { /* tslint:disable:no-null-keyword */ return await this._buildApi.getBuilds(teamProject, [ definitionId ], null, null, null, null, null, null, null, null, null, null, 1, null, 1, QueryDeletedOption.ExcludeDeleted, BuildQueryOrder.FinishTimeDescending); /* tslint:enable:no-null-keyword */ } //Construct the url to the individual build definition (completed view) //https://account.visualstudio.com/DefaultCollection/project/_build#_a=completed&definitionId=34 public static GetBuildDefinitionUrl(remoteUrl: string, definitionId: string): string { return UrlBuilder.AddHashes(BuildService.GetBuildsUrl(remoteUrl), `_a=completed`, `definitionId=${definitionId}`); } //Construct the url to the individual build summary //https://account.visualstudio.com/DefaultCollection/project/_build/index?buildId=1977&_a=summary public static GetBuildSummaryUrl(remoteUrl: string, buildId: string): string { let summaryUrl: string = UrlBuilder.Join(BuildService.GetBuildsUrl(remoteUrl), "index"); summaryUrl = UrlBuilder.AddQueryParams(summaryUrl, `buildId=${buildId}`, `_a=summary`); return summaryUrl; } //Construct the url to the build definitions page for the project public static GetBuildDefinitionsUrl(remoteUrl: string): string { //The new definitions experience is behind a feature flag return BuildService.GetBuildsUrl(remoteUrl); // + "/definitions"; } //Construct the url to the builds page for the project public static GetBuildsUrl(remoteUrl: string): string { return UrlBuilder.Join(remoteUrl, "_build"); } } ================================================ FILE: src/services/coreapi.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamProject, TeamProjectCollection } from "vso-node-api/interfaces/CoreInterfaces"; import { WebApi } from "vso-node-api/WebApi"; import { ICoreApi } from "vso-node-api/CoreApi"; import { CredentialManager } from "../helpers/credentialmanager"; export class CoreApiService { private _coreApi: ICoreApi; constructor(remoteUrl: string) { this._coreApi = new WebApi(remoteUrl, CredentialManager.GetCredentialHandler()).getCoreApi(); } //Get the public async GetProjectCollection(collectionName: string): Promise { return await this._coreApi.getProjectCollection(collectionName); } //Get the public async GetTeamProject(projectName: string): Promise { return await this._coreApi.getProject(projectName, false, false); } } ================================================ FILE: src/services/gitvc.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { GitPullRequest, GitPullRequestSearchCriteria, GitRepository, PullRequestAsyncStatus, PullRequestStatus} from "vso-node-api/interfaces/GitInterfaces"; import { IGitApi } from "vso-node-api/GitApi"; import { WebApi } from "vso-node-api/WebApi"; import { TeamServerContext } from "../contexts/servercontext"; import { CredentialManager } from "../helpers/credentialmanager"; import { UrlBuilder } from "../helpers/urlbuilder"; export class GitVcService { private _gitApi: IGitApi; private static REVIEWER_VOTE_NO_RESPONSE: number = 0; private static REVIEWER_VOTE_APPROVED_WITH_SUGGESTIONS: number = 5; private static REVIEWER_VOTE_APPROVED: number = 10; private static REVIEWER_VOTE_WAITING_FOR_AUTHOR: number = -5; private static REVIEWER_VOTE_REJECTED: number = -10; constructor(context: TeamServerContext) { this._gitApi = new WebApi(context.RepoInfo.CollectionUrl, CredentialManager.GetCredentialHandler()).getGitApi(); } //Returns a Promise containing an array of GitPullRequest objectss for the creator and repository //If creatorId is undefined, all pull requests will be returned public async GetPullRequests(repositoryId: string, creatorId?: string, reviewerId?: string, status?: PullRequestStatus): Promise { const criteria: GitPullRequestSearchCriteria = { creatorId: creatorId, includeLinks: false, repositoryId: repositoryId, reviewerId: reviewerId, sourceRefName: undefined, status: status, targetRefName: undefined }; return await this._gitApi.getPullRequests(repositoryId, criteria); } //Returns a Promise containing an array of GitRepository objects for the project public async GetRepositories(project: string): Promise { return await this._gitApi.getRepositories(project, false); } //Construct the url to the file blame information //https://account.visualstudio.com/defaultcollection/project/_git/VSCode.Extension#path=%2FREADME.md&version=GBmaster&annotate=true public static GetFileBlameUrl(remoteUrl: string, currentFile: string, currentBranch: string): string { const file: string = encodeURIComponent(currentFile); const branch: string = encodeURIComponent(currentBranch); return UrlBuilder.AddHashes(remoteUrl, `path=${file}`, `version=GB${branch}`, `annotate=true`); } //Construct the url to the individual file history //https://account.visualstudio.com/defaultcollection/project/_git/VSCode.Extension#path=%2FREADME.md&version=GBmaster&_a=history public static GetFileHistoryUrl(remoteUrl: string, currentFile: string, currentBranch: string): string { const file: string = encodeURIComponent(currentFile); const branch: string = encodeURIComponent(currentBranch); return UrlBuilder.AddHashes(remoteUrl, `path=${file}`, `version=GB${branch}`, `_a=history`); } //Construct the url to the repository history (by branch) //https://account.visualstudio.com/project/_git/VSCode.Extension/history?itemVersion=GBmaster&_a=history public static GetRepositoryHistoryUrl(remoteUrl: string, currentBranch: string): string { const branch: string = encodeURIComponent(currentBranch); const repoHistoryUrl: string = UrlBuilder.Join(remoteUrl, "history"); return UrlBuilder.AddQueryParams(repoHistoryUrl, `itemVersion=GB${branch}`, `_a=history`); } //Today, simply craft a url to the create pull request web page //https://account.visualstudio.com/DefaultCollection/project/_git/VSCode.Health/pullrequests#_a=createnew&sourceRef=master public static GetCreatePullRequestUrl(remoteUrl: string, currentBranch: string): string { const branch: string = encodeURIComponent(currentBranch); return UrlBuilder.AddHashes(GitVcService.GetPullRequestsUrl(remoteUrl), `_a=createnew`, `sourceRef=${branch}`); } //Construct the url to the view pull request (discussion view) //https://account.visualstudio.com/DefaultCollection/VSOnline/project/_git/Java.VSCode/pullrequest/79184?view=discussion public static GetPullRequestDiscussionUrl(repositoryUrl: string, requestId: string): string { let discussionUrl: string = UrlBuilder.Join(repositoryUrl, "pullrequest", requestId); discussionUrl = UrlBuilder.AddQueryParams(discussionUrl, "view=discussion"); return discussionUrl; } //Construct the url to the main pull requests page //https://account.visualstudio.com/DefaultCollection/_git/project/pullrequests public static GetPullRequestsUrl(repositoryUrl: string): string { return UrlBuilder.Join(repositoryUrl, "pullrequests"); } //Returns the 'score' of the pull request so the client knows if the PR failed, //didn't receive any reponses, succeeded or is waiting for the author. public static GetPullRequestScore(pullRequest: GitPullRequest): PullRequestScore { const mergeStatus: PullRequestAsyncStatus = pullRequest.mergeStatus; if (mergeStatus === PullRequestAsyncStatus.Conflicts || mergeStatus === PullRequestAsyncStatus.Failure || mergeStatus === PullRequestAsyncStatus.RejectedByPolicy) { return PullRequestScore.Failed; } let lowestVote: number = 0; let highestVote: number = 0; if (pullRequest.reviewers !== undefined && pullRequest.reviewers.length > 0) { pullRequest.reviewers.forEach((reviewer) => { const vote: number = reviewer.vote; if (vote < lowestVote) { lowestVote = vote; } if (vote > highestVote) { highestVote = vote; } }); } let finalVote: number = GitVcService.REVIEWER_VOTE_NO_RESPONSE; if (lowestVote < GitVcService.REVIEWER_VOTE_NO_RESPONSE) { finalVote = lowestVote; } else if (highestVote > GitVcService.REVIEWER_VOTE_NO_RESPONSE) { finalVote = highestVote; } if (finalVote === GitVcService.REVIEWER_VOTE_APPROVED_WITH_SUGGESTIONS || finalVote === GitVcService.REVIEWER_VOTE_APPROVED) { return PullRequestScore.Succeeded; } if (finalVote === GitVcService.REVIEWER_VOTE_WAITING_FOR_AUTHOR) { return PullRequestScore.Waiting; } if (finalVote === GitVcService.REVIEWER_VOTE_REJECTED) { return PullRequestScore.Failed; } return PullRequestScore.NoResponse; } } export enum PullRequestScore { Failed = 0, NoResponse = 1, Succeeded = 2, Waiting = 3 } ================================================ FILE: src/services/telemetry.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Constants } from "../helpers/constants"; import { Settings } from "../helpers/settings"; import { TeamServerContext } from "../contexts/servercontext"; import appInsights = require("applicationinsights"); import uuid = require("uuid"); import * as os from "os"; import * as crypto from "crypto"; export class Telemetry { private static _appInsightsClient: Client; private static _serverContext: TeamServerContext; private static _telemetryEnabled: boolean = true; //Default to a new uuid in case the extension fails before being initialized private static _userId: string = "UNKNOWN"; private static _sessionId: string = uuid.v4(); //The sessionId can be updated later private static _productionKey: string = "44267cbb-b9ba-4bce-a37a-338588aa4da3"; //Initialize can be called multiple times. Initially, we have no information about the user but //still want to send telemetry. Once we have user information, we want to update the Telemetry //service with that more specific information. At the same time, we want global/static access //to the Telemetry service so we can send telemetry from just about anywhere at anytime. public static Initialize(settings: Settings, context?: TeamServerContext): void { Telemetry._serverContext = context; Telemetry._telemetryEnabled = settings.AppInsightsEnabled; // Always initialize Application Insights let insightsKey: string = Telemetry._productionKey; if (settings.AppInsightsKey !== undefined) { insightsKey = settings.AppInsightsKey; } appInsights.setup(insightsKey) .setAutoCollectConsole(false) .setAutoCollectPerformance(false) .setAutoCollectRequests(false) .setAutoCollectExceptions(false) .start(); Telemetry._appInsightsClient = appInsights.getClient(insightsKey); //Need to use HTTPS with v0.15.16 of App Insights Telemetry._appInsightsClient.config.endpointUrl = "https://dc.services.visualstudio.com/v2/track"; Telemetry.setUserId(); //Assign common properties to all telemetry sent from the default client Telemetry.setCommonProperties(); } public static SendEvent(event: string, properties?: any): void { Telemetry.ensureInitialized(); if (Telemetry._telemetryEnabled === true) { Telemetry._appInsightsClient.trackEvent(event, properties); } } public static SendFeedback(event: string, properties?: any): void { Telemetry.ensureInitialized(); // SendFeedback doesn't honor the _telemetryEnabled flag Telemetry._appInsightsClient.trackEvent(event, properties); } public static SendException(err: Error, properties?: any): void { Telemetry.ensureInitialized(); if (Telemetry._telemetryEnabled === true) { Telemetry._appInsightsClient.trackException(err, properties); } } //Make sure we're calling it after initializing private static ensureInitialized(): void { if (Telemetry._appInsightsClient === undefined) { throw new Error("Telemetry service was called before being initialized."); } } //Will generate a consistent ApplicationInsights userId private static setUserId(): void { let username: string = "UNKNOWN"; let hostname: string = "UNKNOWN"; if (os.userInfo().username) { username = os.userInfo().username; } if (os.hostname()) { hostname = os.hostname(); } const value: string = `${username}@${hostname}-VSTS`; Telemetry._userId = crypto.createHash("sha1").update(value).digest("hex"); } private static setCommonProperties(): void { Telemetry._appInsightsClient.commonProperties = { "VSTS.TeamFoundationServer.IsHostedServer" : Telemetry._serverContext === undefined ? "UNKNOWN" : Telemetry._serverContext.RepoInfo.IsTeamServices.toString(), "VSTS.TeamFoundationServer.ServerId" : Telemetry._serverContext === undefined ? "UNKNOWN" : Telemetry._serverContext.RepoInfo.Host, "VSTS.TeamFoundationServer.Protocol" : Telemetry._serverContext === undefined ? "UNKNOWN" : Telemetry._serverContext.RepoInfo.Protocol, "VSTS.Core.Machine.OS.Platform" : os.platform(), "VSTS.Core.Machine.OS.Type" : os.type(), "VSTS.Core.Machine.OS.Release" : os.release(), "VSTS.Core.User.Id" : Telemetry._userId, "Plugin.Version" : Constants.ExtensionVersion }; //Set the userid on the AI context so that we can get user counts in the telemetry const aiUserId: string = Telemetry._appInsightsClient.context.keys.userId; Telemetry._appInsightsClient.context.tags[aiUserId] = Telemetry._userId; const aiSessionId: string = Telemetry._appInsightsClient.context.keys.sessionId; Telemetry._appInsightsClient.context.tags[aiSessionId] = Telemetry._sessionId; } } ================================================ FILE: src/services/workitemtracking.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamContext } from "vso-node-api/interfaces/CoreInterfaces"; import { WebApi } from "vso-node-api/WebApi"; import { IWorkItemTrackingApi } from "vso-node-api/WorkItemTrackingApi"; import { QueryExpand, QueryHierarchyItem, QueryResultType, Wiql, WorkItem, WorkItemExpand, WorkItemQueryResult, WorkItemType, WorkItemTypeCategory, WorkItemTypeReference } from "vso-node-api/interfaces/WorkItemTrackingInterfaces"; import { TeamServerContext } from "../contexts/servercontext"; import { CredentialManager } from "../helpers/credentialmanager"; import { UrlBuilder } from "../helpers/urlbuilder"; export class WorkItemTrackingService { private _witApi: IWorkItemTrackingApi; constructor(context: TeamServerContext) { this._witApi = new WebApi(context.RepoInfo.CollectionUrl, CredentialManager.GetCredentialHandler()).getWorkItemTrackingApi(); } //Returns a Promise containing the WorkItem that was created public async CreateWorkItem(context: TeamServerContext, itemType: string, taskTitle: string): Promise { const newWorkItem = [{ op: "add", path: "/fields/" + WorkItemFields.Title, value: taskTitle }]; /* tslint:disable:no-null-keyword */ return await this._witApi.createWorkItem(null, newWorkItem, context.RepoInfo.TeamProject, itemType, false, false); /* tslint:enable:no-null-keyword */ } //Returns a Promise containing an array of SimpleWorkItems based on the passed in wiql public async GetWorkItems(teamProject: string, wiql: string): Promise { return await this.execWorkItemQuery(teamProject, { query: wiql}); } //Returns a Promise containing an array of QueryHierarchyItems (either folders or work item queries) public async GetWorkItemHierarchyItems(teamProject: string): Promise { return await this._witApi.getQueries(teamProject, QueryExpand.Wiql, 1, false); } //Returns a Promise containing a specific query item public async GetWorkItemQuery(teamProject: string, queryPath: string): Promise { return await this._witApi.getQuery(teamProject, queryPath, QueryExpand.Wiql, 1, false); } //Returns a Promise containing the array of work item types available for the team project public async GetWorkItemTypes(teamProject: string): Promise { const types: WorkItemType[] = await this._witApi.getWorkItemTypes(teamProject); const workItemTypes: WorkItemType[] = []; const hiddenTypes: WorkItemTypeReference[] = []; types.forEach((type) => { workItemTypes.push(type); }); const category: WorkItemTypeCategory = await this._witApi.getWorkItemTypeCategory(teamProject, "Microsoft.HiddenCategory"); category.workItemTypes.forEach((hiddenType) => { hiddenTypes.push(hiddenType); }); const filteredTypes: WorkItemType[] = workItemTypes.filter(function (el) { for (let index: number = 0; index < hiddenTypes.length; index++) { if (el.name === hiddenTypes[index].name) { return false; } } return true; }); return filteredTypes; } //Returns a Promise containing a SimpleWorkItem representing the work item specified by id public async GetWorkItemById(id: string): Promise { const workItem: WorkItem = await this._witApi.getWorkItem(parseInt(id), [WorkItemFields.Id, WorkItemFields.Title]); const result: SimpleWorkItem = new SimpleWorkItem(); result.id = workItem.id.toString(); result.label = workItem.fields[WorkItemFields.Title]; return result; } //Returns a Promise containing an array of SimpleWorkItems that are the results of the passed in wiql private async execWorkItemQuery(teamProject: string, wiql: Wiql): Promise { //Querying WIT requires a TeamContext const teamContext: TeamContext = { projectId: undefined, project: teamProject, teamId: undefined, team: undefined }; // Execute the wiql and get the work item ids const queryResult: WorkItemQueryResult = await this._witApi.queryByWiql(wiql, teamContext); const results: SimpleWorkItem[] = []; let workItemIds: number[] = []; if (queryResult.queryResultType === QueryResultType.WorkItem) { workItemIds = queryResult.workItems.map(function(w) {return w.id; }); } else if (queryResult.queryResultType === QueryResultType.WorkItemLink) { workItemIds = queryResult.workItemRelations.map(function(w) {return w.target.id; }); } if (workItemIds.length === 0) { return results; } //Only request the maximum number of work items the API documents that we should if (workItemIds.length >= WorkItemTrackingService.MaxResults) { workItemIds = workItemIds.slice(0, WorkItemTrackingService.MaxResults); } /* tslint:disable:no-null-keyword */ const workItems: WorkItem[] = await this._witApi.getWorkItems(workItemIds, [WorkItemFields.Id, WorkItemFields.Title, WorkItemFields.WorkItemType], null, WorkItemExpand.None); /* tslint:enable:no-null-keyword */ //Keep original sort order that wiql specified for (let index: number = 0; index < workItemIds.length; index++) { const item: WorkItem = workItems.find((i) => i.id === workItemIds[index]); const id: string = item.id.toString(); results.push({ id: id, label: `${id} [${item.fields[WorkItemFields.WorkItemType]}]`, description: item.fields[WorkItemFields.Title] }); } return results; } public async GetQueryResultCount(teamProject: string, wiql: string): Promise { //Querying WIT requires a TeamContext const teamContext: TeamContext = { projectId: undefined, project: teamProject, teamId: undefined, team: undefined }; // Execute the wiql and get count of results const queryResult: WorkItemQueryResult = await this._witApi.queryByWiql({ query: wiql}, teamContext); //If a Promise is returned here, then() will return that Promise //If not, it will wrap the value within a Promise and return that return queryResult.workItems.length; } //Construct the url to the individual work item edit page public static GetEditWorkItemUrl(teamProjectUrl: string, workItemId: string): string { return UrlBuilder.Join(WorkItemTrackingService.GetWorkItemsBaseUrl(teamProjectUrl), "edit", workItemId); } //Construct the url to the creation page for new work item type public static GetNewWorkItemUrl(teamProjectUrl: string, issueType: string, title?: string, assignedTo?: string) : string { //This form will redirect to the form below so let's use this one let url: string = UrlBuilder.Join(WorkItemTrackingService.GetWorkItemsBaseUrl(teamProjectUrl), "create", issueType); let separator: string = "?"; if (title !== undefined) { //title may need to be encoded (issues if first character is '#', for instance) url += separator + "[" + WorkItemFields.Title + "]=" + title; separator = "&"; } if (assignedTo !== undefined) { url += separator + "[" + WorkItemFields.AssignedTo + "]=" + assignedTo; separator = "&"; } return url; } //Construct the url to the particular query results page public static GetMyQueryResultsUrl(teamProjectUrl: string, folderName: string, queryName: string): string { return UrlBuilder.AddQueryParams(WorkItemTrackingService.GetWorkItemsBaseUrl(teamProjectUrl), `path=${encodeURIComponent(folderName + "/" + queryName)}`, `_a=query`); } //Returns the base url for work items public static GetWorkItemsBaseUrl(teamProjectUrl: string): string { return UrlBuilder.Join(teamProjectUrl, "_workitems"); } /* tslint:disable:variable-name */ public static MaxResults: number = 200; /* tslint:enable:variable-name */ } export class SimpleWorkItem { label: string; description: string; id: string; } /* tslint:disable:variable-name */ export class WorkItemFields { static AssignedTo: string = "System.AssignedTo"; static Id: string = "System.Id"; static Title: string = "System.Title"; static WorkItemType: string = "System.WorkItemType"; } /* tslint:enable:variable-name */ ================================================ FILE: src/team-extension.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { StatusBarAlignment, StatusBarItem, ProgressLocation, window, SourceControlInputBox } from "vscode"; import { DeviceFlowAuthenticator, DeviceFlowDetails, IDeviceFlowAuthenticationOptions, IDeviceFlowTokenOptions } from "vsts-device-flow-auth"; import { PinnedQuerySettings } from "./helpers/settings"; import { CommandNames, Constants, DeviceFlowConstants, TelemetryEvents, TfvcTelemetryEvents, WitTypes } from "./helpers/constants"; import { Logger } from "./helpers/logger"; import { Strings } from "./helpers/strings"; import { UserAgentProvider } from "./helpers/useragentprovider"; import { Utils } from "./helpers/utils"; import { BaseQuickPickItem, ButtonMessageItem, VsCodeUtils } from "./helpers/vscodeutils"; import { RepositoryType } from "./contexts/repositorycontext"; import { BuildClient } from "./clients/buildclient"; import { GitClient } from "./clients/gitclient"; import { WitClient } from "./clients/witclient"; import { Telemetry } from "./services/telemetry"; import { ExtensionManager } from "./extensionmanager"; import * as os from "os"; import * as util from "util"; import * as vscode from "vscode"; export class TeamExtension { private _manager: ExtensionManager; private _buildStatusBarItem: StatusBarItem; private _pullRequestStatusBarItem: StatusBarItem; private _pinnedQueryStatusBarItem: StatusBarItem; private _buildClient: BuildClient; private _gitClient: GitClient; private _witClient: WitClient; private _pinnedQuerySettings: PinnedQuerySettings; private _pollingTimer: NodeJS.Timer; private _initialTimer: NodeJS.Timer; private _signedOut: boolean = false; private _signingIn: boolean = false; constructor(manager: ExtensionManager) { this._manager = manager; } //Gets any available build status information and adds it to the status bar public DisplayCurrentBranchBuildStatus(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { this._buildClient.DisplayCurrentBuildStatus(this._manager.RepoContext, false, this._manager.Settings.BuildDefinitionId); } else { this._manager.DisplayErrorMessage(); } } //Initial method to display, select and navigate to my pull requests public GetMyPullRequests(): void { if (this._manager.EnsureInitialized(RepositoryType.GIT)) { if (this._gitClient) { this._gitClient.GetMyPullRequests(); } } else { this._manager.DisplayErrorMessage(); } } //Keeps track of whether the user is signed in (or not). It's used by the //ExtensionManager to display more helpful messages after signing out. public get IsSignedOut(): boolean { return this._signedOut; } //Prompts user for either manual or device-flow mechanism for acquiring a personal access token. //If manual, we provide the same experience as we always have //If device-flow (automatic), we provide the new 'device flow' experience private async requestPersonalAccessToken(): Promise { const choices: BaseQuickPickItem[] = []; choices.push({ label: Strings.DeviceFlowManualPrompt, description: undefined, id: DeviceFlowConstants.ManualOption }); choices.push({ label: Strings.DeviceFlowPrompt, description: undefined, id: DeviceFlowConstants.DeviceFlowOption }); const choice: BaseQuickPickItem = await window.showQuickPick(choices, { matchOnDescription: false, placeHolder: Strings.DeviceFlowPlaceholder }); if (choice) { if (choice.id === DeviceFlowConstants.ManualOption) { Logger.LogDebug(`Manual personal access token option chosen.`); const token: string = await window.showInputBox({ value: "", prompt: `${Strings.ProvideAccessToken} (${this._manager.ServerContext.RepoInfo.Account})`, placeHolder: "", password: true }); if (token) { Telemetry.SendEvent(TelemetryEvents.ManualPat); } return token; } else if (choice.id === DeviceFlowConstants.DeviceFlowOption) { Logger.LogDebug(`Device flow personal access token option chosen.`); const authOptions: IDeviceFlowAuthenticationOptions = { clientId: DeviceFlowConstants.ClientId, redirectUri: DeviceFlowConstants.RedirectUri, userAgent: `${UserAgentProvider.UserAgent}` }; const tokenOptions: IDeviceFlowTokenOptions = { tokenDescription: `Azure Repos VSCode extension: ${this._manager.ServerContext.RepoInfo.AccountUrl} on ${os.hostname()}` }; const dfa: DeviceFlowAuthenticator = new DeviceFlowAuthenticator(this._manager.ServerContext.RepoInfo.AccountUrl, authOptions, tokenOptions); const details: DeviceFlowDetails = await dfa.GetDeviceFlowDetails(); //To sign in, use a web browser to open the page https://aka.ms/devicelogin and enter the code F3VXCTH2L to authenticate. const value: string = await window.showInputBox({ value: details.UserCode, prompt: `${Strings.DeviceFlowCopyCode} (${details.VerificationUrl})`, placeHolder: undefined, password: false }); if (value) { //At this point, user has no way to cancel until our timeout expires. Before this point, they could //cancel out of the showInputBox. After that, they will need to wait for the automatic cancel to occur. Utils.OpenUrl(details.VerificationUrl); //FUTURE: Could we display a message that allows the user to cancel the authentication? If they escape from the //message or click Close, they wouldn't have that chance any longer. If they leave the message displaying, they //have an opportunity to cancel. However, once authenticated, we no longer have an ability to close the message //automatically or change the message that's displayed. :-/ //FUTURE: Add a 'button' on the status bar that can be used to cancel the authentication //Wait for up to 5 minutes before we cancel the stauts polling (Azure's default is 900s/15 minutes) const timeout: number = 5 * 60 * 1000; /* tslint:disable:align */ const timer: NodeJS.Timer = setTimeout(() => { Logger.LogDebug(`Device flow authentication canceled after ${timeout}ms.`); Telemetry.SendEvent(TelemetryEvents.DeviceFlowCanceled); dfa.Cancel(true); //throw on canceling }, timeout); /* tslint:enable:align */ //We need to await on withProgress here because we need a token before continuing forward const title: string = util.format(Strings.DeviceFlowAuthenticatingToTeamServices, details.UserCode); const token: string = await window.withProgress({ location: ProgressLocation.Window, title: title }, async () => { const accessToken: string = await dfa.WaitForPersonalAccessToken(); //Since we will cancel automatically after timeout, if we _do_ get an accessToken then we need to call clearTimeout if (accessToken) { clearTimeout(timer); Telemetry.SendEvent(TelemetryEvents.DeviceFlowPat); } return accessToken; }); return token; } else { Logger.LogDebug(`User has canceled the device flow authentication mechanism.`); } } } return undefined; } public async Signin() { // For Signin, first we need to verify _serverContext if (this._manager.ServerContext !== undefined && this._manager.ServerContext.RepoInfo !== undefined && this._manager.ServerContext.RepoInfo.IsTeamFoundation === true) { this._signedOut = false; Logger.LogDebug(`Starting sign in process`); if (this._manager.ServerContext.RepoInfo.IsTeamFoundationServer === true) { const defaultUsername : string = this.getDefaultUsername(); const username: string = await window.showInputBox({ value: defaultUsername || "", prompt: Strings.ProvideUsername + " (" + this._manager.ServerContext.RepoInfo.Account + ")", placeHolder: "", password: false }); if (username !== undefined && username.length > 0) { const password: string = await window.showInputBox({ value: "", prompt: Strings.ProvidePassword + " (" + username + ")", placeHolder: "", password: true }); if (password !== undefined) { Logger.LogInfo("Signin: Username and Password provided as authentication."); this._manager.CredentialManager.StoreCredentials(this._manager.ServerContext, username, password).then(() => { // We don't test the credentials to make sure they're good here. Do so on the next command that's run. Logger.LogDebug(`Reinitializing after successfully storing credentials for Team Foundation Server.`); this._manager.Reinitialize(); }).catch((err) => { // TODO: Should the message direct the user to open an issue? send feedback? const msg: string = Strings.UnableToStoreCredentials + this._manager.ServerContext.RepoInfo.Host; this._manager.ReportError(err, msg, true); }); } } } else if (this._manager.ServerContext.RepoInfo.IsTeamServices === true && !this._signingIn) { this._signingIn = true; try { const token: string = await this.requestPersonalAccessToken(); if (token !== undefined) { Logger.LogInfo(`Signin: Personal Access Token provided as authentication.`); this._manager.CredentialManager.StoreCredentials(this._manager.ServerContext, Constants.OAuth, token.trim()).then(() => { Logger.LogDebug(`Reinitializing after successfully storing credentials for Azure DevOps Services.`); this._manager.Reinitialize(); }).catch((err) => { // TODO: Should the message direct the user to open an issue? send feedback? const msg: string = `${Strings.UnableToStoreCredentials} ${this._manager.ServerContext.RepoInfo.Host}`; this._manager.ReportError(err, msg, true); }); } } catch (err) { let msg: string = util.format(Strings.ErrorRequestingToken, this._manager.ServerContext.RepoInfo.AccountUrl); if (err.message) { msg = `${msg} (${err.message})`; //If the request wasn't canceled, log a failure of the device flow auth if (err.message.indexOf("Request canceled by user") === -1) { Telemetry.SendEvent(TelemetryEvents.DeviceFlowFailed); } } //FUTURE: Add a ButtonMessageItem to provide additional help? Log a bug? this._manager.ReportError(err, msg, true); } this._signingIn = false; } } else { //If _manager has an error to display, display it and forgo the other. Otherwise, show the default error message. const displayed: boolean = this._manager.DisplayErrorMessage(); if (!displayed) { const messageItem : ButtonMessageItem = { title : Strings.LearnMore, url : Constants.ReadmeLearnMoreUrl, telemetryId: TelemetryEvents.ReadmeLearnMoreClick }; const tfvcInfoItem : ButtonMessageItem = { title : Strings.LearnMoreAboutTfvc, url : Constants.TfvcLearnMoreUrl, telemetryId: TfvcTelemetryEvents.LearnMoreClick }; VsCodeUtils.ShowErrorMessage(Strings.NoRepoInformation, messageItem, tfvcInfoItem); } } } public Signout() { // For Logout, we just need to verify _serverContext and don't want to set this._errorMessage if (this._manager.ServerContext !== undefined && this._manager.ServerContext.RepoInfo !== undefined && this._manager.ServerContext.RepoInfo.IsTeamFoundation === true) { Logger.LogDebug(`Starting sign out process`); this._manager.CredentialManager.RemoveCredentials(this._manager.ServerContext).then(() => { Logger.LogInfo(`Signout: Removed credentials for host '${this._manager.ServerContext.RepoInfo.Host}'`); }).catch((err) => { const msg: string = Strings.UnableToRemoveCredentials + this._manager.ServerContext.RepoInfo.Host; this._manager.ReportError(err, msg, true); }).finally(() => { this._signedOut = true; //keep track of our status so we can display helpful info later this._manager.SignOut(); //tell the ExtensionManager to clean up this.dispose(); //dispose the status bar items }); } else { this._manager.DisplayErrorMessage(Strings.NoRepoInformation); } } //Opens the build summary page for a particular build public OpenBlamePage(): void { if (this._manager.EnsureInitialized(RepositoryType.GIT)) { if (this._gitClient) { this._gitClient.OpenBlamePage(this._manager.RepoContext); } } else { this._manager.DisplayErrorMessage(); } } //Opens the build summary page for a particular build public OpenBuildSummaryPage(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { this._buildClient.OpenBuildSummaryPage(); } else { this._manager.DisplayErrorMessage(); } } //Opens the file history page for the currently active file public OpenFileHistory(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { if (this._manager.RepoContext.Type === RepositoryType.GIT && this._gitClient) { this._gitClient.OpenFileHistory(this._manager.RepoContext); } else if (this._manager.RepoContext.Type === RepositoryType.TFVC) { this._manager.Tfvc.ViewHistory(); } else { this._manager.DisplayErrorMessage(Strings.NoRepoInformation); } } else { this._manager.DisplayErrorMessage(); } } //Opens a browser to a new Bug public OpenNewBug(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { //Bug is in all three templates const taskTitle = VsCodeUtils.GetActiveSelection(); this._witClient.CreateNewItem(WitTypes.Bug, taskTitle); } else { this._manager.DisplayErrorMessage(); } } //Opens a browser to a new pull request for the current branch public OpenNewPullRequest(): void { if (this._manager.EnsureInitialized(RepositoryType.GIT)) { if (this._gitClient) { this._gitClient.OpenNewPullRequest(this._manager.RepoContext.RemoteUrl, this._manager.RepoContext.CurrentBranch); } } else { this._manager.DisplayErrorMessage(); } } //Opens a browser to a new Task public OpenNewTask(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { //Issue is only in Agile and CMMI templates (not Scrum) //Task is in all three templates (Agile, CMMI, Scrum) const taskTitle = VsCodeUtils.GetActiveSelection(); this._witClient.CreateNewItem(WitTypes.Task, taskTitle); } else { this._manager.DisplayErrorMessage(); } } //Opens a browser to a new work item (based on the work item type selected) public OpenNewWorkItem(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { const taskTitle = VsCodeUtils.GetActiveSelection(); this._witClient.CreateNewWorkItem(taskTitle); } else { this._manager.DisplayErrorMessage(); } } //Opens the team project web site public OpenTeamProjectWebSite(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { Telemetry.SendEvent(TelemetryEvents.OpenTeamSite); Logger.LogInfo("OpenTeamProjectWebSite: " + this._manager.ServerContext.RepoInfo.TeamProjectUrl); Utils.OpenUrl(this._manager.ServerContext.RepoInfo.TeamProjectUrl); } else { this._manager.DisplayErrorMessage(); } } //Meant to be used when coming back online via status bar items public RefreshPollingStatus(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { this.refreshPollingItems(); } else { this._manager.DisplayErrorMessage(); } } //Returns the list of work items assigned directly to the current user public ViewMyWorkItems(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { this._witClient.ShowMyWorkItems(); } else { this._manager.DisplayErrorMessage(); } } //Returns the list of work items from the pinned query public ViewPinnedQueryWorkItems(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { this._witClient.ShowPinnedQueryWorkItems(); } else { this._manager.DisplayErrorMessage(); } } //Navigates to a work item chosen from the results of a user-selected "My Queries" work item query //This method first displays the queries under "My Queries" and, when one is chosen, displays the associated work items. //If a work item is chosen, it is opened in the web browser. public ViewWorkItems(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { this._witClient.ShowMyWorkItemQueries(); } else { this._manager.DisplayErrorMessage(); } } public async AssociateWorkItems(): Promise { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { Telemetry.SendEvent(TelemetryEvents.AssociateWorkItems); const workitems: string[] = await this.chooseWorkItems(); for (let i: number = 0; i < workitems.length; i++) { // Append the string to end of the message // Note: we are prefixing the message with a space so that the # char is not in the first column // This helps in case the user ends up editing the comment from the Git command line this.appendToCheckinMessage(" " + workitems[i]); } } else { this._manager.DisplayErrorMessage(); } } private appendToCheckinMessage(line: string): void { this.withSourceControlInputBox((inputBox: SourceControlInputBox) => { const previousMessage = inputBox.value; if (previousMessage) { inputBox.value = previousMessage + "\n" + line; } else { inputBox.value = line; } }); } private getDefaultUsername() : string { if (os.platform() === "win32") { let defaultUsername: string; const domain: string = process.env.USERDOMAIN || ""; const username: string = process.env.USERNAME || ""; if (domain !== undefined) { defaultUsername = domain; } if (username !== undefined) { if (defaultUsername === undefined) { return username; } return defaultUsername + "\\" + username; } } return undefined; } //Set up the initial status bars public InitializeStatusBars() { //Only initialize the status bar item if this is a Git repository if (this._manager.RepoContext.Type === RepositoryType.GIT) { if (!this._pullRequestStatusBarItem) { this._pullRequestStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 99); this._pullRequestStatusBarItem.command = CommandNames.GetPullRequests; this._pullRequestStatusBarItem.text = GitClient.GetPullRequestStatusText(); this._pullRequestStatusBarItem.tooltip = Strings.BrowseYourPullRequests; this._pullRequestStatusBarItem.show(); } } if (!this._buildStatusBarItem) { this._buildStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 98); this._buildStatusBarItem.command = CommandNames.OpenBuildSummaryPage; this._buildStatusBarItem.text = `$(package) $(dash)`; this._buildStatusBarItem.tooltip = Strings.NoBuildsFound; this._buildStatusBarItem.show(); } if (!this._pinnedQueryStatusBarItem) { this._pinnedQueryStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 97); this._pinnedQueryStatusBarItem.command = CommandNames.ViewPinnedQueryWorkItems; this._pinnedQueryStatusBarItem.text = WitClient.GetPinnedQueryStatusText(); this._pinnedQueryStatusBarItem.tooltip = Strings.ViewYourPinnedQuery; this._pinnedQueryStatusBarItem.show(); } } public InitializeClients(repoType: RepositoryType) : void { //Ensure that the repo type is good to go before we initialize the clients for it. If we //can't get a team project for TFVC, we shouldn't initialize the clients. if (this._manager.EnsureInitialized(repoType)) { //We can initialize for any repo type (just skip _gitClient if not Git) this._pinnedQuerySettings = new PinnedQuerySettings(this._manager.ServerContext.RepoInfo.Account); this._buildClient = new BuildClient(this._manager.ServerContext, this._buildStatusBarItem); //Don't initialize the Git client if we aren't a Git repository if (repoType === RepositoryType.GIT) { this._gitClient = new GitClient(this._manager.ServerContext, this._pullRequestStatusBarItem); } this._witClient = new WitClient(this._manager.ServerContext, this._pinnedQuerySettings.PinnedQuery, this._pinnedQueryStatusBarItem); this.startPolling(); } } //Returns a list of strings representing the work items that the user chose // strings are in the form "#id - description" private async chooseWorkItems(): Promise { return await this._witClient.ChooseWorkItems(); } private pollBuildStatus(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { Logger.LogInfo("Polling for latest current build status..."); this._buildClient.DisplayCurrentBuildStatus(this._manager.RepoContext, true, this._manager.Settings.BuildDefinitionId); } } private pollMyPullRequests(): void { //Since we're polling, we don't want to display an error every so often //if user opened a TFVC repository (via EnsureInitialized). So send //ALL to EnsureInitialized but check it before actually polling. if (this._manager.EnsureInitialized(RepositoryType.ANY)) { //Only poll for pull requests when repository is Git if (this._manager.RepoContext.Type === RepositoryType.GIT) { Logger.LogInfo("Polling for pull requests..."); this._gitClient.PollMyPullRequests(); } } } private pollPinnedQuery(): void { if (this._manager.EnsureInitialized(RepositoryType.ANY)) { Logger.LogInfo("Polling for the pinned work itemquery"); this._witClient.PollPinnedQuery(); } } //Polls for latest pull requests and current branch build status information private refreshPollingItems(): void { this.pollMyPullRequests(); this.pollBuildStatus(); this.pollPinnedQuery(); } //Sets up the interval to refresh polling items private startPolling(): void { if (!this._pollingTimer) { this._initialTimer = setTimeout(() => this.refreshPollingItems(), 1000 * 4); this._pollingTimer = setInterval(() => this.refreshPollingItems(), 1000 * 60 * this._manager.Settings.PollingInterval); } } /** * Exposes access to the source control input box for use in other areas. * @param fn A function that works with the input box. */ private withSourceControlInputBox(fn: (input: SourceControlInputBox) => void) { const gitExtension = vscode.extensions.getExtension("vscode.git"); if (gitExtension) { const git = gitExtension.exports; if (git) { git.getRepositories() .then((repos: any[]) => { if (repos && repos.length > 0) { const inputBox = repos[0].inputBox; if (inputBox) { fn(inputBox); } } }); } } } public NotifyBranchChanged(/*TODO: currentBranch: string*/) : void { this.refreshPollingItems(); } public cleanup(): void { if (this._pollingTimer) { if (this._initialTimer) { clearTimeout(this._initialTimer); this._initialTimer = undefined; } clearInterval(this._pollingTimer); this._pollingTimer = undefined; } if (this._pullRequestStatusBarItem !== undefined) { this._pullRequestStatusBarItem.dispose(); this._pullRequestStatusBarItem = undefined; } if (this._buildStatusBarItem !== undefined) { this._buildStatusBarItem.dispose(); this._buildStatusBarItem = undefined; } if (this._pinnedQueryStatusBarItem !== undefined) { this._pinnedQueryStatusBarItem.dispose(); this._pinnedQueryStatusBarItem = undefined; } } dispose() { this.cleanup(); } } ================================================ FILE: src/tfvc/commands/add.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command adds the files passed in. * It returns the list of files that were successfully added. * add [/lock:none|checkin|checkout] [/type:] [/recursive] [/silent] [/noignore] ... */ export class Add implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPaths: string[]; public constructor(serverContext: TeamServerContext, itemPaths: string[]) { CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths"); this._serverContext = serverContext; this._itemPaths = itemPaths; } public GetArguments(): IArgumentProvider { return new ArgumentBuilder("add", this._serverContext) .AddAll(this._itemPaths); } public GetOptions(): any { return {}; } /** * Example of output * folder1\folder2: * file5.txt * file2.java */ public async ParseOutput(executionResult: IExecutionResult): Promise { // Any exit code other than 0 or 1 means that something went wrong, so simply throw the error if (executionResult.exitCode !== 0 && executionResult.exitCode !== 1) { CommandHelper.ProcessErrors(executionResult); } let lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, false, true /*filterEmptyLines*/); //Remove any lines indicating that there were no files to add (e.g., calling add on files that don't exist) lines = lines.filter((e) => !e.startsWith("No arguments matched any files to add.")); //CLC //Ex. /usr/alias/repos/Tfvc.L2VSCodeExtension.RC/file-does-not-exist.md: No file matches. lines = lines.filter((e) => !e.endsWith(" No file matches.")); //tf.exe const filesAdded: string[] = []; let path: string = ""; for (let index: number = 0; index < lines.length; index++) { const line: string = lines[index]; if (CommandHelper.IsFilePath(line)) { path = line; } else { const file: string = this.getFileFromLine(line); filesAdded.push(CommandHelper.GetFilePath(path, file)); } } return filesAdded; } public GetExeArguments(): IArgumentProvider { return new ArgumentBuilder("add", this._serverContext, true /* skipCollectionOption */) .AddAll(this._itemPaths); } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } private getFileFromLine(line: string): string { //There's no prefix on the filename line for the Add command return line; } } ================================================ FILE: src/tfvc/commands/argumentbuilder.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext } from "../../contexts/servercontext"; import { IArgumentProvider } from "../interfaces"; import { TfvcError } from "../tfvcerror"; /** * Create an instance of this class to build up the arguments that should be passed to the command line. */ export class ArgumentBuilder implements IArgumentProvider { private _arguments: string[] = []; private _secretArgumentIndexes: number[] = []; public constructor(command: string, serverContext?: TeamServerContext, skipCollectionOption?: boolean) { if (!command) { throw TfvcError.CreateArgumentMissingError("command"); } this.Add(command); this.AddSwitch("noprompt"); if (serverContext && serverContext.RepoInfo && serverContext.RepoInfo.CollectionUrl) { if (!skipCollectionOption) { //TODO decode URI since CLC does not expect encoded collection urls this.AddSwitchWithValue("collection", serverContext.RepoInfo.CollectionUrl, false); } if (serverContext.CredentialInfo) { this.AddSwitchWithValue("login", (serverContext.CredentialInfo.Domain ? serverContext.CredentialInfo.Domain + "\\" : "") + serverContext.CredentialInfo.Username + "," + serverContext.CredentialInfo.Password, true); } } } public Add(arg: string): ArgumentBuilder { this._arguments.push(arg); return this; } public AddAll(args: string[]): ArgumentBuilder { if (args) { for (let i: number = 0; i < args.length; i++) { this.Add(args[i]); } } return this; } public AddSecret(arg: string): ArgumentBuilder { this.Add(arg); this._secretArgumentIndexes.push(this._arguments.length - 1); return this; } public AddSwitch(switchName: string): ArgumentBuilder { return this.AddSwitchWithValue(switchName, undefined, false); } public AddSwitchWithValue(switchName: string, switchValue: string, isSecret: boolean): ArgumentBuilder { let arg: string; if (!switchValue) { arg = "-" + switchName; } else { arg = "-" + switchName + ":" + switchValue; } if (isSecret) { this.AddSecret(arg); } else { this.Add(arg); } return this; } public Build(): string[] { return this._arguments; } /** * This method builds all the arguments into a single command line. This is needed if * a response file is needed for the commands. */ public BuildCommandLine(): string { let result: string = ""; this._arguments.forEach((arg) => { const escapedArg = this.escapeArgument(arg); result += escapedArg + " "; }); result += "\n"; return result; } /** * Command line arguments should have all embedded double quotes repeated to escape them. * They should also be surrounded by double quotes if they contain a space (or other whitespace). */ private escapeArgument(arg: string) { if (!arg) { return arg; } let escaped = arg.replace(/\"/g, "\"\""); if (/\s/.test(escaped)) { escaped = "\"" + escaped + "\""; } return escaped; } public ToString(): string { let output: string = ""; for (let i = 0; i < this._arguments.length; i++) { let arg: string = this._arguments[i]; if (this._secretArgumentIndexes.indexOf(i) >= 0) { // This arg is a secret so hide the value arg = "********"; } output += arg + " "; } return output.trim(); } /* IArgumentProvider Implementation - START */ public GetCommand(): string { return this._arguments.length > 0 ? this._arguments[0] : ""; } public GetArguments(): string[] { return this.Build(); } public GetCommandLine(): string { return this.BuildCommandLine(); } public GetArgumentsForDisplay(): string { return this.ToString(); } public AddProxySwitch(proxy: string) { this.AddSwitchWithValue("proxy", proxy, false); } /* IArgumentProvider Implementation - END */ } ================================================ FILE: src/tfvc/commands/checkin.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command checks in files into TFVC *

* checkin [/all] [/author:] [/comment:|@valuefile] [/notes:"note"="value"[;"note2"="value2"[;...]]|@notefile] * [/override:|@valuefile] [/recursive] [/validate] [/bypass] [/force] [/noautoresolve] [/associate:[,...]] * [/resolve:[,...]] [/saved] [...] */ export class Checkin implements ITfvcCommand { private _serverContext: TeamServerContext; private _files: string[]; private _comment: string; private _workItemIds: number[]; public constructor(serverContext: TeamServerContext, files: string[], comment?: string, workItemIds?: number[]) { CommandHelper.RequireStringArrayArgument(files, "files"); this._serverContext = serverContext; this._files = files; this._comment = comment; this._workItemIds = workItemIds; } public GetArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("checkin", this._serverContext) .AddAll(this._files); if (this._comment) { builder.AddSwitchWithValue("comment", this.getComment(), false); } if (this._workItemIds && this._workItemIds.length > 0) { builder.AddSwitchWithValue("associate", this.getAssociatedWorkItems(), false); } return builder; } public GetOptions(): any { return {}; } private getComment(): string { // replace newlines with spaces return this._comment.replace(/\r\n/g, " ").replace(/\n/g, " "); } private getAssociatedWorkItems(): string { return this._workItemIds.join(","); } /** * Returns the files that were checked in *

* Output example for success: * /Users/leantk/tfvc-tfs/tfsTest_01/addFold: * Checking in edit: testHere.txt *

* /Users/leantk/tfvc-tfs/tfsTest_01: * Checking in edit: test3.txt * Checking in edit: TestAdd.txt *

* Changeset #20 checked in. *

* Output example for failure: *

* /Users/leantk/tfvc-tfs/tfsTest_01: * Checking in edit: test3.txt * Checking in edit: TestAdd.txt * Unable to perform operation on $/tfsTest_01/TestAdd.txt. The item $/tfsTest_01/TestAdd.txt is locked in workspace new;Leah Antkiewicz. * No files checked in. *

* No files checked in. */ public async ParseOutput(executionResult: IExecutionResult): Promise { if (executionResult.exitCode === 100) { CommandHelper.ProcessErrors(executionResult); } else { return CommandHelper.GetChangesetNumber(executionResult.stdout); } } public GetExeArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("checkin", this._serverContext, true /* skipCollectionOption */) .AddAll(this._files); if (this._comment) { builder.AddSwitchWithValue("comment", this.getComment(), false); } // TF.EXE doesn't support associating work items with checkin //builder.AddSwitchWithValue("associate", this.getAssociatedWorkItems(), false); return builder; } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } } ================================================ FILE: src/tfvc/commands/commandhelper.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as path from "path"; import { parseString } from "xml2js"; import { Constants, TfvcTelemetryEvents } from "../../helpers/constants"; import { Logger } from "../../helpers/logger"; import { Strings } from "../../helpers/strings"; import { Utils } from "../../helpers/utils"; import { IButtonMessageItem } from "../../helpers/vscodeutils.interfaces"; import { TfvcError, TfvcErrorCodes } from "../tfvcerror"; import { IExecutionResult } from "../interfaces"; export class CommandHelper { public static RequireArgument(argument: any, argumentName: string) { if (!argument) { throw TfvcError.CreateArgumentMissingError(argumentName); } } public static RequireStringArgument(argument: string, argumentName: string) { if (!argument || argument.trim().length === 0) { throw TfvcError.CreateArgumentMissingError(argumentName); } } public static RequireStringArrayArgument(argument: string[], argumentName: string) { if (!argument || argument.length === 0) { throw TfvcError.CreateArgumentMissingError(argumentName); } } public static HasError(result: IExecutionResult, errorPattern: string): boolean { if (result && result.stderr && errorPattern) { return new RegExp(errorPattern, "i").test(result.stderr); } return false; } public static ProcessErrors(result: IExecutionResult): void { if (result.exitCode) { let tfvcErrorCode: string = TfvcErrorCodes.UnknownError; let message: string; let messageOptions: IButtonMessageItem[] = []; if (/Authentication failed/.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.AuthenticationFailed; } else if (/workspace could not be determined/i.test(result.stderr) || /The workspace could not be determined from any argument paths or the current working directory/i.test(result.stderr) || // CLC error /Unable to determine the source control server/i.test(result.stderr)) { // EXE error tfvcErrorCode = TfvcErrorCodes.NotATfvcRepository; message = Strings.NoWorkspaceMappings; } else if (/Repository not found/i.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.RepositoryNotFound; } else if (/project collection URL to use could not be determined/i.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.NotATfvcRepository; message = Strings.NotATfvcRepository; } else if (/Access denied connecting.*authenticating as OAuth/i.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.AuthenticationFailed; message = Strings.TokenNotAllScopes; } else if (/'java' is not recognized as an internal or external command/i.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.NotFound; message = Strings.TfInitializeFailureError; } else if (/Error occurred during initialization of VM/i.test(result.stdout)) { //Example: "Error occurred during initialization of VM\nCould not reserve enough space for 2097152KB object heap\n" //This one occurs with the error message in stdout! tfvcErrorCode = TfvcErrorCodes.NotFound; message = `${Strings.TfInitializeFailureError} (${Utils.FormatMessage(result.stdout)})`; } else if (/There is no working folder mapping/i.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.FileNotInMappings; } else if (/could not be found in your workspace, or you do not have permission to access it./i.test(result.stderr)) { tfvcErrorCode = TfvcErrorCodes.FileNotInWorkspace; } else if (/TF30063: You are not authorized to access/i.test(result.stderr)) { //For now, we're assuming this is an indication of a Server workspace tfvcErrorCode = TfvcErrorCodes.NotAuthorizedToAccess; message = Strings.TfServerWorkspace; messageOptions = [{ title : Strings.LearnMore, url : Constants.ServerWorkspaceUrl }]; } else if (/TF400017: The local properties table for the local workspace/i.test(result.stderr)) { //For now, we're assuming this is an indication of a workspace the CLC doesn't know about (but exists locally) tfvcErrorCode = TfvcErrorCodes.WorkspaceNotKnownToClc; message = Strings.ClcCannotAccessWorkspace; messageOptions = [{ title : Strings.MoreDetails, url : Constants.WorkspaceNotDetectedByClcUrl, telemetryId: TfvcTelemetryEvents.ClcCannotAccessWorkspace }]; } //Log any information we receive via either stderr or stdout if (result.stderr) { Logger.LogDebug(`TFVC errors (via stderr): ${result.stderr}`); } if (result.stdout) { Logger.LogDebug(`TFVC errors (via stdout): ${result.stdout}`); } throw new TfvcError({ message: message || Strings.TfExecFailedError, messageOptions: messageOptions, exitCode: result.exitCode, tfvcErrorCode: tfvcErrorCode }); } } /** * This method is used by Checkin to parse out the changeset number. */ public static GetChangesetNumber(stdout: string): string { // parse output for changeset number if (stdout) { const prefix: string = "Changeset #"; const start: number = stdout.indexOf(prefix) + prefix.length; if (start >= 0) { const end: number = stdout.indexOf(" ", start); if (end > start) { return stdout.slice(start, end); } } } return ""; } public static GetNewLineCharacter(stdout: string): string { if (stdout && /\r\n/.test(stdout)) { return "\r\n"; } return "\n"; } public static SplitIntoLines(stdout: string, skipWarnings?: boolean, filterEmptyLines?: boolean): string[] { if (!stdout) { return []; } let lines: string[] = stdout.replace(/\r\n/g, "\n").split("\n"); skipWarnings = skipWarnings === undefined ? true : skipWarnings; // Ignore WARNings that may be above the desired lines if (skipWarnings) { let index: number = 0; while (index < lines.length && lines[index].startsWith("WARN")) { index++; } lines = lines.splice(index); } if (filterEmptyLines) { lines = lines.filter((e) => e.trim() !== ""); } return lines; } public static async ParseXml(xml: string): Promise { if (!xml) { return; } return new Promise((resolve, reject) => { parseString( xml, { tagNameProcessors: [CommandHelper.normalizeName], attrNameProcessors: [CommandHelper.normalizeName] }, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } public static TrimToXml(xml: string): string { if (xml) { const start: number = xml.indexOf(""); if (start >= 0 && end > start) { return xml.slice(start, end + 1); } } return xml; } private static normalizeName(name: string): string { if (name) { return name.replace(/\-/g, "").toLowerCase(); } return name; } /** * Returns true if the line is of the form... * 'folder1:' or 'folder1\folder2:' or 'd:\folder1\folder2:' */ public static IsFilePath(line: string): boolean { if (line && line.length > 0 && line.endsWith(":", line.length)) { return true; } return false; } /** * Returns the full path of the file where... * filePath could be 'folder1\folder2:' * filename is something like 'file.txt' * pathRoot is the root of any relative paths */ public static GetFilePath(filePath: string, filename: string, pathRoot?: string): string { let folderPath: string = filePath; //Remove any ending ':' if (filePath && filePath.length > 0 && filePath.endsWith(":", filePath.length)) { folderPath = filePath.slice(0, filePath.length - 1); } //If path isn't rooted, add in the root if (folderPath && !path.isAbsolute(folderPath) && pathRoot) { folderPath = path.join(pathRoot, folderPath); } else if (!folderPath && pathRoot) { folderPath = pathRoot; } if (folderPath && filename) { return path.join(folderPath, filename); } else if (filename) { return filename; } else { return folderPath; } } } ================================================ FILE: src/tfvc/commands/delete.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command deletes the files passed in. * It returns a list of all files marked for deletion. * delete /detect [/lock:none|checkin|checkout] [/recursive] * delete [/lock:none|checkin|checkout] [/recursive] ... */ export class Delete implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPaths: string[]; public constructor(serverContext: TeamServerContext, itemPaths: string[]) { CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths"); this._serverContext = serverContext; this._itemPaths = itemPaths; } public GetArguments(): IArgumentProvider { return new ArgumentBuilder("delete", this._serverContext) .AddAll(this._itemPaths); } public GetOptions(): any { return {}; } /* Delete returns either 0 (success) or 100 (failure). IF we fail, simply throw. Sample output: //Single file tf.cmd delete folder1\folder2\file2.txt folder1\folder2: file2.txt //Multiple files in a folder tf.cmd delete folder2 folder2: file2.txt newfile.txt folder2 //Deleting a file that doesn't exist tf.cmd delete file2.txt The item C:\repos\Tfvc.L2VSCodeExtension.RC\folder1\file2.txt could not be found in your workspace, or you do not have permission to access it. No arguments matched any files to delete. //Deleting a file with existing pending changes tf.cmd delete file2.txt TF203069: $/L2.VSCodeExtension.RC/folder1/folder2/file2.txt could not be deleted because that change conflicts with one or more other pending *snip* No arguments matched any files to delete. */ public async ParseOutput(executionResult: IExecutionResult): Promise { CommandHelper.ProcessErrors(executionResult); const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, false, true /*filterEmptyLines*/); const filesUndone: string[] = []; let path: string = ""; for (let index: number = 0; index < lines.length; index++) { const line: string = lines[index]; if (CommandHelper.IsFilePath(line)) { path = line; } else if (line) { const file: string = this.getFileFromLine(line); filesUndone.push(CommandHelper.GetFilePath(path, file)); } } return filesUndone; } public GetExeArguments(): IArgumentProvider { return new ArgumentBuilder("delete", this._serverContext, true /* skipCollectionOption */) .AddAll(this._itemPaths); } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } private getFileFromLine(line: string): string { //There's no prefix on the filename line for the Delete command return line; } } ================================================ FILE: src/tfvc/commands/findconflicts.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand, IConflict } from "../interfaces"; import { ConflictType } from "../scm/status"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command finds conflicts existing in the workspace by calling tf resolve -preview * * tf resolve [itemspec] * [/auto:(AutoMerge|TakeTheirs|KeepYours|OverwriteLocal|DeleteConflict|KeepYoursRenameTheirs)] * [/preview] [(/overridetype:overridetype | /converttotype:converttype] [/recursive] [/newname:path] [/noprompt] [/login:username, [password]] */ export class FindConflicts implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPath: string; public constructor(serverContext: TeamServerContext, itemPath: string) { this._serverContext = serverContext; CommandHelper.RequireStringArgument(itemPath, "itemPath"); this._itemPath = itemPath; } public GetArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("resolve", this._serverContext) .Add(this._itemPath) .AddSwitch("recursive") .AddSwitch("preview"); return builder; } public GetOptions(): any { return {}; } /** * Outputs the conflicts found in the workspace in the following format: * * tfsTest_01/addFold/testHere2: The item content has changed * tfsTest_01/TestAdd.txt: The item content has changed */ public async ParseOutput(executionResult: IExecutionResult): Promise { // Any exit code other than 0 or 1 means that something went wrong, so simply throw the error if (executionResult.exitCode !== 0 && executionResult.exitCode !== 1) { CommandHelper.ProcessErrors(executionResult); } const conflicts: IConflict[] = []; //"Picked up _JAVA_OPTIONS: -Xmx1024M" let outputToProcess: string = executionResult.stderr; if (outputToProcess && outputToProcess.includes("_JAVA_OPTIONS")) { //When you don't need _JAVA_OPTIONS set, the results we want are always in stderr (this is the default case) //With _JAVA_OPTIONS set and there are no conflicts, _JAVA_OPTIONS is in stderr but the result we want to process is moved to stdout //With _JAVA_OPTIONS set and there are conflicts, _JAVA_OPTIONS will appear in stderr along with the results also in stderr (and stdout will be empty) if (executionResult.stdout && executionResult.stdout.length > 0) { outputToProcess = executionResult.stdout; } } const lines: string[] = CommandHelper.SplitIntoLines(outputToProcess, false, true); for (let i: number = 0; i < lines.length; i++) { const line: string = lines[i]; if (line.includes("_JAVA_OPTIONS")) { continue; //This is not a conflict } const colonIndex: number = line.lastIndexOf(":"); if (colonIndex >= 0) { const localPath: string = line.slice(0, colonIndex); let type: ConflictType = ConflictType.CONTENT; if (/You have a conflicting pending change/i.test(line) || /A newer version exists on the server/i.test(line)) { // This is the ambiguous response given by the EXE. // We will assume it is both a name and content change for now. type = ConflictType.NAME_AND_CONTENT; } else if (/The item name and content have changed/i.test(line)) { type = ConflictType.NAME_AND_CONTENT; } else if (/The item name has changed/i.test(line)) { type = ConflictType.RENAME; } else if (/The source and target both have changes/i.test(line)) { type = ConflictType.MERGE; } else if (/The item has already been deleted/i.test(line) || /The item has been deleted in the source branch/i.test(line) || /The item has been deleted from the server/i.test(line)) { type = ConflictType.DELETE; } else if (/The item has been deleted in the target branch/i.test(line)) { type = ConflictType.DELETE_TARGET; } conflicts.push({ localPath: localPath, type: type, message: line }); } } return conflicts; } public GetExeArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("resolve", this._serverContext, true /* skipCollectionOption */) .Add(this._itemPath) .AddSwitch("recursive") .AddSwitch("preview"); return builder; } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } } ================================================ FILE: src/tfvc/commands/findworkspace.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Strings } from "../../helpers/strings"; import { IButtonMessageItem } from "../../helpers/vscodeutils.interfaces"; import { Constants, TfvcTelemetryEvents } from "../../helpers/constants"; import { IArgumentProvider, IExecutionResult, ITfvcCommand, IWorkspace, IWorkspaceMapping } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; import { TfvcError, TfvcErrorCodes } from "../tfvcerror"; /** * This command only returns a partial workspace object that allows you to get the name and server. * To get the entire workspace object you should call GetWorkspace with the workspace name. * (This is one of the only commands that expects to be a strictly local operation - no server calls - and so does not * take a server context object in the constructor) */ export class FindWorkspace implements ITfvcCommand { private _localPath: string; private _restrictWorkspace: boolean; public constructor(localPath: string, restrictWorkspace: boolean = false) { CommandHelper.RequireStringArgument(localPath, "localPath"); this._localPath = localPath; this._restrictWorkspace = restrictWorkspace; } public GetArguments(): IArgumentProvider { // Due to a bug in the CLC this command "requires" the login switch although the creds are never used const builder: ArgumentBuilder = new ArgumentBuilder("workfold"); //If desired, restrict the workspace to the localPath (VS Code's current workspace) if (this._restrictWorkspace) { //With TEE, I got an error when passing "login", "fake,fake" and the path at the same time. // A client error occurred: Error refreshing cached workspace WorkspaceInfo (*snip*) from server: // Access denied connecting to TFS server http://java-tfs2015:8081/ (authenticating as fake) //TF.exe is fine without the fake login when a localPath is provided return builder.Add(this._localPath); } return builder.AddSwitchWithValue("login", "fake,fake", true); } public GetOptions(): any { return { cwd: this._localPath }; } /** * Parses the output of the workfold command. (NOT XML) * SAMPLE * Access denied connecting to TFS server https://account.visualstudio.com/ (authenticating as Personal Access Token) <-- line is optional * ===================================================================================================================================================== * Workspace: MyNewWorkspace2 * Collection: http://java-tfs2015:8081/tfs/ * $/tfsTest_01: D:\tmp\test */ public async ParseOutput(executionResult: IExecutionResult): Promise { // Throw if any errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); const stdout = executionResult.stdout; if (!stdout) { return undefined; } // Find the workspace name and collectionUrl const lines = CommandHelper.SplitIntoLines(stdout); let workspaceName: string = ""; let collectionUrl: string = ""; let equalsLineFound: boolean = false; const mappings: IWorkspaceMapping[] = []; let teamProject: string = undefined; for (let i: number = 0; i <= lines.length; i++) { const line: string = lines[i]; if (!line) { continue; } if (line.startsWith("==========")) { equalsLineFound = true; continue; } else if (!equalsLineFound) { continue; } //CLC returns 'Workspace:', tf.exe returns 'Workspace :' if (line.startsWith("Workspace:") || line.startsWith("Workspace :")) { workspaceName = this.getValue(line); } else if (line.startsWith("Collection:")) { collectionUrl = this.getValue(line); } else { // This should be a mapping const mapping: IWorkspaceMapping = this.getMapping(line); if (mapping) { mappings.push(mapping); //If we're restricting workspaces, tf.exe will return the proper (single) folder. While TEE will //return all of the mapped folders (so we have to find the right one based on the folder name passed in) //We will do that further down but this sets up the default for that scenario. if (!teamProject) { teamProject = this.getTeamProject(mapping.serverPath); } } } } if (mappings.length === 0) { throw new TfvcError( { message: Strings.NoWorkspaceMappings, tfvcErrorCode: TfvcErrorCodes.NotATfvcRepository }); } //If we're restricting the workspace, find the proper teamProject name if (this._restrictWorkspace) { for (let i: number = 0; i < mappings.length; i++) { const isWithin: boolean = this.pathIsWithin(this._localPath, mappings[i].localPath); if (isWithin) { const project: string = this.getTeamProject(mappings[i].serverPath); //maintain case in serverPath teamProject = project; break; } } } //If there are mappings but no workspace name, the term 'workspace' couldn't be parsed. According to Bing //translate, other than Klingon, no other supported language translates 'workspace' as 'workspace'. //So if we determine there are mappings but can't get the workspace name, we assume it's a non-ENU //tf executable. One example of this is German. if (mappings.length > 0 && !workspaceName) { const messageOptions: IButtonMessageItem[] = [{ title : Strings.MoreDetails, url : Constants.NonEnuTfExeConfiguredUrl, telemetryId: TfvcTelemetryEvents.ExeNonEnuConfiguredMoreDetails }]; throw new TfvcError( { message: Strings.NotAnEnuTfCommandLine, messageOptions: messageOptions, tfvcErrorCode: TfvcErrorCodes.NotAnEnuTfCommandLine }); } //Decode collectionURL and teamProject here (for cases like 'Collection: http://java-tfs2015:8081/tfs/spaces%20in%20the%20name') const workspace: IWorkspace = { name: workspaceName, server: decodeURI(collectionUrl), defaultTeamProject: decodeURI(teamProject), mappings: mappings }; return workspace; } public GetExeArguments(): IArgumentProvider { return this.GetArguments(); } public GetExeOptions(): any { return this.GetOptions(); } /** * Parses the output of the workfold command (the EXE output is slightly different from the CLC output parsed above) * SAMPLE * Access denied connecting to TFS server https://account.visualstudio.com/ (authenticating as Personal Access Token) <-- line is optional * ===================================================================================================================================================== * Workspace : MyNewWorkspace2 (user name) * Collection: http://server:8081/tfs/ * $/tfsTest_01: D:\tmp\test */ public async ParseExeOutput(executionResult: IExecutionResult): Promise { const workspace: IWorkspace = await this.ParseOutput(executionResult); if (workspace && workspace.name) { // The workspace name includes the user name, so let's fix that const lastOpenParenIndex: number = workspace.name.lastIndexOf(" ("); if (lastOpenParenIndex >= 0) { workspace.name = workspace.name.slice(0, lastOpenParenIndex).trim(); } } return workspace; } /** * This method parses a line of the form "name: value" and returns the value part. */ private getValue(line: string): string { if (line) { const index: number = line.indexOf(":"); if (index >= 0 && index + 1 < line.length) { return line.slice(index + 1).trim(); } } return ""; } /** * This method parses a single line of output returning the mapping if one was found * Examples: * "$/TFVC_11/folder1: D:\tmp\notdefault\folder1" * "(cloaked) $/TFVC_11/folder1:" */ private getMapping(line: string): IWorkspaceMapping { if (line) { const cloaked: boolean = line.trim().toLowerCase().startsWith("(cloaked)"); let end: number = line.indexOf(":"); //EXE: cloaked entries end with ':' //CLC: cloaked entries *don't* end with ':' if (cloaked && end === -1) { end = line.length; } const start: number = cloaked ? line.indexOf(")") + 1 : 0; const serverPath: string = line.slice(start, end).trim(); let localPath: string; //cloaked entries don't have local paths if (end >= 0 && end + 1 < line.length) { localPath = line.slice(end + 1).trim(); } return { serverPath: serverPath, localPath: localPath, cloaked: cloaked }; } return undefined; } /** * Use this method to get the team project name from a TFVC server path. * The team project name is always the first folder in the path. * If no team project name is found an empty string is returned. */ private getTeamProject(serverPath: string): string { if (serverPath && serverPath.startsWith("$/") && serverPath.length > 2) { const index: number = serverPath.indexOf("/", 2); if (index > 0) { return serverPath.slice(2, index); } else { return serverPath.slice(2); } } return ""; } //Checks to see if the openedPath (in VS Code) is within the workspacePath //specified in the workspace. The funcation needs to ensure we get the //"best" (most specific) match. private pathIsWithin(openedPath: string, workspacePath: string): boolean { //Replace all backslashes with forward slashes on both paths openedPath = openedPath.replace(/\\/g, "/"); workspacePath = workspacePath.replace(/\\/g, "/"); //Add trailing separators to ensure they're included in the lastIndexOf //(e.g., to ensure we match "/path2" with "/path2" and not "/path2" with "/path" first) openedPath = this.addTrailingSeparator(openedPath, "/"); workspacePath = this.addTrailingSeparator(workspacePath, "/"); //Lowercase both paths (TFVC should be case-insensitive) openedPath = openedPath.toLowerCase(); workspacePath = workspacePath.toLowerCase(); return openedPath.startsWith(workspacePath); }; //If the path doesn't end with a separator, add one private addTrailingSeparator(path: string, separator: string): string { if (path[path.length - 1] !== separator) { return path += separator; } return path; } } ================================================ FILE: src/tfvc/commands/getfilecontent.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command calls Print to get the contents of the file at the version provided and returns them as a string * file. *

* This command actually wraps the print command: * print [/version:] */ export class GetFileContent implements ITfvcCommand { private _serverContext: TeamServerContext; private _localPath: string; private _versionSpec: string; private _ignoreFileNotFound: boolean; public constructor(serverContext: TeamServerContext, localPath: string, versionSpec?: string, ignoreFileNotFound?: boolean) { CommandHelper.RequireStringArgument(localPath, "localPath"); this._serverContext = serverContext; this._localPath = localPath; this._versionSpec = versionSpec; this._ignoreFileNotFound = ignoreFileNotFound; } public GetArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("print", this._serverContext) .Add(this._localPath); if (this._versionSpec) { builder.AddSwitchWithValue("version", this._versionSpec, false); } return builder; } public GetOptions(): any { return {}; } public async ParseOutput(executionResult: IExecutionResult): Promise { // Check for "The specified file does not exist at the specified version" (or "No file matches" in case of the EXE) // and write out empty string if (this._ignoreFileNotFound && (CommandHelper.HasError(executionResult, "The specified file does not exist at the specified version") || CommandHelper.HasError(executionResult, "No file matches"))) { // The file doesn't exist, but the ignore flag is set, so we will simply return an emtpy string return ""; } // Throw if any OTHER errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); // Split the lines to take advantage of the WARNing skip logic and rejoin them to return const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout); return lines.join(CommandHelper.GetNewLineCharacter(executionResult.stdout)); } public GetExeArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("view", this._serverContext) .Add(this._localPath); if (this._versionSpec) { builder.AddSwitchWithValue("version", this._versionSpec, false); } return builder; } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } } ================================================ FILE: src/tfvc/commands/getinfo.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { Strings } from "../../helpers/strings"; import { IArgumentProvider, IExecutionResult, IItemInfo, ITfvcCommand } from "../interfaces"; import { TfvcError, TfvcErrorCodes } from "../tfvcerror"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command calls Info which returns local and server information about an item in the workspace. *

* info [/recursive] [/version:] ... */ export class GetInfo implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPaths: string[]; public constructor(serverContext: TeamServerContext, itemPaths: string[]) { CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths"); this._serverContext = serverContext; this._itemPaths = itemPaths; } public GetArguments(): IArgumentProvider { return new ArgumentBuilder("info", this._serverContext) .AddAll(this._itemPaths); } public GetOptions(): any { return {}; } /** * Example of output (Exactly the same for tf.cmd and tf.exe) * Local information: * Local path: D:\tmp\TFVC_1\build.xml * Server path: $/TFVC_1/build.xml * Changeset: 18 * Change: none * Type: file * Server information: * Server path: $/TFVC_1/build.xml * Changeset: 18 * Deletion ID: 0 * Lock: none * Lock owner: * Last modified: Nov 18, 2016 11:10:20 AM * Type: file * File type: windows-1252 * Size: 1385 */ public async ParseOutput(executionResult: IExecutionResult): Promise { // Throw if any errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); const itemInfos: IItemInfo[] = []; if (!executionResult.stdout) { return itemInfos; } const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, true, true); let curMode: string = ""; // "" is local mode, "server" is server mode let curItem: IItemInfo; for (let i: number = 0; i < lines.length; i++) { const line: string = lines[i]; // Check the beginning of a new item // "no items match" means that the item requested was not found. In this case // we will return an empty info object in that item's place. if (line.toLowerCase().startsWith("no items match ") || line.toLowerCase().startsWith("local information:")) { // We are starting a new Info section for the next item. // So, finish off any in progress item and start a new one. curMode = ""; if (curItem !== undefined) { itemInfos.push(curItem); } curItem = { serverItem: undefined, localItem: undefined }; } else if (line.toLowerCase().startsWith("server information:")) { // We finished with the local properties and are starting the server properties curMode = "server "; } else { // Add the property to the current item const colonPos: number = line.indexOf(":"); if (colonPos > 0) { const propertyName: string = this.getPropertyName(curMode + line.slice(0, colonPos).trim().toLowerCase()); if (propertyName) { const propertyValue = colonPos + 1 < line.length ? line.slice(colonPos + 1).trim() : ""; curItem[propertyName] = propertyValue; } } } } if (curItem !== undefined) { itemInfos.push(curItem); } // If all of the info objects are "empty" let's report an error if (itemInfos.length > 0 && itemInfos.length === itemInfos.filter((info) => info.localItem === undefined).length) { throw new TfvcError({ message: Strings.NoMatchesFound, tfvcErrorCode: TfvcErrorCodes.NoItemsMatch, exitCode: executionResult.exitCode }); } return itemInfos; } public GetExeArguments(): IArgumentProvider { return this.GetArguments(); } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } private getPropertyName(name: string): string { switch (name) { case "server path": return "serverItem"; case "local path": return "localItem"; case "server changeset": return "serverVersion"; case "changeset": return "localVersion"; case "change": return "change"; case "type": return "type"; case "server lock": return "lock"; case "server lock owner": return "lockOwner"; case "server deletion id": return "deletionId"; case "server last modified": return "lastModified"; case "server file type": return "fileType"; case "server size": return "fileSize"; } return undefined; } } ================================================ FILE: src/tfvc/commands/getversion.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; import { TfvcError, TfvcErrorCodes } from "../tfvcerror"; import { Strings } from "../../helpers/strings"; import { IButtonMessageItem } from "../../helpers/vscodeutils.interfaces"; import { Constants, TfvcTelemetryEvents } from "../../helpers/constants"; /** * This command calls the command line doing a simple call to get the help for the add command. * The first line of all commands is the version info... * Team Explorer Everywhere Command Line Client (version 14.0.3.201603291047) */ export class GetVersion implements ITfvcCommand { public GetArguments(): IArgumentProvider { return new ArgumentBuilder("add") .AddSwitch("?"); } public GetOptions(): any { return {}; } public async ParseOutput(executionResult: IExecutionResult): Promise { //Ex. Team Explorer Everywhere Command Line Client (Version 14.0.3.201603291047) return await this.getVersion(executionResult, /version\s+([\.\d]+)/i); } public GetExeArguments(): IArgumentProvider { return this.GetArguments(); } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { //Ex. Microsoft (R) TF - Team Foundation Version Control Tool, Version 14.102.25619.0 return await this.getVersion(executionResult, /version\s+([\.\d]+)/i); } private async getVersion(executionResult: IExecutionResult, expression: RegExp): Promise { // Throw if any errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); //Find just the version number and return it. Ex. Microsoft (R) TF - Team Foundation Version Control Tool, Version 14.102.25619.0 //Spanish tf.exe example: "Microsoft (R) TF - Herramienta Control de versiones de Team Foundation, versi�n 14.102.25619.0" //value = "Microsoft (R) TF - Herramienta Control de versiones de Team Foundation, versi�n 14.102.25619.0" //French tf.exe example: "Microsoft (R) TF�- Outil Team Foundation Version Control, version�14.102.25619.0" //value = "" //German tf.exe example: "Microsoft (R) TF - Team Foundation-Versionskontrolltool, Version 14.102.25619.0" //value = "14.102.25619.0" const matches: string[] = executionResult.stdout.match(expression); if (matches) { //Sample tf.exe matches: // Version 15.112.2641.0 // 15.112.2641.0 //Sample tf.cmd matches: // Version 14.114.0.201703081734 // 14.114.0.201703081734 return matches[matches.length - 1]; } else { //If we can't find a version, that's pretty important. Therefore, we throw in this instance. const messageOptions: IButtonMessageItem[] = [{ title : Strings.MoreDetails, url : Constants.NonEnuTfExeConfiguredUrl, telemetryId: TfvcTelemetryEvents.ExeNonEnuConfiguredMoreDetails }]; throw new TfvcError({ message: Strings.NotAnEnuTfCommandLine, messageOptions: messageOptions, tfvcErrorCode: TfvcErrorCodes.NotAnEnuTfCommandLine }); } } } ================================================ FILE: src/tfvc/commands/rename.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command renames the file passed in. * It returns... * rename [/lock:none|checkin|checkout] */ export class Rename implements ITfvcCommand { private _serverContext: TeamServerContext; private _sourcePath: string; private _destinationPath: string; public constructor(serverContext: TeamServerContext, sourcePath: string, destinationPath: string) { CommandHelper.RequireStringArgument(sourcePath, "sourcePath"); CommandHelper.RequireStringArgument(destinationPath, "destinationPath"); this._serverContext = serverContext; this._sourcePath = sourcePath; this._destinationPath = destinationPath; } public GetArguments(): IArgumentProvider { return new ArgumentBuilder("rename", this._serverContext) .Add(this._sourcePath) .Add(this._destinationPath); } public GetOptions(): any { return {}; } /** * Example of output //Zero or one argument //An argument error occurred: rename requires exactly two local or server path arguments. //100 //Source file doesn't exist //The item C:\repos\Tfvc.L2VSCodeExtension.RC\team-extension.log could not be found in your workspace, or you do not have permission to access it. //100 //Single file (no path) //file11.txt //0 //Single file (with path) //folder1: //file11.txt //0 */ public async ParseOutput(executionResult: IExecutionResult): Promise { //Throw if any errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, false, true /*filterEmptyLines*/); let path: string = ""; for (let index: number = 0; index < lines.length; index++) { const line: string = lines[index]; if (CommandHelper.IsFilePath(line)) { path = line; } else { const file: string = this.getFileFromLine(line); return CommandHelper.GetFilePath(path, file); } } return ""; } public GetExeArguments(): IArgumentProvider { return new ArgumentBuilder("rename", this._serverContext, true /* skipCollectionOption */) .Add(this._sourcePath) .Add(this._destinationPath); } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } private getFileFromLine(line: string): string { //There's no prefix on the filename line for the Add command return line; } } ================================================ FILE: src/tfvc/commands/resolveconflicts.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { AutoResolveType, IArgumentProvider, IExecutionResult, ITfvcCommand, IConflict } from "../interfaces"; import { ConflictType } from "../scm/status"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command resolves conflicts based on given auto resolve type * * tf resolve [itemspec] * [/auto:(AutoMerge|TakeTheirs|KeepYours|OverwriteLocal|DeleteConflict|KeepYoursRenameTheirs)] * [/preview] [(/overridetype:overridetype | /converttotype:converttype] [/recursive] [/newname:path] [/noprompt] [/login:username, [password]] */ export class ResolveConflicts implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPaths: string[]; private _autoResolveType: AutoResolveType; public constructor(serverContext: TeamServerContext, itemPaths: string[], autoResolveType: AutoResolveType) { this._serverContext = serverContext; CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths"); CommandHelper.RequireArgument(autoResolveType, "autoResolveType"); this._itemPaths = itemPaths; this._autoResolveType = autoResolveType; } public GetArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("resolve", this._serverContext) .AddAll(this._itemPaths) .AddSwitchWithValue("auto", AutoResolveType[this._autoResolveType], false); return builder; } public GetOptions(): any { return {}; } /** * Outputs the resolved conflicts in the following format: * * Resolved /Users/leantk/tfvc-tfs/tfsTest_01/TestAdd.txt as KeepYours * Resolved /Users/leantk/tfvc-tfs/tfsTest_01/addFold/testHere2 as KeepYours */ public async ParseOutput(executionResult: IExecutionResult): Promise { CommandHelper.ProcessErrors(executionResult); const conflicts: IConflict[] = []; const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, true, true); for (let i: number = 0; i < lines.length; i++) { const line: string = lines[i]; const startIndex: number = line.indexOf("Resolved "); const endIndex: number = line.lastIndexOf(" as "); if (startIndex >= 0 && endIndex > startIndex) { conflicts.push({ localPath: line.slice(startIndex + "Resolved ".length, endIndex), type: ConflictType.RESOLVED, message: line }); } } return conflicts; } public GetExeArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("resolve", this._serverContext, true /* skipCollectionOption */) .AddAll(this._itemPaths) .AddSwitchWithValue("auto", AutoResolveType[this._autoResolveType], false); return builder; } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } } ================================================ FILE: src/tfvc/commands/status.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand, IPendingChange } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; import * as fs from "fs"; /** * This command returns the status of the workspace as a list of pending changes. * NOTE: Currently this command does not support all of the options of the command line *

* status [/workspace:] [/shelveset:] [/format:brief|detailed|xml] [/recursive] [/user:] [/nodetect] [...] */ export class Status implements ITfvcCommand { private _serverContext: TeamServerContext; private _localPaths: string[]; private _ignoreFolders: boolean; public constructor(serverContext: TeamServerContext, ignoreFolders: boolean, localPaths?: string[]) { this._serverContext = serverContext; this._ignoreFolders = ignoreFolders; this._localPaths = localPaths; } public GetArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("status", this._serverContext) .AddSwitchWithValue("format", "xml", false) .AddSwitch("recursive"); if (this._localPaths && this._localPaths.length > 0) { for (let i: number = 0; i < this._localPaths.length; i++) { builder.Add(this._localPaths[i]); } } return builder; } public GetOptions(): any { return {}; } /** * Parses the output of the status command when formatted as xml. * SAMPLE * * * * * * * * * */ public async ParseOutput(executionResult: IExecutionResult): Promise { // Throw if any errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); const changes: IPendingChange[] = []; const xml: string = CommandHelper.TrimToXml(executionResult.stdout); // Parse the xml using xml2js const json: any = await CommandHelper.ParseXml(xml); if (json && json.status) { // get all the pending changes first const pending: any = json.status.pendingchanges[0].pendingchange; if (pending) { for (let i: number = 0; i < pending.length; i++) { this.add(changes, this.convert(pending[i].$, false), this._ignoreFolders); } } // next, get all the candidate pending changes const candidate: any = json.status.candidatependingchanges[0].pendingchange; if (candidate) { for (let i: number = 0; i < candidate.length; i++) { this.add(changes, this.convert(candidate[i].$, true), this._ignoreFolders); } } } return changes; } public GetExeArguments(): IArgumentProvider { //return this.GetArguments(); const builder: ArgumentBuilder = new ArgumentBuilder("status", this._serverContext) .AddSwitchWithValue("format", "detailed", false) .AddSwitch("recursive"); if (this._localPaths && this._localPaths.length > 0) { for (let i: number = 0; i < this._localPaths.length; i++) { builder.Add(this._localPaths[i]); } } return builder; } public GetExeOptions(): any { return this.GetOptions(); } /* Parses the output of the status command when formatted as detailed SAMPLE $/jeyou/README.md;C19 User : Jeff Young (TFS) Date : Wednesday, February 22, 2017 1:47:26 PM Lock : none Change : edit Workspace : jeyou-dev00-tfexe-OnPrem Local item : [JEYOU-DEV00] C:\repos\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\README.md File type : utf-8 ------------------------------------------------------------------------------------------------------------------------------------------------------------- Detected Changes: ------------------------------------------------------------------------------------------------------------------------------------------------------------- $/jeyou/therightstuff.txt User : Jeff Young (TFS) Date : Wednesday, February 22, 2017 11:48:34 AM Lock : none Change : add Workspace : jeyou-dev00-tfexe-OnPrem Local item : [JEYOU-DEV00] C:\repos\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\therightstuff.txt 1 change(s), 0 detected change(s) */ public async ParseExeOutput(executionResult: IExecutionResult): Promise { // Throw if any errors are found in stderr or if exitcode is not 0 CommandHelper.ProcessErrors(executionResult); const changes: IPendingChange[] = []; if (!executionResult.stdout) { return changes; } const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, true, false); //leave empty lines let detectedChanges: boolean = false; let curChange: IPendingChange; for (let i: number = 0; i < lines.length; i++) { const line: string = lines[i]; if (line.indexOf(" detected change(s)") > 0) { //This tells us we're done break; } if (!line || line.trim().length === 0) { //If we have a curChange, we're finished with it if (curChange !== undefined) { changes.push(curChange); curChange = undefined; } continue; } if (line.startsWith("--------") || line.toLowerCase().startsWith("detected changes: ")) { //Starting Detected Changes... detectedChanges = true; continue; } if (line.startsWith("$/")) { //$/jeyou/README.md;C19 //versioned //$/jeyou/README.md //isCandidate const parts: string[] = line.split(";C"); curChange = { changeType: undefined, computer: undefined, date: undefined, localItem: undefined, sourceItem: undefined, lock: undefined, owner: undefined, serverItem: (parts && parts.length >= 1 ? parts[0] : undefined), version: (parts && parts.length === 2 ? parts[1] : "0"), workspace: undefined, isCandidate: detectedChanges }; } else { // Add the property to the current item const colonPos: number = line.indexOf(":"); if (colonPos > 0) { const propertyName = this.getPropertyName(line.slice(0, colonPos).trim().toLowerCase()); if (propertyName) { let propertyValue: string = colonPos + 1 < line.length ? line.slice(colonPos + 1).trim() : ""; if (propertyName.toLowerCase() === "localitem") { //Local item : [JEYOU-DEV00] C:\repos\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\README.md const parts: string[] = propertyValue.split("] "); curChange["computer"] = parts[0].substr(1); //pop off the beginning [ propertyValue = parts[1]; } curChange[propertyName] = propertyValue; } } } } return changes; } private getPropertyName(name: string): string { switch (name) { case "local item": return "localItem"; case "source item": return "sourceItem"; case "user": return "owner"; //TODO: I don't think this is accurate case "date": return "date"; case "lock": return "lock"; case "change": return "changeType"; case "workspace": return "workspace"; } return undefined; } private add(changes: IPendingChange[], newChange: IPendingChange, ignoreFolders: boolean) { // Deleted files won't exist, but we still include them in the results if (ignoreFolders && fs.existsSync(newChange.localItem)) { // check to see if the local item is a file or folder const f: string = newChange.localItem; const stats: any = fs.lstatSync(f); if (stats.isDirectory()) { // It's a directory/folder and we don't want those return; } } changes.push(newChange); } private convert(jsonChange: any, isCandidate: boolean): IPendingChange { // TODO check to make sure jsonChange is valid return { changeType: jsonChange.changetype, computer: jsonChange.computer, date: jsonChange.date, localItem: jsonChange.localitem, sourceItem: jsonChange.sourceitem, lock: jsonChange.lock, owner: jsonChange.owner, serverItem: jsonChange.serveritem, version: jsonChange.version, workspace: jsonChange.workspace, isCandidate: isCandidate }; } } ================================================ FILE: src/tfvc/commands/sync.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand, ISyncResults, ISyncItemResult, SyncType } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command gets the latest version of one or more files or folders * (we add the switch nosummary to make sure that errors only print once) * * tf get [itemspec] [/version:versionspec] [/all] [/overwrite] [/force] [/remap] * [/recursive] [/preview] [/noautoresolve] [/noprompt] * [/login:username,[password]] */ export class Sync implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPaths: string[]; private _recursive: boolean; public constructor(serverContext: TeamServerContext, itemPaths: string[], recursive: boolean) { this._serverContext = serverContext; CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths"); this._itemPaths = itemPaths; this._recursive = recursive; } public GetArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("get", this._serverContext) .AddSwitch("nosummary") .AddAll(this._itemPaths); if (this._recursive) { builder.AddSwitch("recursive"); } return builder; } public GetOptions(): any { return {}; } /** * Example output from TF Get: * D:\tmp\test: * Getting addFold * Getting addFold-branch * * D:\tmp\test\addFold-branch: * Getting testHereRename.txt * * D:\tmp\test\addFold: * Getting testHere3 * Getting testHereRename7.txt * * D:\tmp\test: * Getting Rename2.txt * Getting test3.txt * Conflict test_renamed.txt - Unable to perform the get operation because you have a conflicting rename, edit * Getting TestAdd.txt * */ public async ParseOutput(executionResult: IExecutionResult): Promise { // Any exit code other than 0 or 1 means that something went wrong, so simply throw the error if (executionResult.exitCode !== 0 && executionResult.exitCode !== 1) { CommandHelper.ProcessErrors(executionResult); } // Check for up to date message (slightly different in EXE and CLC) if (/All files( are)? up to date/i.test(executionResult.stdout)) { // There was nothing to download so return an empty result return { hasConflicts: false, hasErrors: false, itemResults: [] }; } else { // Get the item results and any warnings or errors const itemResults: ISyncItemResult[] = this.getItemResults(executionResult.stdout); const errorMessages: ISyncItemResult[] = this.getErrorMessages(executionResult.stderr); return { hasConflicts: errorMessages.filter((err) => err.syncType === SyncType.Conflict).length > 0, hasErrors: errorMessages.filter((err) => err.syncType !== SyncType.Conflict).length > 0, itemResults: itemResults.concat(errorMessages) }; } } public GetExeArguments(): IArgumentProvider { const builder: ArgumentBuilder = new ArgumentBuilder("get", this._serverContext, true /* skipCollectionOption */) .AddSwitch("nosummary") .AddAll(this._itemPaths); if (this._recursive) { builder.AddSwitch("recursive"); } return builder; } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } private getItemResults(stdout: string): ISyncItemResult[] { const itemResults: ISyncItemResult[] = []; let folderPath: string = ""; const lines: string[] = CommandHelper.SplitIntoLines(stdout, true, true); for (let i: number = 0; i < lines.length; i++) { const line = lines[i]; if (CommandHelper.IsFilePath(line)) { folderPath = line; } else if (line) { const sr: ISyncItemResult = this.getSyncResultFromLine(folderPath, line); if (sr) { itemResults.push(sr); } } } return itemResults; } private getSyncResultFromLine(folderPath: string, line: string): ISyncItemResult { if (!line) { return undefined; } let newResult: ISyncItemResult = undefined; if (line.startsWith("Getting ")) { newResult = { syncType: SyncType.New, itemPath: CommandHelper.GetFilePath(folderPath, line.slice("Getting ".length).trim()) }; } else if (line.startsWith("Replacing ")) { newResult = { syncType: SyncType.Updated, itemPath: CommandHelper.GetFilePath(folderPath, line.slice("Replacing ".length).trim()) }; } else if (line.startsWith("Deleting ")) { newResult = { syncType: SyncType.Deleted, itemPath: CommandHelper.GetFilePath(folderPath, line.slice("Deleting ".length).trim()) }; } else if (line.startsWith("Conflict ")) { const dashIndex = line.lastIndexOf("-"); newResult = { syncType: SyncType.Conflict, itemPath: CommandHelper.GetFilePath(folderPath, line.slice("Conflict ".length, dashIndex).trim()), message: line.slice(dashIndex + 1).trim() }; } else if (line.startsWith("Warning ")) { const dashIndex = line.lastIndexOf("-"); newResult = { syncType: SyncType.Warning, itemPath: CommandHelper.GetFilePath(folderPath, line.slice("Warning ".length, dashIndex).trim()), message: line.slice(dashIndex + 1).trim() }; } else { // This must be an error. Usually of the form "filename - message" or "filename cannot be deleted reason" let index: number = line.lastIndexOf("-"); if (index >= 0) { newResult = { syncType: SyncType.Error, itemPath: CommandHelper.GetFilePath(folderPath, line.slice(0, index).trim()), message: line.slice(index + 1).trim() }; } else { index = line.indexOf("cannot be deleted"); if (index >= 0) { newResult = { syncType: SyncType.Warning, itemPath: CommandHelper.GetFilePath(folderPath, line.slice(0, index).trim()), message: line.trim() }; } } } return newResult; } /** * An error will be in one of the following forms: * * Warning - Unable to refresh testHereRename.txt because you have a pending edit. * Conflict TestAdd.txt - Unable to perform the get operation because you have a conflicting edit * new4.txt - Unable to perform the get operation because you have a conflicting rename (to be moved from D:\tmp\folder\new5.txt) * D:\tmp\vscodeBugBash\folder1 cannot be deleted because it is not empty. */ private getErrorMessages(stderr: string): ISyncItemResult[] { const errorMessages: ISyncItemResult[] = []; const lines: string[] = CommandHelper.SplitIntoLines(stderr, false, true); for (let i: number = 0; i < lines.length; i++) { // stderr doesn't get any file path lines, so the files will all be just the filenames errorMessages.push(this.getSyncResultFromLine("", lines[i])); } return errorMessages; } } ================================================ FILE: src/tfvc/commands/undo.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../../contexts/servercontext"; import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces"; import { ArgumentBuilder } from "./argumentbuilder"; import { CommandHelper } from "./commandhelper"; /** * This command undoes the changes to the files passed in. * It returns a list of all files undone. * undo [/recursive] ... */ export class Undo implements ITfvcCommand { private _serverContext: TeamServerContext; private _itemPaths: string[]; public constructor(serverContext: TeamServerContext, itemPaths: string[]) { CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths"); this._serverContext = serverContext; this._itemPaths = itemPaths; } public GetArguments(): IArgumentProvider { // If exactly 1 and it is our wildcard, undo all if (this._itemPaths.length === 1 && this._itemPaths[0] === "*") { return new ArgumentBuilder("undo", this._serverContext) .Add(".") .AddSwitch("recursive"); } return new ArgumentBuilder("undo", this._serverContext) .AddAll(this._itemPaths); } public GetOptions(): any { return {}; } /** * Example of output * Undoing edit: file1.java * Undoing add: file2.java */ public async ParseOutput(executionResult: IExecutionResult): Promise { let lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout, false, true /*filterEmptyLines*/); //If we didn't succeed without any issues, we have a bit of work to do. //'tf undo' can return a non-zero exit code when: // * Some of the files have no pending changes (exitCode === 1) // * All of the files have no pending changes (exitCode === 100) //If some of the files have no pending changes, we want to process the ones that did. //If all of the files have no pending changes, return [] //Otherwise, we assume some error occurred so allow that to be thrown. if (executionResult.exitCode !== 0) { //Remove any entries for which there were no pending changes lines = lines.filter((e) => !e.startsWith("No pending changes ")); if (executionResult.exitCode === 100 && lines.length === 0) { //All of the files had no pending changes, return [] return []; } else if (executionResult.exitCode !== 1) { //Otherwise, some other error occurred, const that be thrown. CommandHelper.ProcessErrors(executionResult); } } const filesUndone: string[] = []; let path: string = ""; for (let index: number = 0; index < lines.length; index++) { const line: string = lines[index]; if (CommandHelper.IsFilePath(line)) { path = line; } else if (line) { const file: string = this.getFileFromLine(line); filesUndone.push(CommandHelper.GetFilePath(path, file)); } } return filesUndone; } public GetExeArguments(): IArgumentProvider { return this.GetArguments(); } public GetExeOptions(): any { return this.GetOptions(); } public async ParseExeOutput(executionResult: IExecutionResult): Promise { return await this.ParseOutput(executionResult); } //line could be 'Undoing edit: file1.txt', 'Undoing add: file1.txt' private getFileFromLine(line: string): string { const prefix: string = ": "; //"Undoing edit: ", "Undoing add: ", etc. const idx: number = line.indexOf(prefix); if (idx > 0) { return line.substring(idx + prefix.length); } } } ================================================ FILE: src/tfvc/interfaces.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { IButtonMessageItem } from "../helpers/vscodeutils.interfaces"; import { ConflictType } from "./scm/status"; export interface ITfCommandLine { path: string; minVersion: string; proxy: string; isExe: boolean; } export interface IItemInfo { serverItem: string; localItem: string; localVersion?: string; serverVersion?: string; change?: string; type?: string; lock?: string; lockOwner?: string; deletionId?: string; lastModified?: string; fileType?: string; fileSize?: string; } export interface ICheckinInfo { comment: string; files: string[]; workItemIds: number[]; } export interface IWorkspace { name: string; server: string; computer?: string; owner?: string; comment?: string; mappings: IWorkspaceMapping[]; defaultTeamProject: string; } export interface IWorkspaceMapping { serverPath: string; localPath: string; cloaked: boolean; } export interface IPendingChange { changeType: string; computer: string; date: string; localItem: string; sourceItem: string; lock: string; owner: string; serverItem: string; version: string; workspace: string; isCandidate: boolean; } export enum SyncType { Updated, New, Deleted, Conflict, Warning, Error } export interface ISyncItemResult { syncType: SyncType; itemPath: string; message?: string; } export interface ISyncResults { hasErrors: boolean; hasConflicts: boolean; itemResults: ISyncItemResult[]; } export interface IConflict { localPath: string; type: ConflictType; message: string; } export enum AutoResolveType { AutoMerge, TakeTheirs, KeepYours, OverwriteLocal, DeleteConflict, KeepYoursRenameTheirs } export interface IExecutionResult { exitCode: number; stdout: string; stderr: string; } export interface ITfvcErrorData { error?: Error; message?: string; messageOptions?: IButtonMessageItem[]; stdout?: string; stderr?: string; exitCode?: number; tfvcErrorCode?: string; tfvcCommand?: string; } export interface IArgumentProvider { AddProxySwitch(proxy: string); GetCommand(): string; GetArguments(): string[]; GetCommandLine(): string; GetArgumentsForDisplay(): string; } export interface ITfvcCommand { GetArguments(): IArgumentProvider; GetExeArguments(): IArgumentProvider; GetOptions(): any; GetExeOptions(): any; ParseOutput(executionResult: IExecutionResult): Promise; ParseExeOutput(executionResult: IExecutionResult): Promise; } ================================================ FILE: src/tfvc/scm/commithoverprovider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { workspace, window, languages, Disposable, Uri, HoverProvider, Hover, TextEditor, Position, TextDocument, Range, TextEditorDecorationType, WorkspaceEdit } from "vscode"; import { filterEvent } from "../util"; const scmInputUri = Uri.parse("scm:input"); function isSCMInput(uri: Uri) { return uri.toString() === scmInputUri.toString(); } interface Diagnostic { range: Range; message: string; } export class CommitHoverProvider implements HoverProvider { private decorationType: TextEditorDecorationType; private diagnostics: Diagnostic[] = []; private disposables: Disposable[] = []; private editor: TextEditor; private visibleTextEditorsDisposable: Disposable; constructor() { this.visibleTextEditorsDisposable = window.onDidChangeVisibleTextEditors(this.onVisibleTextEditors, this); this.onVisibleTextEditors(window.visibleTextEditors); this.decorationType = window.createTextEditorDecorationType({ isWholeLine: true, color: "rgb(228, 157, 43)", dark: { color: "rgb(220, 211, 71)" } }); } public get message(): string | undefined { if (!this.editor) { return; } return this.editor.document.getText(); } public set message(message: string | undefined) { if (!this.editor || message === undefined) { return; } const document = this.editor.document; const start = document.lineAt(0).range.start; const end = document.lineAt(document.lineCount - 1).range.end; const range = new Range(start, end); const edit = new WorkspaceEdit(); edit.replace(scmInputUri, range, message); workspace.applyEdit(edit); } private onVisibleTextEditors(editors: TextEditor[]): void { const [editor] = editors.filter((e) => isSCMInput(e.document.uri)); if (!editor) { return; } this.visibleTextEditorsDisposable.dispose(); this.editor = editor; const onDidChange = filterEvent(workspace.onDidChangeTextDocument, (e) => e.document && isSCMInput(e.document.uri)); onDidChange(this.update, this, this.disposables); workspace.onDidChangeConfiguration(this.update, this, this.disposables); languages.registerHoverProvider({ scheme: "scm" }, this); } private update(): void { this.diagnostics = []; //TODO provide any diagnostic info based on the message here (see git commitcontroller) this.editor.setDecorations(this.decorationType, this.diagnostics.map((d) => d.range)); } /* Implement HoverProvider */ provideHover(document: TextDocument, position: Position): Hover | undefined { const [decoration] = this.diagnostics.filter((d) => d.range.contains(position)); if (!decoration || !document) { return; } return new Hover(decoration.message, decoration.range); } dispose(): void { this.disposables.forEach((d) => d.dispose()); } } ================================================ FILE: src/tfvc/scm/decorationprovider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { SourceControlResourceDecorations, Uri } from "vscode"; import { ConflictType, Status } from "./status"; import * as path from "path"; export class DecorationProvider { private static _iconsRootPath: string = path.join(path.dirname(__dirname), "..", "..", "resources", "icons"); public static getDecorations(statuses: Status[], conflictType?: ConflictType): SourceControlResourceDecorations { const status: Status = this.getDominantStatus(statuses); const light = { iconPath: DecorationProvider.getIconPath(status, "light") }; const dark = { iconPath: DecorationProvider.getIconPath(status, "dark") }; return { strikeThrough: DecorationProvider.useStrikeThrough(status, conflictType), light, dark }; } private static getDominantStatus(statuses: Status[]) { if (!statuses || statuses.length === 0) { return undefined; } // if there's only one just return it if (statuses.length === 1) { return statuses[0]; } // The most dominant types are ADD, EDIT, and DELETE let index: number = statuses.findIndex((s) => s === Status.ADD || s === Status.EDIT || s === Status.DELETE); if (index >= 0) { return statuses[index]; } // The next dominant type is RENAME index = statuses.findIndex((s) => s === Status.RENAME); if (index >= 0) { return statuses[index]; } // After that, just return the first one return statuses[0]; } private static getIconUri(iconName: string, theme: string): Uri { return Uri.file(path.join(DecorationProvider._iconsRootPath, theme, `${iconName}.svg`)); } private static getIconPath(status: Status, theme: string): Uri | undefined { switch (status) { case Status.ADD: return DecorationProvider.getIconUri("status-add", theme); case Status.BRANCH: return DecorationProvider.getIconUri("status-branch", theme); case Status.DELETE: return DecorationProvider.getIconUri("status-delete", theme); case Status.EDIT: return DecorationProvider.getIconUri("status-edit", theme); case Status.LOCK: return DecorationProvider.getIconUri("status-lock", theme); case Status.MERGE: return DecorationProvider.getIconUri("status-merge", theme); case Status.RENAME: return DecorationProvider.getIconUri("status-rename", theme); case Status.UNDELETE: return DecorationProvider.getIconUri("status-undelete", theme); default: return void 0; } } private static useStrikeThrough(status: Status, conflictType: ConflictType): boolean { return (status === Status.DELETE) || (status === Status.MERGE && (conflictType === ConflictType.DELETE || conflictType === ConflictType.DELETE_TARGET)); } } ================================================ FILE: src/tfvc/scm/model.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Uri, EventEmitter, Event, Disposable, ProgressLocation, window } from "vscode"; import { Telemetry } from "../../services/telemetry"; import { TfvcTelemetryEvents } from "../../helpers/constants"; import { TfvcRepository } from "../tfvcrepository"; import { filterEvent } from "../util"; import { Resource } from "./resource"; import { ResourceGroup, IncludedGroup, ExcludedGroup, ConflictsGroup } from "./resourcegroups"; import { IConflict, IPendingChange } from "../interfaces"; import { ConflictType, Status } from "./status"; import { TfvcOutput } from "../tfvcoutput"; import * as _ from "underscore"; import * as path from "path"; export class Model implements Disposable { private _disposables: Disposable[] = []; private _repositoryRoot: string; private _repository: TfvcRepository; private _statusAlreadyInProgress: boolean; private _explicitlyExcluded: string[] = []; private _onDidChange = new EventEmitter(); public get onDidChange(): Event { return this._onDidChange.event; } private _conflictsGroup = new ConflictsGroup([]); private _includedGroup = new IncludedGroup([]); private _excludedGroup = new ExcludedGroup([]); public constructor(repositoryRoot: string, repository: TfvcRepository, onWorkspaceChange: Event) { this._repositoryRoot = repositoryRoot; this._repository = repository; //filterEvent should return false if an event is to be filtered const onNonGitChange = filterEvent(onWorkspaceChange, (uri) => { if (!uri || !uri.fsPath) { return false; } // Ignore files that aren't under this._repositoryRoot (e.g., settings.json) const isSubFolder: boolean = uri.fsPath.normalize().startsWith(path.normalize(this._repositoryRoot)); // Ignore workspace changes that take place in the .tf or $tf folder (where path contains /.tf/ or \$tf\) const isTfFolder: boolean = !/\/\.tf\//.test(uri.fsPath) && !/\\\$tf\\/.test(uri.fsPath); // Attempt to ignore the team-extension.log file directly const isLogFile: boolean = !(path.basename(uri.fsPath) === "team-extension.log"); return isSubFolder && isTfFolder && isLogFile; }); onNonGitChange(this.onFileSystemChange, this, this._disposables); } public dispose() { if (this._disposables) { this._disposables.forEach((d) => d.dispose()); this._disposables = []; } } public get ConflictsGroup(): ConflictsGroup { return this._conflictsGroup; } public get IncludedGroup(): IncludedGroup { return this._includedGroup; } public get ExcludedGroup(): ExcludedGroup { return this._excludedGroup; } public get Resources(): ResourceGroup[] { const result: ResourceGroup[] = []; if (this._conflictsGroup.resources.length > 0) { result.push(this._conflictsGroup); } result.push(this._includedGroup); result.push(this._excludedGroup); return result; } private async status(): Promise { if (this._statusAlreadyInProgress) { return; } this._statusAlreadyInProgress = true; try { await this.run(undefined); } finally { this._statusAlreadyInProgress = false; } } private onFileSystemChange(/*TODO: uri: Uri*/): void { this.status(); } private async run(fn: () => Promise): Promise { return window.withProgress({ location: ProgressLocation.SourceControl }, async () => { if (fn) { await fn(); } else { Promise.resolve(); } await this.update(); }); } //Add the items to the explicitly excluded list. public async Exclude(paths: string[]): Promise { if (paths && paths.length > 0) { paths.forEach((path) => { const normalizedPath: string = path.toLowerCase(); if (!_.contains(this._explicitlyExcluded, normalizedPath)) { this._explicitlyExcluded.push(normalizedPath); } }); await this.update(); } } //Unexclude doesn't explicitly INclude. It defers to the status of the individual item. public async Unexclude(paths: string[]): Promise { if (paths && paths.length > 0) { paths.forEach((path) => { const normalizedPath: string = path.toLowerCase(); if (_.contains(this._explicitlyExcluded, normalizedPath)) { this._explicitlyExcluded = _.without(this._explicitlyExcluded, normalizedPath); } }); await this.update(); } } public async Refresh(): Promise { await this.update(); } private async update(): Promise { const changes: IPendingChange[] = await this._repository.GetStatus(); let foundConflicts: IConflict[] = []; // Without any server context we can't run delete or resolve commands if (this._repository.HasContext) { // Get the list of conflicts //TODO: Optimize out this call unless it is needed. This call takes over 4 times longer than the status call and is unecessary most of the time. foundConflicts = await this._repository.FindConflicts(); foundConflicts.forEach((conflict) => { if (conflict.message) { TfvcOutput.AppendLine(`[Resolve] ${conflict.message}`); } }); } const conflict: IConflict = foundConflicts.find((c) => c.type === ConflictType.NAME_AND_CONTENT || c.type === ConflictType.RENAME); if (conflict) { if (conflict.type === ConflictType.RENAME) { Telemetry.SendEvent(TfvcTelemetryEvents.RenameConflict); } else { Telemetry.SendEvent(TfvcTelemetryEvents.NameAndContentConflict); } } const included: Resource[] = []; const excluded: Resource[] = []; const conflicts: Resource[] = []; changes.forEach((raw) => { const conflict: IConflict = foundConflicts.find((c) => this.conflictMatchesPendingChange(raw, c)); const resource: Resource = new Resource(raw, conflict); if (resource.HasStatus(Status.CONFLICT)) { return conflicts.push(resource); } else { //If explicitly excluded, that has highest priority if (_.contains(this._explicitlyExcluded, resource.resourceUri.fsPath.toLowerCase())) { return excluded.push(resource); } //Versioned changes should always be included (as long as they're not deletes) if (resource.IsVersioned && !resource.HasStatus(Status.DELETE)) { return included.push(resource); } //Pending changes should be included if (!resource.PendingChange.isCandidate) { return included.push(resource); } //Others: //Candidate changes should be excluded return excluded.push(resource); } }); this._conflictsGroup = new ConflictsGroup(conflicts); this._includedGroup = new IncludedGroup(included); this._excludedGroup = new ExcludedGroup(excluded); this._onDidChange.fire(); } private conflictMatchesPendingChange(change: IPendingChange, conflict: IConflict): boolean { let result: boolean = false; if (change && change.localItem && conflict && conflict.localPath) { // TODO: If resource or conflict are renames we have a lot more work to do // We are postponing this work for now until we have evidence that it happens a lot let path2: string = conflict.localPath; // If path2 is relative then assume it is relative to the repo root if (!path.isAbsolute(path2)) { path2 = path.join(this._repositoryRoot, path2); } // First compare the source item result = change.localItem.toLowerCase() === path2.toLowerCase(); } return result; } } ================================================ FILE: src/tfvc/scm/resource.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as path from "path"; import { Command, SourceControlResourceState, SourceControlResourceDecorations, Uri } from "vscode"; import { IConflict, IPendingChange } from "../interfaces"; import { TfvcSCMProvider } from "../tfvcscmprovider"; import { ConflictType, GetStatuses, Status } from "./status"; import { DecorationProvider } from "./decorationprovider"; import { Strings } from "../../helpers/strings"; import { TfvcCommandNames } from "../../helpers/constants"; export class Resource implements SourceControlResourceState { private _uri: Uri; private _statuses: Status[]; private _change: IPendingChange; private _version: string; private _conflictType: ConflictType; constructor(change: IPendingChange, conflict: IConflict) { this._change = change; this._uri = Uri.file(change.localItem); this._statuses = GetStatuses(change.changeType); this._version = change.version; if (conflict) { this._statuses.push(Status.CONFLICT); this._conflictType = conflict.type; } } public get PendingChange(): IPendingChange { return this._change; } public get Statuses(): Status[] { return this._statuses; } public get ConflictType(): ConflictType { return this._conflictType; } public HasStatus(status: Status): boolean { return this._statuses.findIndex((s) => s === status) >= 0; } get IsVersioned(): boolean { return this._version !== "0"; } /** * This method gets a vscode file uri that represents the server path and version that the local item is based on. */ public GetServerUri(): Uri { const serverItem: string = this._change.sourceItem ? this._change.sourceItem : this._change.serverItem; // For conflicts set the version to "T"ip so that we will compare against the latest version const versionSpec: string = this.HasStatus(Status.CONFLICT) ? "T" : "C" + this._change.version; return Uri.file(serverItem).with({ scheme: TfvcSCMProvider.scmScheme, query: versionSpec }); } public GetTitle(): string { const basename = path.basename(this._change.localItem); const sourceBasename = this._change.sourceItem ? path.basename(this._change.sourceItem) : ""; if (this.HasStatus(Status.CONFLICT)) { switch (this._conflictType) { case ConflictType.CONTENT: case ConflictType.MERGE: case ConflictType.RENAME: case ConflictType.NAME_AND_CONTENT: if (this.HasStatus(Status.ADD)) { return `${basename} (${Strings.ConflictAlreadyExists})`; } // Use the default title for all other cases break; case ConflictType.DELETE: return `${basename} (${Strings.ConflictAlreadyDeleted})`; case ConflictType.DELETE_TARGET: return `${basename} (${Strings.ConflictDeletedLocally})`; } } if (this.HasStatus(Status.RENAME)) { return sourceBasename ? `${basename} <- ${sourceBasename}` : `${basename}`; } else if (this.HasStatus(Status.EDIT)) { return `${basename}`; } return ""; } /* Implement SourceControlResourceState */ get resourceUri(): Uri { return this._uri; } get decorations(): SourceControlResourceDecorations { // TODO Add conflict type to the resource constructor and pass it here return DecorationProvider.getDecorations(this._statuses); } //Set the command to invoke when a Resource is selected in the viewlet get command(): Command { return { command: TfvcCommandNames.Open, title: "Open", arguments: [this] }; } } ================================================ FILE: src/tfvc/scm/resourcegroups.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Strings } from "../../helpers/strings"; import { Resource } from "./resource"; export abstract class ResourceGroup { get id(): string { return this._id; } get label(): string { return this._label; } get resources(): Resource[] { return this._resources; } public constructor(private _id: string, private _label: string, private _resources: Resource[]) { } } export class ConflictsGroup extends ResourceGroup { static readonly ID = "conflicts"; constructor(resources: Resource[]) { super(ConflictsGroup.ID, Strings.ConflictsGroupName, resources); } } export class IncludedGroup extends ResourceGroup { static readonly ID = "included"; constructor(resources: Resource[]) { super(IncludedGroup.ID, Strings.IncludedGroupName, resources); } } export class ExcludedGroup extends ResourceGroup { static readonly ID = "excluded"; constructor(resources: Resource[]) { super(ExcludedGroup.ID, Strings.ExcludedGroupName, resources); } } ================================================ FILE: src/tfvc/scm/status.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; export function GetStatuses(statusText: string): Status[] { const result: Status[] = []; if (!statusText) { return result; } const statusStrings: string[] = statusText.split(","); for (let i: number = 0; i < statusStrings.length; i++) { switch (statusStrings[i].trim().toLowerCase()) { case "add": result.push(Status.ADD); break; case "branch": result.push(Status.BRANCH); break; case "delete": result.push(Status.DELETE); break; case "edit": result.push(Status.EDIT); break; case "lock": result.push(Status.LOCK); break; case "merge": result.push(Status.MERGE); break; case "rename": result.push(Status.RENAME); break; case "source rename": result.push(Status.RENAME); break; case "undelete": result.push(Status.UNDELETE); break; default: result.push(Status.UNKNOWN); break; } } return result; } export enum Status { ADD, RENAME, EDIT, DELETE, UNDELETE, LOCK, BRANCH, MERGE, CONFLICT, UNKNOWN } export enum ConflictType { CONTENT, RENAME, DELETE, DELETE_TARGET, NAME_AND_CONTENT, MERGE, RESOLVED } ================================================ FILE: src/tfvc/scm/tfvccontentprovider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { workspace, Uri, Disposable, Event, EventEmitter } from "vscode"; import { TfvcSCMProvider } from "../tfvcscmprovider"; import { TfvcRepository } from "../tfvcrepository"; import { TfvcTelemetryEvents } from "../../helpers/constants"; import { Telemetry } from "../../services/telemetry"; export class TfvcContentProvider { private _tfvcRepository: TfvcRepository; private _rootPath: string; private _disposables: Disposable[] = []; private _onDidChangeEmitter = new EventEmitter(); get onDidChange(): Event { return this._onDidChangeEmitter.event; } constructor(repository: TfvcRepository, rootPath: string, onTfvcChange: Event) { this._tfvcRepository = repository; this._rootPath = rootPath; this._disposables.push( onTfvcChange(this.fireChangeEvents, this), workspace.registerTextDocumentContentProvider(TfvcSCMProvider.scmScheme, this) ); } private fireChangeEvents(): void { //TODO need to understand why these events are needed and how the list of uris should be purged // Currently firing these events creates an infinite loop //for (let uri of this.uris) { // this.onDidChangeEmitter.fire(uri); //} } async provideTextDocumentContent(uri: Uri): Promise { let path: string = uri.fsPath; const versionSpec: string = uri.query; if (versionSpec.toLowerCase() === "c0") { // Changeset 0 does not exist. This is most likely an Add, so just return empty contents return ""; } // If path is a server path, we need to fix the format // First option is Windows, second is Mac if (path && (path.startsWith("\\$\\") || path.startsWith("/$/"))) { // convert "/$/proj/folder/file" to "$/proj/folder/file"; path = uri.path.slice(1); } try { Telemetry.SendEvent(this._tfvcRepository.IsExe ? TfvcTelemetryEvents.GetFileContentExe : TfvcTelemetryEvents.GetFileContentClc); const contents: string = await this._tfvcRepository.GetFileContent(path, versionSpec); return contents; } catch (err) { return ""; } } dispose(): void { if (this._disposables) { this._disposables.forEach((d) => d.dispose()); this._disposables = []; } } } ================================================ FILE: src/tfvc/tfcommandlinerunner.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as cp from "child_process"; import { TeamServerContext } from "../contexts/servercontext"; import { Constants, TelemetryEvents } from "../helpers/constants"; import { Logger } from "../helpers/logger"; import { Strings } from "../helpers/strings"; import { IButtonMessageItem } from "../helpers/vscodeutils.interfaces"; import { IDisposable, toDisposable, dispose } from "./util"; import { IArgumentProvider, IExecutionResult, ITfCommandLine } from "./interfaces"; import { TfvcError, TfvcErrorCodes } from "./tfvcerror"; import { TfvcRepository } from "./tfvcrepository"; import { TfvcSettings } from "./tfvcsettings"; import { TfvcVersion } from "./tfvcversion"; import { TfvcOutput } from "./tfvcoutput"; import * as _ from "underscore"; import * as fs from "fs"; import * as path from "path"; /** * This is a static class that facilitates running the TFVC command line. * To use this class create a repository object or call Exec directly. */ export class TfCommandLineRunner { /** * Call this method to get the repository object that allows you to perform TFVC commands. */ public static CreateRepository(serverContext: TeamServerContext, repositoryRootFolder: string, env: any = {}): TfvcRepository { const tfvc: ITfCommandLine = TfCommandLineRunner.GetCommandLine(); return new TfvcRepository(serverContext, tfvc, repositoryRootFolder, env, tfvc.isExe); } public static GetCommandLine(localPath?: string): ITfCommandLine { Logger.LogDebug(`TFVC Creating Tfvc object with localPath='${localPath}'`); // Get Proxy from settings const settings: TfvcSettings = new TfvcSettings(); const proxy: string = settings.Proxy; Logger.LogDebug(`Using TFS proxy: ${proxy}`); let tfvcPath: string = localPath; if (!tfvcPath) { // get the location from settings tfvcPath = settings.Location; Logger.LogDebug(`TFVC Retrieved from settings; localPath='${tfvcPath}'`); if (!tfvcPath) { Logger.LogWarning(`TFVC Couldn't find where the TF command lives on disk.`); throw new TfvcError({ message: Strings.TfvcLocationMissingError, tfvcErrorCode: TfvcErrorCodes.LocationMissing }); } } // check to make sure that the file exists in that location const exists: boolean = fs.existsSync(tfvcPath); if (exists) { // if it exists, check to ensure that it's a file and not a folder const stats: fs.Stats = fs.lstatSync(tfvcPath); if (!stats || (!stats.isFile() && !stats.isSymbolicLink())) { Logger.LogWarning(`TFVC ${tfvcPath} exists but isn't a file or symlink.`); throw new TfvcError({ message: Strings.TfMissingError, tfvcErrorCode: TfvcErrorCodes.NotFound }); } } else { Logger.LogWarning(`TFVC ${tfvcPath} does not exist.`); throw new TfvcError({ message: Strings.TfMissingError, tfvcErrorCode: TfvcErrorCodes.NotFound }); } // Determine the min version const isExe: boolean = path.extname(tfvcPath).toLowerCase() === ".exe"; let minVersion: string = "14.0.4"; //CLC min version if (isExe) { minVersion = "14.102.0"; //Minimum tf.exe version } return { path: tfvcPath, minVersion: minVersion, isExe: isExe, proxy: proxy }; } /** * This method checks the version of the CLC against the minimum version that we expect. * It throws an error if the version does not meet or exceed the minimum. */ public static CheckVersion(tfvc: ITfCommandLine, version: string): void { if (!version) { // If the version isn't set just return Logger.LogDebug(`TFVC CheckVersion called without a version.`); return; } // check the version of TFVC command line Logger.LogDebug(`TFVC Minimum required version: ${tfvc.minVersion}`); Logger.LogDebug(`TFVC (TF.exe, TF.cmd) version: ${version}`); const minVersion: TfvcVersion = TfvcVersion.FromString(tfvc.minVersion); const curVersion: TfvcVersion = TfvcVersion.FromString(version); if (TfvcVersion.Compare(curVersion, minVersion) < 0) { Logger.LogWarning(`TFVC ${version} is less that the min version of ${tfvc.minVersion}.`); let options: IButtonMessageItem[] = []; if (tfvc.isExe) { //Provide more information on how to update tf.exe to the minimum version required options = [{ title : Strings.VS2015Update3CSR, url : Constants.VS2015U3CSRUrl, telemetryId: TelemetryEvents.VS2015U3CSR }]; } throw new TfvcError({ message: `${Strings.TfVersionWarning}${minVersion.ToString()}`, messageOptions: options, tfvcErrorCode: TfvcErrorCodes.MinVersionWarning }); } } public static async Exec(tfvc: ITfCommandLine, cwd: string, args: IArgumentProvider, options: any = {}): Promise { // default to the cwd passed in, but allow options.cwd to overwrite it options = _.extend({ cwd }, options || {}); // TODO: do we want to handle proxies or not for the EXE? for tf.exe the user could simply setup the proxy at the command line. // tf.exe remembers the proxy settings and uses them as it needs to. if (tfvc.proxy && !tfvc.isExe) { args.AddProxySwitch(tfvc.proxy); } Logger.LogDebug(`TFVC: tf ${args.GetArgumentsForDisplay()}`); if (options.log !== false) { TfvcOutput.AppendLine(`tf ${args.GetArgumentsForDisplay()}`); } return await TfCommandLineRunner.run(tfvc, args, options, tfvc.isExe); } public static DisposeStatics() { if (TfCommandLineRunner._runningInstance) { TfCommandLineRunner._runningInstance.kill(); TfCommandLineRunner._runningInstance = undefined; } } /********************************************************************************************* * The following private methods manage the TF process that we cache for faster load times. * The static members are that cache. *********************************************************************************************/ private static _location: string; private static _options: any; private static _runningInstance: cp.ChildProcess; /** * The Run method will attempt to use the cached TF process, if possible, to run the command and then * return the results. Whether it uses the cached one or starts a new TF process, we will immediately start * a new TF instance and for later use. */ private static async run(tfvc: ITfCommandLine, args: IArgumentProvider, options: any, isExe: boolean): Promise { const start: number = new Date().getTime(); const tfInstance: cp.ChildProcess = await TfCommandLineRunner.getMatchingTfInstance(tfvc, options); // now that we have the matching one, start a new process (but don't wait on it to finish) TfCommandLineRunner.startNewTfInstance(tfvc, options); // Use the tf instance to perform the command const argsForStandardInput: string = args.GetCommandLine(); const result: IExecutionResult = await TfCommandLineRunner.runCommand(argsForStandardInput, tfInstance, isExe); // log the results const end: number = new Date().getTime(); Logger.LogDebug(`TFVC: ${args.GetCommand()} exit code: ${result.exitCode} (duration: ${end - start}ms)`); return result; } /** * Currently we only cache one TF process. If that process matches the tfvc location and options of the process which * has been requested, we simply return the cached instance. * If there isn't a match or there isn't one cached, we kill any existing running instance and created a new one. */ private static async getMatchingTfInstance(tfvc: ITfCommandLine, options: any): Promise { if (!TfCommandLineRunner._runningInstance || tfvc.path !== TfCommandLineRunner._location || !TfCommandLineRunner.optionsMatch(options, TfCommandLineRunner._options)) { if (TfCommandLineRunner._runningInstance) { TfCommandLineRunner._runningInstance.kill(); } // spawn a new instance of TF with these options return await TfCommandLineRunner.startNewTfInstance(tfvc, options); } // return the cached instance return TfCommandLineRunner._runningInstance; } private static async startNewTfInstance(tfvc: ITfCommandLine, options: any): Promise { // Start up a new instance of TF for later use TfCommandLineRunner._options = options; TfCommandLineRunner._location = tfvc.path; TfCommandLineRunner._runningInstance = await TfCommandLineRunner.spawn(tfvc.path, options); return TfCommandLineRunner._runningInstance; } private static optionsMatch(options1: any, options2: any): boolean { return (!options1 && !options2) || (options1.cwd === options2.cwd); } private static async spawn(location: string, options: any): Promise { if (!options) { options = {}; } options.env = _.assign({}, process.env, options.env || {}); const start: number = new Date().getTime(); options.stdio = ["pipe", "pipe", "pipe"]; const child: cp.ChildProcess = await cp.spawn(location, ["@"], options); const end: number = new Date().getTime(); Logger.LogDebug(`TFVC: spawned new process (duration: ${end - start}ms)`); return child; } private static async runCommand(argsForStandardInput: string, child: cp.ChildProcess, isExe: boolean): Promise { const disposables: IDisposable[] = []; child.stdin.end(argsForStandardInput, "utf8"); const once = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { ee.once(name, fn); disposables.push(toDisposable(() => ee.removeListener(name, fn))); }; const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { ee.on(name, fn); disposables.push(toDisposable(() => ee.removeListener(name, fn))); }; const [exitCode, stdout, stderr] = await Promise.all([ new Promise((c, e) => { once(child, "error", e); once(child, "exit", c); }), new Promise((c) => { const buffers: string[] = []; on(child.stdout, "data", (b) => { buffers.push(b); }); once(child.stdout, "close", () => { let stdout: string = buffers.join(""); if (isExe) { // TF.exe repeats the command line as part of the standard out when using the @ response file options // So, we look for the noprompt option to allow us to know where that line is so we can strip it off const start: number = stdout.indexOf("-noprompt"); if (start >= 0) { const end: number = stdout.indexOf("\n", start); stdout = stdout.slice(end + 1); } } c(stdout); }); }), new Promise((c) => { const buffers: string[] = []; on(child.stderr, "data", (b) => buffers.push(b)); once(child.stderr, "close", () => c(buffers.join(""))); }) ]); dispose(disposables); return { exitCode, stdout, stderr }; } } ================================================ FILE: src/tfvc/tfvc-extension.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as path from "path"; import { commands, Uri, window } from "vscode"; import { RepositoryType } from "../contexts/repositorycontext"; import { TfvcContext } from "../contexts/tfvccontext"; import { ExtensionManager } from "../extensionmanager"; import { TfvcCommandNames, TfvcTelemetryEvents } from "../helpers/constants"; import { Strings } from "../helpers/strings"; import { UrlBuilder } from "../helpers/urlbuilder"; import { Utils } from "../helpers/utils"; import { VsCodeUtils } from "../helpers/vscodeutils"; import { IButtonMessageItem } from "../helpers/vscodeutils.interfaces"; import { Telemetry } from "../services/telemetry"; import { Resource } from "./scm/resource"; import { Status } from "./scm/status"; import { TfvcSCMProvider } from "./tfvcscmprovider"; import { TfvcErrorCodes } from "./tfvcerror"; import { TfvcRepository } from "./tfvcrepository"; import { UIHelper } from "./uihelper"; import { AutoResolveType, ICheckinInfo, IItemInfo, ISyncResults } from "./interfaces"; import { TfvcOutput } from "./tfvcoutput"; export class TfvcExtension { private _repo: TfvcRepository; private _manager: ExtensionManager; constructor(manager: ExtensionManager) { this._manager = manager; } public async Checkin(): Promise { this.displayErrors( async () => { // get the checkin info from the SCM viewlet const checkinInfo: ICheckinInfo = TfvcSCMProvider.GetCheckinInfo(); if (!checkinInfo) { window.showInformationMessage(Strings.NoChangesToCheckin); return; } Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.CheckinExe : TfvcTelemetryEvents.CheckinClc); const changeset: string = await this._repo.Checkin(checkinInfo.files, checkinInfo.comment, checkinInfo.workItemIds); TfvcOutput.AppendLine(`Changeset ${changeset} checked in.`); TfvcSCMProvider.ClearCheckinMessage(); TfvcSCMProvider.Refresh(); }, "Checkin"); } /** * This command runs a delete command on the selected file. It gets a Uri object from vscode. */ public async Delete(uri?: Uri): Promise { this.displayErrors( async () => { if (uri) { const basename: string = path.basename(uri.fsPath); try { const message: string = `Are you sure you want to delete '${basename}'?`; if (await UIHelper.PromptForConfirmation(message, Strings.DeleteFile)) { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.DeleteExe : TfvcTelemetryEvents.DeleteClc); await this._repo.Delete([uri.fsPath]); } } catch (err) { //Provide a better error message if the file to be deleted isn't in the workspace (e.g., it's a new file) if (err.tfvcErrorCode && err.tfvcErrorCode === TfvcErrorCodes.FileNotInWorkspace) { this._manager.DisplayErrorMessage(`Cannot delete '${basename}' as it is not in your workspace.`); } else { throw err; } } } else { this._manager.DisplayWarningMessage(Strings.CommandRequiresExplorerContext); } }, "Delete"); } public async Exclude(resources?: Resource[]): Promise { this.displayErrors( async () => { if (resources && resources.length > 0) { //Keep an in-memory list of items that were explicitly excluded. The list is not persisted at this time. const paths: string[] = []; resources.forEach((resource) => { paths.push(resource.resourceUri.fsPath); }); await TfvcSCMProvider.Exclude(paths); } }, "Exclude"); } public async Include(resources?: Resource[]): Promise { this.displayErrors( async () => { if (resources && resources.length > 0) { const pathsToUnexclude: string[] = []; const pathsToAdd: string[] = []; const pathsToDelete: string[] = []; resources.forEach((resource) => { const path: string = resource.resourceUri.fsPath; //Unexclude each file passed in pathsToUnexclude.push(path); //At this point, an unversioned file could be a candidate file, so call Add. //Once it is added, it should be a Pending change. if (!resource.IsVersioned) { pathsToAdd.push(path); } //If a file is a candidate change and has been deleted (e.g., outside of //the TFVC command), we need to ensure that it gets 'tf delete' run on it. if (resource.PendingChange.isCandidate && resource.HasStatus(Status.DELETE)) { pathsToDelete.push(path); } }); //If we need to add files, run a single Add with those files if (pathsToAdd.length > 0) { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.AddExe : TfvcTelemetryEvents.AddClc); await this._repo.Add(pathsToAdd); } //If we need to delete files, run a single Delete with those files if (pathsToDelete.length > 0) { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.DeleteExe : TfvcTelemetryEvents.DeleteClc); await this._repo.Delete(pathsToDelete); } //Otherwise, ensure its not in the explicitly excluded list (if it's already there) //Unexclude doesn't explicitly INclude. It defers to the status of the individual item. await TfvcSCMProvider.Unexclude(pathsToUnexclude); } }, "Include"); } /** * This is the default action when an resource is clicked in the viewlet. * For ADD, AND UNDELETE just show the local file. * For DELETE just show the server file. * For EDIT AND RENAME show the diff window (server on left, local on right). */ public async Open(resource?: Resource): Promise { this.displayErrors( async () => { if (resource) { const left: Uri = TfvcSCMProvider.GetLeftResource(resource); const right: Uri = TfvcSCMProvider.GetRightResource(resource); const title: string = resource.GetTitle(); if (!right) { // TODO console.error("oh no"); return; } if (!left) { return await commands.executeCommand("vscode.open", right); } return await commands.executeCommand("vscode.diff", left, right, title); } }, "Open"); } public async OpenDiff(resource?: Resource): Promise { this.displayErrors( async () => { if (resource) { return await TfvcSCMProvider.OpenDiff(resource); } }, "OpenDiff"); } public async OpenFile(resource?: Resource): Promise { this.displayErrors( async () => { if (resource) { return await commands.executeCommand("vscode.open", resource.resourceUri); } }, "OpenFile"); } public async Refresh(): Promise { this.displayErrors( async () => { await TfvcSCMProvider.Refresh(); }, "Refresh"); } /** * This command runs a rename command on the selected file. It gets a Uri object from vscode. */ public async Rename(uri?: Uri): Promise { this.displayErrors( async () => { if (uri) { const basename: string = path.basename(uri.fsPath); const newFilename: string = await window.showInputBox({ value: basename, prompt: Strings.RenamePrompt, placeHolder: undefined, password: false }); if (newFilename && newFilename !== basename) { const dirName: string = path.dirname(uri.fsPath); const destination: string = path.join(dirName, newFilename); try { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.RenameExe : TfvcTelemetryEvents.RenameClc); await this._repo.Rename(uri.fsPath, destination); } catch (err) { //Provide a better error message if the file to be renamed isn't in the workspace (e.g., it's a new file) if (err.tfvcErrorCode && err.tfvcErrorCode === TfvcErrorCodes.FileNotInWorkspace) { this._manager.DisplayErrorMessage(`Cannot rename '${basename}' as it is not in your workspace.`); } else { throw err; } } } } else { this._manager.DisplayWarningMessage(Strings.CommandRequiresExplorerContext); } }, "Rename"); } public async Resolve(resource: Resource, autoResolveType: AutoResolveType): Promise { this.displayErrors( async () => { if (resource) { const localPath: string = resource.resourceUri.fsPath; const resolveTypeString: string = UIHelper.GetDisplayTextForAutoResolveType(autoResolveType); const basename: string = path.basename(localPath); const message: string = `Are you sure you want to resolve changes in '${basename}' as ${resolveTypeString}?`; if (await UIHelper.PromptForConfirmation(message, resolveTypeString)) { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.ResolveConflictsExe : TfvcTelemetryEvents.ResolveConflictsClc); await this._repo.ResolveConflicts([localPath], autoResolveType); TfvcSCMProvider.Refresh(); } } else { this._manager.DisplayWarningMessage(Strings.CommandRequiresFileContext); } }, "Resolve"); } public async ShowOutput(): Promise { TfvcOutput.Show(); } /** * This command runs a 'tf get' command on the VSCode workspace folder and * displays the results to the user. */ public async Sync(): Promise { this.displayErrors( async () => { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.SyncExe : TfvcTelemetryEvents.SyncClc); const results: ISyncResults = await this._repo.Sync([this._repo.Path], true); await UIHelper.ShowSyncResults(results, results.hasConflicts || results.hasErrors, true); }, "Sync"); } /** * This command runs an undo command on the currently open file in the VSCode workspace folder and * editor. If the undo command applies to the file, the pending changes will be undone. The * file system watcher will update the UI soon thereafter. No results are displayed to the user. */ public async Undo(resources?: Resource[]): Promise { this.displayErrors( async () => { if (resources) { const pathsToUndo: string[] = []; resources.forEach((resource) => { pathsToUndo.push(resource.resourceUri.fsPath); }); //When calling from UI, we have the uri of the resource from which the command was invoked if (pathsToUndo.length > 0) { const basename: string = path.basename(pathsToUndo[0]); let message: string = `Are you sure you want to undo changes to '${basename}'?`; if (pathsToUndo.length > 1) { message = `Are you sure you want to undo changes to ${pathsToUndo.length.toString()} files?`; } if (await UIHelper.PromptForConfirmation(message, Strings.UndoChanges)) { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.UndoExe : TfvcTelemetryEvents.UndoClc); await this._repo.Undo(pathsToUndo); } } } }, "Undo"); } /** * This command runs an undo command on all of the currently open files in the VSCode workspace folder * If the undo command applies to the file, the pending changes will be undone. The * file system watcher will update the UI soon thereafter. No results are displayed to the user. */ public async UndoAll(): Promise { this.displayErrors( async () => { if (TfvcSCMProvider.HasItems()) { const message: string = `Are you sure you want to undo all changes?`; if (await UIHelper.PromptForConfirmation(message, Strings.UndoChanges)) { Telemetry.SendEvent(this._repo.IsExe ? TfvcTelemetryEvents.UndoAllExe : TfvcTelemetryEvents.UndoAllClc); await this._repo.Undo(["*"]); } } else { window.showInformationMessage(Strings.NoChangesToUndo); return; } }, "UndoAll"); } /** * This command runs the info command on the passed in itemPath and * opens a web browser to the appropriate history page. */ public async ViewHistory(): Promise { //Since this command provides Team Services functionality, we need //to ensure it is initialized for Team Services if (!this._manager.EnsureInitialized(RepositoryType.TFVC)) { this._manager.DisplayErrorMessage(); return; } try { let itemPath: string; const editor = window.activeTextEditor; //Get the path to the file open in the VSCode editor (if any) if (editor) { itemPath = editor.document.fileName; } if (!itemPath) { //If no file open in editor, just display the history url of the entire repo this.showRepositoryHistory(); return; } const itemInfos: IItemInfo[] = await this._repo.GetInfo([itemPath]); //With a single file, show that file's history if (itemInfos && itemInfos.length === 1) { Telemetry.SendEvent(TfvcTelemetryEvents.OpenFileHistory); const serverPath: string = itemInfos[0].serverItem; const file: string = encodeURIComponent(serverPath); let historyUrl: string = UrlBuilder.Join(this._manager.RepoContext.RemoteUrl, "_versionControl"); historyUrl = UrlBuilder.AddQueryParams(historyUrl, `path=${file}`, `_a=history`); Utils.OpenUrl(historyUrl); return; } else { //If the file is in the workspace folder (but not mapped), just display the history url of the entire repo this.showRepositoryHistory(); } } catch (err) { if (err.tfvcErrorCode && err.tfvcErrorCode === TfvcErrorCodes.FileNotInMappings) { //If file open in editor is not in the mappings, just display the history url of the entire repo this.showRepositoryHistory(); } else { this._manager.DisplayErrorMessage(err.message); } } } private async displayErrors(funcToTry: (prefix) => Promise, prefix: string): Promise { if (!this._manager.EnsureInitializedForTFVC()) { this._manager.DisplayErrorMessage(); return; } //This occurs in the case where we 1) sign in successfully, 2) sign out, 3) sign back in but with invalid credentials //Essentially, the tfvcExtension.InitializeClients call hasn't been made successfully yet. if (!this._repo) { this._manager.DisplayErrorMessage(Strings.UserMustSignIn); return; } try { await funcToTry(prefix); } catch (err) { let messageOptions: IButtonMessageItem[] = []; TfvcOutput.AppendLine(Utils.FormatMessage(`[${prefix}] ${err.message}`)); //If we also have text in err.stdout, provide that to the output channel if (err.stdout) { //TODO: perhaps just for 'Checkin'? Or the CLC? TfvcOutput.AppendLine(Utils.FormatMessage(`[${prefix}] ${err.stdout}`)); } //If an exception provides its own messageOptions, use them if (err.messageOptions && err.messageOptions.length > 0) { messageOptions = err.messageOptions; } else { messageOptions.push({ title : Strings.ShowTfvcOutput, command: TfvcCommandNames.ShowOutput }); } VsCodeUtils.ShowErrorMessage(err.message, ...messageOptions); } } public async InitializeClients(repoType: RepositoryType): Promise { //We only need to initialize for Tfvc repositories if (repoType !== RepositoryType.TFVC) { return; } const tfvcContext: TfvcContext = this._manager.RepoContext; this._repo = tfvcContext.TfvcRepository; } private showRepositoryHistory(): void { Telemetry.SendEvent(TfvcTelemetryEvents.OpenRepositoryHistory); let historyUrl: string = UrlBuilder.Join(this._manager.RepoContext.RemoteUrl, "_versionControl"); historyUrl = UrlBuilder.AddQueryParams(historyUrl, `_a=history`); Utils.OpenUrl(historyUrl); } dispose() { // nothing to dispose } } ================================================ FILE: src/tfvc/tfvcerror.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Strings } from "../helpers/strings"; import { IButtonMessageItem } from "../helpers/vscodeutils.interfaces"; import { ITfvcErrorData } from "./interfaces"; export class TfvcError { error: Error; message: string; messageOptions: IButtonMessageItem[] = []; stdout: string; stderr: string; exitCode: number; tfvcErrorCode: string; tfvcCommand: string; public static CreateArgumentMissingError(argumentName: string): TfvcError { return new TfvcError({ // This is a developer error - no need to localize message: `Argument is required: ${argumentName}`, tfvcErrorCode: TfvcErrorCodes.MissingArgument }); } /** * Only throw this error in the case where you detect an invalid state and cannot continue. */ public static CreateInvalidStateError(): TfvcError { return new TfvcError({ message: "The TFVC SCMProvider is in an invalid state for this action.", tfvcErrorCode: TfvcErrorCodes.InInvalidState }); } public static CreateUnknownError(err: Error) { return new TfvcError({ error: err, message: err.message, tfvcErrorCode: TfvcErrorCodes.UnknownError }); } public constructor(data: ITfvcErrorData) { if (!data) { throw TfvcError.CreateArgumentMissingError("data"); } if (data.error) { this.error = data.error; this.message = data.error.message; } else { this.error = undefined; } this.message = this.message || data.message || Strings.TfExecFailedError; this.messageOptions = data.messageOptions || []; this.stdout = data.stdout; this.stderr = data.stderr; this.exitCode = data.exitCode; this.tfvcErrorCode = data.tfvcErrorCode; this.tfvcCommand = data.tfvcCommand; } public toString(): string { let result = this.message + " Details: " + `exitCode: ${this.exitCode}, ` + `errorCode: ${this.tfvcErrorCode}, ` + `command: ${this.tfvcCommand}, ` + `stdout: ${this.stdout}, ` + `stderr: ${this.stderr}`; if (this.error) { result += " Stack: " + (this.error).stack; } return result; } } export class TfvcErrorCodes { public static get MissingArgument(): string { return "MissingArgument"; } public static get AuthenticationFailed(): string { return "AuthenticationFailed"; } public static get NotAuthorizedToAccess(): string { return "NotAuthorizedToAccess"; } public static get NotATfvcRepository(): string { return "NotATfvcRepository"; } public static get NotAnEnuTfCommandLine(): string { return "NotAnEnuTfCommandLine"; } public static get LocationMissing(): string { return "TfvcLocationMissing"; } public static get NotFound(): string { return "TfvcNotFound"; } public static get MinVersionWarning(): string { return "TfvcMinVersionWarning"; } public static get RepositoryNotFound(): string { return "RepositoryNotFound"; } public static get FileNotInMappings(): string { return "FileNotInMappings"; } public static get FileNotInWorkspace(): string { return "FileNotInWorkspace"; } public static get InInvalidState(): string { return "TfvcInInvalidState"; } public static get NoItemsMatch(): string { return "TfvcNoItemsMatch"; } public static get UnknownError(): string { return "UnknownError"; } public static get WorkspaceNotKnownToClc(): string { return "WorkspaceNotKnownToClc"; } }; ================================================ FILE: src/tfvc/tfvcoutput.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Disposable, OutputChannel, window } from "vscode"; export class TfvcOutput { private static _outputChannel: OutputChannel; public static async CreateChannel(disposables: Disposable[]): Promise { if (TfvcOutput._outputChannel !== undefined) { return; } TfvcOutput._outputChannel = window.createOutputChannel("TFVC"); if (disposables) { disposables.push(TfvcOutput._outputChannel); } } public static AppendLine(line: string) { if (TfvcOutput._outputChannel) { TfvcOutput._outputChannel.append(line + "\n"); } } public static Show() { if (TfvcOutput._outputChannel) { TfvcOutput._outputChannel.show(); } } } ================================================ FILE: src/tfvc/tfvcrepository.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext} from "../contexts/servercontext"; import { Logger } from "../helpers/logger"; import { ITfvcCommand, IExecutionResult } from "./interfaces"; import { TfCommandLineRunner } from "./tfcommandlinerunner"; import { AutoResolveType, IArgumentProvider, IConflict, IItemInfo, IPendingChange, ISyncResults, ITfCommandLine, IWorkspace } from "./interfaces"; import { Add } from "./commands/add"; import { Checkin } from "./commands/checkin"; import { Delete } from "./commands/delete"; import { FindConflicts } from "./commands/findconflicts"; import { FindWorkspace } from "./commands/findworkspace"; import { GetInfo } from "./commands/getinfo"; import { GetFileContent } from "./commands/getfilecontent"; import { GetVersion } from "./commands/getversion"; import { Rename } from "./commands/rename"; import { ResolveConflicts } from "./commands/resolveconflicts"; import { Status } from "./commands/status"; import { Sync } from "./commands/sync"; import { Undo } from "./commands/undo"; import { TfvcSettings } from "./tfvcsettings"; import * as _ from "underscore"; /** * The Repository class allows you to perform TFVC commands on the workspace represented * by the repositoryRootFolder. */ export class TfvcRepository { private _serverContext: TeamServerContext; private _tfCommandLine: ITfCommandLine; private _repositoryRootFolder: string; private _env: any; private _versionAlreadyChecked: boolean = false; private _settings: TfvcSettings; private _isExe: boolean = false; public constructor(serverContext: TeamServerContext, tfCommandLine: ITfCommandLine, repositoryRootFolder: string, env: any = {}, isExe: boolean) { Logger.LogDebug(`TFVC Repository created with repositoryRootFolder='${repositoryRootFolder}'`); this._serverContext = serverContext; this._tfCommandLine = tfCommandLine; this._repositoryRootFolder = repositoryRootFolder; this._env = env; this._isExe = isExe; this._settings = new TfvcSettings(); // Add the environment variables that we need to make sure the CLC runs as fast as possible and // provides English strings back to us to parse. this._env.TF_NOTELEMETRY = "TRUE"; this._env.TF_ADDITIONAL_JAVA_ARGS = "-Duser.country=US -Duser.language=en"; } public get TfvcLocation(): string { return this._tfCommandLine.path; } public get HasContext(): boolean { return this._serverContext !== undefined && this._serverContext.CredentialInfo !== undefined && this._serverContext.RepoInfo.CollectionUrl !== undefined; } public get IsExe(): boolean { return this._isExe; } public get Path(): string { return this._repositoryRootFolder; } public get RestrictWorkspace(): boolean { return this._settings.RestrictWorkspace; } public async Add(itemPaths: string[]): Promise { Logger.LogDebug(`TFVC Repository.Add`); return this.RunCommand( new Add(this._serverContext, itemPaths)); } public async Checkin(files: string[], comment: string, workItemIds: number[]): Promise { Logger.LogDebug(`TFVC Repository.Checkin`); return this.RunCommand( new Checkin(this._serverContext, files, comment, workItemIds)); } public async Delete(itemPaths: string[]): Promise { Logger.LogDebug(`TFVC Repository.Delete`); return this.RunCommand( new Delete(this._serverContext, itemPaths)); } public async FindConflicts(itemPath?: string): Promise { Logger.LogDebug(`TFVC Repository.FindConflicts`); return this.RunCommand( new FindConflicts(this._serverContext, itemPath ? itemPath : this._repositoryRootFolder)); } public async FindWorkspace(localPath: string): Promise { Logger.LogDebug(`TFVC Repository.FindWorkspace with localPath='${localPath}'`); return this.RunCommand( new FindWorkspace(localPath, this._settings.RestrictWorkspace)); } public async GetInfo(itemPaths: string[]): Promise { Logger.LogDebug(`TFVC Repository.GetInfo`); return this.RunCommand( new GetInfo(this._serverContext, itemPaths)); } public async GetFileContent(itemPath: string, versionSpec?: string): Promise { Logger.LogDebug(`TFVC Repository.GetFileContent`); return this.RunCommand( new GetFileContent(this._serverContext, itemPath, versionSpec, true)); } public async GetStatus(ignoreFiles?: boolean): Promise { Logger.LogDebug(`TFVC Repository.GetStatus`); let statusCommand: Status = new Status(this._serverContext, ignoreFiles === undefined ? true : ignoreFiles); //If we're restricting the workspace, pass in the repository root folder to Status if (this._settings.RestrictWorkspace) { statusCommand = new Status(this._serverContext, ignoreFiles === undefined ? true : ignoreFiles, [this._repositoryRootFolder]); } return this.RunCommand(statusCommand); } public async Rename(sourcePath: string, destinationPath: string): Promise { Logger.LogDebug(`TFVC Repository.Rename`); return this.RunCommand( new Rename(this._serverContext, sourcePath, destinationPath)); } public async ResolveConflicts(itemPaths: string[], autoResolveType: AutoResolveType): Promise { Logger.LogDebug(`TFVC Repository.ResolveConflicts`); return this.RunCommand( new ResolveConflicts(this._serverContext, itemPaths, autoResolveType)); } public async Sync(itemPaths: string[], recursive: boolean): Promise { Logger.LogDebug(`TFVC Repository.Sync`); return this.RunCommand( new Sync(this._serverContext, itemPaths, recursive)); } public async Undo(itemPaths: string[]): Promise { Logger.LogDebug(`TFVC Repository.Undo`); return this.RunCommand( new Undo(this._serverContext, itemPaths)); } public async CheckVersion(): Promise { if (!this._versionAlreadyChecked) { Logger.LogDebug(`TFVC Repository.CheckVersion`); // Set the versionAlreadyChecked flag first in case one of the other lines throws this._versionAlreadyChecked = true; const version: string = await this.RunCommand(new GetVersion()); TfCommandLineRunner.CheckVersion(this._tfCommandLine, version); return version; } return undefined; } public async RunCommand(cmd: ITfvcCommand): Promise { if (this._tfCommandLine.isExe) { //This is the tf.exe path const result: IExecutionResult = await this.exec(cmd.GetExeArguments(), cmd.GetExeOptions()); // We will call ParseExeOutput to give the command a chance to handle any specific errors itself. const output: T = await cmd.ParseExeOutput(result); return output; } else { //This is the CLC path const result: IExecutionResult = await this.exec(cmd.GetArguments(), cmd.GetOptions()); // We will call ParseOutput to give the command a chance to handle any specific errors itself. const output: T = await cmd.ParseOutput(result); return output; } } private async exec(args: IArgumentProvider, options: any = {}): Promise { options.env = _.assign({}, options.env || {}); options.env = _.assign(options.env, this._env); return await TfCommandLineRunner.Exec(this._tfCommandLine, this._repositoryRootFolder, args, options); } } ================================================ FILE: src/tfvc/tfvcscmprovider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { Logger } from "../helpers/logger"; import { TfvcCommandNames } from "../helpers/constants"; import { commands, scm, Uri, Disposable, SourceControl, SourceControlResourceGroup, Event, workspace } from "vscode"; import { CommitHoverProvider } from "./scm/commithoverprovider"; import { Model } from "./scm/model"; import { Status } from "./scm/status"; import { Resource } from "./scm/resource"; import { TfvcContext } from "../contexts/tfvccontext"; import { anyEvent, filterEvent, mapEvent } from "./util"; import { ExtensionManager } from "../extensionmanager"; import { RepositoryType } from "../contexts/repositorycontext"; import { TfvcOutput } from "./tfvcoutput"; import { TfvcContentProvider } from "./scm/tfvccontentprovider"; import { TfvcError } from "./tfvcerror"; import { ICheckinInfo } from "./interfaces"; /** * This class provides the SCM implementation for TFVC. * Note: to switch SCM providers you must do the following: * F1 -> SCM: Enable SCM Preview * F1 -> SCM: Switch SCM Provider -> Choose TFVC from the pick list */ export class TfvcSCMProvider { public static scmScheme: string = "tfvc"; private static instance: TfvcSCMProvider = undefined; private _extensionManager: ExtensionManager; private _model: Model; private _disposables: Disposable[] = []; private _tempDisposables: Disposable[] = []; private _sourceControl: SourceControl; constructor(extensionManager: ExtensionManager) { this._extensionManager = extensionManager; } /* Static helper methods */ public static ClearCheckinMessage(): void { scm.inputBox.value = ""; } public static GetCheckinInfo(): ICheckinInfo { const tfvcProvider: TfvcSCMProvider = TfvcSCMProvider.getProviderInstance(); try { const files: string[] = []; const commitMessage: string = scm.inputBox.value; const workItemIds: number[] = TfvcSCMProvider.getWorkItemIdsFromMessage(commitMessage); const resources: Resource[] = tfvcProvider._model.IncludedGroup.resources; if (!resources || resources.length === 0) { return undefined; } for (let i: number = 0; i < resources.length; i++) { files.push(resources[i].PendingChange.localItem); } return { files: files, comment: commitMessage, workItemIds: workItemIds }; } catch (err) { Logger.LogDebug("Failed to GetCheckinInfo. Details: " + err.message); throw TfvcError.CreateUnknownError(err); } } private static getWorkItemIdsFromMessage(message: string) { const ids: number[] = []; try { // Find all the work item mentions in the string. // This returns an array like: ["#1", "#12", "#33"] const matches: string[] = message ? message.match(/#(\d+)/gm) : []; if (matches) { for (let i: number = 0; i < matches.length; i++) { const id: number = parseInt(matches[i].slice(1)); if (!isNaN(id)) { ids.push(id); } } } } catch (err) { Logger.LogDebug("Failed to get all workitems from message: " + message); } return ids; } public static async Exclude(paths: string[]): Promise { const tfvcProvider: TfvcSCMProvider = TfvcSCMProvider.getProviderInstance(); await tfvcProvider._model.Exclude(paths); }; public static async Refresh(): Promise { const tfvcProvider: TfvcSCMProvider = TfvcSCMProvider.getProviderInstance(); await tfvcProvider._model.Refresh(); }; public static async Unexclude(paths: string[]): Promise { const tfvcProvider: TfvcSCMProvider = TfvcSCMProvider.getProviderInstance(); await tfvcProvider._model.Unexclude(paths); }; /* Public methods */ private conflictsGroup: SourceControlResourceGroup; private includedGroup: SourceControlResourceGroup; private excludedGroup: SourceControlResourceGroup; public async Initialize(): Promise { await TfvcOutput.CreateChannel(this._disposables); await this.setup(); // Now that everything is setup, we can register the provider and set up our singleton instance // This registration can only happen once TfvcSCMProvider.instance = this; this._sourceControl = scm.createSourceControl(TfvcSCMProvider.scmScheme, "TFVC"); this._disposables.push(this._sourceControl); this.conflictsGroup = this._sourceControl.createResourceGroup(this._model.ConflictsGroup.id, this._model.ConflictsGroup.label); this.includedGroup = this._sourceControl.createResourceGroup(this._model.IncludedGroup.id, this._model.IncludedGroup.label); this.excludedGroup = this._sourceControl.createResourceGroup(this._model.ExcludedGroup.id, this._model.ExcludedGroup.label); this.conflictsGroup.hideWhenEmpty = true; //Set the command to run when user accepts changes via Ctrl+Enter in input box. this._sourceControl.acceptInputCommand = { command: TfvcCommandNames.Checkin, title: "Checkin" }; this._disposables.push(this.conflictsGroup); this._disposables.push(this.includedGroup); this._disposables.push(this.excludedGroup); } private onDidModelChange(): void { if (!this.conflictsGroup) { return; } this.conflictsGroup.resourceStates = this._model.ConflictsGroup.resources; this.includedGroup.resourceStates = this._model.IncludedGroup.resources; this.excludedGroup.resourceStates = this._model.ExcludedGroup.resources; this._sourceControl.count = this.count; } public async Reinitialize(): Promise { this.cleanup(); await this.setup(); } private async setup(): Promise { const rootPath = workspace.rootPath; if (!rootPath) { // no root means no need for an scm provider return; } // Check if this is a TFVC repository if (!this._extensionManager.RepoContext || this._extensionManager.RepoContext.Type !== RepositoryType.TFVC || this._extensionManager.RepoContext.IsTeamFoundation === false) { // We don't have a TFVC context, so don't load the provider return; } const repoContext: TfvcContext = this._extensionManager.RepoContext; const fsWatcher = workspace.createFileSystemWatcher("**"); const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete); const onTfvcChange = filterEvent(onWorkspaceChange, (uri) => /^\$tf\//.test(workspace.asRelativePath(uri))); this._model = new Model(repoContext.RepoFolder, repoContext.TfvcRepository, onWorkspaceChange); // Hook up the model change event to trigger our own event this._disposables.push(this._model.onDidChange(this.onDidModelChange, this)); let version: string = "unknown"; try { version = await repoContext.TfvcRepository.CheckVersion(); } catch (err) { this._extensionManager.DisplayWarningMessage(err.message); } TfvcOutput.AppendLine("Using TFVC command line: " + repoContext.TfvcRepository.TfvcLocation + " (" + version + ")"); const commitHoverProvider: CommitHoverProvider = new CommitHoverProvider(); const contentProvider: TfvcContentProvider = new TfvcContentProvider(repoContext.TfvcRepository, rootPath, onTfvcChange); //const checkoutStatusBar = new CheckoutStatusBar(model); //const syncStatusBar = new SyncStatusBar(model); //const autoFetcher = new AutoFetcher(model); //const mergeDecorator = new MergeDecorator(model); this._tempDisposables.push( commitHoverProvider, contentProvider, fsWatcher //checkoutStatusBar, //syncStatusBar, //autoFetcher, //mergeDecorator ); // Refresh the model now that we are done setting up await this._model.Refresh(); } private cleanup() { // dispose all the temporary items if (this._tempDisposables) { this._tempDisposables.forEach((d) => d.dispose()); this._tempDisposables = []; } // dispose of the model if (this._model) { this._model.dispose(); this._model = undefined; } } get onDidChange(): Event { return mapEvent(this._model.onDidChange, () => this); } public get count(): number { // TODO is this too simple? The Git provider does more return this._model.Resources.reduce((r, g) => r + g.resources.length, 0); } dispose(): void { TfvcSCMProvider.instance = undefined; this.cleanup(); if (this._disposables) { this._disposables.forEach((d) => d.dispose()); this._disposables = []; } } /** * If Tfvc is the active provider, returns the number of items it is tracking. */ public static HasItems(): boolean { const tfvcProvider: TfvcSCMProvider = TfvcSCMProvider.instance; if (tfvcProvider) { if (tfvcProvider.count > 0) { return true; } } return false; } /** * Gets the uri for the previous version of the file. */ public static GetLeftResource(resource: Resource): Uri { if (resource.HasStatus(Status.CONFLICT) || resource.HasStatus(Status.EDIT) || resource.HasStatus(Status.RENAME)) { return resource.GetServerUri(); } else { return undefined; } } /** * Gets the uri for the current version of the file (except for deleted files). */ public static GetRightResource(resource: Resource): Uri { if (resource.HasStatus(Status.DELETE)) { return resource.GetServerUri(); } else { // Adding the version spec query, because this eventually gets passed to getOriginalResource return resource.resourceUri.with({ query: `C${resource.PendingChange.version}` }); } } private static getProviderInstance(): TfvcSCMProvider { const tfvcProvider: TfvcSCMProvider = TfvcSCMProvider.instance; if (!tfvcProvider) { // We are not the active provider Logger.LogDebug("TFVC is not the active provider."); throw TfvcError.CreateInvalidStateError(); } return tfvcProvider; } public static async OpenDiff(resource: Resource): Promise { return await commands.executeCommand(TfvcCommandNames.Open, resource); } } ================================================ FILE: src/tfvc/tfvcsettings.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import * as os from "os"; import { BaseSettings } from "../helpers/settings"; //TODO: Consider making this class 'static' so we can get values wherever we need them. Be aware //that if we take a transitive reference to VSCode, the unit tests for the commands we use this //class from will no longer run. export class TfvcSettings extends BaseSettings { private _location: string; private _proxy: string; private _restrictWorkspace: boolean; constructor() { super(); this._location = this.readSetting(SettingNames.Location, undefined); // Support replacing leading ~/ on macOS and linux if (this._location && this._location.startsWith("~/") && (os.platform() === "darwin" || os.platform() === "linux")) { this._location = this._location.replace(/^~(\/)/, `${os.homedir()}$1`); } if (this._location) { this._location = this._location.trim(); } this._proxy = this.readSetting(SettingNames.Proxy, undefined); this._restrictWorkspace = this.readSetting(SettingNames.RestrictWorkspace, false); } public get Location(): string { return this._location; } public get Proxy(): string { return this._proxy; } public get RestrictWorkspace(): boolean { return this._restrictWorkspace; } } class SettingNames { public static get Location(): string { return "tfvc.location"; } public static get Proxy(): string { return "tfvc.proxy"; } public static get RestrictWorkspace(): string { return "tfvc.restrictWorkspace"; } } ================================================ FILE: src/tfvc/tfvcversion.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; /** * This class represents the Version of the TF command line. */ export class TfvcVersion { private static separator: string = "."; private _major: number; private _minor: number; private _revision: number; private _build: string; public static FromString(version: string): TfvcVersion { const parts: string[] = version ? version.split(TfvcVersion.separator) : []; const major: number = parts.length >= 1 ? Number(parts[0]) : 0; const minor: number = parts.length >= 2 ? Number(parts[1]) : 0; const revision: number = parts.length >= 3 ? Number(parts[2]) : 0; const build: string = parts.length >= 4 ? parts.slice(3).join(TfvcVersion.separator) : ""; return new TfvcVersion(major, minor, revision, build); } public static Compare(version1: TfvcVersion, version2: TfvcVersion): number { if (version1._major !== version2._major) { return version1._major - version2._major; } if (version1._minor !== version2._minor) { return version1._minor - version2._minor; } if (version1._revision !== version2._revision) { return version1._revision - version2._revision; } return 0; } public constructor(major: number, minor: number, revision: number, build: string) { this._major = major; this._minor = minor; this._revision = revision; this._build = build; } public get Major(): number { return this._major; } public get Minor(): number { return this._minor; } public get Revision(): number { return this._revision; } public get Build(): string { return this._build; } public ToString(): string { return this._major + TfvcVersion.separator + this._minor + TfvcVersion.separator + this._revision + (this._build ? TfvcVersion.separator + this._build : ""); } } ================================================ FILE: src/tfvc/uihelper.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { QuickPickItem, window, workspace } from "vscode"; import { Strings } from "../helpers/strings"; import { AutoResolveType, IPendingChange, ISyncResults, ISyncItemResult, SyncType } from "./interfaces"; import { TfvcOutput } from "./tfvcoutput"; import * as path from "path"; export class UIHelper { public static async ChoosePendingChange(changes: IPendingChange[]): Promise { if (changes && changes.length > 0) { // First, create an array of quick pick items from the changes const items: QuickPickItem[] = []; for (let i: number = 0; i < changes.length; i++) { items.push({ label: UIHelper.GetFileName(changes[i]), description: changes[i].changeType, detail: UIHelper.GetRelativePath(changes[i]) }); } // Then, show the quick pick window and get back the one they chose const item: QuickPickItem = await window.showQuickPick( items, { matchOnDescription: true, placeHolder: Strings.ChooseItemQuickPickPlaceHolder }); // Finally, find the matching pending change and return it if (item) { for (let i: number = 0; i < changes.length; i++) { if (UIHelper.GetRelativePath(changes[i]) === item.detail) { return changes[i]; } } } } else if (changes && changes.length === 0) { const items: QuickPickItem[] = []; items.push({ label: Strings.TfNoPendingChanges, description: undefined, detail: undefined }); await window.showQuickPick(items); } return undefined; } /** * This method displays the results of the sync command in the output window and optionally in the QuickPick window as well. */ public static async ShowSyncResults(syncResults: ISyncResults, showPopup: boolean, onlyShowErrors): Promise { const items: QuickPickItem[] = []; if (syncResults.itemResults.length === 0) { TfvcOutput.AppendLine(Strings.AllFilesUpToDate); items.push({ label: Strings.AllFilesUpToDate, description: undefined, detail: undefined }); } else { for (let i: number = 0; i < syncResults.itemResults.length; i++) { const item: ISyncItemResult = syncResults.itemResults[i]; if (onlyShowErrors && !UIHelper.isSyncError(item.syncType)) { continue; } const type: string = this.GetDisplayTextForSyncType(item.syncType); TfvcOutput.AppendLine(type + ": " + item.itemPath + " : " + item.message); items.push({ label: type, description: item.itemPath, detail: item.message }); } } if (showPopup) { await window.showQuickPick(items); } } private static isSyncError(type: SyncType): boolean { switch (type) { case SyncType.Conflict: case SyncType.Error: case SyncType.Warning: return true; case SyncType.Deleted: case SyncType.New: case SyncType.Updated: return false; default: return false; } } public static GetDisplayTextForSyncType(type: SyncType): string { switch (type) { case SyncType.Conflict: return Strings.SyncTypeConflict; case SyncType.Deleted: return Strings.SyncTypeDeleted; case SyncType.Error: return Strings.SyncTypeError; case SyncType.New: return Strings.SyncTypeNew; case SyncType.Updated: return Strings.SyncTypeUpdated; case SyncType.Warning: return Strings.SyncTypeWarning; default: return Strings.SyncTypeUpdated; } } public static GetDisplayTextForAutoResolveType(type: AutoResolveType): string { switch (type) { case AutoResolveType.AutoMerge: return Strings.AutoResolveTypeAutoMerge; case AutoResolveType.DeleteConflict: return Strings.AutoResolveTypeDeleteConflict; case AutoResolveType.KeepYours: return Strings.AutoResolveTypeKeepYours; case AutoResolveType.KeepYoursRenameTheirs: return Strings.AutoResolveTypeKeepYoursRenameTheirs; case AutoResolveType.OverwriteLocal: return Strings.AutoResolveTypeOverwriteLocal; case AutoResolveType.TakeTheirs: return Strings.AutoResolveTypeTakeTheirs; default: return Strings.AutoResolveTypeAutoMerge; } } public static GetFileName(change: IPendingChange): string { if (change && change.localItem) { const filename: string = path.parse(change.localItem).base; return filename; } return ""; } public static GetRelativePath(change: IPendingChange): string { if (change && change.localItem && workspace) { return workspace.asRelativePath(change.localItem); } return change.localItem; } public static async PromptForConfirmation(message: string, okButtonText?: string): Promise { okButtonText = okButtonText ? okButtonText : "OK"; //TODO: use Modal api once vscode.d.ts exposes it (currently proposed) const pick: string = await window.showWarningMessage(message, /*{ modal: true },*/ okButtonText); return pick === okButtonText; } } ================================================ FILE: src/tfvc/util.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; /* tslint:disable:no-null-keyword */ import { Event } from "vscode"; export function log(...args: any[]): void { console.log.apply(console, ["tfvc:", ...args]); } export interface IDisposable { dispose(): void; } export function dispose(disposables: T[]): T[] { disposables.forEach((d) => d.dispose()); return []; } export function toDisposable(dispose: () => void): IDisposable { return { dispose }; } export function combinedDisposable(disposables: IDisposable[]): IDisposable { return toDisposable(() => dispose(disposables)); } export function mapEvent(event: Event, map: (i: I) => O): Event { return (listener, thisArgs = null, disposables?) => event((i) => listener.call(thisArgs, map(i)), null, disposables); } export function filterEvent(event: Event, filter: (e: T) => boolean): Event { return (listener, thisArgs = null, disposables?) => event((e) => filter(e) && listener.call(thisArgs, e), null, disposables); } export function anyEvent(...events: Event[]): Event { return (listener, thisArgs = null, disposables?) => combinedDisposable(events.map((event) => event((i) => listener.call(thisArgs, i), disposables))); } export function done(promise: Promise): Promise { return promise.then(() => void 0, () => void 0); } export function once(event: Event): Event { return (listener, thisArgs = null, disposables?) => { const result = event( (e) => { result.dispose(); return listener.call(thisArgs, e); }, null, disposables); return result; }; } export function eventToPromise(event: Event): Promise { return new Promise((c) => once(event)(c)); } /* tslint:enable:no-null-keyword */ ================================================ FILE: test/contexts/contexthelper.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { ISettings } from "../../src/helpers/settings"; //Used to test the GitContext, ExternalContext classes export class SettingsMock implements ISettings { /* tslint:disable:variable-name */ constructor(public AppInsightsEnabled: boolean, public AppInsightsKey: string, public LoggingLevel: string, public PollingInterval: number, public RemoteUrl: string, public TeamProject: string, public BuildDefinitionId: number, public ShowWelcomeMessage: boolean, public ShowFarewellMessage: boolean) { //nothing to do } /* tslint:enable:variable-name */ } ================================================ FILE: test/contexts/externalcontext.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { RepositoryType } from "../../src/contexts/repositorycontext"; import { ExternalContext } from "../../src/contexts/externalcontext"; import { SettingsMock } from "./contexthelper"; describe("ExternalContext", function() { const TEST_REPOS_FOLDER: string = "testrepos"; const DOT_GIT_FOLDER: string = "dotgit"; beforeEach(function() { // console.log("__dirname: " + __dirname); }); it("should verify all undefined properties for undefined rootPath", function() { //Verify an undefined path does not set any values const ctx: ExternalContext = new ExternalContext(undefined); assert.equal(ctx.CurrentRef, undefined); assert.equal(ctx.CurrentBranch, undefined); assert.equal(ctx.RepositoryParentFolder, undefined); assert.isFalse(ctx.IsSsh); assert.isFalse(ctx.IsTeamFoundation); assert.isFalse(ctx.IsTeamServices); assert.equal(ctx.RemoteUrl, undefined); assert.equal(ctx.RepoFolder, undefined); assert.equal(ctx.TeamProjectName, undefined); assert.equal(ctx.Type, RepositoryType.EXTERNAL); }); it("should verify values for valid rootPath path", function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); //const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); const ctx: ExternalContext = new ExternalContext(repoPath); assert.equal(ctx.CurrentRef, undefined); assert.equal(ctx.CurrentBranch, undefined); assert.equal(ctx.RepositoryParentFolder, undefined); assert.isFalse(ctx.IsSsh); assert.isFalse(ctx.IsTeamFoundation); assert.isFalse(ctx.IsTeamServices); assert.equal(ctx.RemoteUrl, undefined); assert.equal(ctx.TeamProjectName, undefined); assert.equal(ctx.Type, RepositoryType.EXTERNAL); assert.equal(ctx.RepoFolder, repoPath); }); it("should cover dispose", function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); //const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); const ctx: ExternalContext = new ExternalContext(repoPath); ctx.dispose(); }); it("should verify values for valid rootPath path and settings", async function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const ctx: ExternalContext = new ExternalContext(repoPath); const mock: SettingsMock = new SettingsMock(false, undefined, undefined, 1, "https://xplatalm.visualstudio.com", "L2.VSCodeExtension.RC", undefined, true, true); const initialized: Boolean = await ctx.Initialize(mock); assert.isTrue(initialized); assert.isTrue(ctx.IsTeamServices); assert.isTrue(ctx.IsTeamFoundation); assert.equal(ctx.RemoteUrl, mock.RemoteUrl); assert.equal(ctx.TeamProjectName, mock.TeamProject); assert.equal(ctx.Type, RepositoryType.EXTERNAL); }); it("should verify initialize is false for missing RemoteUrl", async function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const ctx: ExternalContext = new ExternalContext(repoPath); const mock: SettingsMock = new SettingsMock(false, undefined, undefined, 1, undefined, "L2.VSCodeExtension.RC", undefined, true, true); const initialized: Boolean = await ctx.Initialize(mock); assert.isFalse(initialized); assert.isFalse(ctx.IsSsh); assert.isFalse(ctx.IsTeamServices); assert.isFalse(ctx.IsTeamFoundation); assert.equal(ctx.RemoteUrl, undefined); assert.equal(ctx.TeamProjectName, undefined); assert.equal(ctx.Type, RepositoryType.EXTERNAL); }); it("should verify initialize is false for missing TeamProject", async function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const ctx: ExternalContext = new ExternalContext(repoPath); const mock: SettingsMock = new SettingsMock(false, undefined, undefined, 1, "https://xplatalm.visualstudio.com", undefined, undefined, true, true); const initialized: Boolean = await ctx.Initialize(mock); assert.isFalse(initialized); assert.isFalse(ctx.IsSsh); assert.isFalse(ctx.IsTeamServices); assert.isFalse(ctx.IsTeamFoundation); assert.equal(ctx.RemoteUrl, undefined); assert.equal(ctx.TeamProjectName, undefined); assert.equal(ctx.Type, RepositoryType.EXTERNAL); }); it("should verify initialize is false for missing RemoteUrl and TeamProject", async function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const ctx: ExternalContext = new ExternalContext(repoPath); const mock: SettingsMock = new SettingsMock(false, undefined, undefined, 1, undefined, undefined, undefined, true, true); const initialized: Boolean = await ctx.Initialize(mock); assert.isFalse(initialized); assert.isFalse(ctx.IsSsh); assert.isFalse(ctx.IsTeamServices); assert.isFalse(ctx.IsTeamFoundation); assert.equal(ctx.RemoteUrl, undefined); assert.equal(ctx.TeamProjectName, undefined); assert.equal(ctx.Type, RepositoryType.EXTERNAL); }); }); ================================================ FILE: test/contexts/gitcontext.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { GitContext } from "../../src/contexts/gitcontext"; import { RepositoryType } from "../../src/contexts/repositorycontext"; describe("GitContext", function() { const TEST_REPOS_FOLDER: string = "testrepos"; const DOT_GIT_FOLDER: string = "dotgit"; beforeEach(function() { // console.log("__dirname: " + __dirname); }); it("should verify all undefined properties for undefined GitContext path", function() { //Verify an undefined path does not set any values const gc: GitContext = new GitContext(undefined); assert.equal(gc.CurrentBranch, undefined); assert.equal(gc.RemoteUrl, undefined); assert.equal(gc.RepositoryParentFolder, undefined); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify undefined values for invalid GitContext path", function() { //Actually pass a value to constructor (instead of undefined), values should be undefined const gc: GitContext = new GitContext(__dirname + "invalid"); assert.equal(gc.CurrentBranch, undefined); assert.equal(gc.RemoteUrl, undefined); assert.equal(gc.RepositoryParentFolder, undefined); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify initialize returns true", async function() { const gc: GitContext = new GitContext(__dirname); assert.isTrue(await gc.Initialize()); }); it("should cover dispose", async function() { const gc: GitContext = new GitContext(__dirname); gc.dispose(); }); it("should verify repository with an empty origin remote", function() { const repoName: string = "emptyconfig"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, undefined); assert.equal(gc.CurrentRef, undefined); assert.isFalse(gc.IsSsh); assert.isFalse(gc.IsTeamFoundation); assert.isFalse(gc.IsTeamServices); assert.equal(gc.RemoteUrl, undefined); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify GitHub origin remote", function() { const repoName: string = "githubrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "master"); assert.equal(gc.CurrentRef, "refs/heads/master"); assert.isFalse(gc.IsSsh); assert.isFalse(gc.IsTeamFoundation); assert.isFalse(gc.IsTeamServices); assert.equal(gc.RemoteUrl, undefined); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify TeamServices origin remote", function() { const repoName: string = "gitrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "jeyou/approved-pr"); assert.equal(gc.CurrentRef, "refs/heads/jeyou/approved-pr"); assert.isFalse(gc.IsSsh); assert.isTrue(gc.IsTeamFoundation); assert.isTrue(gc.IsTeamServices); assert.equal(gc.RemoteUrl, "https://account.visualstudio.com/DefaultCollection/teamproject/_git/gitrepo"); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.isUndefined(gc.TeamProjectName); //For Git repositories, teamproject comes from vsts/info (not remoteUrl) assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify TeamServices origin remote cloned with old-style ssh url", function() { const repoName: string = "gitrepo-old-ssh"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "master"); assert.equal(gc.CurrentRef, "refs/heads/master"); assert.isTrue(gc.IsSsh); assert.isTrue(gc.IsTeamFoundation); assert.isTrue(gc.IsTeamServices); //The remote URL is the https and no longer has port number assert.equal(gc.RemoteUrl, "https://account.visualstudio.com/DefaultCollection/_git/repository"); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify TeamServices origin remote cloned with ssh", function () { const repoName: string = "gitrepo-ssh"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "master"); assert.equal(gc.CurrentRef, "refs/heads/master"); assert.isTrue(gc.IsSsh); assert.isTrue(gc.IsTeamFoundation); assert.isTrue(gc.IsTeamServices); //The remote URL is the https and no longer has port number assert.equal(gc.RemoteUrl, "https://account.visualstudio.com/DefaultCollection/_git/repository"); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify TeamServices origin remote cloned with ssh", function () { const repoName: string = "gitrepo-ssh.v3"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "master"); assert.equal(gc.CurrentRef, "refs/heads/master"); assert.isTrue(gc.IsSsh); assert.isTrue(gc.IsTeamFoundation); assert.isTrue(gc.IsTeamServices); //The remote URL is the https and no longer has port number assert.equal(gc.RemoteUrl, "https://mytest.azure.com/account/project/_git/repository"); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify TeamFoundationServer origin remote", function() { const repoName: string = "tfsrepo"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "master"); assert.equal(gc.CurrentRef, "refs/heads/master"); assert.isFalse(gc.IsSsh); assert.isTrue(gc.IsTeamFoundation); assert.isFalse(gc.IsTeamServices); assert.equal(gc.RemoteUrl, "http://devmachine:8080/tfs/DefaultCollection/_git/GitAgile"); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); it("should verify TeamFoundationServer origin remote cloned with ssh", function() { const repoName: string = "tfsrepo-ssh"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER); const gc: GitContext = new GitContext(repoPath, DOT_GIT_FOLDER); assert.equal(gc.CurrentBranch, "master"); assert.equal(gc.CurrentRef, "refs/heads/master"); assert.isTrue(gc.IsSsh); //SSH isn't supported on server yet and that is indicated by isTeamFoundation === false assert.isFalse(gc.IsTeamFoundation); assert.isFalse(gc.IsTeamServices); //The remote URL is the same as the original assert.equal(gc.RemoteUrl, "ssh://devmachine:22/tfs/DefaultCollection/_git/GitJava"); assert.equal(gc.RepositoryParentFolder, path.join(__dirname, TEST_REPOS_FOLDER, repoName)); assert.equal(gc.RepoFolder, repoPath); assert.equal(gc.Type, RepositoryType.GIT); }); }); ================================================ FILE: test/contexts/servercontext.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { TeamServerContext } from "../../src/contexts/servercontext"; describe("TeamServerContext", function() { it("should verify thrown Error for undefined remoteUrl", function() { try { new TeamServerContext(undefined); } catch (error) { assert.equal(error.message, "remoteUrl is undefined"); } }); it("should verify context is a TeamFoundation context with Azure DevOps Services", function() { // This could be a TFVC repository const context: TeamServerContext = new TeamServerContext("https://account.visualstudio.com/DefaultCollection/teamproject/"); assert.isTrue(context.RepoInfo.IsTeamServices); assert.isTrue(context.RepoInfo.IsTeamFoundation); assert.isFalse(context.RepoInfo.IsTeamFoundationServer); }); it("should verify context is a IsTeamServices and TeamFoundation context", function() { const context: TeamServerContext = new TeamServerContext("https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.isTrue(context.RepoInfo.IsTeamServices); assert.isTrue(context.RepoInfo.IsTeamFoundation); assert.isFalse(context.RepoInfo.IsTeamFoundationServer); }); it("should verify context is a TeamFoundation context with TFS account", function() { const context: TeamServerContext = new TeamServerContext("http://server:8080/tfs/DefaultCollection/teamproject"); assert.isFalse(context.RepoInfo.IsTeamServices, "isTeamServices should be false"); //TODO: assert.isTrue(context.RepoInfo.IsTeamFoundation, "isTeamFoundation should be true"); //TODO: assert.isFalse(context.RepoInfo.IsTeamFoundationServer, "isTeamFoundationServer should be true"); }); }); ================================================ FILE: test/contexts/testrepos/emptyconfig/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly ================================================ FILE: test/contexts/testrepos/githubrepo/dotgit/HEAD ================================================ ref: refs/heads/master ================================================ FILE: test/contexts/testrepos/githubrepo/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo/dotgit/HEAD ================================================ ref: refs/heads/jeyou/approved-pr ================================================ FILE: test/contexts/testrepos/gitrepo/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = https://account.visualstudio.com/DefaultCollection/teamproject/_git/gitrepo fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [branch "jeyou/topic"] remote = origin merge = refs/heads/jeyou/topic [branch "jeyou/rejected-pr"] remote = origin merge = refs/heads/jeyou/rejected-pr [branch "jeyou/waitingonauthor-pr"] remote = origin merge = refs/heads/jeyou/waitingonauthor-pr [branch "jeyou/approved-pr"] remote = origin merge = refs/heads/jeyou/approved-pr ================================================ FILE: test/contexts/testrepos/gitrepo/dotgit/refs/heads/jeyou/approved-pr ================================================ 6af83646f6ff33f312489fe333746009dbd5a5a4 ================================================ FILE: test/contexts/testrepos/gitrepo-old-ssh/dotgit/HEAD ================================================ ref: refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo-old-ssh/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = ssh://account@account.visualstudio.com:22/DefaultCollection/_git/repository fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo-old-ssh/dotgit/refs/heads/master ================================================ 1f9e7da1c871e1b12df8ea4dc87e355152264eb8 ================================================ FILE: test/contexts/testrepos/gitrepo-ssh/dotgit/HEAD ================================================ ref: refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo-ssh/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = ssh://account@vs-ssh.visualstudio.com:22/DefaultCollection/_ssh/repository fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo-ssh/dotgit/refs/heads/master ================================================ 1f9e7da1c871e1b12df8ea4dc87e355152264eb8 ================================================ FILE: test/contexts/testrepos/gitrepo-ssh.v3/dotgit/HEAD ================================================ ref: refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo-ssh.v3/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = git@ssh.mytest.azure.com:v3/account/project/repository fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: test/contexts/testrepos/gitrepo-ssh.v3/dotgit/refs/heads/master ================================================ 2f9e7da1c871e1b12df8ea4dc87e355152264eb8 ================================================ FILE: test/contexts/testrepos/tfsrepo/dotgit/HEAD ================================================ ref: refs/heads/master ================================================ FILE: test/contexts/testrepos/tfsrepo/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = http://devmachine:8080/tfs/DefaultCollection/_git/GitAgile fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: test/contexts/testrepos/tfsrepo/dotgit/refs/heads/master ================================================ 5b2a2c0d7886e3f4ef2b29b8fd8254184c857371 ================================================ FILE: test/contexts/testrepos/tfsrepo-ssh/dotgit/HEAD ================================================ ref: refs/heads/master ================================================ FILE: test/contexts/testrepos/tfsrepo-ssh/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = ssh://devmachine:22/tfs/DefaultCollection/_git/GitJava fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: test/contexts/testrepos/tfsrepo-ssh/dotgit/refs/heads/master ================================================ 1f9e7da1c871e1b12df8ea4dc87e355152264eb8 ================================================ FILE: test/helpers/logger.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Logger, LoggingLevel } from "../../src/helpers/logger"; describe("Logger", function() { beforeEach(function() { // }); it("should cover Initialize", function() { Logger.SetLoggingLevel("error"); Logger.LogWarning("unit test warning message"); Logger.LogInfo("unit test info message"); Logger.LogDebug("unit test debug message"); Logger.LogObject({ property: "value" }); }); it("should verify setting LoggingLevel", function() { Logger.SetLoggingLevel("debug"); assert.equal(Logger.LoggingLevel, LoggingLevel.Debug); Logger.SetLoggingLevel("error"); assert.equal(Logger.LoggingLevel, LoggingLevel.Error); Logger.SetLoggingLevel("info"); assert.equal(Logger.LoggingLevel, LoggingLevel.Info); Logger.SetLoggingLevel("verbose"); assert.equal(Logger.LoggingLevel, LoggingLevel.Verbose); Logger.SetLoggingLevel("warn"); assert.equal(Logger.LoggingLevel, LoggingLevel.Warn); Logger.SetLoggingLevel("foo"); assert.isUndefined(Logger.LoggingLevel); Logger.SetLoggingLevel(undefined); assert.isUndefined(Logger.LoggingLevel); }); it("should verify setting LogPath", function() { Logger.LogPath = undefined; assert.equal(Logger.LogPath, ""); Logger.LogPath = "/usr/logger/logfile"; assert.equal(Logger.LogPath, "/usr/logger/logfile"); }); it("should ensure getNow()", function() { const now: string = Logger.Now; //calls private getNow() const date: number = Date.parse(now); assert.isDefined(date); }); }); ================================================ FILE: test/helpers/repoutils.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { RepoUtils } from "../../src/helpers/repoutils"; describe("RepoUtils", function() { beforeEach(function() { // }); it("should ensure valid Team Foundation Server Git urls", function() { let url : string; //Server names with ports are valid url = "http://pioneer-new-dt:8080/tfs/DevDiv_Projects2/_git/JavaALM"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://minint-i0lvs2o:8080/tfs/DefaultCollection/_git/GitProject"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://java-tfs2015:8081/tfs/DefaultCollection/_git/GitJava"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://sources2010/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://sources2010/tfs/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); //Multi-part server names are valid url = "http://java-tfs01.3redis.local/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://java-tfs01.loseit.local:8080/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://stdtfs.amways.local/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); //IP addresses would be valid url = "http://192.168.0.1/sources2010/tfs/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); url = "http://192.168.0.1:8084/sources2010/tfs/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); //SSH urls would be valid url = "ssh://sources2010:22/tfs/DefaultCollection/_git/GitAgile"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url)); }); it("should ensure Azure DevOps Services urls are not valid Team Foundation Server Git urls", function() { //If given a team foundation services url, IsTeamFoundationServerRepo will return false let url : string; url = "https://mseng.visualstudio.com/VSOnline/_git/Java.VSCode.CredentialStore"; assert.isFalse(RepoUtils.IsTeamFoundationServerRepo(url)); url = "https://mseng.visualstudio.com/DefaultCollection/VSOnline/_git/Java.IntelliJ"; assert.isFalse(RepoUtils.IsTeamFoundationServerRepo(url)); url = "https://test.azure.com/mseng/VSOnline/_git/Java.VSCode.CredentialStore"; assert.isFalse(RepoUtils.IsTeamFoundationServerRepo(url)); url = "https://test.azure.com/mseng/VSOnline/_git/Java.IntelliJ"; assert.isFalse(RepoUtils.IsTeamFoundationServerRepo(url)); url = "ssh://mseng@mseng.visualstudio.com:22/DefaultCollection/VSOnline/_git/Java.IntelliJ/"; assert.isFalse(RepoUtils.IsTeamFoundationServerRepo(url)); url = "git@ssh.mytest.azure.com:v3/account/project/repository"; assert.isFalse(RepoUtils.IsTeamFoundationServerRepo(url)); }); it("should ensure valid Azure Repos Git urls", function() { let url : string; url = "https://mseng.visualstudio.com/VSOnline/_git/Java.VSCode.CredentialStore"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "https://mseng.visualstudio.com/DefaultCollection/VSOnline/_git/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "https://test.azure.com/mseng/VSOnline/_git/Java.VSCode.CredentialStore"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "https://test.azure.com/mseng/VSOnline/_git/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); //SSH urls would be valid url = "ssh://mseng@mseng.visualstudio.com:22/DefaultCollection/VSOnline/_git/Java.IntelliJ/"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); //New-style SSH urls (with _ssh instead of _git) should be valid as well url = "ssh://acctname@vs-ssh.visualstudio.com:22/DefaultCollection/_ssh/reponame"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "git@ssh.mytest.azure.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "git@vs-ssh.visualstudio.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url)); }); it("should ensure Team Foundation Server urls are not valid Azure DevOps Services Git urls", function() { let url : string; url = "http://pioneer-new-dt:8080/tfs/DevDiv_Projects2/_git/JavaALM"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://minint-i0lvs2o:8080/tfs/DefaultCollection/_git/GitProject"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://java-tfs2015:8081/tfs/DefaultCollection/_git/GitJava"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://sources2010/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://sources2010/tfs/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://java-tfs01.3redis.local/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://java-tfs01.loseit.local:8080/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://stdtfs.amways.local/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://192.168.0.1/sources2010/tfs/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "http://192.168.0.1:8084/sources2010/tfs/DefaultCollection/_git/repo"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "ssh://sources2010:22/tfs/DefaultCollection/_git/GitAgile"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); url = "git@ssh.mytest.azure.com:v2/account/project/repository"; assert.isFalse(RepoUtils.IsTeamFoundationServicesRepo(url)); }); it("should ensure valid Azure DevOps Services Git urls on azure.com domain", function() { let url : string; url = "https://test.azure.com/mseng/VSOnline/_git/Java.VSCode.CredentialStore"; assert.isTrue(RepoUtils.IsTeamFoundationServicesAzureRepo(url)); url = "https://test.azure.com/mseng/VSOnline/_git/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationServicesAzureRepo(url)); url = "git@ssh.mytest.azure.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationServicesAzureRepo(url)); }); it("should ensure Azure DevOps Services URLs on visualstudio.com domain are not valid Azure DevOps Services Git URLs on azure.com domain", function() { let url : string; url = "https://mseng.visualstudio.com/VSOnline/_git/Java.VSCode.CredentialStore"; assert.isFalse(RepoUtils.IsTeamFoundationServicesAzureRepo(url)); url = "https://mseng.visualstudio.com/DefaultCollection/VSOnline/_git/Java.IntelliJ"; assert.isFalse(RepoUtils.IsTeamFoundationServicesAzureRepo(url)); url = "https://mseng.visualstudio.com/azure.com/azure.com/_git/azure.com"; assert.isFalse(RepoUtils.IsTeamFoundationServicesAzureRepo(url)); }); it("should ensure valid Team Foundation Git urls", function() { let url : string; url = "http://sources2010/tfs/DefaultCollection/_git/repo"; assert.isTrue(RepoUtils.IsTeamFoundationGitRepo(url)); url = "https://account.visualstudio.com/DefaultCollection/VSOnline/_git/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationGitRepo(url)); url = "https://test.azure.com/account/VSOnline/_git/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationGitRepo(url)); url = "git@ssh.mytest.azure.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationGitRepo(url)); url = "git@vs-ssh.visualstudio.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationGitRepo(url)); }); it("should allow any other url as a valid Team Foundation repository", function() { let url : string; //This test exists to inform the developer of the fact that if we can't determine the url, //we have to assume that it is a TFVC repository. See the RepoUtils class for more details. //This is true because we know it isn't Git but not that it isn't Tfvc // url = "https://account.visualstudio.com/DefaultCollection/VSOnline/Java.IntelliJ"; // assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url), "Java.IntelliJ url is not detected as a valid Azure DevOps Services repo"); //This is true because we know it isn't Git but not that it isn't Tfvc //(we could write explicit code for GitHub but are not choosing to do so now) url = "git@github.com:Microsoft/Git-Credential-Manager-for-Mac-and-Linux.git"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url), "GitHub SSH url is not detected as a valid Azure DevOps Services repo"); //This is true because we know it isn't Git but not that it isn't Tfvc //(we could write explicit code for GitHub but are not choosing to do so now) url = "https://github.com/Microsoft/azure-repos-vscode.git"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url), "GitHub url is not detected as a valid Azure DevOps Services repo"); //This is true because we know it isn't Git but not that it isn't Tfvc url = "foo"; assert.isTrue(RepoUtils.IsTeamFoundationServerRepo(url), "foo url is not detected as a valid Azure DevOps Services repo"); }); it("should detect a valid Azure DevOps Services repository but not as a Git repository", function() { let url : string; url = "https://account.visualstudio.com/DefaultCollection/VSOnline/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url), "Java.IntelliJ url is not detected as a valid Azure DevOps Services repo"); assert.isFalse(RepoUtils.IsTeamFoundationGitRepo(url), "Java.IntelliJ url is detected as a valid Git repo"); url = "https://test.azure.com/account/VSOnline/Java.IntelliJ"; assert.isTrue(RepoUtils.IsTeamFoundationServicesRepo(url), "Java.IntelliJ url is not detected as a valid Azure DevOps Services repo"); assert.isFalse(RepoUtils.IsTeamFoundationGitRepo(url), "Java.IntelliJ url is detected as a valid Git repo"); }); it("should verify if a V3 ssh url", function() { let url : string; url = "git@ssh.mytest.azure.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationServicesV3SshRepo(url)); url = "git@vs-ssh.visualstudio.com:v3/account/project/repository"; assert.isTrue(RepoUtils.IsTeamFoundationServicesV3SshRepo(url)); url = "ssh://sources2010:22/tfs/DefaultCollection/_git/GitAgile"; assert.isFalse(RepoUtils.IsTeamFoundationServicesV3SshRepo(url)); }); it("should convert V3 ssh url to git url", function() { let url : string; url = "git@ssh.mytest.azure.com:v3/account/project/repository"; assert.equal(RepoUtils.ConvertSshV3ToUrl(url), "https://mytest.azure.com/account/project/_git/repository"); url = "git@vs-ssh.visualstudio.com:v3/account/project/repository"; assert.equal(RepoUtils.ConvertSshV3ToUrl(url), "https://account.visualstudio.com/project/_git/repository"); }); }); ================================================ FILE: test/helpers/testrepos/gitreposubfolder/dotgit/config ================================================ [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly ================================================ FILE: test/helpers/testrepos/gitreposubfolder/folder/subfolder/README.md ================================================ Just an empty README.md file. ================================================ FILE: test/helpers/urlbuilder.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { UrlBuilder } from "../../src/helpers/urlbuilder"; describe("UrlBuilder", function() { beforeEach(function() { // }); it("should ensure undefined baseUrl returns undefined", function() { const url: string = undefined; const result: string = UrlBuilder.Join(url); assert.isUndefined(result); }); it("should ensure baseUrl with trailing slash returns the original url", function() { const url: string = "http://xplatalm.visualstudio.com/"; const result: string = UrlBuilder.Join(url); assert.equal(url, result); }); it("should ensure baseUrl without trailing slash returns the original url", function() { const url: string = "http://xplatalm.visualstudio.com"; const result: string = UrlBuilder.Join(url); assert.equal(url, result); }); it("should ensure baseUrl with trailing slash and undefined args returns the original url", function() { const url: string = "http://xplatalm.visualstudio.com/"; const result: string = UrlBuilder.Join(url, undefined); assert.equal(url, result); }); it("should ensure baseUrl without trailing slash and undefined args returns the original url", function() { const url: string = "http://xplatalm.visualstudio.com"; const result: string = UrlBuilder.Join(url, undefined); assert.equal(url, result); }); it("should ensure baseUrl with trailing slash and empty args returns the original url", function() { const url: string = "http://xplatalm.visualstudio.com/"; const args: string = ""; const result: string = UrlBuilder.Join(url, args); assert.equal(url, result); }); it("should ensure baseUrl without trailing slash and empty args returns the original url", function() { const url: string = "http://xplatalm.visualstudio.com"; const args: string = ""; const result: string = UrlBuilder.Join(url, args); assert.equal(url, result); }); //Single path argument it("should ensure baseUrl with trailing slash and single arg returns the proper url", function() { const url: string = "http://xplatalm.visualstudio.com/"; const args: string = "_build"; const result: string = UrlBuilder.Join(url, args); assert.equal(`${url}${args}`, result); }); it("should ensure baseUrl without trailing slash and single arg returns the proper url", function() { const url: string = "http://xplatalm.visualstudio.com"; const args: string = "_build"; const result: string = UrlBuilder.Join(url, args); assert.equal(`${url}/${args}`, result); }); //Multiple path arguments it("should ensure baseUrl with trailing slash and multiple args returns the proper url", function() { const url: string = "http://xplatalm.visualstudio.com/"; const result: string = UrlBuilder.Join(url, "_build", "index", "buildId=42"); assert.equal(`${url}_build/index/buildId=42`, result); }); it("should ensure baseUrl without trailing slash and multiple args returns the proper url", function() { const url: string = "http://xplatalm.visualstudio.com"; const result: string = UrlBuilder.Join(url, "_build", "index", "buildId=42"); assert.equal(`${url}/_build/index/buildId=42`, result); }); it("should ensure baseUrl and multiple args and various leading slashes returns the proper url", function() { const url: string = "http://xplatalm.visualstudio.com"; const result: string = UrlBuilder.Join(url, "/_build", "/index", "/buildId=42"); assert.equal(`${url}/_build/index/buildId=42`, result); }); /* AddQueryParams */ it("should ensure AddQueryParams supports undefined baseUrl", function() { const url: string = undefined; const result: string = UrlBuilder.AddQueryParams(url); assert.isUndefined(result); }); it("should ensure AddQueryParams supports baseUrl with no query params", function() { const url: string = "http://xplatalm.visualstudio.com/index"; const result: string = UrlBuilder.AddQueryParams(url); assert.equal(url, result); }); it("should ensure AddQueryParams supports baseUrl with trailing slash and single query params", function() { const url: string = "http://xplatalm.visualstudio.com/index/"; const result: string = UrlBuilder.AddQueryParams(url, "buildId=42"); assert.equal("http://xplatalm.visualstudio.com/index?buildId=42", result); }); it("should ensure AddQueryParams supports single query parameter", function() { const url: string = "http://xplatalm.visualstudio.com/index"; let result: string = UrlBuilder.AddQueryParams(url, "buildId=42"); assert.equal(`${url}?buildId=42`, result); //Test with leading "?" result = UrlBuilder.AddQueryParams(url, "?buildId=42"); assert.equal(`${url}?buildId=42`, result); //Test with leading "&" (which is incorrect in this case of a single query parameter) result = UrlBuilder.AddQueryParams(url, "&buildId=42"); assert.equal(`${url}?buildId=42`, result); }); it("should ensure AddQueryParams supports multiple query parameters", function() { const url: string = "http://xplatalm.visualstudio.com/index"; let result: string = UrlBuilder.AddQueryParams(url, "buildId=42", "whatever=andever", "foo=bar"); assert.equal(`${url}?buildId=42&whatever=andever&foo=bar`, result); //Test with leading "&" result = UrlBuilder.AddQueryParams(url, "buildId=42", "whatever=andever", "foo=bar"); assert.equal(`${url}?buildId=42&whatever=andever&foo=bar`, result); //Test with leading "&" (which aren't needed when using this class) result = UrlBuilder.AddQueryParams(url, "buildId=42", "&whatever=andever", "&foo=bar"); assert.equal(`${url}?buildId=42&whatever=andever&foo=bar`, result); }); /* AddHashes */ it("should ensure AddHashes supports undefined baseUrl and undefined arg", function() { let url: string = undefined; const arg: string = undefined; let result: string = UrlBuilder.AddHashes(url, arg); assert.isUndefined(result); url = "http://xplatalm.visualstudio.com"; result = UrlBuilder.AddHashes(url, arg); assert.equal(url, result); }); it("should ensure AddHashes supports url with trailing slash", function() { const url: string = "http://xplatalm.visualstudio.com/"; const arg: string = "#path"; const result: string = UrlBuilder.AddHashes(url, arg); assert.equal(`http://xplatalm.visualstudio.com${arg}`, result); }); it("should ensure AddHashes supports single arg with or without leading #", function() { const url: string = "http://xplatalm.visualstudio.com"; let arg: string = "#path"; let result: string = UrlBuilder.AddHashes(url, arg); assert.equal(`${url}${arg}`, result); arg = "path"; result = UrlBuilder.AddHashes(url, arg); assert.equal(`${url}#${arg}`, result); }); it("should ensure AddHashes supports multiple arg with or without leading #", function() { const url: string = "http://xplatalm.visualstudio.com"; let result: string = UrlBuilder.AddHashes(url, "path", "file"); assert.equal(`${url}#path&file`, result); result = UrlBuilder.AddHashes(url, "#path", "&file"); assert.equal(`${url}#path&file`, result); }); /* Combined function calls */ it("should ensure combined function calls work as expected", function() { const url: string = "http://xplatalm.visualstudio.com/_build"; let result: string = UrlBuilder.Join(url, "index"); result = UrlBuilder.AddQueryParams(result, "buildId=42", "_a=summary"); assert.equal(`${url}/index?buildId=42&_a=summary`, result); result = UrlBuilder.AddHashes(url, "_a=completed"); assert.equal(`${url}#_a=completed`, result); result = UrlBuilder.AddHashes(url, "_a=completed", "definitionId=42"); assert.equal(`${url}#_a=completed&definitionId=42`, result); }); }); ================================================ FILE: test/helpers/utils.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert, expect } from "chai"; const path = require("path"); import { BuildResult } from "vso-node-api/interfaces/BuildInterfaces"; import { Utils } from "../../src/helpers/utils"; import { Strings } from "../../src/helpers/strings"; describe("Utils", function() { const TEST_REPOS_FOLDER: string = "testrepos"; const DOT_GIT_FOLDER: string = "dotgit"; beforeEach(function() { // }); it("should verify IsOffline", function() { let reason: any = { code: "ENOENT" }; assert.isTrue(Utils.IsOffline(reason)); reason = { code: "ENOTFOUND" }; assert.isTrue(Utils.IsOffline(reason)); reason = { code: "EAI_AGAIN" }; assert.isTrue(Utils.IsOffline(reason)); let reason2: any = { statusCode: "ENOENT" }; assert.isTrue(Utils.IsOffline(reason2)); reason2 = { statusCode: "ENOTFOUND" }; assert.isTrue(Utils.IsOffline(reason2)); reason2 = { statusCode: "EAI_AGAIN" }; assert.isTrue(Utils.IsOffline(reason2)); reason = { code: "404" }; assert.isFalse(Utils.IsOffline(reason)); }); it("should verify IsUnauthorized", function() { const reason = { code: 401 }; assert.isTrue(Utils.IsUnauthorized(reason)); const reason2 = { statusCode: 401 }; assert.isTrue(Utils.IsUnauthorized(reason2)); //If no reason, isUnauthorized should be false assert.isFalse(Utils.IsUnauthorized(undefined)); }); it("should verify GetMessageForStatusCode with 401", function() { const reason = { code: "401" }; const message: string = Utils.GetMessageForStatusCode(reason); assert.equal(message, Strings.StatusCode401); }); it("should verify GetMessageForStatusCode for offline - ENOENT", function() { const reason = { code: "ENOENT" }; const message: string = Utils.GetMessageForStatusCode(reason); assert.equal(message, Strings.StatusCodeOffline); }); it("should verify GetMessageForStatusCode for offline - ENOTFOUND", function() { const reason = { code: "ENOTFOUND" }; const message: string = Utils.GetMessageForStatusCode(reason); assert.equal(message, Strings.StatusCodeOffline); }); it("should verify GetMessageForStatusCode for offline - EAI_AGAIN", function() { const reason = { code: "EAI_AGAIN" }; const message: string = Utils.GetMessageForStatusCode(reason); assert.equal(message, Strings.StatusCodeOffline); }); it("should verify GetMessageForStatusCode for proxy - ECONNRESET", function() { const reason = { code: "ECONNRESET" }; process.env.HTTP_PROXY = "azure-repos-vscode unit tests"; const message: string = Utils.GetMessageForStatusCode(reason); process.env.HTTP_PROXY = ""; assert.equal(message, Strings.ProxyUnreachable); }); it("should verify GetMessageForStatusCode for proxy - ECONNREFUSED", function() { const reason = { code: "ECONNREFUSED" }; process.env.HTTP_PROXY = "azure-repos-vscode unit tests"; const message: string = Utils.GetMessageForStatusCode(reason); process.env.HTTP_PROXY = ""; assert.equal(message, Strings.ProxyUnreachable); }); it("should verify GetMessageForStatusCode for no proxy - ECONNRESET", function() { const reason = { code: "ECONNRESET" }; process.env.HTTP_PROXY = ""; const message: string = Utils.GetMessageForStatusCode(reason, "default message"); assert.equal(message, "default message"); }); it("should verify GetMessageForStatusCode for no proxy - ECONNREFUSED", function() { const reason = { code: "ECONNREFUSED" }; process.env.HTTP_PROXY = ""; const message: string = Utils.GetMessageForStatusCode(reason, "default message"); assert.equal(message, "default message"); }); it("should verify GetMessageForStatusCode for 404", function() { const reason = { statusCode: "404" }; const msg = "This should be the message that is returned."; const message: string = Utils.GetMessageForStatusCode(reason, msg); assert.equal(message, msg); }); it("should verify GetMessageForStatusCode for 401 with prefix", function() { const reason = { statusCode: "401" }; const msg = Strings.StatusCode401; const prefix: string = "PREFIX:"; const message: string = Utils.GetMessageForStatusCode(reason, msg, prefix); assert.equal(message, prefix + " " + msg); }); it("should verify FindGitFolder with subfolder", function() { const repoName: string = "gitreposubfolder"; const repoPath: string = path.join(__dirname, TEST_REPOS_FOLDER, repoName, "folder", "subfolder"); // Pass in DOT_GIT_FOLDER to find our test repo folder const actualRepoPath: string = Utils.FindGitFolder(repoPath, DOT_GIT_FOLDER); // Although we started with a subfolder in the repository, ensure we get the DOT_GIT_FOLDER assert.equal(actualRepoPath, path.join(__dirname, TEST_REPOS_FOLDER, repoName, DOT_GIT_FOLDER)); }); it("should verify FindGitFolder with no found .git folder", function() { const repoPath: string = __dirname; //We need use DOT_GIT_FOLDER here since the test resides in a .git repository const actualRepoPath: string = Utils.FindGitFolder(repoPath, DOT_GIT_FOLDER); assert.isUndefined(actualRepoPath); }); it("should verify GetBuildResultIcon with all values", function() { expect(Utils.GetBuildResultIcon(BuildResult.Succeeded)).to.equal("octicon-check"); expect(Utils.GetBuildResultIcon(BuildResult.Canceled)).to.equal("octicon-alert"); expect(Utils.GetBuildResultIcon(BuildResult.Failed)).to.equal("octicon-stop"); expect(Utils.GetBuildResultIcon(BuildResult.PartiallySucceeded)).to.equal("octicon-alert"); expect(Utils.GetBuildResultIcon(BuildResult.None)).to.equal("octicon-question"); expect(Utils.GetBuildResultIcon(undefined)).to.equal("octicon-question"); }); it("should verify IsProxyEnabled", function() { const httpProxy: string = process.env.HTTP_PROXY; const httpsProxy: string = process.env.HTTPS_PROXY; try { process.env.HTTP_PROXY = "azure-repos-vscode unit tests"; assert.isTrue(Utils.IsProxyEnabled()); process.env.HTTP_PROXY = ""; assert.isFalse(Utils.IsProxyEnabled()); process.env.HTTPS_PROXY = "azure-repos-vscode unit tests"; assert.isTrue(Utils.IsProxyEnabled()); process.env.HTTPS_PROXY = ""; assert.isFalse(Utils.IsProxyEnabled()); } finally { if (httpProxy) { process.env.HTTP_PROXY = httpProxy; } if (httpsProxy) { process.env.HTTPS_PROXY = httpsProxy; } } }); it("should verify IsProxyIssue", function() { const httpProxy: string = process.env.HTTP_PROXY; try { process.env.HTTP_PROXY = "azure-repos-vscode unit tests"; let reason: any = { code: "ECONNRESET" }; assert.isTrue(Utils.IsProxyIssue(reason)); let reason2: any = { statusCode: "ECONNRESET" }; assert.isTrue(Utils.IsProxyIssue(reason2)); let reason3: any = { code: "ECONNREFUSED" }; assert.isTrue(Utils.IsProxyIssue(reason3)); let reason4: any = { statusCode: "ECONNREFUSED" }; assert.isTrue(Utils.IsProxyIssue(reason4)); //With proxy enabled, an undefined message should be false assert.isFalse(Utils.IsProxyIssue(undefined)); process.env.HTTP_PROXY = ""; //With proxy not set, the following should not be proxy issues reason = { code: "ECONNRESET" }; assert.isFalse(Utils.IsProxyIssue(reason)); reason2 = { statusCode: "ECONNRESET" }; assert.isFalse(Utils.IsProxyIssue(reason2)); reason3 = { code: "ECONNREFUSED" }; assert.isFalse(Utils.IsProxyIssue(reason3)); reason4 = { statusCode: "ECONNREFUSED" }; assert.isFalse(Utils.IsProxyIssue(reason4)); } finally { if (httpProxy) { process.env.HTTP_PROXY = httpProxy; } } }); }); ================================================ FILE: test/index.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; // // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING // // This file is providing the test runner to use when running extension tests. // By default the test runner in use is Mocha based. // // You can provide your own test runner if you want to override it by exporting // a function run(testRoot: string, clb: (error:Error) => void) that the extension // host can call to run the tests. The test runner is expected to use console.log // to report the results back to the caller. When the tests are finished, return // a possible error to the callback or null if none. /* tslint:disable:no-var-keyword */ var testRunner = require("vscode/lib/testrunner"); /* tslint:enable:no-var-keyword */ // You can directly control Mocha options by uncommenting the following lines // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ ui: "bdd", // Switched to bdd; use tdd for the TDD UI is being used in extension.test.ts (suite, test, etc.) useColors: true // colored output from test results }); module.exports = testRunner; ================================================ FILE: test/info/credentialinfo.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { CredentialInfo } from "../../src/info/credentialinfo"; import { ExtensionRequestHandler } from "../../src/info/extensionrequesthandler"; describe("CredentialInfo", function() { it("should ensure all properties work as expected", function() { const credentialInfo: CredentialInfo = new CredentialInfo("username", "password", "domain", "workstation"); assert.isDefined(credentialInfo); assert.isDefined(credentialInfo.CredentialHandler); assert.equal(credentialInfo.Username, "username"); assert.equal(credentialInfo.Password, "password"); assert.equal(credentialInfo.Domain, "domain"); assert.equal(credentialInfo.Workstation, "workstation"); }); it("should ensure PAT returns a BasicCredentialHandler", function() { const credentialInfo: CredentialInfo = new CredentialInfo("pat-token"); const basic: ExtensionRequestHandler = (credentialInfo.CredentialHandler); assert.isDefined(basic); assert.equal(basic.Password, "pat-token"); }); it("should ensure username + password returns an NtlmCredentialHandler", function() { const credentialInfo: CredentialInfo = new CredentialInfo("username", "password"); assert.isDefined(credentialInfo); assert.isDefined(credentialInfo.CredentialHandler); const ntlm: ExtensionRequestHandler = (credentialInfo.CredentialHandler); assert.isDefined(ntlm); assert.equal(ntlm.Username, "username"); assert.equal(ntlm.Password, "password"); }); it("should ensure username + password + domain returns an NtlmCredentialHandler", function() { const credentialInfo: CredentialInfo = new CredentialInfo("username", "password", "domain"); assert.isDefined(credentialInfo); assert.isDefined(credentialInfo.CredentialHandler); const ntlm: ExtensionRequestHandler = (credentialInfo.CredentialHandler); assert.isDefined(ntlm); assert.equal(ntlm.Username, "username"); assert.equal(ntlm.Password, "password"); }); it("should ensure username + password + domain + workstation returns an NtlmCredentialHandler", function() { const credentialInfo: CredentialInfo = new CredentialInfo("username", "password", "domain", "workstation"); assert.isDefined(credentialInfo); assert.isDefined(credentialInfo.CredentialHandler); const ntlm: ExtensionRequestHandler = (credentialInfo.CredentialHandler); assert.isDefined(ntlm); assert.equal(ntlm.Username, "username"); assert.equal(ntlm.Password, "password"); assert.equal(ntlm.Domain, "domain"); assert.equal(ntlm.Workstation, "workstation"); }); it("should ensure properties work as intended", function() { const credentialInfo: CredentialInfo = new CredentialInfo("pat-token"); const basic: ExtensionRequestHandler = (credentialInfo.CredentialHandler); credentialInfo.CredentialHandler = undefined; assert.isUndefined(credentialInfo.CredentialHandler); credentialInfo.CredentialHandler = basic; assert.isNotNull(credentialInfo.CredentialHandler); }); }); ================================================ FILE: test/info/repositoryinfo.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { RepositoryInfo } from "../../src/info/repositoryinfo"; describe("RepositoryInfo", function() { /* Team Foundation Server URLs */ it("should verify host, account and isTeamFoundationServer for valid remoteUrl", function() { const url: string = "http://jeyou-dev00000:8080/tfs/DefaultCollection/_git/GitAgile"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "jeyou-dev00000:8080"); //TODO: Should host on-prem contain the port number? assert.equal(repoInfo.Account, "jeyou-dev00000:8080"); //TODO: Should account on-prem contain the port number? assert.isUndefined(repoInfo.AccountUrl); // we only get this when we pass a JSON blob assert.isTrue(repoInfo.IsTeamFoundation); assert.isTrue(repoInfo.IsTeamFoundationServer); assert.isFalse(repoInfo.IsTeamServices); assert.equal(repoInfo.RepositoryUrl, url); assert.equal(repoInfo.Protocol, "http:"); // For on-prem currently, these should not be set assert.equal(repoInfo.CollectionId, undefined); assert.equal(repoInfo.CollectionName, undefined); assert.equal(repoInfo.CollectionUrl, undefined); assert.equal(repoInfo.RepositoryId, undefined); assert.equal(repoInfo.RepositoryName, undefined); assert.equal(repoInfo.TeamProject, undefined); assert.equal(repoInfo.TeamProjectUrl, undefined); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor", function() { const repositoryInfo: any = {    "serverUrl": "http://server:8080/tfs",    "collection": {       "id": "d543db53-9479-46c1-9d33-2cb9cb76f622",       "name": "DefaultCollection",       "url": "http://server:8080/tfs/_apis/projectCollections/d543db53-9479-46c1-9d33-2cb9cb76f622"    },    "repository": {       "id": "23344766-d9c7-4661-856d-b2096753c5e3",       "name": "repositoryName",       "url": "http://server:8080/tfs/DefaultCollection/_apis/git/repositories/23344766-d9c7-4661-856d-b2096753c5e3",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "http://server:8080/tfs/DefaultCollection/_apis/projects/8c03f107-42c7-4dea-a3cc-e52972d841a9",          "state": 1,          "revision": 7       },       "remoteUrl": "http://server:8080/tfs/DefaultCollection/teamproject/_git/repositoryName"    } }; const repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "server:8080"); //TODO: Should host on-prem contain the port number? assert.equal(repoInfo.Account, "server:8080"); //TODO: Should account on-prem contain the port number? assert.equal(repoInfo.AccountUrl, "http://server:8080/tfs"); assert.equal(repoInfo.CollectionId, "d543db53-9479-46c1-9d33-2cb9cb76f622"); assert.equal(repoInfo.CollectionName, "DefaultCollection"); assert.equal(repoInfo.CollectionUrl, "http://server:8080/tfs/DefaultCollection"); assert.isFalse(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isTrue(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "23344766-d9c7-4661-856d-b2096753c5e3"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "http://server:8080/tfs/DefaultCollection/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "http://server:8080/tfs/DefaultCollection/teamproject"); }); /* Team Services URLs */ it("should verify undefined remoteUrl", function() { assert.throws(() => { new RepositoryInfo(undefined); }); }); it("should verify host, account and isTeamServices for valid remoteUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.Protocol, "https:"); assert.equal(repoInfo.RepositoryUrl, url); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); }); it("should verify host, account and isTeamServices for valid azure remoteUrl", function() { const url: string = "https://test.azure.com/account/teamproject/_git/repositoryName"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.Protocol, "https:"); assert.equal(repoInfo.RepositoryUrl, url); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); }); it("should verify host, account and isTeamServices for valid remoteUrl - limited refs - full", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/teamproject/_git/_full/repositoryName"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.Protocol, "https:"); assert.equal(repoInfo.RepositoryUrl, "https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); }); it("should verify host, account and isTeamServices for valid azure remoteUrl - limited refs - full", function() { const url: string = "https://test.azure.com/account/teamproject/_git/_full/repositoryName"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.Protocol, "https:"); assert.equal(repoInfo.RepositoryUrl, "https://test.azure.com/account/teamproject/_git/repositoryName"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); }); it("should verify host, account and isTeamServices for valid remoteUrl - limited refs - optimized", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/teamproject/_git/_optimized/repositoryName"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.Protocol, "https:"); assert.equal(repoInfo.RepositoryUrl, "https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); }); it("should verify host, account and isTeamServices for valid azure remoteUrl - limited refs - optimized", function() { const url: string = "https://test.azure.com/account/teamproject/_git/_optimized/repositoryName"; const repoInfo: RepositoryInfo = new RepositoryInfo(url); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.Protocol, "https:"); assert.equal(repoInfo.RepositoryUrl, "https://test.azure.com/account/teamproject/_git/repositoryName"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); const repositoryInfo: any = {    "serverUrl": "https://account.visualstudio.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "account",       "url": "https://account.visualstudio.com/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://account.visualstudio.com/DefaultCollection/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://account.visualstudio.com/DefaultCollection/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://account.visualstudio.com/teamproject/_git/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://account.visualstudio.com"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); assert.equal(repoInfo.CollectionName, "account"); assert.equal(repoInfo.CollectionUrl, "https://account.visualstudio.com"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://account.visualstudio.com/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://account.visualstudio.com/teamproject"); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor for azure", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); const repositoryInfo: any = {    "serverUrl": "https://test.azure.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "account",       "url": "https://test.azure.com/account/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://test.azure.com/account/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://test.azure.com/account/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://test.azure.com/account/teamproject/_git/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://test.azure.com/account"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); assert.equal(repoInfo.CollectionName, "account"); assert.equal(repoInfo.CollectionUrl, "https://test.azure.com/account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://test.azure.com/account/teamproject"); }); it("should verify 'collection in the domain' case insensitivity in repositoryInfo to RepositoryInfo constructor", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); // To properly test 'collection in the domain' case insensitivity, ensure the collection name is a different case than the account (e.g., 'ACCOUNT' versus 'account') const repositoryInfo: any = {    "serverUrl": "https://account.visualstudio.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "ACCOUNT",       "url": "https://account.visualstudio.com/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://account.visualstudio.com/DefaultCollection/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://account.visualstudio.com/DefaultCollection/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://account.visualstudio.com/teamproject/_git/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://account.visualstudio.com"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); // CollectionName should maintain the same case as in the JSON assert.equal(repoInfo.CollectionName, "ACCOUNT"); // CollectionUrl should not contain the collection name since both account and collection name are the same (case insensitive) assert.equal(repoInfo.CollectionUrl, "https://account.visualstudio.com"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://account.visualstudio.com/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://account.visualstudio.com/teamproject"); }); it("should verify 'collection in the domain' case insensitivity in repositoryInfo to RepositoryInfo constructor for azure", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); // To properly test 'collection in the domain' case insensitivity, ensure the collection name is a different case than the account (e.g., 'ACCOUNT' versus 'account') const repositoryInfo: any = {    "serverUrl": "https://test.azure.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "ACCOUNT",       "url": "https://test.azure.com/account/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://test.azure.com/account/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://test.azure.com/account/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://test.azure.com/account/teamproject/_git/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://test.azure.com/account"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); // CollectionName should maintain the same case as in the JSON assert.equal(repoInfo.CollectionName, "ACCOUNT"); // CollectionUrl should not contain the collection name since this is an azure-backed/test.azure.com account & collection name are the same (case insensitive) assert.equal(repoInfo.CollectionUrl, "https://test.azure.com/account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://test.azure.com/account/teamproject"); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor - limited refs - full", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); const repositoryInfo: any = {    "serverUrl": "https://account.visualstudio.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "account",       "url": "https://account.visualstudio.com/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://account.visualstudio.com/DefaultCollection/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://account.visualstudio.com/DefaultCollection/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://account.visualstudio.com/teamproject/_git/_full/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://account.visualstudio.com"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); assert.equal(repoInfo.CollectionName, "account"); assert.equal(repoInfo.CollectionUrl, "https://account.visualstudio.com"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://account.visualstudio.com/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://account.visualstudio.com/teamproject"); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor - limited refs - full: for azure", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); const repositoryInfo: any = {    "serverUrl": "https://test.azure.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "account",       "url": "https://test.azure.com/account/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://test.azure.com/account/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://test.azure.com/account/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://test.azure.com/account/teamproject/_git/_full/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://test.azure.com/account"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); assert.equal(repoInfo.CollectionName, "account"); assert.equal(repoInfo.CollectionUrl, "https://test.azure.com/account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://test.azure.com/account/teamproject"); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor - limited refs - optimized", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://account.visualstudio.com/DefaultCollection/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); const repositoryInfo: any = {    "serverUrl": "https://account.visualstudio.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "account",       "url": "https://account.visualstudio.com/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://account.visualstudio.com/DefaultCollection/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://account.visualstudio.com/DefaultCollection/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://account.visualstudio.com/teamproject/_git/_optimized/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "account.visualstudio.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://account.visualstudio.com"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); assert.equal(repoInfo.CollectionName, "account"); assert.equal(repoInfo.CollectionUrl, "https://account.visualstudio.com"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://account.visualstudio.com/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://account.visualstudio.com/teamproject"); }); it("should verify valid values in repositoryInfo to RepositoryInfo constructor - limited refs - optimized: for azure", function() { let repoInfo: RepositoryInfo = new RepositoryInfo("https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); const repositoryInfo: any = {    "serverUrl": "https://test.azure.com",    "collection": {       "id": "5e082e28-e8b2-4314-9200-629619e91098",       "name": "account",       "url": "https://test.azure.com/account/_apis/projectCollections/5e082e28-e8b2-4314-9200-629619e91098"    },    "repository": {       "id": "cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "name": "repositoryName",       "url": "https://test.azure.com/account/_apis/git/repositories/cc015c05-de20-4e3f-b3bc-3662b6bc0e42",       "project": {          "id": "ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "name": "teamproject",          "description": "Our team project",          "url": "https://test.azure.com/account/_apis/projects/ecbf2301-0e62-4b0d-a12d-1992f2ea95a8",          "state": 1,          "revision": 14558       },       "remoteUrl": "https://test.azure.com/account/teamproject/_git/_optimized/repositoryName"    } }; repoInfo = new RepositoryInfo(repositoryInfo); assert.equal(repoInfo.Host, "test.azure.com"); assert.equal(repoInfo.Account, "account"); assert.equal(repoInfo.AccountUrl, "https://test.azure.com/account"); assert.equal(repoInfo.CollectionId, "5e082e28-e8b2-4314-9200-629619e91098"); assert.equal(repoInfo.CollectionName, "account"); assert.equal(repoInfo.CollectionUrl, "https://test.azure.com/account"); assert.isTrue(repoInfo.IsTeamServices); assert.isTrue(repoInfo.IsTeamFoundation); assert.isFalse(repoInfo.IsTeamFoundationServer); assert.equal(repoInfo.RepositoryId, "cc015c05-de20-4e3f-b3bc-3662b6bc0e42"); assert.equal(repoInfo.RepositoryName, "repositoryName"); assert.equal(repoInfo.RepositoryUrl, "https://test.azure.com/account/teamproject/_git/repositoryName"); assert.equal(repoInfo.TeamProject, "teamproject"); assert.equal(repoInfo.TeamProjectUrl, "https://test.azure.com/account/teamproject"); }); }); ================================================ FILE: test/info/userinfo.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { UserInfo } from "../../src/info/userinfo"; describe("UserInfo", function() { it("should verify the constructor sets the proper values", function() { const user: UserInfo = new UserInfo("id", "providerDisplayName", "customDisplayName"); assert.equal(user.Id, "id"); assert.equal(user.ProviderDisplayName, "providerDisplayName"); assert.equal(user.CustomDisplayName, "customDisplayName"); }); }); ================================================ FILE: test/services/build.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { BuildService } from "../../src/services/build"; describe("BuildService", function() { beforeEach(function() { // }); it("should verify GetBuildDefinitionsUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; //The new definitions experience is behind a feature flag //assert.equal(BuildService.GetBuildDefinitionsUrl(url), url + "/_build/definitions"); assert.equal(BuildService.GetBuildDefinitionsUrl(url), url + "/_build"); }); it("should verify GetBuildDefinitionUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const arg: string = "42"; assert.equal(BuildService.GetBuildDefinitionUrl(url, arg), url + "/_build#_a=completed&definitionId=" + arg); }); it("should verify GetBuildSummaryUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const arg: string = "42"; assert.equal(BuildService.GetBuildSummaryUrl(url, arg), url + "/_build/index?buildId=" + arg + "&_a=summary"); }); it("should verify GetBuildsUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; assert.equal(BuildService.GetBuildsUrl(url), url + "/_build"); }); }); ================================================ FILE: test/services/gitvc.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { GitPullRequest, PullRequestAsyncStatus } from "vso-node-api/interfaces/GitInterfaces"; import { GitVcService, PullRequestScore } from "../../src/services/gitvc"; describe("GitVcService", function() { beforeEach(function() { // }); it("should verify GetCreatePullRequestUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const branch: string = "branch"; assert.equal(GitVcService.GetCreatePullRequestUrl(url, branch), url + "/pullrequests#_a=createnew&sourceRef=" + branch); }); it("should verify GetFileBlameUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const file: string = "team-extension.ts"; const branch: string = "branch"; assert.equal(GitVcService.GetFileBlameUrl(url, file, branch), url + "#path=" + file + "&version=GB" + branch + "&annotate=true"); }); it("should verify GetFileHistoryUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const file: string = "team-extension.ts"; const branch: string = "branch"; assert.equal(GitVcService.GetFileHistoryUrl(url, file, branch), url + "#path=" + file + "&version=GB" + branch + "&_a=history"); }); it("should verify GetPullRequestDiscussionUrl", function() { const repositoryUrl: string = "https://account.visualstudio.com/DefaultCollection/_git/project"; const id: string = "42"; assert.equal(GitVcService.GetPullRequestDiscussionUrl(repositoryUrl, id), repositoryUrl + "/pullrequest/" + id + "?view=discussion"); }); it("should verify GetPullRequestsUrl", function() { const repositoryUrl: string = "https://account.visualstudio.com/DefaultCollection/_git/project"; assert.equal(GitVcService.GetPullRequestsUrl(repositoryUrl), repositoryUrl + "/pullrequests"); }); it("should verify GetPullRequestDiscussionUrl", function() { const repositoryUrl: string = "https://account.visualstudio.com/DefaultCollection/_git/project"; const branch: string = "branch"; assert.equal(GitVcService.GetRepositoryHistoryUrl(repositoryUrl, branch), repositoryUrl + "/history" + "?itemVersion=GB" + branch + "&_a=history"); }); it("should verify failed pull request score", function() { const pullRequest: GitPullRequest = { mergeStatus: PullRequestAsyncStatus.Conflicts, _links: undefined, closedDate: undefined, codeReviewId: undefined, commits: undefined, completionOptions: undefined, completionQueueTime: undefined, createdBy: undefined, creationDate: undefined, description: undefined, mergeId: undefined, lastMergeCommit: undefined, lastMergeSourceCommit: undefined, lastMergeTargetCommit: undefined, pullRequestId: undefined, remoteUrl: undefined, repository: undefined, reviewers: undefined, sourceRefName: undefined, status: undefined, targetRefName: undefined, title: undefined, autoCompleteSetBy: undefined, closedBy: undefined, artifactId: undefined, supportsIterations: undefined, url: undefined, workItemRefs: undefined }; assert.equal(GitVcService.GetPullRequestScore(pullRequest), PullRequestScore.Failed); pullRequest.mergeStatus = PullRequestAsyncStatus.Failure; assert.equal(GitVcService.GetPullRequestScore(pullRequest), PullRequestScore.Failed); pullRequest.mergeStatus = PullRequestAsyncStatus.RejectedByPolicy; assert.equal(GitVcService.GetPullRequestScore(pullRequest), PullRequestScore.Failed); }); }); ================================================ FILE: test/services/workitemtracking.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { WorkItemFields, WorkItemTrackingService } from "../../src/services/workitemtracking"; describe("WorkItemTrackingService", function() { beforeEach(function() { // }); it("should verify GetEditWorkItemUrl", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const id: string = "42"; assert.equal(WorkItemTrackingService.GetEditWorkItemUrl(url, id), url + "/_workitems" + "/edit/" + id); }); it("should verify GetNewWorkItemUrl with only url and issueType", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const issueType: string = "Bug"; assert.equal(WorkItemTrackingService.GetNewWorkItemUrl(url, issueType), url + "/_workitems" + "/create/" + issueType); }); it("should verify GetNewWorkItemUrl with url, issueType and title", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const issueType: string = "Bug"; const title: string = "Fix this bug!"; assert.equal(WorkItemTrackingService.GetNewWorkItemUrl(url, issueType, title), url + "/_workitems" + "/create/" + issueType + "?[" + WorkItemFields.Title + "]=" + title); }); it("should verify GetNewWorkItemUrl with url, issueType, title and assignedTo", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const issueType: string = "Bug"; const title: string = "Fix this bug!"; const assignedTo: string = "raisap@outlook.com"; assert.equal(WorkItemTrackingService.GetNewWorkItemUrl(url, issueType, title, assignedTo), url + "/_workitems" + "/create/" + issueType + "?[" + WorkItemFields.Title + "]=" + title + "&" + "[" + WorkItemFields.AssignedTo + "]=" + assignedTo); }); it("should verify GetMyQueryResultsUrl with url and queryName", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; const queryName: string = "My Favorite Query"; assert.equal(WorkItemTrackingService.GetMyQueryResultsUrl(url, "My Queries", queryName), url + "/_workitems" + "?path=" + encodeURIComponent("My Queries/") + encodeURIComponent(queryName) + "&_a=query"); }); it("should verify GetWorkItemsBaseUrl with url", function() { const url: string = "https://account.visualstudio.com/DefaultCollection/project"; assert.equal(WorkItemTrackingService.GetWorkItemsBaseUrl(url), url + "/_workitems"); }); }); ================================================ FILE: test/tfvc/commands/add.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { Add } from "../../../src/tfvc/commands/add"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-AddCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor - windows", function() { const localPaths: string[] = ["c:\\repos\\Tfvc.L2VSCodeExtension.RC\\README.md"]; new Add(undefined, localPaths); }); it("should verify constructor - mac/linux", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; new Add(undefined, localPaths); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; new Add(context, localPaths); }); it("should verify constructor - undefined args", function() { assert.throws(() => new Add(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Add = new Add(undefined, localPaths); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Add = new Add(undefined, localPaths); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Add = new Add(undefined, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "add -noprompt " + localPaths[0]); }); it("should verify Exe arguments", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Add = new Add(undefined, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "add -noprompt " + localPaths[0]); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Add = new Add(context, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "add -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0]); }); it("should verify Exe arguments with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Add = new Add(context, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "add -noprompt ******** " + localPaths[0]); }); it("should verify parse output - no files to add", async function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/file-does-not-exist.md"]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "No arguments matched any files to add.", stderr: undefined }; const filesAdded: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesAdded.length, 0); }); it("should verify parse output - single empty folder - no errors", async function() { const localPaths: string[] = ["empty-folder"]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "empty-folder:\n" + "empty-folder\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesAdded.length, 1); //In this case, the CLC returns: //empty-folder: //empty-folder //So we will have to return empty-folder\empty-folder. Not ideal but let's ensure we're getting that. assert.equal(filesAdded[0], path.join(localPaths[0], localPaths[0])); }); it("should verify parse output - single folder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "file1.txt")]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "folder1:\n" + "file1.txt\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesAdded.length, 1); assert.equal(filesAdded[0], localPaths[0]); }); it("should verify parse output - single subfolder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "folder2", "file2.txt")]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "folder2") + ":\n" + "file2.txt\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesAdded.length, 1); assert.equal(filesAdded[0], localPaths[0]); }); it("should verify parse output - single folder+file - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "file1.txt")]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "fold er1:\n" + "file1.txt\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesAdded.length, 1); assert.equal(filesAdded[0], localPaths[0]); }); /// Verify ParseExeOutput values (for tf.exe) it("should verify parse Exe output - no files to add", async function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/file-does-not-exist.md"]; const cmd: Add = new Add(undefined, localPaths); //This return value is different for tf.exe than the CLC const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/file-does-not-exist.md: No file matches.", stderr: undefined }; const filesAdded: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesAdded.length, 0); }); it("should verify parse Exe output - single empty folder - no errors", async function() { const localPaths: string[] = ["empty-folder"]; const cmd: Add = new Add(undefined, localPaths); //This return value is different for tf.exe than the CLC const executionResult: IExecutionResult = { exitCode: 0, stdout: "empty-folder\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesAdded.length, 1); //In this case, the EXE returns (this differs from the CLC): //empty-folder assert.equal(filesAdded[0], localPaths[0]); }); it("should verify parse Exe output - single folder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "file1.txt")]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "folder1:\n" + "file1.txt\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesAdded.length, 1); assert.equal(filesAdded[0], localPaths[0]); }); it("should verify parse Exe output - single subfolder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "folder2", "file2.txt")]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "folder2") + ":\n" + "file2.txt\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesAdded.length, 1); assert.equal(filesAdded[0], localPaths[0]); }); it("should verify parse Exe output - single folder+file - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "file1.txt")]; const cmd: Add = new Add(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "fold er1:\n" + "file1.txt\n", stderr: undefined }; const filesAdded: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesAdded.length, 1); assert.equal(filesAdded[0], localPaths[0]); }); }); ================================================ FILE: test/tfvc/commands/argumentbuilder.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { ArgumentBuilder } from "../../../src/tfvc/commands/argumentbuilder"; import { IArgumentProvider } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; describe("Tfvc-ArgumentBuilder", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const cmd: string = "mycmd"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd); assert.equal(builder.GetCommand(), cmd); const args = builder.Build(); assert.equal(args[0], cmd); assert.equal(args[1], "-noprompt"); assert.equal(args.length, 2); }); it("should verify constructor with context", function() { const cmd: string = "mycmd"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd, context); assert.equal(builder.GetCommand(), cmd); const args = builder.Build(); assert.equal(args[0], cmd); assert.equal(args[1], "-noprompt"); assert.equal(args[2], "-collection:" + collectionUrl); assert.equal(args[3], "-login:" + user + "," + pass); assert.equal(args.length, 4); }); it("should verify constructor with context - user and domain", function() { context.CredentialInfo = new CredentialInfo(user, pass, "domain", "workstation"); const cmd: string = "mycmd"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd, context); assert.equal(builder.GetCommand(), cmd); const args = builder.Build(); assert.equal(args[0], cmd); assert.equal(args[1], "-noprompt"); assert.equal(args[2], "-collection:" + collectionUrl); assert.equal(args[3], "-login:" + `domain\\${user}` + "," + pass); assert.equal(args.length, 4); }); it("should verify constructor error", function() { assert.throws(() => new ArgumentBuilder(undefined), TfvcError, /Argument is required/); }); it("should verify ToString", function() { const cmd: string = "mycmd"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd, context); assert.equal(builder.ToString(), "mycmd -noprompt -collection:" + collectionUrl + " ********"); }); it("should verify BuildCommandLine with context", function() { const cmd: string = "mycmd"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd, context); assert.equal(builder.BuildCommandLine().trim(), "mycmd -noprompt -collection:" + collectionUrl + " -login:" + user + "," + pass); }); it("should verify BuildCommandLine", function() { const cmd: string = "mycmd"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd); assert.equal(builder.BuildCommandLine().trim(), "mycmd -noprompt"); }); it("should verify BuildCommandLine with spaces in options", function() { const cmd: string = "mycmd"; const path: string = "/path/with a space/file.txt"; const option: string = "option with space"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd); builder.Add(path); builder.AddSwitch(option); builder.AddSwitchWithValue(option, path, false); assert.equal(builder.BuildCommandLine().trim(), "mycmd -noprompt \"/path/with a space/file.txt\" \"-option with space\" \"-option with space:/path/with a space/file.txt\""); }); it("should verify BuildCommandLine with spaces in some", function() { const cmd: string = "mycmd"; const path: string = "/path/with a space/file.txt"; const option: string = "option"; const builder: ArgumentBuilder = new ArgumentBuilder(cmd); builder.Add(path); builder.AddSwitch(option); builder.AddSwitchWithValue(option, path, false); assert.equal(builder.BuildCommandLine().trim(), "mycmd -noprompt \"/path/with a space/file.txt\" -option \"-option:/path/with a space/file.txt\""); }); it("should verify interface implemented", function() { const cmd: string = "mycmd"; const argProvider: IArgumentProvider = new ArgumentBuilder(cmd, context); // GetCommand assert.equal(argProvider.GetCommand(), cmd); // GetArguments const args = argProvider.GetArguments(); assert.equal(args[0], cmd); assert.equal(args[1], "-noprompt"); assert.equal(args[2], "-collection:" + collectionUrl); assert.equal(args[3], "-login:" + user + "," + pass); assert.equal(args.length, 4); // GetArgumentsForDisplay assert.equal(argProvider.GetArgumentsForDisplay(), "mycmd -noprompt -collection:" + collectionUrl + " ********"); // GetCommandLine assert.equal(argProvider.GetCommandLine(), "mycmd -noprompt -collection:" + collectionUrl + " -login:" + user + "," + pass + " \n"); // AddProxySwitch argProvider.AddProxySwitch("TFSProxy"); assert.equal(argProvider.GetArgumentsForDisplay(), "mycmd -noprompt -collection:" + collectionUrl + " ******** -proxy:TFSProxy"); }); }); ================================================ FILE: test/tfvc/commands/checkin.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Checkin } from "../../../src/tfvc/commands/checkin"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-CheckinCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const files: string[] = ["/path/to/workspace/file.txt"]; new Checkin(undefined, files); }); it("should verify constructor with context", function() { const files: string[] = ["/path/to/workspace/file.txt"]; new Checkin(context, files); }); it("should verify constructor - undefined args", function() { assert.throws(() => new Checkin(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "checkin -noprompt " + files[0]); }); it("should verify Exe arguments", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "checkin -noprompt " + files[0]); }); it("should verify arguments with context", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "checkin -noprompt -collection:" + collectionUrl + " ******** " + files[0]); }); it("should verify Exe arguments with context", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "checkin -noprompt ******** " + files[0]); }); it("should verify arguments with workitems", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files, undefined, [1, 2, 3]); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "checkin -noprompt -collection:" + collectionUrl + " ******** " + files[0] + " -associate:1,2,3"); }); it("should verify Exe arguments with workitems", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files, undefined, [1, 2, 3]); //Note that no associate option should be here (tf.exe doesn't support it) assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "checkin -noprompt ******** " + files[0]); }); it("should verify arguments with comment", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files, "a comment\nthat has\r\nmultiple lines"); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "checkin -noprompt -collection:" + collectionUrl + " ******** " + files[0] + " -comment:a comment that has multiple lines"); }); it("should verify Exe arguments with comment", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files, "a comment\nthat has\r\nmultiple lines"); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "checkin -noprompt ******** " + files[0] + " -comment:a comment that has multiple lines"); }); it("should verify arguments with all params", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files, "a comment", [1, 2, 3]); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "checkin -noprompt -collection:" + collectionUrl + " ******** " + files[0] + " -comment:a comment -associate:1,2,3"); }); it("should verify Exe arguments with all params", function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(context, files, "a comment", [1, 2, 3]); //Note that no associate option should be here (tf.exe doesn't support it) assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "checkin -noprompt ******** " + files[0] + " -comment:a comment"); }); it("should verify parse output - no output", async function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const result: string = await cmd.ParseOutput(executionResult); assert.equal(result, ""); }); it("should verify parse output - no errors", async function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/Users/leantk/tfvc-tfs/tfsTest_01/addFold:\n" + "Checking in edit: testHere.txt\n" + "\n" + "/Users/leantk/tfvc-tfs/tfsTest_01:\n" + "Checking in edit: test3.txt\n" + "Checking in edit: TestAdd.txt\n" + "\n" + "Changeset #23 checked in.\n", stderr: undefined }; const result: string = await cmd.ParseOutput(executionResult); assert.equal(result, "23"); }); it("should verify parse output - with error", async function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); const executionResult: IExecutionResult = { exitCode: 100, stdout: "/Users/leantk/tfvc-tfs/tfsTest_01:\n" + "Checking in edit: test3.txt\n" + "Checking in edit: TestAdd.txt\n" + "No files checked in.\n", stderr: "A resolvable conflict was flagged by the server: No files checked in due to conflicting changes. Resolve the conflicts and try the check-in again.\n" }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); } }); // // // it("should verify parse Exe output - no output", async function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const result: string = await cmd.ParseExeOutput(executionResult); assert.equal(result, ""); }); it("should verify parse Exe output - no errors", async function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/Users/leantk/tfvc-tfs/tfsTest_01/addFold:\n" + "Checking in edit: testHere.txt\n" + "\n" + "/Users/leantk/tfvc-tfs/tfsTest_01:\n" + "Checking in edit: test3.txt\n" + "Checking in edit: TestAdd.txt\n" + "\n" + "Changeset #23 checked in.\n", stderr: undefined }; const result: string = await cmd.ParseExeOutput(executionResult); assert.equal(result, "23"); }); it("should verify parse Exe output - with error", async function() { const files: string[] = ["/path/to/workspace/file.txt"]; const cmd: Checkin = new Checkin(undefined, files); const executionResult: IExecutionResult = { exitCode: 100, stdout: "/Users/leantk/tfvc-tfs/tfsTest_01:\n" + "Checking in edit: test3.txt\n" + "Checking in edit: TestAdd.txt\n" + "No files checked in.\n", stderr: "A resolvable conflict was flagged by the server: No files checked in due to conflicting changes. Resolve the conflicts and try the check-in again.\n" }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); } }); }); ================================================ FILE: test/tfvc/commands/commandhelper.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { Strings } from "../../../src/helpers/strings"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TfvcError, TfvcErrorCodes } from "../../../src/tfvc/tfvcerror"; import { CommandHelper } from "../../../src/tfvc/commands/commandhelper"; describe("Tfvc-CommandHelper", function() { it("should verify RequireArgument - success", function() { const arg: any = { prop1: "prop 1" }; CommandHelper.RequireArgument(arg, "arg"); }); it("should verify RequireArgument - failure", function() { const arg: any = undefined; assert.throws(() => CommandHelper.RequireArgument(arg, "arg"), TfvcError, /Argument is required/); }); it("should verify RequireStringArgument - success", function() { const arg: string = "myString"; CommandHelper.RequireStringArgument(arg, "arg"); }); it("should verify RequireStringArgument - failure", function() { const arg: string = ""; assert.throws(() => CommandHelper.RequireStringArgument(arg, "arg"), TfvcError, /Argument is required/); }); it("should verify RequireStringArrayArgument - success", function() { const arg: string[] = ["myString"]; CommandHelper.RequireStringArrayArgument(arg, "arg"); }); it("should verify RequireStringArrayArgument - failure - empty array", function() { const arg: string[] = []; assert.throws(() => CommandHelper.RequireStringArrayArgument(arg, "arg"), TfvcError, /Argument is required/); }); it("should verify RequireStringArrayArgument - failure - undefined", function() { const arg: string[] = undefined; assert.throws(() => CommandHelper.RequireStringArrayArgument(arg, "arg"), TfvcError, /Argument is required/); }); it("should verify HasError - no errors", function() { const result: IExecutionResult = { exitCode: 123, stdout: undefined, stderr: undefined }; assert.equal(CommandHelper.HasError(result, ".*"), false); assert.equal(CommandHelper.HasError(result, ""), false); assert.equal(CommandHelper.HasError(result, undefined), false); // Make sure undefined for result returns false as well assert.equal(CommandHelper.HasError(undefined, undefined), false); }); it("should verify HasError - has errors", function() { const result: IExecutionResult = { exitCode: 123, stdout: undefined, stderr: "Something bad happened!" }; assert.equal(CommandHelper.HasError(result, "Something bad happened!"), true); assert.equal(CommandHelper.HasError(result, "Something bad happened"), true); assert.equal(CommandHelper.HasError(result, "bad happened"), true); assert.equal(CommandHelper.HasError(result, " bad happened"), true); assert.equal(CommandHelper.HasError(result, "happened"), true); assert.equal(CommandHelper.HasError(result, "bad"), true); assert.equal(CommandHelper.HasError(result, "good"), false); assert.equal(CommandHelper.HasError(result, undefined), false); }); it("should verify ProcessErrors - no errors", function() { const result: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; CommandHelper.ProcessErrors(result); }); it("should verify ProcessErrors - bad exit code only", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: undefined }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify ProcessErrors - auth failed", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "Authentication failed" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.AuthenticationFailed); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify ProcessErrors - auth failed", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "workspace could not be determined" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); assert.isTrue(err.message.startsWith(Strings.NoWorkspaceMappings)); } }); it("should verify ProcessErrors - no workspace", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "workspace could not be determined" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); assert.isTrue(err.message.startsWith(Strings.NoWorkspaceMappings)); } }); it("should verify ProcessErrors - no repo", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "Repository not found" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.RepositoryNotFound); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify ProcessErrors - no collection", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "project collection URL to use could not be determined" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); assert.isTrue(err.message.startsWith(Strings.NotATfvcRepository)); } }); it("should verify ProcessErrors - access denied", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "Access denied connecting: some other text: authenticating as OAuth" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.AuthenticationFailed); assert.isTrue(err.message.startsWith(Strings.TokenNotAllScopes)); } }); it("should verify ProcessErrors - no java", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "'java' is not recognized as an internal or external command" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotFound); assert.isTrue(err.message.startsWith(Strings.TfInitializeFailureError)); } }); it("should verify ProcessErrors - no mapping", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "There is no working folder mapping" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.FileNotInMappings); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify ProcessErrors - not in workspace", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "could not be found in your workspace, or you do not have permission to access it." }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.FileNotInWorkspace); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify ProcessErrors - Server workspace detection (TF30063)", function() { const result: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: "TF30063: You are not authorized to access anything because I said so" }; try { CommandHelper.ProcessErrors(result); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAuthorizedToAccess); assert.isTrue(err.message.startsWith(Strings.TfServerWorkspace)); assert.isDefined(err.messageOptions); assert.isTrue(err.messageOptions.length === 1); } }); it("should verify SplitIntoLines", function() { const text: string = "one\ntwo\r\nthree\r\nfour\nfive\n"; const lines: string[] = CommandHelper.SplitIntoLines(text); assert.equal(lines.length, 6); assert.equal(lines[0], "one"); assert.equal(lines[1], "two"); assert.equal(lines[2], "three"); assert.equal(lines[3], "four"); assert.equal(lines[4], "five"); assert.equal(lines[5], ""); }); it("should verify SplitIntoLines - undefined input", function() { const lines: string[] = CommandHelper.SplitIntoLines(undefined); assert.isDefined(lines); assert.equal(lines.length, 0); }); it("should verify SplitIntoLines - empty input", function() { const text: string = ""; const lines: string[] = CommandHelper.SplitIntoLines(text); assert.isDefined(lines); assert.equal(lines.length, 0); }); it("should verify SplitIntoLines - trim WARNings", function() { const text: string = "WARN 1\nWARN 2\nwarning\none\ntwo\r\n\n"; const lines: string[] = CommandHelper.SplitIntoLines(text); assert.equal(lines.length, 5); assert.equal(lines[0], "warning"); assert.equal(lines[1], "one"); assert.equal(lines[2], "two"); assert.equal(lines[3], ""); assert.equal(lines[4], ""); }); it("should verify SplitIntoLines - leave WARNings", function() { const text: string = "WARN 1\nWARN 2\nwarning\none\ntwo\r\n\n"; const lines: string[] = CommandHelper.SplitIntoLines(text, false); assert.equal(lines.length, 7); assert.equal(lines[0], "WARN 1"); assert.equal(lines[1], "WARN 2"); assert.equal(lines[2], "warning"); assert.equal(lines[3], "one"); assert.equal(lines[4], "two"); assert.equal(lines[5], ""); assert.equal(lines[6], ""); }); it("should verify SplitIntoLines - filter empty lines", function() { const text: string = "zero\n \none\ntwo\r\n"; //ensure there's a line with just spaces too const lines: string[] = CommandHelper.SplitIntoLines(text, false, true); assert.equal(lines.length, 3); assert.equal(lines[0], "zero"); assert.equal(lines[1], "one"); assert.equal(lines[2], "two"); }); it("should verify SplitIntoLines - leave empty lines", function() { const text: string = "one\ntwo\n\nthree\nfour\n\n"; const lines: string[] = CommandHelper.SplitIntoLines(text); assert.equal(lines.length, 7); assert.equal(lines[0], "one"); assert.equal(lines[1], "two"); assert.equal(lines[2], ""); assert.equal(lines[3], "three"); assert.equal(lines[4], "four"); assert.equal(lines[5], ""); assert.equal(lines[6], ""); }); it("should verify TrimToXml", async function() { const text: string = "WARN 1\nWARN 2\nwarning\n\r\n\r\n\n\n"; const xml: string = CommandHelper.TrimToXml(text); assert.equal(xml, "\r\n\r\n"); }); it("should verify TrimToXml - empty values", async function() { assert.equal(CommandHelper.TrimToXml(undefined), undefined); assert.equal(CommandHelper.TrimToXml(""), ""); }); it("should verify ParseXml - undefined input", async function() { const xml: any = await CommandHelper.ParseXml(undefined); assert.isUndefined(xml); }); it("should verify ParseXml - invalid xml input", async function() { try { await CommandHelper.ParseXml("<<<<"); assert.fail("didn't throw!"); } catch (err) { assert.isTrue(err.message.startsWith("Unexpected end")); } }); it("should verify ParseXml", async function() { const text: string = "\r\nchild two\r\n\n\n"; const xml: any = await CommandHelper.ParseXml(text); const expectedJSON = { "one": { "$": { "attr1": "35", "attr2": "two" }, "child1": [{ "$": { "attr1": "44", "attr2": "55", "attr3": "three" } }], "child2": [ "child two" ] } }; assert.equal(JSON.stringify(xml), JSON.stringify(expectedJSON)); }); it("should verify GetChangesetNumber", async function() { const text: string = "/Users/leantk/tfvc-tfs/tfsTest_01/addFold:\n" + "Checking in edit: testHere.txt\n" + "\n" + "/Users/leantk/tfvc-tfs/tfsTest_01:\n" + "Checking in edit: test3.txt\n" + "Checking in edit: TestAdd.txt\n" + "\n" + "Changeset #23 checked in.\n"; const changeset: string = CommandHelper.GetChangesetNumber(text); assert.equal(changeset, "23"); }); it("should verify GetChangesetNumber - exact", async function() { const text: string = "Changeset #20 checked in."; const changeset: string = CommandHelper.GetChangesetNumber(text); assert.equal(changeset, "20"); }); it("should verify GetChangesetNumber - no match", async function() { const text: string = "WARN 1\nWARN 2\ntext\nChangeset # checked in.\n\r\ntext\r\nmore text\n\n"; const changeset: string = CommandHelper.GetChangesetNumber(text); assert.equal(changeset, ""); }); it("should verify GetNewLineCharacter", async function() { assert.equal(CommandHelper.GetNewLineCharacter(undefined), "\n"); assert.equal(CommandHelper.GetNewLineCharacter(""), "\n"); assert.equal(CommandHelper.GetNewLineCharacter("blah blah\nblah blah\n\rblah\nblah"), "\n"); assert.equal(CommandHelper.GetNewLineCharacter("blah blah\nblah blah\n\rblah\r\nblah"), "\r\n"); assert.equal(CommandHelper.GetNewLineCharacter("blah /r/nblah blah/n blah\r blah\r blah"), "\n"); }); it("should verify GetFilePath", async function() { assert.equal(CommandHelper.GetFilePath(undefined, undefined, undefined), undefined); assert.equal(CommandHelper.GetFilePath("/path/folder", "file.txt", undefined), path.join("/path/folder", "file.txt")); assert.equal(CommandHelper.GetFilePath("/path/folder:", "file.txt", undefined), path.join("/path/folder", "file.txt")); assert.equal(CommandHelper.GetFilePath(undefined, "file.txt", undefined), "file.txt"); assert.equal(CommandHelper.GetFilePath("/path/folder", undefined, undefined), "/path/folder"); assert.equal(CommandHelper.GetFilePath("/path/folder:", undefined, undefined), "/path/folder"); assert.equal(CommandHelper.GetFilePath("folder:", "file.txt", undefined), path.join("folder", "file.txt")); assert.equal(CommandHelper.GetFilePath("folder:", "file.txt", "/root"), path.join("/root/folder", "file.txt")); assert.equal(CommandHelper.GetFilePath(undefined, "file.txt", "/root"), path.join("/root", "file.txt")); }); }); ================================================ FILE: test/tfvc/commands/delete.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { Delete } from "../../../src/tfvc/commands/delete"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-DeleteCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor - windows", function() { const localPaths: string[] = ["c:\\repos\\Tfvc.L2VSCodeExtension.RC\\README.md"]; new Delete(undefined, localPaths); }); it("should verify constructor - mac/linux", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; new Delete(undefined, localPaths); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; new Delete(context, localPaths); }); it("should verify constructor - undefined args", function() { assert.throws(() => new Delete(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Delete = new Delete(undefined, localPaths); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Delete = new Delete(undefined, localPaths); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Delete = new Delete(undefined, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "delete -noprompt " + localPaths[0]); }); it("should verify Exe arguments", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Delete = new Delete(undefined, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "delete -noprompt " + localPaths[0]); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Delete = new Delete(context, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "delete -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0]); }); it("should verify Exe arguments with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Delete = new Delete(context, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "delete -noprompt ******** " + localPaths[0]); }); it("should verify parse output - single file - no errors", async function() { const localPaths: string[] = ["README.md"]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "README.md\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], "README.md"); }); it("should verify parse output - single empty folder - no errors", async function() { const localPaths: string[] = ["empty-folder"]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "empty-folder:\n" + "empty-folder\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesDeleted.length, 1); //In this case, the CLC returns: //empty-folder: //empty-folder //So we will have to return empty-folder\empty-folder. Not ideal but let's ensure we're getting that. assert.equal(filesDeleted[0], path.join(localPaths[0], localPaths[0])); }); it("should verify parse output - single folder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "file1.txt")]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "folder1:\n" + "file1.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], localPaths[0]); }); it("should verify parse output - single subfolder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "folder2", "file2.txt")]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "folder2") + ":\n" + "file2.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], localPaths[0]); }); it("should verify parse output - single folder+file - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "file1.txt")]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "fold er1:\n" + "file1.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], localPaths[0]); }); it("should verify parse output - multiple files", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = noChangesPaths; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "file1.txt") + ":\n" + "file1.txt\n" + path.join("folder2", "file2.txt") + ":\n" + "file2.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseOutput(executionResult); assert.isDefined(filesDeleted); assert.equal(filesDeleted.length, 2); }); // // // // it("should verify parse Exe output - single file - no errors", async function() { const localPaths: string[] = ["README.md"]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "README.md\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], "README.md"); }); it("should verify parse Exe output - single empty folder - no errors", async function() { const localPaths: string[] = ["empty-folder"]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "empty-folder:\n" + "empty-folder\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesDeleted.length, 1); //In this case, the CLC returns: //empty-folder: //empty-folder //So we will have to return empty-folder\empty-folder. Not ideal but let's ensure we're getting that. assert.equal(filesDeleted[0], path.join(localPaths[0], localPaths[0])); }); it("should verify parse Exe output - single folder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "file1.txt")]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "folder1:\n" + "file1.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], localPaths[0]); }); it("should verify parse Exe output - single subfolder+file - no errors", async function() { const localPaths: string[] = [path.join("folder1", "folder2", "file2.txt")]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "folder2") + ":\n" + "file2.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], localPaths[0]); }); it("should verify parse Exe output - single folder+file - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "file1.txt")]; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "fold er1:\n" + "file1.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesDeleted.length, 1); assert.equal(filesDeleted[0], localPaths[0]); }); it("should verify parse Exe output - multiple files", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = noChangesPaths; const cmd: Delete = new Delete(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "file1.txt") + ":\n" + "file1.txt\n" + path.join("folder2", "file2.txt") + ":\n" + "file2.txt\n", stderr: undefined }; const filesDeleted: string[] = await cmd.ParseExeOutput(executionResult); assert.isDefined(filesDeleted); assert.equal(filesDeleted.length, 2); }); }); ================================================ FILE: test/tfvc/commands/findconflicts.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Strings } from "../../../src/helpers/strings"; import { FindConflicts } from "../../../src/tfvc/commands/findconflicts"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult, IConflict } from "../../../src/tfvc/interfaces"; import { ConflictType } from "../../../src/tfvc/scm/status"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-FindConflictsCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const localPath: string = "/usr/alias/repo1"; new FindConflicts(undefined, localPath); }); it("should verify constructor with context", function() { const localPath: string = "/usr/alias/repo1"; new FindConflicts(context, localPath); }); it("should verify constructor - undefined args", function() { assert.throws(() => new FindConflicts(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "resolve -noprompt " + localPath + " -recursive -preview"); }); it("should verify Exe arguments", function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "resolve -noprompt " + localPath + " -recursive -preview"); }); it("should verify arguments with context", function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(context, localPath); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "resolve -noprompt -collection:" + collectionUrl + " ******** " + localPath + " -recursive -preview"); }); it("should verify Exe arguments with context", function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(context, localPath); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "resolve -noprompt ******** " + localPath + " -recursive -preview"); }); it("should verify parse output - no output", async function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const results: IConflict[] = await cmd.ParseOutput(executionResult); assert.equal(results.length, 0); }); it("should verify parse output - one of each type", async function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "", stderr: "contentChange.txt: The item content has changed\n" + "addConflict.txt: Another item with the same name exists on the server\n" + "nameChange.txt: The item name has changed\n" + "nameAndContentChange.txt: The item name and content have changed\n" + "anotherNameAndContentChange.txt: You have a conflicting pending change\n" + "contentChange2.txt: The item content has changed\n" + "deleted.txt: The item has already been deleted\n" + "branchEdit.txt: The source and target both have changes\n" + "branchDelete.txt: The item has been deleted in the target branch" }; const results: IConflict[] = await cmd.ParseOutput(executionResult); assert.equal(results.length, 9); assert.equal(results[0].localPath, "contentChange.txt"); assert.equal(results[0].type, ConflictType.CONTENT); assert.equal(results[1].localPath, "addConflict.txt"); assert.equal(results[1].type, ConflictType.CONTENT); assert.equal(results[2].localPath, "nameChange.txt"); assert.equal(results[2].type, ConflictType.RENAME); assert.equal(results[3].localPath, "nameAndContentChange.txt"); assert.equal(results[3].type, ConflictType.NAME_AND_CONTENT); assert.equal(results[4].localPath, "anotherNameAndContentChange.txt"); assert.equal(results[4].type, ConflictType.NAME_AND_CONTENT); assert.equal(results[5].localPath, "contentChange2.txt"); assert.equal(results[5].type, ConflictType.CONTENT); assert.equal(results[6].localPath, "deleted.txt"); assert.equal(results[6].type, ConflictType.DELETE); assert.equal(results[7].localPath, "branchEdit.txt"); assert.equal(results[7].type, ConflictType.MERGE); assert.equal(results[8].localPath, "branchDelete.txt"); assert.equal(results[8].type, ConflictType.DELETE_TARGET); }); //With _JAVA_OPTIONS set and there are conflicts, _JAVA_OPTIONS will appear in stderr along with the results also in stderr (and stdout will be empty) it("should verify parse output - one of each type - _JAVA_OPTIONS", async function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "", stderr: "contentChange.txt: The item content has changed\n" + "addConflict.txt: Another item with the same name exists on the server\n" + "nameChange.txt: The item name has changed\n" + "nameAndContentChange.txt: The item name and content have changed\n" + "anotherNameAndContentChange.txt: You have a conflicting pending change\n" + "contentChange2.txt: The item content has changed\n" + "deleted.txt: The item has already been deleted\n" + "branchEdit.txt: The source and target both have changes\n" + "branchDelete.txt: The item has been deleted in the target branch\n" + "Picked up _JAVA_OPTIONS: -Xmx1024M" }; const results: IConflict[] = await cmd.ParseOutput(executionResult); assert.equal(results.length, 9); assert.equal(results[0].localPath, "contentChange.txt"); assert.equal(results[0].type, ConflictType.CONTENT); assert.equal(results[1].localPath, "addConflict.txt"); assert.equal(results[1].type, ConflictType.CONTENT); assert.equal(results[2].localPath, "nameChange.txt"); assert.equal(results[2].type, ConflictType.RENAME); assert.equal(results[3].localPath, "nameAndContentChange.txt"); assert.equal(results[3].type, ConflictType.NAME_AND_CONTENT); assert.equal(results[4].localPath, "anotherNameAndContentChange.txt"); assert.equal(results[4].type, ConflictType.NAME_AND_CONTENT); assert.equal(results[5].localPath, "contentChange2.txt"); assert.equal(results[5].type, ConflictType.CONTENT); assert.equal(results[6].localPath, "deleted.txt"); assert.equal(results[6].type, ConflictType.DELETE); assert.equal(results[7].localPath, "branchEdit.txt"); assert.equal(results[7].type, ConflictType.MERGE); assert.equal(results[8].localPath, "branchDelete.txt"); assert.equal(results[8].type, ConflictType.DELETE_TARGET); }); //With _JAVA_OPTIONS set and there are no conflicts, _JAVA_OPTIONS is in stderr but the result we want to process is moved to stdout it("should verify parse output - no conflicts - _JAVA_OPTIONS", async function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "There are no conflicts to resolve.\n", stderr: "Picked up _JAVA_OPTIONS: -Xmx1024M\n" }; const results: IConflict[] = await cmd.ParseOutput(executionResult); assert.equal(results.length, 0); }); it("should verify parse output - errors - exit code 100", async function() { const localPath: string = "/usr/alias/repo 1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 100, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify parse Exe output - no output", async function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const results: IConflict[] = await cmd.ParseExeOutput(executionResult); assert.equal(results.length, 0); }); it("should verify parse Exe output - one of each type", async function() { const localPath: string = "/usr/alias/repo1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 1, stdout: "", stderr: "folder1\\anothernewfile2.txt: A newer version exists on the server.\n" + "folder1\\anothernewfile4.txt: The item has been deleted from the server.\n" }; const results: IConflict[] = await cmd.ParseExeOutput(executionResult); assert.equal(results.length, 2); assert.equal(results[0].localPath, "folder1\\anothernewfile2.txt"); assert.equal(results[0].type, ConflictType.NAME_AND_CONTENT); assert.equal(results[1].localPath, "folder1\\anothernewfile4.txt"); assert.equal(results[1].type, ConflictType.DELETE); }); it("should verify parse Exe output - errors - exit code 100", async function() { const localPath: string = "/usr/alias/repo 1"; const cmd: FindConflicts = new FindConflicts(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 100, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/commands/findworkspace.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Strings } from "../../../src/helpers/strings"; import { TfvcError, TfvcErrorCodes } from "../../../src/tfvc/tfvcerror"; import { FindWorkspace } from "../../../src/tfvc/commands/findworkspace"; import { IExecutionResult, IWorkspace } from "../../../src/tfvc/interfaces"; describe("Tfvc-FindWorkspaceCommand", function() { beforeEach(function() { // }); it("should verify constructor", function() { const localPath: string = "/path/to/workspace"; new FindWorkspace(localPath); }); it("should verify constructor - undefined args", function() { assert.throws(() => new FindWorkspace(undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); assert.deepEqual(cmd.GetOptions(), { cwd: "/path/to/workspace" }); }); it("should verify GetExeOptions", function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); assert.deepEqual(cmd.GetExeOptions(), { cwd: "/path/to/workspace" }); }); it("should verify arguments", function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "workfold -noprompt ********"); }); it("should verify GetExeArguments", function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "workfold -noprompt ********"); }); it("should verify working folder", function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); assert.equal(cmd.GetOptions().cwd, localPath); }); it("should verify EXE working folder", function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); assert.equal(cmd.GetExeOptions().cwd, localPath); }); it("should verify parse output - no output", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace, undefined); }); it("should verify parse output - no errors", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 1); assert.isFalse(workspace.mappings[0].cloaked); assert.equal(workspace.mappings[0].localPath, `/path`); assert.equal(workspace.mappings[0].serverPath, `$/project1`); }); it("should verify parse output - no errors - cloaked folders - entire project cloaked", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + " (cloaked) $/project2\n" + "$/project3: /path3", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 3); assert.isTrue(workspace.mappings[1].cloaked); assert.isUndefined(workspace.mappings[1].localPath); assert.equal(workspace.mappings[1].serverPath, `$/project2`); }); it("should verify parse output - no errors - cloaked folders - middle project sub-folder cloaked", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2\n" + " (cloaked) $/project2/main:\n" + "$/project3: /path3", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 4); assert.isTrue(workspace.mappings[2].cloaked); assert.isUndefined(workspace.mappings[2].localPath); assert.equal(workspace.mappings[2].serverPath, `$/project2/main`); }); it("should verify parse output - no errors - cloaked folders - last project sub-folder cloaked", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2\n" + "$/project3: /path3\n" + "$/project4: /path4\n" + " (cloaked) $/project4/main:", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 5); assert.isTrue(workspace.mappings[4].cloaked); assert.isUndefined(workspace.mappings[4].localPath); assert.equal(workspace.mappings[4].serverPath, `$/project4/main`); }); it("should verify parse output - German - no 'workspace' and 'collection'", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Arbeitsbereich: DESKTOP-KI56MCL (Jeff Young (TFS))\n" + "Sammlung : http://java-tfs2015:8081/tfs/defaultcollection\n" + "$/project1: /path", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NotAnEnuTfCommandLine)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAnEnuTfCommandLine); } }); it("should verify parse output - not a tf workspace", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: "An argument error occurred: The workspace could not be determined from any argument paths or the current working directory." }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NoWorkspaceMappings)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); } }); it("should verify parse output - no mappings error", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NoWorkspaceMappings)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); } }); it("should verify parse output - no errors - restrictWorkspace", async function() { const localPath: string = "/path2/to/workspace/project2"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); //This test should find project2 as the team project since the localPath contains project2 and we have restrictWorkspace assert.equal(workspace.defaultTeamProject, "project2"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 2); }); //The CLC will always return *all* server mappings in the workspace even if you pass a particular local folder //TF.exe will only return the server mappings in the workspace that apply to the particular local folder it("should verify parse output - no errors - restrictWorkspace - sub-folder", async function() { const localPath: string = "/path2/to/workspace/project2/sub-folder"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); //This test should find project2 as the team project since the localPath contains project2 and we have restrictWorkspace assert.equal(workspace.defaultTeamProject, "project2"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 2); }); it("should verify parse output - no errors - restrictWorkspace - sub-folder - Windows path", async function() { const localPath: string = "c:\\path2\\to\\workspace\\project2\\sub-folder\\"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: c:\\path\n" + "$/project2: c:\\path2", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); //This test should find project2 as the team project since the localPath contains project2 and we have restrictWorkspace assert.equal(workspace.defaultTeamProject, "project2"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 2); }); it("should verify parse output - no errors - encoded output", async function() { const localPath: string = "/path/to/workspace/project1"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/spaces%20in%20the%20name/\n" + "$/project1: /path", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/spaces in the name/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 1); }); /*********************************************************************************************** * The methods below are duplicates of the parse output methods but call the parseExeOutput. ***********************************************************************************************/ it("should verify parse EXE output - no output", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace, undefined); }); it("should verify parse EXE output - no errors", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=============================================================================\n" + "Workspace : MyWorkspace (Jason Prickett)\n" + "Collection: http://server:8080/tfs/\n" + " $/project1/subfolder: /path\n", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 1); }); it("should verify parse EXE output - no errors - cloaked folders - entire project cloaked", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + " (cloaked) $/project2\n" + "$/project3: /path3", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 3); assert.isTrue(workspace.mappings[1].cloaked); assert.isUndefined(workspace.mappings[1].localPath); assert.equal(workspace.mappings[1].serverPath, `$/project2`); }); it("should verify parse EXE output - no errors - cloaked folders - middle project sub-folder cloaked", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2\n" + " (cloaked) $/project2/main:\n" + "$/project3: /path3", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 4); assert.isTrue(workspace.mappings[2].cloaked); assert.isUndefined(workspace.mappings[2].localPath); assert.equal(workspace.mappings[2].serverPath, `$/project2/main`); }); it("should verify parse EXE output - no errors - cloaked folders - last project sub-folder cloaked", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2\n" + "$/project3: /path3\n" + "$/project4: /path4\n" + " (cloaked) $/project4/main:", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 5); assert.isTrue(workspace.mappings[4].cloaked); assert.isUndefined(workspace.mappings[4].localPath); assert.equal(workspace.mappings[4].serverPath, `$/project4/main`); }); it("should verify parse EXE output - German - no 'workspace' and 'collection'", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Arbeitsbereich: DESKTOP-KI56MCL (Jeff Young (TFS))\n" + "Sammlung : http://java-tfs2015:8081/tfs/defaultcollection\n" + "$/project1: /path", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NotAnEnuTfCommandLine)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAnEnuTfCommandLine); } }); it("should verify parse EXE output - not a tf workspace", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: "Unable to determine the source control server." }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NoWorkspaceMappings)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); } }); it("should verify parse EXE output - no mappings error", async function() { const localPath: string = "/path/to/workspace"; const cmd: FindWorkspace = new FindWorkspace(localPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=============================================================================\n" + "Workspace : MyWorkspace (Jason Prickett)\n" + "Collection: http://server:8080/tfs/\n", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NoWorkspaceMappings)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotATfvcRepository); } }); it("should verify parse EXE output - no errors - restrictWorkspace", async function() { const localPath: string = "/path2/to/workspace/project2"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project1: /path\n" + "$/project2: /path2", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); //This test should find project2 as the team project since the localPath contains project2 and we have restrictWorkspace assert.equal(workspace.defaultTeamProject, "project2"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 2); }); it("should verify parse EXE output - no errors - restrictWorkspace - sub-folder", async function() { const localPath: string = "/path2/to/workspace/project2/sub-folder"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); //TF.exe won't return "$/project1: /path1" if it's in the overall workspace (see the CLC test of the same scenario, above) const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project2: /path2", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); //This test should find project2 as the team project since the localPath contains project2 and we have restrictWorkspace assert.equal(workspace.defaultTeamProject, "project2"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 1); }); it("should verify parse EXE output - no errors - restrictWorkspace - sub-folder - Windows path", async function() { const localPath: string = "c:\\path2\\to\\workspace\\project2\\sub-folder\\"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); //TF.exe won't return "$/project1: c:\\path1" if it's in the overall workspace (see the CLC test of the same scenario, earlier) const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/\n" + "$/project2: c:\\path2", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/"); //This test should find project2 as the team project since the localPath contains project2 and we have restrictWorkspace assert.equal(workspace.defaultTeamProject, "project2"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 1); }); it("should verify parse EXE output - no errors - encoded output", async function() { const localPath: string = "/path/to/workspace/project1"; const cmd: FindWorkspace = new FindWorkspace(localPath, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "=====================================================================================================================================================\n" + "Workspace: MyWorkspace\n" + "Collection: http://server:8080/tfs/spaces%20in%20the%20name/\n" + "$/project1: /path", stderr: undefined }; const workspace: IWorkspace = await cmd.ParseExeOutput(executionResult); assert.equal(workspace.name, "MyWorkspace"); assert.equal(workspace.server, "http://server:8080/tfs/spaces in the name/"); assert.equal(workspace.defaultTeamProject, "project1"); assert.equal(workspace.comment, undefined); assert.equal(workspace.computer, undefined); assert.equal(workspace.owner, undefined); assert.equal(workspace.mappings.length, 1); }); }); ================================================ FILE: test/tfvc/commands/getfilecontent.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { GetFileContent } from "../../../src/tfvc/commands/getfilecontent"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; import { Strings } from "../../../src/helpers/strings"; describe("Tfvc-GetFileContentCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor - windows", function() { const localPath: string = "c:\\repos\\Tfvc.L2VSCodeExtension.RC\\README.md"; new GetFileContent(undefined, localPath); }); it("should verify constructor - mac/linux", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; new GetFileContent(undefined, localPath); }); it("should verify constructor with context", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; new GetFileContent(context, localPath); }); it("should verify constructor - undefined args", function() { assert.throws(() => new GetFileContent(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetArguments", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "print -noprompt " + localPath); }); it("should verify GetArguments with context", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(context, localPath); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "print -noprompt -collection:" + collectionUrl + " ******** " + localPath); }); it("should verify GetArguments + versionSpec", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath, "42"); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "print -noprompt " + localPath + " -version:42"); }); it("should verify GetArguments + versionSpec with context", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(context, localPath, "42"); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "print -noprompt -collection:" + collectionUrl + " ******** " + localPath + " -version:42"); }); it("should verify GetExeOptions", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify GetExeArguments", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "view -noprompt " + localPath); }); it("should verify GetExeArguments with context", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(context, localPath); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "view -noprompt -collection:" + collectionUrl + " ******** " + localPath); }); it("should verify GetExeArguments + versionSpec", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath, "42"); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "view -noprompt " + localPath + " -version:42"); }); it("should verify GetExeArguments + versionSpec with context", function() { const localPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"; const cmd: GetFileContent = new GetFileContent(context, localPath, "42"); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "view -noprompt -collection:" + collectionUrl + " ******** " + localPath + " -version:42"); }); it("should verify parse output - single file - no errors", async function() { const localPath: string = "README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); const fileContent: string = "This is the content of the README.md file\n...and I mean that.\n"; const executionResult: IExecutionResult = { exitCode: 0, stdout: fileContent, stderr: undefined }; const content: string = await cmd.ParseOutput(executionResult); assert.equal(content, fileContent); }); it("should verify parse output - no file matches", async function() { const localPath: string = "folder1/file1.txt"; const cmd: GetFileContent = new GetFileContent(undefined, localPath, undefined, true); //ignoring file not found const executionResult: IExecutionResult = { exitCode: 1, stdout: undefined, stderr: "No file matches what you passed." }; const content: string = await cmd.ParseOutput(executionResult); assert.equal(content, ""); }); it("should verify parse output - file doesn't exist", async function() { const localPath: string = "folder1/file1.txt"; const cmd: GetFileContent = new GetFileContent(undefined, localPath, "66", true); //ignoring file not found const executionResult: IExecutionResult = { exitCode: 1, stdout: undefined, stderr: "The specified file does not exist at the specified version or something..." }; const content: string = await cmd.ParseOutput(executionResult); assert.equal(content, ""); }); it("should verify parse output - error exit code", async function() { const localPath: string = "folder1/file1.txt"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify parse Exe output - single file - no errors", async function() { const localPath: string = "README.md"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); const fileContent: string = "This is the content of the README.md file\n...and I mean that.\n"; const executionResult: IExecutionResult = { exitCode: 0, stdout: fileContent, stderr: undefined }; const content: string = await cmd.ParseExeOutput(executionResult); assert.equal(content, fileContent); }); it("should verify parse Exe output - error exit code", async function() { const localPath: string = "folder1/file1.txt"; const cmd: GetFileContent = new GetFileContent(undefined, localPath); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/commands/getinfo.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Strings } from "../../../src/helpers/strings"; import { GetInfo } from "../../../src/tfvc/commands/getinfo"; import { TfvcError, TfvcErrorCodes } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult, IItemInfo } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-GetInfoCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const localPaths: string[] = ["/path/to/workspace"]; new GetInfo(undefined, localPaths); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/path/to/workspace"]; new GetInfo(context, localPaths); }); it("should verify constructor - undefined args", function() { assert.throws(() => new GetInfo(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "info -noprompt " + localPaths[0]); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(context, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "info -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0]); }); it("should verify GetExeArguments", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "info -noprompt " + localPaths[0]); }); it("should verify GetExeArguments with context", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(context, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "info -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0]); }); it("should verify parse output - no output", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseOutput(executionResult); assert.equal(itemInfos.length, 0); }); it("should verify parse output - single item", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Local information:\n" + "Local path: /path/to/file.txt\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 18, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1385\n", stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseOutput(executionResult); assert.equal(itemInfos.length, 1); assert.equal(itemInfos[0].localItem, "/path/to/file.txt"); assert.equal(itemInfos[0].serverItem, "$/TFVC_1/file.txt"); assert.equal(itemInfos[0].localVersion, "18"); assert.equal(itemInfos[0].change, "none"); assert.equal(itemInfos[0].type, "file"); assert.equal(itemInfos[0].serverVersion, "18"); assert.equal(itemInfos[0].deletionId, "0"); assert.equal(itemInfos[0].lock, "none"); assert.equal(itemInfos[0].lockOwner, ""); assert.equal(itemInfos[0].lastModified, "Nov 18, 2016 11:10:20 AM"); assert.equal(itemInfos[0].fileType, "windows-1252"); assert.equal(itemInfos[0].fileSize, "1385"); }); it("should verify parse output - multiple items", async function() { const localPaths: string[] = ["/path/to/workspace/file.txt", "/path/to/workspace/file2.txt"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Local information:\n" + "Local path: /path/to/file.txt\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 18, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1385\n" + "\n" + "Local information:\n" + "Local path: /path/to/file2.txt\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 19, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1386\n", stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseOutput(executionResult); assert.equal(itemInfos.length, 2); assert.equal(itemInfos[0].localItem, "/path/to/file.txt"); assert.equal(itemInfos[0].serverItem, "$/TFVC_1/file.txt"); assert.equal(itemInfos[0].localVersion, "18"); assert.equal(itemInfos[0].change, "none"); assert.equal(itemInfos[0].type, "file"); assert.equal(itemInfos[0].serverVersion, "18"); assert.equal(itemInfos[0].deletionId, "0"); assert.equal(itemInfos[0].lock, "none"); assert.equal(itemInfos[0].lockOwner, ""); assert.equal(itemInfos[0].lastModified, "Nov 18, 2016 11:10:20 AM"); assert.equal(itemInfos[0].fileType, "windows-1252"); assert.equal(itemInfos[0].fileSize, "1385"); assert.equal(itemInfos[1].localItem, "/path/to/file2.txt"); assert.equal(itemInfos[1].serverItem, "$/TFVC_1/file2.txt"); assert.equal(itemInfos[1].localVersion, "19"); assert.equal(itemInfos[1].change, "none"); assert.equal(itemInfos[1].type, "file"); assert.equal(itemInfos[1].serverVersion, "19"); assert.equal(itemInfos[1].deletionId, "0"); assert.equal(itemInfos[1].lock, "none"); assert.equal(itemInfos[1].lockOwner, ""); assert.equal(itemInfos[1].lastModified, "Nov 19, 2016 11:10:20 AM"); assert.equal(itemInfos[1].fileType, "windows-1252"); assert.equal(itemInfos[1].fileSize, "1386"); }); it("should verify parse output - multiple items with errors", async function() { const localPaths: string[] = ["/path/to/workspace/file.txt", "nomatch", "/path/to/workspace/file2.txt"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Local information:\n" + "Local path: /path/to/file.txt\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 18, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1385\n" + "\n" + "No items match nomatch\n" + "\n" + "Local information:\n" + "Local path: /path/to/file2.txt\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 19, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1386\n", stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseOutput(executionResult); assert.equal(itemInfos.length, 3); assert.equal(itemInfos[0].localItem, "/path/to/file.txt"); assert.equal(itemInfos[0].serverItem, "$/TFVC_1/file.txt"); assert.equal(itemInfos[0].localVersion, "18"); assert.equal(itemInfos[0].change, "none"); assert.equal(itemInfos[0].type, "file"); assert.equal(itemInfos[0].serverVersion, "18"); assert.equal(itemInfos[0].deletionId, "0"); assert.equal(itemInfos[0].lock, "none"); assert.equal(itemInfos[0].lockOwner, ""); assert.equal(itemInfos[0].lastModified, "Nov 18, 2016 11:10:20 AM"); assert.equal(itemInfos[0].fileType, "windows-1252"); assert.equal(itemInfos[0].fileSize, "1385"); assert.equal(itemInfos[1].localItem, undefined); // This indicates that the file could not be found, but other files could assert.equal(itemInfos[2].localItem, "/path/to/file2.txt"); assert.equal(itemInfos[2].serverItem, "$/TFVC_1/file2.txt"); assert.equal(itemInfos[2].localVersion, "19"); assert.equal(itemInfos[2].change, "none"); assert.equal(itemInfos[2].type, "file"); assert.equal(itemInfos[2].serverVersion, "19"); assert.equal(itemInfos[2].deletionId, "0"); assert.equal(itemInfos[2].lock, "none"); assert.equal(itemInfos[2].lockOwner, ""); assert.equal(itemInfos[2].lastModified, "Nov 19, 2016 11:10:20 AM"); assert.equal(itemInfos[2].fileType, "windows-1252"); assert.equal(itemInfos[2].fileSize, "1386"); }); it("should verify parse output - all errors", async function() { const localPaths: string[] = ["/path/to/workspace/file.txt", "/path/to/workspace/file2.txt"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "No items match /path/to/workspace/file.txt\n" + "\n" + "No items match /path/to/workspace/file2.txt\n", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NoMatchesFound)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NoItemsMatch); } }); /*********************************************************************************************** * The methods below are duplicates of the parse output methods but call the parseExeOutput. ***********************************************************************************************/ it("should verify parse EXE output - no output", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseExeOutput(executionResult); assert.equal(itemInfos.length, 0); }); it("should verify parse EXE output - single item", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Local information:\n" + "Local path: /path/to/file.txt\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 18, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1385\n", stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseExeOutput(executionResult); assert.equal(itemInfos.length, 1); assert.equal(itemInfos[0].localItem, "/path/to/file.txt"); assert.equal(itemInfos[0].serverItem, "$/TFVC_1/file.txt"); assert.equal(itemInfos[0].localVersion, "18"); assert.equal(itemInfos[0].change, "none"); assert.equal(itemInfos[0].type, "file"); assert.equal(itemInfos[0].serverVersion, "18"); assert.equal(itemInfos[0].deletionId, "0"); assert.equal(itemInfos[0].lock, "none"); assert.equal(itemInfos[0].lockOwner, ""); assert.equal(itemInfos[0].lastModified, "Nov 18, 2016 11:10:20 AM"); assert.equal(itemInfos[0].fileType, "windows-1252"); assert.equal(itemInfos[0].fileSize, "1385"); }); it("should verify parse EXE output - multiple items", async function() { const localPaths: string[] = ["/path/to/workspace/file.txt", "/path/to/workspace/file2.txt"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Local information:\n" + "Local path: /path/to/file.txt\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 18, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1385\n" + "Local information:\n" + "Local path: /path/to/file2.txt\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 19, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1386\n", stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseExeOutput(executionResult); assert.equal(itemInfos.length, 2); assert.equal(itemInfos[0].localItem, "/path/to/file.txt"); assert.equal(itemInfos[0].serverItem, "$/TFVC_1/file.txt"); assert.equal(itemInfos[0].localVersion, "18"); assert.equal(itemInfos[0].change, "none"); assert.equal(itemInfos[0].type, "file"); assert.equal(itemInfos[0].serverVersion, "18"); assert.equal(itemInfos[0].deletionId, "0"); assert.equal(itemInfos[0].lock, "none"); assert.equal(itemInfos[0].lockOwner, ""); assert.equal(itemInfos[0].lastModified, "Nov 18, 2016 11:10:20 AM"); assert.equal(itemInfos[0].fileType, "windows-1252"); assert.equal(itemInfos[0].fileSize, "1385"); assert.equal(itemInfos[1].localItem, "/path/to/file2.txt"); assert.equal(itemInfos[1].serverItem, "$/TFVC_1/file2.txt"); assert.equal(itemInfos[1].localVersion, "19"); assert.equal(itemInfos[1].change, "none"); assert.equal(itemInfos[1].type, "file"); assert.equal(itemInfos[1].serverVersion, "19"); assert.equal(itemInfos[1].deletionId, "0"); assert.equal(itemInfos[1].lock, "none"); assert.equal(itemInfos[1].lockOwner, ""); assert.equal(itemInfos[1].lastModified, "Nov 19, 2016 11:10:20 AM"); assert.equal(itemInfos[1].fileType, "windows-1252"); assert.equal(itemInfos[1].fileSize, "1386"); }); it("should verify parse EXE output - multiple items with errors", async function() { const localPaths: string[] = ["/path/to/workspace/file.txt", "nomatch", "/path/to/workspace/file2.txt"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Local information:\n" + "Local path: /path/to/file.txt\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file.txt\n" + "Changeset: 18\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 18, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1385\n" + "No items match nomatch\n" + "Local information:\n" + "Local path: /path/to/file2.txt\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Change: none\n" + "Type: file\n" + "Server information:\n" + "Server path: $/TFVC_1/file2.txt\n" + "Changeset: 19\n" + "Deletion ID: 0\n" + "Lock: none\n" + "Lock owner:\n" + "Last modified: Nov 19, 2016 11:10:20 AM\n" + "Type: file\n" + "File type: windows-1252\n" + "Size: 1386\n", stderr: undefined }; const itemInfos: IItemInfo[] = await cmd.ParseExeOutput(executionResult); assert.equal(itemInfos.length, 3); assert.equal(itemInfos[0].localItem, "/path/to/file.txt"); assert.equal(itemInfos[0].serverItem, "$/TFVC_1/file.txt"); assert.equal(itemInfos[0].localVersion, "18"); assert.equal(itemInfos[0].change, "none"); assert.equal(itemInfos[0].type, "file"); assert.equal(itemInfos[0].serverVersion, "18"); assert.equal(itemInfos[0].deletionId, "0"); assert.equal(itemInfos[0].lock, "none"); assert.equal(itemInfos[0].lockOwner, ""); assert.equal(itemInfos[0].lastModified, "Nov 18, 2016 11:10:20 AM"); assert.equal(itemInfos[0].fileType, "windows-1252"); assert.equal(itemInfos[0].fileSize, "1385"); assert.equal(itemInfos[1].localItem, undefined); // This indicates that the file could not be found, but other files could assert.equal(itemInfos[2].localItem, "/path/to/file2.txt"); assert.equal(itemInfos[2].serverItem, "$/TFVC_1/file2.txt"); assert.equal(itemInfos[2].localVersion, "19"); assert.equal(itemInfos[2].change, "none"); assert.equal(itemInfos[2].type, "file"); assert.equal(itemInfos[2].serverVersion, "19"); assert.equal(itemInfos[2].deletionId, "0"); assert.equal(itemInfos[2].lock, "none"); assert.equal(itemInfos[2].lockOwner, ""); assert.equal(itemInfos[2].lastModified, "Nov 19, 2016 11:10:20 AM"); assert.equal(itemInfos[2].fileType, "windows-1252"); assert.equal(itemInfos[2].fileSize, "1386"); }); it("should verify parse EXE output - all errors", async function() { const localPaths: string[] = ["/path/to/workspace/file.txt", "/path/to/workspace/file2.txt"]; const cmd: GetInfo = new GetInfo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "No items match /path/to/workspace/file.txt\n" + "No items match /path/to/workspace/file2.txt\n", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.isTrue(err.message.startsWith(Strings.NoMatchesFound)); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NoItemsMatch); } }); }); ================================================ FILE: test/tfvc/commands/getversion.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { GetVersion } from "../../../src/tfvc/commands/getversion"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TfvcErrorCodes } from "../../../src/tfvc/tfvcerror"; import { Strings } from "../../../src/helpers/strings"; describe("Tfvc-GetVersionCommand", function() { it("should verify GetOptions", function() { const cmd: GetVersion = new GetVersion(); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const cmd: GetVersion = new GetVersion(); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const cmd: GetVersion = new GetVersion(); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "add -noprompt -?"); }); it("should verify Exe arguments", function() { const cmd: GetVersion = new GetVersion(); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "add -noprompt -?"); }); it("should verify parse output - no version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAnEnuTfCommandLine); assert.isTrue(err.message.startsWith(Strings.NotAnEnuTfCommandLine)); threw = true; } finally { assert.isTrue(threw); } }); it("should verify parse Exe output - no version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAnEnuTfCommandLine); assert.isTrue(err.message.startsWith(Strings.NotAnEnuTfCommandLine)); threw = true; } finally { assert.isTrue(threw); } }); it("should verify parse output - valid version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Team Explorer Everywhere Command Line Client (version 14.0.3.201603291047)", stderr: undefined }; const version: string = await cmd.ParseOutput(executionResult); assert.equal(version, "14.0.3.201603291047"); }); it("should verify parse EXE output - valid version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Microsoft (R) TF - Team Foundation Version Control Tool, Version 14.102.25619.0", stderr: undefined }; const version: string = await cmd.ParseExeOutput(executionResult); assert.equal(version, "14.102.25619.0"); }); it("should verify parse output - error exit code", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); threw = true; } finally { assert.isTrue(threw); } }); it("should verify parse output - java object heap error", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 1, stdout: "Error occurred during initialization of VM\r\nCould not reserve enough space for 2097152KB object heap\r\n", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotFound); assert.isTrue(err.message.startsWith(Strings.TfInitializeFailureError)); threw = true; } finally { assert.isTrue(threw, "Checking for Java object heap error did not throw an error."); } }); it("should verify parse Exe output - error exit code", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); threw = true; } finally { assert.isTrue(threw); } }); it("should verify parse EXE output - Spanish version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Microsoft (R) TF - Herramienta Control de versiones de Team Foundation, versi�n 14.102.25619.0", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAnEnuTfCommandLine); assert.isTrue(err.message.startsWith(Strings.NotAnEnuTfCommandLine)); threw = true; } finally { assert.isTrue(threw, "Checking Spanish version did not throw an error."); } }); it("should verify parse EXE output - French version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Microsoft (R) TF�- Outil Team Foundation Version Control, version�14.102.25619.0", stderr: undefined }; let threw: boolean = false; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.tfvcErrorCode, TfvcErrorCodes.NotAnEnuTfCommandLine); assert.isTrue(err.message.startsWith(Strings.NotAnEnuTfCommandLine)); threw = true; } finally { assert.isTrue(threw, "Checking French version did not throw an error."); } }); it("should verify parse EXE output - German version", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Microsoft (R) TF - Team Foundation-Versionskontrolltool, Version 14.102.25619.0", stderr: undefined }; const version: string = await cmd.ParseExeOutput(executionResult); assert.equal(version, "14.102.25619.0"); }); it("should verify parse EXE output - version is not in the first line", async function() { const cmd: GetVersion = new GetVersion(); const executionResult: IExecutionResult = { exitCode: 0, stdout: "\r\nc:\\TFS\\folder1\\folder two\\folder3\\folder4\\folder5> add -noprompt -?\r\n" + "Microsoft (R) TF - Team Foundation Version Control Tool, Version 14.98.25331.0\r\n" + "Copyright (c) Microsoft Corporation. All rights reserved.\r\n" + "Adds new files and folders from a local file system location to Team Foundation\r\n" + "version control.\r\n" + "\r\n" + "tf vc add [itemspec] [/lock:(none|checkin|checkout)] [/encoding:filetype]\r\n" + " [/noprompt] [/recursive] [/noignore] [/login:username,[password]]\r\n" + "\r\n", stderr: undefined }; const version: string = await cmd.ParseExeOutput(executionResult); assert.equal(version, "14.98.25331.0"); }); }); ================================================ FILE: test/tfvc/commands/rename.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { Strings } from "../../../src/helpers/strings"; import { Rename } from "../../../src/tfvc/commands/rename"; import { TfvcError, TfvcErrorCodes } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-RenameCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor - windows", function() { const startPath: string = "c:\\repos\\Tfvc.L2VSCodeExtension.RC\\"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); new Rename(undefined, sourcePath, destinationPath); }); it("should verify constructor - mac/linux", function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); new Rename(undefined, sourcePath, destinationPath); }); it("should verify constructor with context", function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); new Rename(context, sourcePath, destinationPath); }); it("should verify constructor - no destination path", function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); assert.throws(() => new Rename(undefined, sourcePath, undefined), TfvcError, /Argument is required/); }); it("should verify constructor - no source path", function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const destinationPath: string = path.join(startPath, "READU.md"); assert.throws(() => new Rename(undefined, undefined, destinationPath), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const cmd: Rename = new Rename(context, "sourcePath", "destinationPath"); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const cmd: Rename = new Rename(context, "sourcePath", "destinationPath"); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "rename -noprompt -collection:" + collectionUrl + " ******** " + sourcePath + " " + destinationPath); }); it("should verify GetExeArguments", function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "rename -noprompt ******** " + sourcePath + " " + destinationPath); }); it("should verify parse output - no output", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const result: string = await cmd.ParseOutput(executionResult); assert.equal(result, ""); }); it("should verify parse output - single line", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: `READU.md`, stderr: undefined }; const result: string = await cmd.ParseOutput(executionResult); assert.equal(result, "READU.md"); }); it("should verify parse output - with path", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: `${startPath}:\nREADU.md`, stderr: undefined }; const result: string = await cmd.ParseOutput(executionResult); assert.equal(result, destinationPath); }); it("should verify parse output - source file not in workspace", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: `The item ${sourcePath} could not be found in your workspace, or you do not have permission to access it.\n` }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.FileNotInWorkspace); } }); it("should verify parse output - error exit code", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); /*********************************************************************************************** * The methods below are duplicates of the parse output methods but call the parseExeOutput. ***********************************************************************************************/ it("should verify parse EXE output - no output", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const result: string = await cmd.ParseExeOutput(executionResult); assert.equal(result, ""); }); it("should verify parse EXE output - single line", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: `READU.md`, stderr: undefined }; const result: string = await cmd.ParseExeOutput(executionResult); assert.equal(result, "READU.md"); }); it("should verify parse EXE output - with path", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 0, stdout: `${startPath}:\nREADU.md`, stderr: undefined }; const result: string = await cmd.ParseExeOutput(executionResult); assert.equal(result, destinationPath); }); it("should verify parse EXE output - source file not in workspace", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 100, stdout: undefined, stderr: `The item ${sourcePath} could not be found in your workspace, or you do not have permission to access it.\n` }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.equal(err.tfvcErrorCode, TfvcErrorCodes.FileNotInWorkspace); } }); it("should verify parse EXE output - error exit code", async function() { const startPath: string = "/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/"; const sourcePath: string = path.join(startPath, "README.md"); const destinationPath: string = path.join(startPath, "READU.md"); const cmd: Rename = new Rename(context, sourcePath, destinationPath); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/commands/resolveconflicts.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Strings } from "../../../src/helpers/strings"; import { ResolveConflicts } from "../../../src/tfvc/commands/resolveconflicts"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { AutoResolveType, IExecutionResult, IConflict } from "../../../src/tfvc/interfaces"; import { ConflictType } from "../../../src/tfvc/scm/status"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-ResolveConflictsCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; new ResolveConflicts(context, localPaths, AutoResolveType.KeepYours); }); it("should verify constructor - undefined args", function() { assert.throws(() => new ResolveConflicts(undefined, undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "resolve -noprompt " + localPaths[0] + " -auto:KeepYours"); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(context, localPaths, AutoResolveType.KeepYours); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "resolve -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0] + " -auto:KeepYours"); }); it("should verify arguments with TakeTheirs", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(context, localPaths, AutoResolveType.TakeTheirs); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "resolve -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0] + " -auto:TakeTheirs"); }); it("should verify GetExeArguments", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "resolve -noprompt " + localPaths[0] + " -auto:KeepYours"); }); it("should verify GetExeArguments with context", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(context, localPaths, AutoResolveType.KeepYours); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "resolve -noprompt ******** " + localPaths[0] + " -auto:KeepYours"); }); it("should verify GetExeArguments with TakeTheirs", function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(context, localPaths, AutoResolveType.TakeTheirs); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "resolve -noprompt ******** " + localPaths[0] + " -auto:TakeTheirs"); }); it("should verify parse output - no output", async function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const results: IConflict[] = await cmd.ParseOutput(executionResult); assert.equal(results.length, 0); }); it("should verify parse output - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt", "/usr/alias/repo1/file2.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Resolved /usr/alias/repo1/file.txt as KeepYours\n" + "Resolved /usr/alias/repo1/file2.txt as KeepYours", stderr: undefined }; const results: IConflict[] = await cmd.ParseOutput(executionResult); assert.equal(results.length, 2); assert.equal(results[0].localPath, "/usr/alias/repo1/file.txt"); assert.equal(results[0].type, ConflictType.RESOLVED); assert.equal(results[1].localPath, "/usr/alias/repo1/file2.txt"); assert.equal(results[1].type, ConflictType.RESOLVED); }); it("should verify parse output - errors - exit code 100", async function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); const executionResult: IExecutionResult = { exitCode: 100, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); /*********************************************************************************************** * The methods below are duplicates of the parse output methods but call the parseExeOutput. ***********************************************************************************************/ it("should verify parse EXE output - no output", async function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const results: IConflict[] = await cmd.ParseExeOutput(executionResult); assert.equal(results.length, 0); }); it("should verify parse EXE output - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt", "/usr/alias/repo1/file2.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Resolved /usr/alias/repo1/file.txt as KeepYours\n" + "Resolved /usr/alias/repo1/file2.txt as KeepYours", stderr: undefined }; const results: IConflict[] = await cmd.ParseExeOutput(executionResult); assert.equal(results.length, 2); assert.equal(results[0].localPath, "/usr/alias/repo1/file.txt"); assert.equal(results[0].type, ConflictType.RESOLVED); assert.equal(results[1].localPath, "/usr/alias/repo1/file2.txt"); assert.equal(results[1].type, ConflictType.RESOLVED); }); it("should verify parse EXE output - errors - exit code 100", async function() { const localPaths: string[] = ["/usr/alias/repo1/file.txt"]; const cmd: ResolveConflicts = new ResolveConflicts(undefined, localPaths, AutoResolveType.KeepYours); const executionResult: IExecutionResult = { exitCode: 100, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/commands/status.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Status } from "../../../src/tfvc/commands/status"; import { IPendingChange, IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; import { Strings } from "../../../src/helpers/strings"; describe("Tfvc-StatusCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const localPaths: string[] = ["/path/to/workspace"]; new Status(undefined, true, localPaths); }); it("should verify constructor - no paths", function() { new Status(undefined, true); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/path/to/workspace"]; new Status(context, true, localPaths); }); it("should verify constructor with context - no paths", function() { new Status(context, true); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "status -noprompt -format:xml -recursive " + localPaths[0]); }); it("should verify Exe arguments", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "status -noprompt -format:detailed -recursive " + localPaths[0]); }); it("should verify arguments - no paths", function() { const cmd: Status = new Status(undefined, true); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "status -noprompt -format:xml -recursive"); }); it("should verify Exe arguments - no paths", function() { const cmd: Status = new Status(undefined, true); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "status -noprompt -format:detailed -recursive"); }); it("should verify arguments - multiple paths", function() { const localPaths: string[] = ["/path/to/workspace", "/path/to/workspace2"]; const cmd: Status = new Status(undefined, true, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "status -noprompt -format:xml -recursive " + localPaths[0] + " " + localPaths[1]); }); it("should verify Exe arguments - multiple paths", function() { const localPaths: string[] = ["/path/to/workspace", "/path/to/workspace2"]; const cmd: Status = new Status(undefined, true, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "status -noprompt -format:detailed -recursive " + localPaths[0] + " " + localPaths[1]); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(context, true, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "status -noprompt -collection:" + collectionUrl + " ******** -format:xml -recursive " + localPaths[0]); }); it("should verify Exe arguments with context", function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(context, true, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "status -noprompt -collection:" + collectionUrl + " ******** -format:detailed -recursive " + localPaths[0]); }); it("should verify parse output - no output", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseOutput(executionResult); assert.equal(changes.length, 0); }); it("should verify parse Exe output - no output", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseExeOutput(executionResult); assert.equal(changes.length, 0); }); it("should verify parse output - valid json", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const stdout: string = ` `; const executionResult: IExecutionResult = { exitCode: 0, stdout: `${stdout}`, stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseOutput(executionResult); assert.equal(changes.length, 2); assert.equal(changes[0].changeType, "rename"); assert.equal(changes[0].computer, "JPRICKET-DEV2"); assert.equal(changes[0].date, "2017-02-08T11:12:06.766-0500"); assert.isFalse(changes[0].isCandidate); assert.equal(changes[0].localItem, "/tmp/tfsTest03_44/Folder333/DemandEquals_renamed.java"); assert.equal(changes[0].lock, "none"); assert.equal(changes[0].owner, "NORTHAMERICA\\jpricket"); assert.equal(changes[0].serverItem, "$/tfsTest_03/Folder333/DemandEquals_renamed.java"); assert.equal(changes[0].sourceItem, "$/tfsTest_03/Folder333/DemandEquals.java"); assert.equal(changes[0].version, "217"); assert.equal(changes[0].workspace, "Folder1_00"); assert.equal(changes[1].changeType, "add"); assert.equal(changes[1].computer, "JPRICKET-DEV2"); assert.equal(changes[1].date, "2016-07-13T12:36:51.060-0400"); assert.isTrue(changes[1].isCandidate); assert.equal(changes[1].localItem, "/tmp/test/test.txt"); assert.equal(changes[1].lock, "none"); assert.equal(changes[1].owner, "jason"); assert.equal(changes[1].serverItem, "$/tfsTest_01/test.txt"); assert.isUndefined(changes[1].sourceItem); //It's not a rename assert.equal(changes[1].version, "0"); assert.equal(changes[1].workspace, "Folder1_00"); }); it("should verify parse Exe output - pending changes only - no errors", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "$/jeyou/README.md;C19\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : edit\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\README.md\n" + " File type : utf-8\n" + "\n" + " 1 change(s), 0 detected change(s)\n", stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseExeOutput(executionResult); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, "edit"); assert.equal(changes[0].computer, "JEYOU-DEV00"); assert.equal(changes[0].date, "Wednesday, February 22, 2017 1:47:26 PM"); assert.isFalse(changes[0].isCandidate); assert.equal(changes[0].localItem, "C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\README.md"); assert.equal(changes[0].lock, "none"); assert.equal(changes[0].owner, "Jeff Young (TFS)"); assert.equal(changes[0].serverItem, "$/jeyou/README.md"); assert.isUndefined(changes[0].sourceItem); //It's not a rename assert.equal(changes[0].version, "19"); assert.equal(changes[0].workspace, "jeyou-dev00-tfexe-OnPrem"); }); it("should verify parse Exe output - pending and detected changes - no errors", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "$/jeyou/README.md;C19\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : edit\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\README.md\n" + " File type : utf-8\n" + "\n" + "-----------------\n" + "Detected Changes:\n" + "-----------------\n" + "$/jeyou/therightstuff.txt\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : add\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\therightstuff.txt\n" + "\n" + " 1 change(s), 1 detected change(s)\n", stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseExeOutput(executionResult); assert.equal(changes.length, 2); assert.equal(changes[0].changeType, "edit"); assert.equal(changes[0].computer, "JEYOU-DEV00"); assert.equal(changes[0].date, "Wednesday, February 22, 2017 1:47:26 PM"); assert.isFalse(changes[0].isCandidate); assert.equal(changes[0].localItem, "C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\README.md"); assert.equal(changes[0].lock, "none"); assert.equal(changes[0].owner, "Jeff Young (TFS)"); assert.equal(changes[0].serverItem, "$/jeyou/README.md"); assert.isUndefined(changes[0].sourceItem); //It's not a rename assert.equal(changes[0].version, "19"); assert.equal(changes[0].workspace, "jeyou-dev00-tfexe-OnPrem"); assert.equal(changes[1].changeType, "add"); assert.equal(changes[1].computer, "JEYOU-DEV00"); assert.equal(changes[1].date, "Wednesday, February 22, 2017 1:47:26 PM"); assert.isTrue(changes[1].isCandidate); assert.equal(changes[1].localItem, "C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\therightstuff.txt"); assert.equal(changes[1].lock, "none"); assert.equal(changes[1].owner, "Jeff Young (TFS)"); assert.equal(changes[1].serverItem, "$/jeyou/therightstuff.txt"); assert.isUndefined(changes[1].sourceItem); //It's not a rename assert.equal(changes[1].version, "0"); assert.equal(changes[1].workspace, "jeyou-dev00-tfexe-OnPrem"); }); it("should verify parse Exe output - detected changes only - no errors", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "-----------------\n" + "Detected Changes:\n" + "-----------------\n" + "$/jeyou/therightstuff.txt\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : add\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\therightstuff.txt\n" + "\n" + " 0 change(s), 1 detected change(s)\n", stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseExeOutput(executionResult); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, "add"); assert.equal(changes[0].computer, "JEYOU-DEV00"); assert.equal(changes[0].date, "Wednesday, February 22, 2017 1:47:26 PM"); assert.isTrue(changes[0].isCandidate); assert.equal(changes[0].localItem, "C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\therightstuff.txt"); assert.equal(changes[0].lock, "none"); assert.equal(changes[0].owner, "Jeff Young (TFS)"); assert.equal(changes[0].serverItem, "$/jeyou/therightstuff.txt"); assert.isUndefined(changes[0].sourceItem); //It's not a rename assert.equal(changes[0].version, "0"); assert.equal(changes[0].workspace, "jeyou-dev00-tfexe-OnPrem"); }); it("should verify parse Exe output - multiple pending and multiple detected changes - no errors", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "$/jeyou/README.md;C19\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : edit\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\README.md\n" + " File type : utf-8\n" + "\n" + "$/jeyou/folder1/READU.md;C42\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : edit\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\folder1\\READU.md\n" + " File type : utf-8\n" + "\n" + "-----------------\n" + "Detected Changes:\n" + "-----------------\n" + "$/jeyou/therightstuff.txt\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : add\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\therightstuff.txt\n" + "\n" + "$/jeyou/folder1/nkotb.txt\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : add\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\folder1\\nkotb.txt\n" + "\n" + " 2 change(s), 2 detected change(s)\n", stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseExeOutput(executionResult); assert.equal(changes.length, 4); //Other tests verify the actual values (so skip them here) }); it("should verify parse Exe output - pending rename only - no errors", async function() { const localPaths: string[] = ["/path/to/workspace"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "$/jeyou/READU.md;C19\n" + " User : Jeff Young (TFS)\n" + " Date : Wednesday, February 22, 2017 1:47:26 PM\n" + " Lock : none\n" + " Change : rename\n" + " Workspace : jeyou-dev00-tfexe-OnPrem\n" + " Source item: $/jeyou/README.md\n" + " Local item : [JEYOU-DEV00] C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\READU.md\n" + " File type : utf-8\n" + "\n" + " 1 change(s), 0 detected change(s)\n", stderr: undefined }; const changes: IPendingChange[] = await cmd.ParseExeOutput(executionResult); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, "rename"); assert.equal(changes[0].computer, "JEYOU-DEV00"); assert.equal(changes[0].date, "Wednesday, February 22, 2017 1:47:26 PM"); assert.isFalse(changes[0].isCandidate); assert.equal(changes[0].localItem, "C:\\repos\\TfExe.Tfvc.L2VSCodeExtension.RC.TFS\\READU.md"); assert.equal(changes[0].lock, "none"); assert.equal(changes[0].owner, "Jeff Young (TFS)"); assert.equal(changes[0].serverItem, "$/jeyou/READU.md"); assert.equal(changes[0].sourceItem, "$/jeyou/README.md"); assert.equal(changes[0].version, "19"); assert.equal(changes[0].workspace, "jeyou-dev00-tfexe-OnPrem"); }); it("should verify parse output - error exit code", async function() { const localPaths: string[] = ["folder1/file1.txt", "folder2/file2.txt"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); it("should verify parse Exe output - error exit code", async function() { const localPaths: string[] = ["folder1/file1.txt", "folder2/file2.txt"]; const cmd: Status = new Status(undefined, true, localPaths); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/commands/sync.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { Strings } from "../../../src/helpers/strings"; import { Sync } from "../../../src/tfvc/commands/sync"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult, ISyncResults, SyncType } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-SyncCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); it("should verify constructor", function() { const localPaths: string[] = ["/usr/alias/repo1"]; new Sync(undefined, localPaths, true); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/usr/alias/repo1"]; new Sync(context, localPaths, true); }); it("should verify constructor - undefined args", function() { assert.throws(() => new Sync(undefined, undefined, false), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, false); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, false); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, false); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "get -noprompt -nosummary " + localPaths[0]); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(context, localPaths, false); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "get -noprompt -collection:" + collectionUrl + " ******** -nosummary " + localPaths[0]); }); it("should verify arguments with context and recursive", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(context, localPaths, true); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "get -noprompt -collection:" + collectionUrl + " ******** -nosummary " + localPaths[0] + " -recursive"); }); it("should verify getExeArguments", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, false); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "get -noprompt -nosummary " + localPaths[0]); }); it("should verify getExeArguments with context", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(context, localPaths, false); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "get -noprompt ******** -nosummary " + localPaths[0]); }); it("should verify getExeArguments with context and recursive", function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(context, localPaths, true); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "get -noprompt ******** -nosummary " + localPaths[0] + " -recursive"); }); it("should verify parse output - no output", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 0); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); }); it("should verify parse output - up to date", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "All files up to date.", stderr: undefined }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 0); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); }); it("should verify parse output - single file edit - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/usr/alias/repo1/test:\n" + "Replacing test1.txt\n", stderr: undefined }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 1); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.Updated); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo1/test", "test1.txt")); }); it("should verify parse output - single file add - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/usr/alias/repo1/test:\n" + "Getting test1.txt\n", stderr: undefined }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 1); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.New); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo1/test", "test1.txt")); }); it("should verify parse output - single file add - spaces - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo 1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/usr/alias/repo 1/test:\n" + "Getting test 1.txt\n", stderr: undefined }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 1); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.New); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo 1/test", "test 1.txt")); }); it("should verify parse output - multiple files - with conflict", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repo1:\n" + "Getting addFold\n" + "Getting addFold-branch\n" + "\n" + "/usr/alias/repo1/addFold-branch:\n" + "Replacing testHereRename.txt\n" + "\n" + "/usr/alias/repo1/addFold:\n" + "Getting testHere3\n" + "Replacing testHereRename7.txt\n" + "\n" + "/usr/alias/repo1/last:\n" + "Deleting Rename2.txt\n" + "Replacing test3.txt\n" + "Getting TestAdd.txt\n" + "\n", stderr: "Conflict test_renamed.txt - Unable to perform the get operation because you have a conflicting rename, edit\n" }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 9); assert.equal(results.hasConflicts, true); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.New); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo1", "addFold")); assert.equal(results.itemResults[1].syncType, SyncType.New); assert.equal(results.itemResults[1].itemPath, path.join("/usr/alias/repo1", "addFold-branch")); assert.equal(results.itemResults[2].syncType, SyncType.Updated); assert.equal(results.itemResults[2].itemPath, path.join("/usr/alias/repo1/addFold-branch", "testHereRename.txt")); assert.equal(results.itemResults[3].syncType, SyncType.New); assert.equal(results.itemResults[3].itemPath, path.join("/usr/alias/repo1/addFold", "testHere3")); assert.equal(results.itemResults[4].syncType, SyncType.Updated); assert.equal(results.itemResults[4].itemPath, path.join("/usr/alias/repo1/addFold", "testHereRename7.txt")); assert.equal(results.itemResults[5].syncType, SyncType.Deleted); assert.equal(results.itemResults[5].itemPath, path.join("/usr/alias/repo1/last", "Rename2.txt")); assert.equal(results.itemResults[6].syncType, SyncType.Updated); assert.equal(results.itemResults[6].itemPath, path.join("/usr/alias/repo1/last", "test3.txt")); assert.equal(results.itemResults[7].syncType, SyncType.New); assert.equal(results.itemResults[7].itemPath, path.join("/usr/alias/repo1/last", "TestAdd.txt")); //stderr lines always come last assert.equal(results.itemResults[8].syncType, SyncType.Conflict); assert.equal(results.itemResults[8].itemPath, "test_renamed.txt"); assert.equal(results.itemResults[8].message, "Unable to perform the get operation because you have a conflicting rename, edit"); }); it("should verify parse output - errors - exit code 1", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repo1:\n", stderr: "Conflict new.txt - Unable to perform the get operation because you have a conflicting edit\n" + "new4.txt - Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)\n" + "Warning new111.txt - Unable to perform the get operation because it is writable" }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 3); assert.equal(results.hasConflicts, true); assert.equal(results.hasErrors, true); assert.equal(results.itemResults[0].syncType, SyncType.Conflict); assert.equal(results.itemResults[0].itemPath, "new.txt"); assert.equal(results.itemResults[0].message, "Unable to perform the get operation because you have a conflicting edit"); assert.equal(results.itemResults[1].syncType, SyncType.Error); assert.equal(results.itemResults[1].itemPath, "new4.txt"); assert.equal(results.itemResults[1].message, "Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)"); assert.equal(results.itemResults[2].syncType, SyncType.Warning); assert.equal(results.itemResults[2].itemPath, "new111.txt"); assert.equal(results.itemResults[2].message, "Unable to perform the get operation because it is writable"); }); it("should verify parse output - errors - no conflicts", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repo1:\n", stderr: "new4.txt - Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)\n" + "Warning new111.txt - Unable to perform the get operation because it is writable\n" + "/usr/alias/repo1/folder cannot be deleted because it is not empty" }; const results: ISyncResults = await cmd.ParseOutput(executionResult); assert.equal(results.itemResults.length, 3); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, true); assert.equal(results.itemResults[0].syncType, SyncType.Error); assert.equal(results.itemResults[0].itemPath, "new4.txt"); assert.equal(results.itemResults[0].message, "Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)"); assert.equal(results.itemResults[1].syncType, SyncType.Warning); assert.equal(results.itemResults[1].itemPath, "new111.txt"); assert.equal(results.itemResults[1].message, "Unable to perform the get operation because it is writable"); assert.equal(results.itemResults[2].syncType, SyncType.Warning); assert.equal(results.itemResults[2].itemPath, "/usr/alias/repo1/folder"); assert.equal(results.itemResults[2].message, "/usr/alias/repo1/folder cannot be deleted because it is not empty"); }); it("should verify parse output - errors - exit code 100", async function() { const localPaths: string[] = ["/usr/alias/repo 1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 100, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); /*********************************************************************************************** * The methods below are duplicates of the parse output methods but call the parseExeOutput. ***********************************************************************************************/ it("should verify parse EXE output - no output", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 0); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); }); it("should verify parse EXE output - up to date", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "All files are up to date.", stderr: undefined }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 0); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); }); it("should verify parse EXE output - single file edit - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/usr/alias/repo1/test:\n" + "Replacing test1.txt\n", stderr: undefined }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 1); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.Updated); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo1/test", "test1.txt")); }); it("should verify parse EXE output - single file add - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/usr/alias/repo1/test:\n" + "Getting test1.txt\n", stderr: undefined }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 1); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.New); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo1/test", "test1.txt")); }); it("should verify parse EXE output - single file add - spaces - no errors", async function() { const localPaths: string[] = ["/usr/alias/repo 1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 0, stdout: "/usr/alias/repo 1/test:\n" + "Getting test 1.txt\n", stderr: undefined }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 1); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.New); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo 1/test", "test 1.txt")); }); it("should verify parse EXE output - multiple files - with conflict", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repo1:\n" + "Getting addFold\n" + "Getting addFold-branch\n" + "\n" + "/usr/alias/repo1/addFold-branch:\n" + "Replacing testHereRename.txt\n" + "\n" + "/usr/alias/repo1/addFold:\n" + "Getting testHere3\n" + "Replacing testHereRename7.txt\n" + "\n" + "/usr/alias/repo1/last:\n" + "Deleting Rename2.txt\n" + "Replacing test3.txt\n" + "Getting TestAdd.txt\n" + "\n", stderr: "Conflict test_renamed.txt - Unable to perform the get operation because you have a conflicting rename, edit\n" }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 9); assert.equal(results.hasConflicts, true); assert.equal(results.hasErrors, false); assert.equal(results.itemResults[0].syncType, SyncType.New); assert.equal(results.itemResults[0].itemPath, path.join("/usr/alias/repo1", "addFold")); assert.equal(results.itemResults[1].syncType, SyncType.New); assert.equal(results.itemResults[1].itemPath, path.join("/usr/alias/repo1", "addFold-branch")); assert.equal(results.itemResults[2].syncType, SyncType.Updated); assert.equal(results.itemResults[2].itemPath, path.join("/usr/alias/repo1/addFold-branch", "testHereRename.txt")); assert.equal(results.itemResults[3].syncType, SyncType.New); assert.equal(results.itemResults[3].itemPath, path.join("/usr/alias/repo1/addFold", "testHere3")); assert.equal(results.itemResults[4].syncType, SyncType.Updated); assert.equal(results.itemResults[4].itemPath, path.join("/usr/alias/repo1/addFold", "testHereRename7.txt")); assert.equal(results.itemResults[5].syncType, SyncType.Deleted); assert.equal(results.itemResults[5].itemPath, path.join("/usr/alias/repo1/last", "Rename2.txt")); assert.equal(results.itemResults[6].syncType, SyncType.Updated); assert.equal(results.itemResults[6].itemPath, path.join("/usr/alias/repo1/last", "test3.txt")); assert.equal(results.itemResults[7].syncType, SyncType.New); assert.equal(results.itemResults[7].itemPath, path.join("/usr/alias/repo1/last", "TestAdd.txt")); //stderr lines always come last assert.equal(results.itemResults[8].syncType, SyncType.Conflict); assert.equal(results.itemResults[8].itemPath, "test_renamed.txt"); assert.equal(results.itemResults[8].message, "Unable to perform the get operation because you have a conflicting rename, edit"); }); it("should verify parse EXE output - errors - exit code 1", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repo1:\n", stderr: "Conflict new.txt - Unable to perform the get operation because you have a conflicting edit\n" + "new4.txt - Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)\n" + "Warning new111.txt - Unable to perform the get operation because it is writable" }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 3); assert.equal(results.hasConflicts, true); assert.equal(results.hasErrors, true); assert.equal(results.itemResults[0].syncType, SyncType.Conflict); assert.equal(results.itemResults[0].itemPath, "new.txt"); assert.equal(results.itemResults[0].message, "Unable to perform the get operation because you have a conflicting edit"); assert.equal(results.itemResults[1].syncType, SyncType.Error); assert.equal(results.itemResults[1].itemPath, "new4.txt"); assert.equal(results.itemResults[1].message, "Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)"); assert.equal(results.itemResults[2].syncType, SyncType.Warning); assert.equal(results.itemResults[2].itemPath, "new111.txt"); assert.equal(results.itemResults[2].message, "Unable to perform the get operation because it is writable"); }); it("should verify parse EXE output - errors - no conflicts", async function() { const localPaths: string[] = ["/usr/alias/repo1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 1, stdout: "/usr/alias/repo1:\n", stderr: "new4.txt - Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)\n" + "Warning new111.txt - Unable to perform the get operation because it is writable\n" + "/usr/alias/repo1/folder cannot be deleted because it is not empty" }; const results: ISyncResults = await cmd.ParseExeOutput(executionResult); assert.equal(results.itemResults.length, 3); assert.equal(results.hasConflicts, false); assert.equal(results.hasErrors, true); assert.equal(results.itemResults[0].syncType, SyncType.Error); assert.equal(results.itemResults[0].itemPath, "new4.txt"); assert.equal(results.itemResults[0].message, "Unable to perform the get operation because you have a conflicting rename (to be moved from /path/new5.txt)"); assert.equal(results.itemResults[1].syncType, SyncType.Warning); assert.equal(results.itemResults[1].itemPath, "new111.txt"); assert.equal(results.itemResults[1].message, "Unable to perform the get operation because it is writable"); assert.equal(results.itemResults[2].syncType, SyncType.Warning); assert.equal(results.itemResults[2].itemPath, "/usr/alias/repo1/folder"); assert.equal(results.itemResults[2].message, "/usr/alias/repo1/folder cannot be deleted because it is not empty"); }); it("should verify parse EXE output - errors - exit code 100", async function() { const localPaths: string[] = ["/usr/alias/repo 1"]; const cmd: Sync = new Sync(undefined, localPaths, true); const executionResult: IExecutionResult = { exitCode: 100, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 100); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/commands/undo.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import * as path from "path"; import { Strings } from "../../../src/helpers/strings"; import { Undo } from "../../../src/tfvc/commands/undo"; import { TfvcError } from "../../../src/tfvc/tfvcerror"; import { IExecutionResult } from "../../../src/tfvc/interfaces"; import { TeamServerContext } from "../../../src/contexts/servercontext"; import { CredentialInfo } from "../../../src/info/credentialinfo"; import { RepositoryInfo } from "../../../src/info/repositoryinfo"; describe("Tfvc-UndoCommand", function() { const serverUrl: string = "http://server:8080/tfs"; const repoUrl: string = "http://server:8080/tfs/collection1/_git/repo1"; const collectionUrl: string = "http://server:8080/tfs/collection1"; const user: string = "user1"; const pass: string = "pass1"; let context: TeamServerContext; beforeEach(function() { context = new TeamServerContext(repoUrl); context.CredentialInfo = new CredentialInfo(user, pass); context.RepoInfo = new RepositoryInfo({ serverUrl: serverUrl, collection: { name: "collection1", id: "" }, repository: { remoteUrl: repoUrl, id: "", name: "", project: { name: "project1" } } }); }); //new Undo(this._serverContext, itemPaths)); it("should verify constructor - windows", function() { const localPaths: string[] = ["c:\\repos\\Tfvc.L2VSCodeExtension.RC\\README.md"]; new Undo(undefined, localPaths); }); it("should verify constructor - mac/linux", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; new Undo(undefined, localPaths); }); it("should verify constructor with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; new Undo(context, localPaths); }); it("should verify constructor - undefined args", function() { assert.throws(() => new Undo(undefined, undefined), TfvcError, /Argument is required/); }); it("should verify GetOptions", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(context, localPaths); assert.deepEqual(cmd.GetOptions(), {}); }); it("should verify GetExeOptions", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(context, localPaths); assert.deepEqual(cmd.GetExeOptions(), {}); }); it("should verify arguments", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(undefined, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "undo -noprompt " + localPaths[0]); }); it("should verify arguments with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(context, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "undo -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0]); }); it("should verify UndoAll arguments", function() { const localPaths: string[] = ["*"]; const cmd: Undo = new Undo(undefined, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "undo -noprompt . -recursive"); }); it("should verify UndoAll arguments with context", function() { const localPaths: string[] = ["*"]; const cmd: Undo = new Undo(context, localPaths); assert.equal(cmd.GetArguments().GetArgumentsForDisplay(), "undo -noprompt -collection:" + collectionUrl + " ******** . -recursive"); }); it("should verify GetExeArguments", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(undefined, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "undo -noprompt " + localPaths[0]); }); it("should verify GetExeArguments with context", function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(context, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "undo -noprompt -collection:" + collectionUrl + " ******** " + localPaths[0]); }); it("should verify GetExeArguments UndoAll arguments", function() { const localPaths: string[] = ["*"]; const cmd: Undo = new Undo(undefined, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "undo -noprompt . -recursive"); }); it("should verify GetExeArguments UndoAll arguments with context", function() { const localPaths: string[] = ["*"]; const cmd: Undo = new Undo(context, localPaths); assert.equal(cmd.GetExeArguments().GetArgumentsForDisplay(), "undo -noprompt -collection:" + collectionUrl + " ******** . -recursive"); }); it("should verify parse output - no output", async function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 0); }); it("should verify parse output - single file edit - no errors", async function() { const localPaths: string[] = ["README.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Undoing edit: README.md\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], "README.md"); }); it("should verify parse output - single file add - no errors", async function() { const localPaths: string[] = ["README.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Undoing add: README.md\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], "README.md"); }); it("should verify parse output - multiple file add - no errors", async function() { const localPaths: string[] = ["README.md", "README2.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Undoing add: README.md\n" + "Undoing add: README2.md\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 2); assert.equal(filesUndone[0], "README.md"); assert.equal(filesUndone[1], "README2.md"); }); it("should verify parse output - single folder+file edit - no errors", async function() { const localPaths: string[] = [path.join("folder1", "file1.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "folder1:\n" + "Undoing edit: file1.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); it("should verify parse output - single subfolder+file add - no errors", async function() { const localPaths: string[] = [path.join("folder1", "folder2", "file2.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "folder2") + ":\n" + "Undoing edit: file2.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); it("should verify parse output - single folder+file edit - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "file1.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "fold er1:\n" + "Undoing edit: file1.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); it("should verify parse output - single subfolder+file add - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "fol der2", "file2.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("fold er1", "fol der2") + ":\n" + "Undoing edit: file2.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); //If we have at least 1 file undone but at least 1 with no pending changes, exit code is 1 //Proceed normally but ignore the files that have no pending changes. it("should verify parse output - multiple files - several no pending changes", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = ["README.md"].concat(noChangesPaths); const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 1, stdout: "Undoing add: README.md\n" + "No pending changes were found for " + noChangesPaths[0] + "\n" + "No pending changes were found for " + noChangesPaths[1] + "\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], "README.md"); }); //If all files have no pending changes, exit code is 100 but we don't want to fail it("should verify parse output - multiple files - all no pending changes", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = noChangesPaths; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 100, stdout: "" + "No pending changes were found for " + noChangesPaths[0] + "\n" + "No pending changes were found for " + noChangesPaths[1] + "\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseOutput(executionResult); assert.isDefined(filesUndone); assert.equal(filesUndone.length, 0); }); it("should verify parse output - error exit code", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = noChangesPaths; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); /*********************************************************************************************** * The methods below are duplicates of the parse output methods but call the parseExeOutput. ***********************************************************************************************/ it("should verify parse EXE output - no output", async function() { const localPaths: string[] = ["/usr/alias/repos/Tfvc.L2VSCodeExtension.RC/README.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: undefined, stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 0); }); it("should verify parse EXE output - single file edit - no errors", async function() { const localPaths: string[] = ["README.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Undoing edit: README.md\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], "README.md"); }); it("should verify parse EXE output - single file add - no errors", async function() { const localPaths: string[] = ["README.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Undoing add: README.md\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], "README.md"); }); it("should verify parse EXE output - multiple file add - no errors", async function() { const localPaths: string[] = ["README.md", "README2.md"]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "Undoing add: README.md\n" + "Undoing add: README2.md\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 2); assert.equal(filesUndone[0], "README.md"); assert.equal(filesUndone[1], "README2.md"); }); it("should verify parse EXE output - single folder+file edit - no errors", async function() { const localPaths: string[] = [path.join("folder1", "file1.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "folder1:\n" + "Undoing edit: file1.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); it("should verify parse EXE output - single subfolder+file add - no errors", async function() { const localPaths: string[] = [path.join("folder1", "folder2", "file2.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("folder1", "folder2") + ":\n" + "Undoing edit: file2.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); it("should verify parse EXE output - single folder+file edit - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "file1.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: "fold er1:\n" + "Undoing edit: file1.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); it("should verify parse EXE output - single subfolder+file add - spaces - no errors", async function() { const localPaths: string[] = [path.join("fold er1", "fol der2", "file2.txt")]; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 0, stdout: path.join("fold er1", "fol der2") + ":\n" + "Undoing edit: file2.txt\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], localPaths[0]); }); //If we have at least 1 file undone but at least 1 with no pending changes, exit code is 1 //Proceed normally but ignore the files that have no pending changes. it("should verify parse EXE output - multiple files - several no pending changes", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = ["README.md"].concat(noChangesPaths); const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 1, stdout: "Undoing add: README.md\n" + "No pending changes were found for " + noChangesPaths[0] + ".\n" + "No pending changes were found for " + noChangesPaths[1] + ".\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.equal(filesUndone.length, 1); assert.equal(filesUndone[0], "README.md"); }); //If all files have no pending changes, exit code is 100 but we don't want to fail it("should verify parse EXE output - multiple files - all no pending changes", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = noChangesPaths; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 100, stdout: "" + "No pending changes were found for " + noChangesPaths[0] + ".\n" + "No pending changes were found for " + noChangesPaths[1] + ".\n", stderr: undefined }; const filesUndone: string[] = await cmd.ParseExeOutput(executionResult); assert.isDefined(filesUndone); assert.equal(filesUndone.length, 0); }); it("should verify parse EXE output - error exit code", async function() { const noChangesPaths: string[] = [path.join("folder1", "file1.txt"), path.join("folder2", "file2.txt")]; const localPaths: string[] = noChangesPaths; const cmd: Undo = new Undo(undefined, localPaths); const executionResult: IExecutionResult = { exitCode: 42, stdout: "Something bad this way comes.", stderr: undefined }; try { await cmd.ParseExeOutput(executionResult); } catch (err) { assert.equal(err.exitCode, 42); assert.isTrue(err.message.startsWith(Strings.TfExecFailedError)); } }); }); ================================================ FILE: test/tfvc/scm/resourcegroup.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Strings } from "../../../src/helpers/strings"; import { ConflictsGroup, ExcludedGroup, IncludedGroup } from "../../../src/tfvc/scm/resourcegroups"; describe("Tfvc-ResourceGroups", function() { beforeEach(function() { // }); it("should verify ConflictsGroup - constructor", function() { const group: ConflictsGroup = new ConflictsGroup([]); assert.equal(group.id, "conflicts"); assert.equal(group.label, Strings.ConflictsGroupName); assert.equal(group.resources.length, 0); }); it("should verify ExcludedGroup - constructor", function() { const group: ExcludedGroup = new ExcludedGroup([]); assert.equal(group.id, "excluded"); assert.equal(group.label, Strings.ExcludedGroupName); assert.equal(group.resources.length, 0); }); it("should verify IncludedGroup - constructor", function() { const group: IncludedGroup = new IncludedGroup([]); assert.equal(group.id, "included"); assert.equal(group.label, Strings.IncludedGroupName); assert.equal(group.resources.length, 0); }); }); ================================================ FILE: test/tfvc/scm/status.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { expect } from "chai"; import { GetStatuses, Status } from "../../../src/tfvc/scm/status"; describe("Tfvc-Version", function() { beforeEach(function() { // }); it("should verify GetStatuses - empty strings", function() { expect(GetStatuses("")).to.have.same.members([]); expect(GetStatuses(undefined)).to.have.same.members([]); }); it("should verify GetStatuses - single value", function() { expect(GetStatuses("add")).to.have.same.members([Status.ADD]); expect(GetStatuses("branch")).to.have.same.members([Status.BRANCH]); expect(GetStatuses("delete")).to.have.same.members([Status.DELETE]); expect(GetStatuses("edit")).to.have.same.members([Status.EDIT]); expect(GetStatuses("lock")).to.have.same.members([Status.LOCK]); expect(GetStatuses("merge")).to.have.same.members([Status.MERGE]); expect(GetStatuses("rename")).to.have.same.members([Status.RENAME]); expect(GetStatuses("source rename")).to.have.same.members([Status.RENAME]); expect(GetStatuses("undelete")).to.have.same.members([Status.UNDELETE]); expect(GetStatuses("blah blah")).to.have.same.members([Status.UNKNOWN]); }); it("should verify GetStatuses - multiple values", function() { expect(GetStatuses("add, edit")).to.have.same.members([Status.ADD, Status.EDIT]); expect(GetStatuses("branch , lock")).to.have.same.members([Status.BRANCH, Status.LOCK]); expect(GetStatuses(" delete ,merge ")).to.have.same.members([Status.DELETE, Status.MERGE]); }); it("should verify GetStatuses - multiple values with unknown", function() { expect(GetStatuses("add, unk, edit")).to.have.same.members([Status.ADD, Status.UNKNOWN, Status.EDIT]); expect(GetStatuses("unk , branch , lock")).to.have.same.members([Status.UNKNOWN, Status.BRANCH, Status.LOCK]); expect(GetStatuses(" delete ,merge, unk ")).to.have.same.members([Status.DELETE, Status.MERGE, Status.UNKNOWN]); }); }); ================================================ FILE: test/tfvc/tfvcerror.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Strings } from "../../src/helpers/strings"; import { TfvcError, TfvcErrorCodes } from "../../src/tfvc/tfvcerror"; describe("Tfvc-Error", function() { beforeEach(function() { // }); it("should verify constructor - undefined", function() { assert.throws(() => new TfvcError(undefined), TfvcError, /Argument is required/); }); it("should verify constructor - empty data", function() { const error: TfvcError = new TfvcError({ error: undefined, exitCode: 0, message: undefined, stderr: undefined, stdout: undefined, tfvcCommand: undefined, tfvcErrorCode: undefined }); assert.equal(error.error, undefined); assert.equal(error.exitCode, 0); assert.equal(error.message, Strings.TfExecFailedError); assert.equal(error.stderr, undefined); assert.equal(error.stdout, undefined); assert.equal(error.tfvcCommand, undefined); assert.equal(error.tfvcErrorCode, undefined); }); it("should verify constructor - error not empty", function() { const error: TfvcError = new TfvcError({ error: { name: "err1", message: "error1 message" }, exitCode: 0, message: undefined, stderr: undefined, stdout: undefined, tfvcCommand: undefined, tfvcErrorCode: undefined }); assert.equal(error.error.name, "err1"); assert.equal(error.error.message, "error1 message"); assert.equal(error.exitCode, 0); assert.equal(error.message, "error1 message"); assert.equal(error.stderr, undefined); assert.equal(error.stdout, undefined); assert.equal(error.tfvcCommand, undefined); assert.equal(error.tfvcErrorCode, undefined); }); it("should verify constructor - error.message over message", function() { const error: TfvcError = new TfvcError({ error: { name: "err1", message: "error1 message" }, exitCode: 0, message: "other message", stderr: undefined, stdout: undefined, tfvcCommand: undefined, tfvcErrorCode: undefined }); assert.equal(error.error.name, "err1"); assert.equal(error.error.message, "error1 message"); assert.equal(error.exitCode, 0); assert.equal(error.message, "error1 message"); assert.equal(error.stderr, undefined); assert.equal(error.stdout, undefined); assert.equal(error.tfvcCommand, undefined); assert.equal(error.tfvcErrorCode, undefined); }); it("should verify constructor - no error", function() { const error: TfvcError = new TfvcError({ error: undefined, exitCode: 100, message: "other message", stderr: "standard error", stdout: "standard output", tfvcCommand: "command1", tfvcErrorCode: TfvcErrorCodes.LocationMissing }); assert.equal(error.error, undefined); assert.equal(error.exitCode, 100); assert.equal(error.message, "other message"); assert.equal(error.stderr, "standard error"); assert.equal(error.stdout, "standard output"); assert.equal(error.tfvcCommand, "command1"); assert.equal(error.tfvcErrorCode, TfvcErrorCodes.LocationMissing); }); it("should verify CreateArgumentMissingError", function() { const error: TfvcError = TfvcError.CreateArgumentMissingError("arg1"); assert.equal(error.error, undefined); assert.equal(error.exitCode, undefined); assert.equal(error.message, "Argument is required: arg1"); assert.equal(error.stderr, undefined); assert.equal(error.stdout, undefined); assert.equal(error.tfvcCommand, undefined); assert.equal(error.tfvcErrorCode, TfvcErrorCodes.MissingArgument); }); it("should verify CreateInvalidStateError", function() { const error: TfvcError = TfvcError.CreateInvalidStateError(); assert.equal(error.error, undefined); assert.equal(error.exitCode, undefined); assert.equal(error.message, "The TFVC SCMProvider is in an invalid state for this action."); assert.equal(error.stderr, undefined); assert.equal(error.stdout, undefined); assert.equal(error.tfvcCommand, undefined); assert.equal(error.tfvcErrorCode, TfvcErrorCodes.InInvalidState); }); it("should verify CreateUnknownError", function() { const error: TfvcError = TfvcError.CreateUnknownError({ name: "err1", message: "error1 message" }); assert.equal(error.error.name, "err1"); assert.equal(error.error.message, "error1 message"); assert.equal(error.exitCode, undefined); assert.equal(error.message, "error1 message"); assert.equal(error.stderr, undefined); assert.equal(error.stdout, undefined); assert.equal(error.tfvcCommand, undefined); assert.equal(error.tfvcErrorCode, TfvcErrorCodes.UnknownError); }); it("should verify toString", function() { const error: TfvcError = new TfvcError({ error: { name: "err1", message: "error1 message", stack: "here; then there" }, exitCode: 11, message: undefined, stderr: "standard error", stdout: "standard output", tfvcCommand: "command1", tfvcErrorCode: TfvcErrorCodes.MinVersionWarning }); assert.equal(error.toString(), "error1 message Details: exitCode: 11, errorCode: TfvcMinVersionWarning, command: command1, stdout: standard output, stderr: standard error Stack: here; then there"); }); }); ================================================ FILE: test/tfvc/tfvcversion.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { TfvcVersion } from "../../src/tfvc/tfvcversion"; describe("Tfvc-Version", function() { beforeEach(function() { // }); it("should verify constructor", function() { const version: TfvcVersion = new TfvcVersion(12, 11, 10, ""); assert.equal(version.ToString(), "12.11.10"); assert.equal(version.Major, 12); assert.equal(version.Minor, 11); assert.equal(version.Revision, 10); assert.equal(version.Build, ""); }); it("should verify constructor - with build", function() { const version: TfvcVersion = new TfvcVersion(12, 11, 10, "buildpart"); assert.equal(version.ToString(), "12.11.10.buildpart"); assert.equal(version.Major, 12); assert.equal(version.Minor, 11); assert.equal(version.Revision, 10); assert.equal(version.Build, "buildpart"); }); it("should verify constructor - with dotted build", function() { const version: TfvcVersion = new TfvcVersion(12, 11, 10, "build.part."); assert.equal(version.ToString(), "12.11.10.build.part."); assert.equal(version.Major, 12); assert.equal(version.Minor, 11); assert.equal(version.Revision, 10); assert.equal(version.Build, "build.part."); }); it("should verify FromString", function() { const version: TfvcVersion = TfvcVersion.FromString("12.11.10.build.part."); assert.equal(version.ToString(), "12.11.10.build.part."); assert.equal(version.Major, 12); assert.equal(version.Minor, 11); assert.equal(version.Revision, 10); assert.equal(version.Build, "build.part."); }); it("should verify FromString - missing build", function() { const version: TfvcVersion = TfvcVersion.FromString("12.11.10"); assert.equal(version.ToString(), "12.11.10"); assert.equal(version.Major, 12); assert.equal(version.Minor, 11); assert.equal(version.Revision, 10); assert.equal(version.Build, ""); }); it("should verify FromString - missing revision", function() { const version: TfvcVersion = TfvcVersion.FromString("12.11"); assert.equal(version.ToString(), "12.11.0"); assert.equal(version.Major, 12); assert.equal(version.Minor, 11); assert.equal(version.Revision, 0); assert.equal(version.Build, ""); }); it("should verify FromString - undefined", function() { const version: TfvcVersion = TfvcVersion.FromString(undefined); assert.equal(version.ToString(), "0.0.0"); assert.equal(version.Major, 0); assert.equal(version.Minor, 0); assert.equal(version.Revision, 0); assert.equal(version.Build, ""); }); it("should verify Compare", function() { const version1: TfvcVersion = TfvcVersion.FromString("12.11"); const version2: TfvcVersion = TfvcVersion.FromString("12.11.10"); assert.isTrue(TfvcVersion.Compare(version1, version2) < 0); assert.isTrue(TfvcVersion.Compare(version2, version1) > 0); }); it("should verify Compare - major difference", function() { const version1: TfvcVersion = TfvcVersion.FromString("12.11"); const version2: TfvcVersion = TfvcVersion.FromString("13.1.1"); assert.isTrue(TfvcVersion.Compare(version1, version2) < 0); assert.isTrue(TfvcVersion.Compare(version2, version1) > 0); }); it("should verify Compare - minor difference", function() { const version1: TfvcVersion = TfvcVersion.FromString("12.11"); const version2: TfvcVersion = TfvcVersion.FromString("12.13.1"); assert.isTrue(TfvcVersion.Compare(version1, version2) < 0); assert.isTrue(TfvcVersion.Compare(version2, version1) > 0); }); it("should verify Compare - equals", function() { const version1: TfvcVersion = TfvcVersion.FromString("12.11.10"); const version2: TfvcVersion = TfvcVersion.FromString("12.11.10"); assert.isTrue(TfvcVersion.Compare(version1, version2) === 0); assert.isTrue(TfvcVersion.Compare(version2, version1) === 0); }); }); ================================================ FILE: test-integration/clients/teamservicesclient.integration.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert, expect } from "chai"; import { Mocks } from "../helpers-integration/mocks"; import { TestSettings } from "../helpers-integration/testsettings"; import { CredentialManager } from "../../src/helpers/credentialmanager"; import { UserAgentProvider } from "../../src/helpers/useragentprovider"; import { TeamServerContext } from "../../src/contexts/servercontext"; import { TeamServicesApi } from "../../src/clients/teamservicesclient"; describe("TeamServicesClient-Integration", function() { this.timeout(TestSettings.SuiteTimeout); //http://mochajs.org/#timeouts const credentialManager: CredentialManager = new CredentialManager(); const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); before(function() { UserAgentProvider.VSCodeVersion = "0.0.0"; return credentialManager.StoreCredentials(ctx, TestSettings.AccountUser, TestSettings.Password); }); beforeEach(function() { return credentialManager.GetCredentials(ctx); }); //afterEach(function() { }); after(function() { return credentialManager.RemoveCredentials(ctx); }); it("should verify repositoryClient.getVstsInfo", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const repositoryClient: TeamServicesApi = new TeamServicesApi(TestSettings.RemoteRepositoryUrl, [CredentialManager.GetCredentialHandler()]); const repoInfo: any = await repositoryClient.getVstsInfo(); assert.isNotNull(repoInfo, "repoInfo was null when it shouldn't have been"); assert.equal(repoInfo.serverUrl, TestSettings.AccountUrl); assert.equal(repoInfo.collection.name, TestSettings.CollectionName); assert.equal(repoInfo.repository.remoteUrl, TestSettings.RemoteRepositoryUrl); assert.equal(repoInfo.repository.id, TestSettings.RepositoryId); expect(repoInfo.repository.name).to.equal(TestSettings.RepositoryName); }); it("should verify repositoryClient.getVstsInfo and 404", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts try { const repositoryClient: TeamServicesApi = new TeamServicesApi(TestSettings.RemoteRepositoryUrl + "1", [CredentialManager.GetCredentialHandler()]); const repoInfo: any = await repositoryClient.getVstsInfo(); assert.isNull(repoInfo); } catch (err) { assert.isNotNull(err, "err was null when it shouldn't have been"); expect(err.statusCode).to.equal(404); } }); it("should verify accountClient.connect", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const accountClient: TeamServicesApi = new TeamServicesApi(TestSettings.AccountUrl, [CredentialManager.GetCredentialHandler()]); const settings: any = await accountClient.connect(); //console.log(settings); assert.isNotNull(settings, "settings was null when it shouldn't have been"); assert.isNotNull(settings.providerDisplayName); assert.isNotNull(settings.customDisplayName); assert.isNotNull(settings.authorizedUser.providerDisplayName); assert.isNotNull(settings.authorizedUser.customDisplayName); }); it("should verify repositoryClient.validateTfvcCollectionUrl", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts let success: boolean = false; try { const repositoryClient: TeamServicesApi = new TeamServicesApi(TestSettings.RemoteTfvcRepositoryUrl, [CredentialManager.GetCredentialHandler()]); await repositoryClient.validateTfvcCollectionUrl(); success = true; } finally { assert.isTrue(success); } }); it("should verify repositoryClient.validateTfvcCollectionUrl and 404", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts try { const repositoryClient: TeamServicesApi = new TeamServicesApi(TestSettings.RemoteTfvcRepositoryUrl + "1", [CredentialManager.GetCredentialHandler()]); await repositoryClient.validateTfvcCollectionUrl(); assert.fail(undefined, undefined, "validateTfvcCollectionUrl should have thrown but didn't."); //It shouldn't get here } catch (err) { assert.isNotNull(err, "err was null when it shouldn't have been"); expect(err.statusCode).to.equal(404); } }); }); ================================================ FILE: test-integration/contexts/servercontext.integration.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert, expect } from "chai"; import { Mocks } from "../helpers-integration/mocks"; import { TestSettings } from "../helpers-integration/testsettings"; import { CredentialManager } from "../../src/helpers/credentialmanager"; import { TeamServerContext } from "../../src/contexts/servercontext"; describe("ServerContext-Integration", function() { this.timeout(TestSettings.SuiteTimeout); const credentialManager: CredentialManager = new CredentialManager(); const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); before(function() { return credentialManager.StoreCredentials(ctx, TestSettings.AccountUser, TestSettings.Password); }); beforeEach(function() { return credentialManager.GetCredentials(ctx); }); // afterEach(function() { }); after(function() { return credentialManager.RemoveCredentials(ctx); }); it("should verify ServerContext CredentialHandler, UserInfo", function(done) { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; //Get coverage on CredentialHandler and UserInfo assert.isNotNull(ctx.CredentialHandler); expect(ctx.UserInfo).to.equal(undefined); done(); }); }); ================================================ FILE: test-integration/helpers/credentialmanager.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert } from "chai"; import { Mocks } from "../helpers-integration/mocks"; import { TestSettings } from "../helpers-integration/testsettings"; import { CredentialManager } from "../../src/helpers/credentialmanager"; import { CredentialInfo } from "../../src/info/credentialinfo"; import { TeamServerContext } from "../../src/contexts/servercontext"; import { Constants } from "../../src/helpers/constants"; describe("CredentialManager-Integration", function() { this.timeout(TestSettings.SuiteTimeout); //http://mochajs.org/#timeouts const credentialManager: CredentialManager = new CredentialManager(); const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); before(function() { // }); beforeEach(function() { // }); //afterEach(function() { }); after(function() { //Ensure we're clean after each test (even if one failed) return credentialManager.RemoveCredentials(ctx); }); it("should verify store, get, remove credentials for Azure DevOps Services (no token in settings)", async function() { try { await credentialManager.StoreCredentials(ctx, TestSettings.AccountUser, TestSettings.Password); let credInfo: CredentialInfo = await credentialManager.GetCredentials(ctx); //For PATs, username stored with StoreCredentials doesn't matter (it's always returned as OAuth) assert.equal(credInfo.Username, Constants.OAuth); assert.equal(credInfo.Password, TestSettings.Password); await credentialManager.RemoveCredentials(ctx); credInfo = await credentialManager.GetCredentials(ctx); //Ensure the creds we added get removed assert.isUndefined(credInfo); } catch (err) { console.log(err); } }); it("should verify store, get, remove credentials for Team Foundation Server", async function() { try { const ctx: TeamServerContext = Mocks.TeamServerContext("http://java-tfs2015:8081/tfs/DefaultCollection/_git/GitJava"); //const account: string = "java-tfs2015:8081"; const username: string = "domain\\user"; const password: string = "password"; await credentialManager.StoreCredentials(ctx, username, password); let credInfo: CredentialInfo = await credentialManager.GetCredentials(ctx); assert.equal(credInfo.Domain, "domain"); assert.equal(credInfo.Username, "user"); assert.equal(credInfo.Password, password); await credentialManager.RemoveCredentials(ctx); credInfo = await credentialManager.GetCredentials(ctx); //Ensure the creds we added get removed assert.isUndefined(credInfo); } catch (err) { console.log(err); } }); it("should verify azure accounts use both host + account", async function() { try { const ctxAccount1: TeamServerContext = Mocks.TeamServerContext("http://mytest.azure.com/account1/_git/GitJava"); const ctxAccount2: TeamServerContext = Mocks.TeamServerContext("http://mytest.azure.com/account2/_git/GitJava"); await credentialManager.StoreCredentials(ctxAccount1, TestSettings.AccountUser, TestSettings.Password); //ensure account1 has credentials let credInfo: CredentialInfo = await credentialManager.GetCredentials(ctxAccount1); //For PATs, username stored with StoreCredentials doesn't matter (it's always returned as OAuth) assert.equal(credInfo.Username, Constants.OAuth); assert.equal(credInfo.Password, TestSettings.Password); //Ensure account2 does not use the credentials of account1 credInfo = await credentialManager.GetCredentials(ctxAccount2); assert.isUndefined(credInfo); //Cleanup await credentialManager.RemoveCredentials(ctxAccount1); credInfo = await credentialManager.GetCredentials(ctxAccount1); //Ensure the creds we added get removed assert.isUndefined(credInfo); } catch (err) { console.log(err); } }); it("should verify visualstudio accounts only use host", async function() { try { const ctxAccount1: TeamServerContext = Mocks.TeamServerContext("http://account.visualstudio.com/DefaultCollection/_git/GitJava"); const ctxAccount2: TeamServerContext = Mocks.TeamServerContext("http://account.visualstudio.com/DefaultCollection/_git/GitJava"); await credentialManager.StoreCredentials(ctxAccount1, TestSettings.AccountUser, TestSettings.Password); //ensure account1 has credentials let credInfo: CredentialInfo = await credentialManager.GetCredentials(ctxAccount1); //For PATs, username stored with StoreCredentials doesn't matter (it's always returned as OAuth) assert.equal(credInfo.Username, Constants.OAuth); assert.equal(credInfo.Password, TestSettings.Password); //Ensure account2 does use the credentials of account1 credInfo = await credentialManager.GetCredentials(ctxAccount2); assert.equal(credInfo.Username, Constants.OAuth); assert.equal(credInfo.Password, TestSettings.Password); //Cleanup await credentialManager.RemoveCredentials(ctxAccount1); credInfo = await credentialManager.GetCredentials(ctxAccount1); //Ensure the creds we added get removed assert.isUndefined(credInfo); } catch (err) { console.log(err); } }); }); ================================================ FILE: test-integration/helpers-integration/mocks.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { TeamServerContext } from "../../src/contexts/servercontext"; import { RepositoryInfo } from "../../src/info/repositoryinfo"; import { TestSettings } from "./testsettings"; export class Mocks { public static TeamServerContext(repositoryUrl: string): TeamServerContext { return new TeamServerContext(repositoryUrl); } public static RepositoryInfo(): RepositoryInfo { const repositoryInfo: any = { "serverUrl":"undefined", "collection":{ "id":"undefined", "name":"undefined", "url":"undefined" }, "repository":{ "id":"undefined", "name":"undefined", "url":"undefined", "project":{ "id":"undefined", "name":"undefined", "url":"undefined", "state":1, "revision":115 }, "remoteUrl":"undefined" } }; repositoryInfo.serverUrl = TestSettings.AccountUrl; repositoryInfo.collection.name = TestSettings.CollectionName; repositoryInfo.collection.url = TestSettings.AccountUrl + "/_apis/projectCollections/" + TestSettings.CollectionGuid; repositoryInfo.repository.id = TestSettings.RepositoryId; repositoryInfo.repository.name = TestSettings.RepositoryName; repositoryInfo.repository.url = TestSettings.AccountUrl + "/DefaultCollection/_apis/git/repositories/" + TestSettings.RepositoryId; repositoryInfo.repository.project.name = TestSettings.TeamProject; repositoryInfo.repository.project.url = TestSettings.AccountUrl + "/DefaultCollection/_apis/projects/" + + TestSettings.ProjectGuid; repositoryInfo.repository.remoteUrl = TestSettings.RemoteRepositoryUrl; return new RepositoryInfo(repositoryInfo); } } ================================================ FILE: test-integration/helpers-integration/testsettings.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; export class TestSettings { public static get SuiteTimeout(): number { const timeout: string = process.env.MSVSTS_TEAM_SUITE_TIMEOUT; return (timeout ? Number(timeout) : 30000); } public static get TestTimeout(): number { const timeout: string = process.env.MSVSTS_TEAM_TEST_TIMEOUT; return (timeout ? Number(timeout) : 8000); } public static get Password(): string { const token: string = process.env.MSVSTS_TEAM_ACCESS_TOKEN; const password: string = process.env.MSVSTS_TEAM_ACCESS_PASSWORD; if (password) { return password; } //Token should have READ for build, code and wit //Token should have READ,WRITE for wit (to test CreateWorkItem which is not exposed to user) return (token || "undefined-password"); } //Returns just any old token (doesn't have to be an env var) public static get SettingsPassword(): string { return "kegnf4wasx3n5nwdj5lkutvjavtbbfblygsxcggvpphpmfwvjjov"; } public static get Account(): string { const account: string = process.env.MSVSTS_TEAM_ACCOUNT; return (account || "undefined-account"); } public static get AccountUrl(): string { const accountUrl: string = process.env.MSVSTS_TEAM_ACCOUNT_URL; return (accountUrl || "undefined-account-url"); } public static get AccountUser(): string { const accountUser : string = process.env.MSVSTS_TEAM_ACCOUNT_USER; return (accountUser || "OAuth-integration-tests"); } public static get BuildDefinitionId(): number { const id: string = process.env.MSVSTS_TEAM_BUILD_DEFINITION_ID; return (id ? Number(id) : -1); } public static get BuildId(): number { const id: string = process.env.MSVSTS_TEAM_BUILD_ID; return (id ? Number(id) : -1); } public static get CollectionName(): string { const collectionName: string = process.env.MSVSTS_TEAM_COLLECTION_NAME; return (collectionName || "undefined-collection-name"); } public static get RemoteRepositoryUrl(): string { const remoteRepositoryUrl: string = process.env.MSVSTS_TEAM_REMOTE_REPOSITORY_URL; return (remoteRepositoryUrl || "undefined-remote-repository-url"); } public static get RemoteTfvcRepositoryUrl(): string { const remoteRepositoryUrl: string = process.env.MSVSTS_TEAM_REMOTE_TFVC_REPOSITORY_URL; return (remoteRepositoryUrl || "undefined-remote-tfvc-repository-url"); } public static get RepositoryId(): string { const repositoryId: string = process.env.MSVSTS_TEAM_REPOSITORY_ID; return (repositoryId || "undefined-repository-id"); } public static get RepositoryName(): string { const repositoryName: string = process.env.MSVSTS_TEAM_REPOSITORY_NAME; return (repositoryName || "undefined-repository-name"); } public static get TeamProject(): string { const teamProject: string = process.env.MSVSTS_TEAM_TEAM_PROJECT; return (teamProject || "undefined-team-project"); } public static get WorkItemId(): number { const id: string = process.env.MSVSTS_TEAM_WORK_ITEM_ID; return (id ? Number(id) : -1); } public static get WorkItemQueryId(): string { const workItemQueryId: string = process.env.MSVSTS_TEAM_WORK_ITEM_QUERY_ID; return (workItemQueryId || "undefined-workitem-query-id"); } public static get WorkItemLinkQueryPath(): string { const workItemQueryPath: string = process.env.MSVSTS_TEAM_WORK_ITEM_LINK_QUERY_PATH; return (workItemQueryPath || "undefined-workitem-link-query-id"); } public static get WorkItemQueryPath(): string { const workItemQueryPath: string = process.env.MSVSTS_TEAM_WORK_ITEM_QUERY_PATH; return (workItemQueryPath || "undefined-workitem-query-path"); } public static get WorkItemTwoHundredTasksQueryPath(): string { const workItemQueryPath: string = process.env.MSVSTS_TEAM_WORK_ITEM_TWO_HUNDRED_QUERY_PATH; return (workItemQueryPath || "undefined-workitem-twohundredtasks-query-path"); } public static get WorkItemZeroResultsQueryPath(): string { const workItemQueryPath: string = process.env.MSVSTS_TEAM_WORK_ITEM_ZERO_RESULTS_QUERY_PATH; return (workItemQueryPath || "undefined-workitem-query-path"); } public static get ProjectGuid(): string { const remoteRepositoryUrl: string = process.env.MSVSTS_TEAM_PROJECT_GUID; return (remoteRepositoryUrl || "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); } public static get CollectionGuid(): string { const remoteRepositoryUrl: string = process.env.MSVSTS_TEAM_COLLECTION_GUID; return (remoteRepositoryUrl || "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); } } ================================================ FILE: test-integration/index.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; // // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING // // This file is providing the test runner to use when running extension tests. // By default the test runner in use is Mocha based. // // You can provide your own test runner if you want to override it by exporting // a function run(testRoot: string, clb: (error:Error) => void) that the extension // host can call to run the tests. The test runner is expected to use console.log // to report the results back to the caller. When the tests are finished, return // a possible error to the callback or null if none. /* tslint:disable:no-var-keyword */ var testRunner = require("vscode/lib/testrunner"); /* tslint:enable:no-var-keyword */ // You can directly control Mocha options by uncommenting the following lines // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ ui: "bdd", // Switched to bdd; use tdd for the TDD UI is being used in extension.test.ts (suite, test, etc.) useColors: true // colored output from test results }); module.exports = testRunner; ================================================ FILE: test-integration/services/build.integration.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert, expect } from "chai"; import { Build, BuildBadge } from "vso-node-api/interfaces/BuildInterfaces"; import { Mocks } from "../helpers-integration/mocks"; import { TestSettings } from "../helpers-integration/testsettings"; import { CredentialManager } from "../../src/helpers/credentialmanager"; import { UserAgentProvider } from "../../src/helpers/useragentprovider"; import { TeamServerContext } from "../../src/contexts/servercontext"; import { BuildService } from "../../src/services/build"; import { WellKnownRepositoryTypes } from "../../src/helpers/constants"; describe("BuildService-Integration", function() { this.timeout(TestSettings.SuiteTimeout); const credentialManager: CredentialManager = new CredentialManager(); const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); before(function() { UserAgentProvider.VSCodeVersion = "0.0.0"; return credentialManager.StoreCredentials(ctx, TestSettings.AccountUser, TestSettings.Password); }); beforeEach(function() { return credentialManager.GetCredentials(ctx); }); // afterEach(function() { }); after(function() { return credentialManager.RemoveCredentials(ctx); }); it("should verify BuildService.GetBuildDefinitions", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: BuildService = new BuildService(ctx); const definitions = await svc.GetBuildDefinitions(TestSettings.TeamProject); assert.isNotNull(definitions, "definitions was null when it shouldn't have been"); //console.log(definitions.length); expect(definitions.length).to.be.at.least(1); }); it("should verify BuildService.GetBuildById", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: BuildService = new BuildService(ctx); const build: Build = await svc.GetBuildById(TestSettings.BuildId); assert.isNotNull(build, "build was null when it shouldn't have been"); //console.log(definitions.length); expect(build.buildNumber).to.equal(TestSettings.BuildId.toString()); }); it("should verify BuildService.GetBuilds", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: BuildService = new BuildService(ctx); const builds = await svc.GetBuilds(TestSettings.TeamProject); assert.isNotNull(builds, "builds was null when it shouldn't have been"); //console.log(builds.length); expect(builds.length).to.be.at.least(1); }); it("should verify BuildService.GetBuildsByDefinitionId", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: BuildService = new BuildService(ctx); const builds: Build[] = await svc.GetBuildsByDefinitionId(TestSettings.TeamProject, TestSettings.BuildDefinitionId); assert.isNotNull(builds, "builds was null when it shouldn't have been"); //console.log(definitions.length); expect(builds.length).to.equal(1); }); it("should verify BuildService.GetBuildBadge", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: BuildService = new BuildService(ctx); const badge: BuildBadge = await svc.GetBuildBadge(TestSettings.TeamProject, WellKnownRepositoryTypes.TfsGit, TestSettings.RepositoryId, "refs/heads/master"); assert.isNotNull(badge, "badge was null when it shouldn't have been"); }); }); ================================================ FILE: test-integration/services/gitvc.integration.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert, expect } from "chai"; import { GitPullRequest, GitRepository } from "vso-node-api/interfaces/GitInterfaces"; import { Mocks } from "../helpers-integration/mocks"; import { TestSettings } from "../helpers-integration/testsettings"; import { CredentialManager } from "../../src/helpers/credentialmanager"; import { UserAgentProvider } from "../../src/helpers/useragentprovider"; import { TeamServerContext } from "../../src/contexts/servercontext"; import { GitVcService, PullRequestScore } from "../../src/services/gitvc"; describe("GitVcService-Integration", function() { this.timeout(TestSettings.SuiteTimeout); const credentialManager: CredentialManager = new CredentialManager(); const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); before(function() { UserAgentProvider.VSCodeVersion = "0.0.0"; return credentialManager.StoreCredentials(ctx, TestSettings.AccountUser, TestSettings.Password); }); beforeEach(function() { return credentialManager.GetCredentials(ctx); }); // afterEach(function() { }); after(function() { return credentialManager.RemoveCredentials(ctx); }); it("should verify GitVcService.GetRepositories", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: GitVcService = new GitVcService(ctx); const repos: GitRepository[] = await svc.GetRepositories(TestSettings.TeamProject); assert.isNotNull(repos, "repos was null when it shouldn't have been"); //console.log(repos.length); expect(repos.length).to.equal(2); }); it("should verify GitVcService.GetPullRequests", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: GitVcService = new GitVcService(ctx); const requests: GitPullRequest[] = await svc.GetPullRequests(ctx.RepoInfo.RepositoryId); assert.isNotNull(requests, "requests was null when it shouldn't have been"); //console.log(requests.length); expect(requests.length).to.equal(4); }); it("should verify GitVcService.GetPullRequestScore", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: GitVcService = new GitVcService(ctx); const requests: GitPullRequest[] = await svc.GetPullRequests(ctx.RepoInfo.RepositoryId); const totals = []; requests.forEach((request) => { totals.push({ "id" : request.pullRequestId, "score" : GitVcService.GetPullRequestScore(request) }); }); assert.equal(totals.length, 4); for (let index = 0; index < totals.length; index++) { const element: any = totals[index]; if (element.id === 5) { assert.equal(element.score, PullRequestScore.Succeeded); continue; } if (element.id === 4) { assert.equal(element.score, PullRequestScore.Waiting); continue; } if (element.id === 3) { assert.equal(element.score, PullRequestScore.Failed); continue; } if (element.id === 2) { assert.equal(element.score, PullRequestScore.NoResponse); continue; } else { //We got a PR we didn't expect but length\count was still the same. assert.isTrue(false, "Expected count of pull requests is incorrect."); } } }); }); ================================================ FILE: test-integration/services/workitemtracking.integration.test.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ "use strict"; import { assert, expect } from "chai"; import { QueryHierarchyItem, WorkItem, WorkItemType } from "vso-node-api/interfaces/WorkItemTrackingInterfaces"; import { Mocks } from "../helpers-integration/mocks"; import { TestSettings } from "../helpers-integration/testsettings"; import { WitQueries } from "../../src/helpers/constants"; import { CredentialManager } from "../../src/helpers/credentialmanager"; import { UserAgentProvider } from "../../src/helpers/useragentprovider"; import { TeamServerContext } from "../../src/contexts/servercontext"; import { SimpleWorkItem, WorkItemTrackingService } from "../../src/services/workitemtracking"; describe("WorkItemTrackingService-Integration", function() { this.timeout(TestSettings.SuiteTimeout); const credentialManager: CredentialManager = new CredentialManager(); const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); before(function() { UserAgentProvider.VSCodeVersion = "0.0.0"; return credentialManager.StoreCredentials(ctx, TestSettings.AccountUser, TestSettings.Password); }); beforeEach(function() { return credentialManager.GetCredentials(ctx); }); // afterEach(function() { }); after(function() { return credentialManager.RemoveCredentials(ctx); }); //Even though CreateWorkItem isn't exposed in the extension, run it so we can get to 200, then 20,000 //work items in the team project. At that point, we can test other scenarios around WIT. it("should verify WorkItemTrackingService.CreateWorkItem", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const itemType : string = "Bug"; const today: Date = new Date(); const title: string = "Work item created by integration test (" + today.toLocaleString() + ")"; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const item: WorkItem = await svc.CreateWorkItem(ctx, itemType, title); assert.isNotNull(item, "item was null when it shouldn't have been"); }); it("should verify WorkItemTrackingService.GetWorkItems", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const items: SimpleWorkItem[] = await svc.GetWorkItems(TestSettings.TeamProject, WitQueries.MyWorkItems); assert.isNotNull(items, "items was null when it shouldn't have been"); //console.log(items); }); it("should verify WorkItemTrackingService.GetQueryResultCount", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const query: QueryHierarchyItem = await svc.GetWorkItemQuery(TestSettings.TeamProject, TestSettings.WorkItemQueryPath); assert.isNotNull(query); //console.log(query); expect(query.id).to.equal(TestSettings.WorkItemQueryId); const count: number = await svc.GetQueryResultCount(TestSettings.TeamProject, query.wiql); //console.log("count = " + count); expect(count).to.be.at.least(2); }); it("should verify WorkItemTrackingService.GetWorkItemHierarchyItems", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const items: QueryHierarchyItem[] = await svc.GetWorkItemHierarchyItems(TestSettings.TeamProject); assert.isNotNull(items); //console.log(items.length); expect(items.length).to.equal(2); }); it("should verify WorkItemTrackingService.GetWorkItemQuery", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const query: QueryHierarchyItem = await svc.GetWorkItemQuery(TestSettings.TeamProject, TestSettings.WorkItemQueryPath); assert.isNotNull(query); //console.log(query); expect(query.id).to.equal(TestSettings.WorkItemQueryId); const items: SimpleWorkItem[] = await svc.GetWorkItems(TestSettings.TeamProject, query.wiql); assert.isTrue(items.length > 0); }); it("should verify WorkItemTrackingService.GetWorkItemTypes", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const items: WorkItemType[] = await svc.GetWorkItemTypes(TestSettings.TeamProject); assert.isNotNull(items); //console.log(items.length); expect(items.length).to.equal(7); }); it("should verify WorkItemTrackingService.GetWorkItemById", async function() { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); const item: SimpleWorkItem = await svc.GetWorkItemById(TestSettings.WorkItemId.toString()); assert.isNotNull(item); //console.log(items.length); expect(item.id).to.equal(TestSettings.WorkItemId.toString()); }); it("should verify WorkItemTrackingService.GetWorkItemQuery with a Link query", function(done) { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); svc.GetWorkItemQuery(TestSettings.TeamProject, TestSettings.WorkItemLinkQueryPath).then( function (query) { assert.isNotNull(query); //console.log(query); svc.GetWorkItems(TestSettings.TeamProject, query.wiql).then((items) => { assert.isTrue(items.length > 0, "Expected at least 1 result but didn't get any."); //assert.isTrue(items.length === 200, "Expected the maximum of 200 work items but didn't get that amount."); // current maximum work items returned done(); }).catch((err) => { done(err); }); }, function (err) { done(err); } ); }); it("should verify WorkItemTrackingService.GetWorkItemQuery with maximum 200 results", function(done) { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); svc.GetWorkItemQuery(TestSettings.TeamProject, TestSettings.WorkItemTwoHundredTasksQueryPath).then( function (query) { assert.isNotNull(query); //console.log(query); svc.GetWorkItems(TestSettings.TeamProject, query.wiql).then((items) => { assert.isTrue(items.length > 0, "Expected at least 1 result but didn't get any."); // current maximum work items returned assert.isTrue(items.length === 200, "Expected the maximum of 200 work items but didn't get that amount."); done(); }).catch((err) => { done(err); }); }, function (err) { done(err); } ); }); it("should verify WorkItemTrackingService.GetWorkItemQuery which returns zero results", function(done) { this.timeout(TestSettings.TestTimeout); //http://mochajs.org/#timeouts const ctx: TeamServerContext = Mocks.TeamServerContext(TestSettings.RemoteRepositoryUrl); ctx.CredentialHandler = CredentialManager.GetCredentialHandler(); ctx.RepoInfo = Mocks.RepositoryInfo(); ctx.UserInfo = undefined; const svc: WorkItemTrackingService = new WorkItemTrackingService(ctx); svc.GetWorkItemQuery(TestSettings.TeamProject, TestSettings.WorkItemZeroResultsQueryPath).then( function (query) { assert.isNotNull(query); //console.log(query); svc.GetWorkItems(TestSettings.TeamProject, query.wiql).then((items) => { assert.isTrue(items.length === 0, "Expected zero results but actually got some."); //assert.isTrue(items.length === 200, "Expected the maximum of 200 work items but didn't get that amount."); // current maximum work items returned done(); }).catch((err) => { done(err); }); }, function (err) { done(err); } ); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "outDir": "out", "sourceMap": true, "removeComments": false, "lib": [ "es6" ], "allowJs": true, "noUnusedParameters": true, "noUnusedLocals": true, "forceConsistentCasingInFileNames": true }, "include": [ "src/**/*", "test/**/*", "test-integration/**/*" ], "exclude": [ "node_modules" ] } ================================================ FILE: tslint.json ================================================ { "rules": { "align": [true, "parameters", "arguments", "statements" ], "arrow-parens": true, "class-name": true, "curly": true, "eofline": true, "forin": true, "import-spacing": true, "indent": [true, "spaces", 4], "label-position": true, "max-line-length": [false, 160], "newline-before-return": false, "no-arg": true, "no-bitwise": true, "no-console": [true, "debug", "info", "time", "timeEnd", "trace" ], "no-consecutive-blank-lines": true, "no-construct": true, "no-debugger": true, "no-duplicate-variable": true, "no-empty": true, "no-eval": true, "no-null-keyword": true, "no-string-literal": false, "no-switch-case-fall-through": true, "no-trailing-whitespace": true, "no-var-keyword": true, "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace" ], "one-variable-per-declaration": true, "prefer-const": true, "prefer-method-signature": true, "quotemark": [true, "double"], "radix": false, "semicolon": true, "space-before-function-paren": [ false, {"anonymous": "always", "named": "never", "asyncArrow": "always"} ], "trailing-comma": [false, { "multiline": "never", "singleline": "never" }], "triple-equals": [true, "allow-null-check"], "variable-name": [true, "check-format", "allow-leading-underscore", "ban-keywords" ], "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator" ] } }