├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── pr-validate.yml │ ├── release-on-tag.yml │ └── vsce-publish.yml ├── .gitignore ├── .grenrc.js ├── .nvmrc ├── .prettierrc.js ├── .storybook ├── addons.ts ├── config.ts └── main.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── browser ├── package.json ├── src │ ├── actions │ │ ├── copyText.ts │ │ ├── messagebus.ts │ │ └── results.ts │ ├── components │ │ ├── Dialog │ │ │ └── index.tsx │ │ ├── Footer │ │ │ └── index.tsx │ │ ├── Header │ │ │ ├── author │ │ │ │ └── index.tsx │ │ │ ├── branch │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ └── LogView │ │ │ ├── BranchGraph │ │ │ ├── index.tsx │ │ │ └── svgGenerator.ts │ │ │ ├── Commit │ │ │ ├── Author │ │ │ │ └── index.tsx │ │ │ ├── Avatar │ │ │ │ └── index.tsx │ │ │ ├── FileEntry │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ │ ├── LogEntry │ │ │ └── index.tsx │ │ │ ├── LogEntryList │ │ │ └── index.tsx │ │ │ ├── LogView │ │ │ └── index.tsx │ │ │ ├── Refs │ │ │ ├── Head.tsx │ │ │ ├── Remote.tsx │ │ │ └── Tag.tsx │ │ │ └── gitmojify.ts │ ├── constants │ │ ├── actions.ts │ │ └── resultActions.ts │ ├── containers │ │ └── App │ │ │ └── index.tsx │ ├── definitions.ts │ ├── global.d.ts │ ├── index.ejs │ ├── index.tsx │ ├── main.css │ ├── middleware │ │ ├── index.ts │ │ └── logger.ts │ ├── reducers │ │ ├── authors.ts │ │ ├── avatars.ts │ │ ├── branches.ts │ │ ├── graph.ts │ │ ├── index.ts │ │ ├── logEntries.ts │ │ ├── settings.ts │ │ └── vscode.ts │ ├── store │ │ └── index.ts │ └── types.ts ├── stories │ └── index.stories.tsx └── tsconfig.json ├── gitHistory.code-workspace ├── githistoryvscode_settings.json ├── images ├── compareCommits.gif ├── fileHistoryCommandv3.gif ├── gitLogv3.gif ├── icon-dark.svg ├── icon-light.svg ├── icon.png └── lineHistoryCommandv3.gif ├── jest.config.js ├── jest.extension.config.js ├── package-lock.json ├── package.json ├── resources ├── fileicons │ ├── images │ │ ├── Document_16x.svg │ │ ├── Document_16x_inverse.svg │ │ ├── FolderOpen_16x.svg │ │ ├── FolderOpen_16x_inverse.svg │ │ ├── Folder_16x.svg │ │ └── Folder_16x_inverse.svg │ └── vs_minimal_icons.json └── icons │ ├── dark │ ├── status-added.svg │ ├── status-conflict.svg │ ├── status-copied.svg │ ├── status-deleted.svg │ ├── status-ignored.svg │ ├── status-modified.svg │ └── status-renamed.svg │ ├── emoji.json │ ├── light │ ├── status-added.svg │ ├── status-conflict.svg │ ├── status-copied.svg │ ├── status-deleted.svg │ ├── status-ignored.svg │ ├── status-modified.svg │ └── status-renamed.svg │ └── misc │ ├── dialog-info.svg │ └── dialog-warning.svg ├── src ├── adapter │ ├── avatar │ │ ├── base.ts │ │ ├── github.ts │ │ ├── gravatar.ts │ │ └── types.ts │ ├── exec │ │ ├── gitCommandExec.ts │ │ ├── index.ts │ │ └── types.ts │ ├── helpers.ts │ ├── index.ts │ ├── parsers │ │ ├── actionDetails │ │ │ └── parser.ts │ │ ├── fileStat │ │ │ └── parser.ts │ │ ├── fileStatStatus │ │ │ └── parser.ts │ │ ├── index.ts │ │ ├── log │ │ │ └── parser.ts │ │ ├── serviceRegistry.ts │ │ └── types.ts │ ├── repository │ │ ├── constants.ts │ │ ├── factory.ts │ │ ├── git.d.ts │ │ ├── git.ts │ │ ├── gitArgsService.ts │ │ ├── gitRemoteService.ts │ │ ├── index.ts │ │ ├── serviceRegistry.ts │ │ └── types.ts │ ├── serviceRegistry.ts │ └── types.ts ├── application │ ├── applicationShell.ts │ ├── commandManager.ts │ ├── disposableRegistry.ts │ ├── documentManager.ts │ ├── serviceRegistry.ts │ ├── stateStore.ts │ ├── types.ts │ ├── types │ │ ├── commandManager.ts │ │ ├── disposableRegistry.ts │ │ ├── documentManager.ts │ │ ├── stateStore.ts │ │ └── workspace.ts │ └── workspace.ts ├── commandFactories │ ├── commitFactory.ts │ ├── fileCommitFactory.ts │ ├── serviceRegistry.ts │ └── types.ts ├── commandHandlers │ ├── commit │ │ ├── commitViewExplorer.ts │ │ ├── compare.ts │ │ ├── compareViewExplorer.ts │ │ ├── gitCheckout.ts │ │ ├── gitCherryPick.ts │ │ ├── gitCommit.ts │ │ ├── gitCommitDetails.ts │ │ ├── gitMerge.ts │ │ ├── gitRebase.ts │ │ └── revert.ts │ ├── file │ │ ├── file.ts │ │ └── mimeTypes.ts │ ├── fileCommit │ │ ├── fileCompare.ts │ │ └── fileHistory.ts │ ├── gitHistory.ts │ ├── handlerManager.ts │ ├── ref │ │ └── gitRef.ts │ ├── registration.ts │ ├── serviceRegistry.ts │ └── types.ts ├── commands │ ├── baseCommand.ts │ ├── baseCommitCommand.ts │ ├── baseFileCommitCommand.ts │ ├── commit │ │ ├── checkout.ts │ │ ├── cherryPick.ts │ │ ├── compare.ts │ │ ├── createBranch.ts │ │ ├── createTag.ts │ │ ├── hideCommitViewExplorer.ts │ │ ├── merge.ts │ │ ├── rebase.ts │ │ ├── revert.ts │ │ ├── selectForComparion.ts │ │ └── viewDetails.ts │ └── fileCommit │ │ ├── compareFile.ts │ │ ├── compareFileAcrossCommits.ts │ │ ├── compareFileWithPrevious.ts │ │ ├── compareFileWithWorkspace.ts │ │ ├── fileHistory.ts │ │ ├── selectFileForComparison.ts │ │ ├── viewFile.ts │ │ └── viewPreviousFile.ts ├── common │ ├── cache.ts │ ├── constants.ts │ ├── enumHelper.ts │ ├── helpers.ts │ ├── log.ts │ ├── stopWatch.ts │ ├── telemetry.ts │ ├── types.ts │ ├── uiLogger.ts │ └── uiService.ts ├── constants.ts ├── extension.ts ├── formatters │ ├── commitFormatter.ts │ └── types.ts ├── ioc │ ├── container.ts │ ├── index.ts │ ├── serviceManager.ts │ └── types.ts ├── logger.ts ├── nodes │ ├── factory.ts │ ├── nodeBuilder.ts │ ├── nodeIcons.ts │ ├── serviceRegistry.ts │ ├── treeNodes.ts │ └── types.ts ├── platform │ ├── fileSystem.ts │ ├── platformService.ts │ ├── serviceRegistry.ts │ └── types.ts ├── server │ ├── apiController.ts │ └── htmlViewer.ts ├── types.ts └── viewers │ ├── commitViewer.ts │ ├── commitViewerFactory.ts │ ├── serviceRegistry.ts │ └── types.ts ├── test ├── common.ts ├── extension │ ├── __mocks__ │ │ └── vscode.ts │ ├── branches.test.ts │ ├── repoSetup.ts │ ├── setup.ts │ └── testRunner.ts ├── mocks.ts ├── non-extension │ ├── __mocks__ │ │ ├── vsc │ │ │ ├── README.md │ │ │ ├── arrays.ts │ │ │ ├── charCode.ts │ │ │ ├── extHostedTypes.ts │ │ │ ├── htmlContent.ts │ │ │ ├── index.ts │ │ │ ├── position.ts │ │ │ ├── range.ts │ │ │ ├── selection.ts │ │ │ ├── strings.ts │ │ │ ├── telemetryReporter.ts │ │ │ ├── uri.ts │ │ │ └── uuid.ts │ │ ├── vscode-mock.ts │ │ └── vscode.ts │ └── adapter │ │ └── parsers │ │ ├── actionDetails │ │ └── parser.test.ts │ │ ├── actionedDetailsParser.test.ts │ │ ├── fileStat │ │ └── parser.test.ts │ │ └── fileStatus │ │ └── parser.test.ts ├── runTest.ts └── setup.ts ├── tsconfig.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Tab indentation 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | # The indent size used in the `package.json` file cannot be changed 14 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 15 | [{.travis.yml,npm-shrinkwrap.json,package.json}] 16 | indent_style = space 17 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | rules: { 13 | "@typescript-eslint/no-non-null-assertion": "off", // allow non-null assertion 14 | "@typescript-eslint/interface-name-prefix": "off", // skip interface names which does not start with "I" prefix 15 | "@typescript-eslint/no-explicit-any": "off", // allow datatype any 16 | "@typescript-eslint/explicit-function-return-type": "off", // ignore missing return types for the time being 17 | "@typescript-eslint/no-empty-interface": "off", // allow empty interfaces 18 | "@typescript-eslint/ban-ts-ignore": "off", // allow @ts-ignore comment for the time being 19 | "@typescript-eslint/no-unused-vars": "off", 20 | "@typescript-eslint/no-use-before-define": "off", 21 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 22 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the Bug** 11 | 12 | Description of what the bug is. 13 | 14 | **Steps To Reproduce** 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | **Expected Behavior** 21 | 22 | What you expected to happen. 23 | 24 | **Environment** 25 | 26 | - OS: 27 | - VSCode version: 28 | - Git History version: 29 | -------------------------------------------------------------------------------- /.github/workflows/pr-validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | Build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '16.14.x' 14 | - run: npm install 15 | - run: npm run vscode:prepublish 16 | - run: npm run test 17 | Test_Extension: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: '12.8.x' 24 | - run: npm install 25 | - name: Compile 26 | run: tsc --skipLibCheck -p ./ 27 | - name: Start xvfb 28 | run: /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 29 | - name: Run extension tests 30 | env: 31 | DISPLAY: ':99.0' 32 | run: npm run test-extension 33 | -------------------------------------------------------------------------------- /.github/workflows/release-on-tag.yml: -------------------------------------------------------------------------------- 1 | name: Generate Release Notes 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | Build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '16.14.x' 16 | - run: npm install github-release-notes@0.17.1 -g 17 | - name: Write PreRelease Notes 18 | env: 19 | GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | GITHUB_EVENT_REF: ${{ github.event.ref }} 21 | run: | 22 | tagname="${GITHUB_EVENT_REF/refs\/tags\//}" 23 | gren release --override -d --tags=$tagname 24 | -------------------------------------------------------------------------------- /.github/workflows/vsce-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish and Upload 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | if: "!github.event.release.prerelease" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '16.14.x' 16 | - run: npm install 17 | # - run: npm install -g @vscode/vsce github-release-notes 18 | - name: Read package.json verison 19 | uses: tyankatsu0105/read-package-version-actions@v1 20 | id: package-version 21 | # - name: Generate and Commit Changelog 22 | # env: 23 | # GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | # run: | 25 | # git fetch origin main 26 | # git checkout main 27 | # gren changelog --override 28 | # git -c user.name="Don Jayamanne" -c user.email="don.jayamanne@yahoo.com" commit --no-verify -m "Updated CHANGELOG.md - Github Actions" -- CHANGELOG.md 29 | # git push 30 | - name: vsce publish 31 | run: vsce publish -p "${{ secrets.VSCE_TOKEN }}" 32 | - name: vsce package 33 | run: vsce package 34 | - name: Upload to Release 35 | uses: actions/upload-release-asset@v1.0.1 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 40 | asset_path: ${{ format('githistory-{0}.vsix', steps.package-version.outputs.version) }} 41 | asset_name: ${{ format('githistory-{0}.vsix', steps.package-version.outputs.version) }} 42 | asset_content_type: application/zip 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | out 3 | dist 4 | node_modules 5 | *.pyc 6 | **/.vscode/.ropeproject/** 7 | **/testFiles/**/.cache/** 8 | *.noseids 9 | .vscode-test 10 | coverage/** 11 | __pycache__ 12 | npm-debug.log 13 | **/.mypy_cache/** 14 | !yarn.lock 15 | coverage/ 16 | temp/** 17 | -------------------------------------------------------------------------------- /.grenrc.js: -------------------------------------------------------------------------------- 1 | 2 | function getAuthor(placeholders) { 3 | if (placeholders.author === 'DonJayamanne') { 4 | // skip owner 5 | return ''; 6 | } 7 | 8 | if (placeholders.author === null) { 9 | // skip when no author could be found 10 | return ''; 11 | } 12 | 13 | return `- @${placeholders.author}`; 14 | } 15 | function parseCommitLine(placeholders) { 16 | return `- [${placeholders.sha.substr(0, 7)}] ${placeholders.message} ${getAuthor(placeholders)}` 17 | } 18 | 19 | module.exports = { 20 | dataSource: "commits", 21 | "template": { 22 | commit: parseCommitLine 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.14.2 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 4, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/addons.ts: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.ts: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from "@storybook/react"; 2 | import { withInfo } from "@storybook/addon-info"; 3 | 4 | // automatically import all files ending in *.stories.tsx 5 | const req = require.context("../src", true, /.stories.tsx$/); 6 | 7 | function loadStories() { 8 | addDecorator(withInfo); 9 | req.keys().forEach(req); 10 | } 11 | 12 | configure(loadStories, module); 13 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | stories: [path.join(__dirname, '../browser/stories/**/*.stories.*')], 6 | addons: ['@storybook/addon-actions', '@storybook/addon-links'], 7 | webpackFinal: async config => { 8 | config.module.rules.push({ 9 | test: /\.(ts|tsx)$/, 10 | include: [path.join(__dirname, '../browser/src'), path.join(__dirname, '../browser/stories')], 11 | use: [ 12 | { 13 | loader: require.resolve('ts-loader'), 14 | }, 15 | { 16 | loader: require.resolve('react-docgen-typescript-loader'), 17 | }, 18 | ], 19 | }); 20 | config.plugins.push( 21 | new TsconfigPathsPlugin({ 22 | configFile: path.join(__dirname, '../browser/tsconfig.json'), 23 | }), 24 | ); 25 | config.resolve.extensions.push('.js', '.ts', '.tsx'); 26 | 27 | return config; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": false, 14 | "smartStep": true, 15 | "sourceMaps": true, 16 | "outFiles": [ 17 | "${workspaceRoot}/dist/**/*.js" 18 | ], 19 | "preLaunchTask": "webpack-dev", 20 | "skipFiles": [ 21 | "/**" 22 | ] 23 | }, 24 | { 25 | "name": "Launch Unit Tests", 26 | "type": "node", 27 | "request": "launch", 28 | "cwd": "${workspaceFolder}", 29 | "args": [ 30 | "--inspect-brk", 31 | "${workspaceRoot}/node_modules/.bin/jest", 32 | "--runInBand", 33 | "--config", 34 | "${workspaceRoot}/jest.config.js" 35 | ], 36 | "console": "integratedTerminal", 37 | "internalConsoleOptions": "neverOpen", 38 | "skipFiles": [ 39 | "/**" 40 | ] 41 | }, 42 | { 43 | "name": "Run Extension Tests", 44 | "type": "extensionHost", 45 | "request": "launch", 46 | "runtimeExecutable": "${execPath}", 47 | "args": [ 48 | // Ensure this folder exists. 49 | "${workspaceFolder}/temp/testing/test_gitHistory", 50 | "--disable-extensions", 51 | "--extensionDevelopmentPath=${workspaceFolder}", 52 | "--extensionTestsPath=${workspaceFolder}/dist/test/extension/testRunner" 53 | ], 54 | "outFiles": [ 55 | "${workspaceFolder}/dist/test/**/*.js" 56 | ], 57 | "skipFiles": [ 58 | "/**" 59 | ], 60 | "env": { 61 | "IS_TEST_MODE": "1" 62 | }, 63 | "sourceMaps": true, 64 | "internalConsoleOptions": "openOnSessionStart", 65 | "preLaunchTask": "test-compile" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "node_modules": true, 5 | ".vscode-test": true, 6 | "coverage": true, 7 | "out": true, 8 | "dist": true 9 | }, 10 | "search.exclude": { 11 | "out": true, 12 | "dist": true, 13 | "coverage": true, 14 | "node_modules": true, 15 | ".vscode-test": true 16 | }, 17 | "typescript.tsdk": "./node_modules/typescript/lib", 18 | "[typescript]": { 19 | "editor.formatOnSave": true 20 | }, 21 | "[javascript]": { 22 | "editor.formatOnSave": true 23 | }, 24 | "[javascriptreact]": { 25 | "editor.formatOnSave": true 26 | }, 27 | "[typescriptreact]": { 28 | "editor.formatOnSave": true 29 | }, 30 | "editor.codeActionsOnSave": { 31 | "source.fixAll.eslint": true 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | // A task runner that calls a custom npm script that compiles the extension. 9 | { 10 | "version": "2.0.0", 11 | "presentation": { 12 | "echo": true, 13 | "reveal": "always", 14 | "focus": false, 15 | "panel": "shared" 16 | }, 17 | "tasks": [ 18 | { 19 | "type": "npm", 20 | "script": "lint", 21 | "problemMatcher": [ 22 | "$eslint-stylish" 23 | ] 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "lint-staged", 28 | "problemMatcher": [ 29 | "$eslint-stylish" 30 | ] 31 | }, 32 | { 33 | "type": "npm", 34 | "label": "test-compile", 35 | "script": "test-compile", 36 | "problemMatcher": [ 37 | "$tsc-watch" 38 | ] 39 | }, 40 | { 41 | "label": "webpack-dev", 42 | "type": "npm", 43 | "isBackground": true, 44 | "script": "webpack-dev", 45 | "problemMatcher": [ 46 | { 47 | "pattern": [ 48 | { 49 | "regexp": ".", 50 | "file": 1, 51 | "location": 2, 52 | "message": 3 53 | } 54 | ], 55 | "background": { 56 | "activeOnStart": true, 57 | "beginsPattern": { 58 | "regexp": "webpack is watching the files" 59 | }, 60 | "endsPattern": { 61 | "regexp": "Entrypoint main = extension.js" 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .github 3 | .vscode 4 | 5 | browser 6 | coverage 7 | out 8 | test 9 | src 10 | node_modules 11 | 12 | temp 13 | 14 | images/**/*.gif 15 | tsconfig.json 16 | webpack.config.js 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to build 2 | - Install nodejs 12.8.+ 3 | - install npm 4 | - Clone repo 5 | - Open repo folder in VS Code 6 | - Run command `npm install` in terminal 7 | - Go to `Task-> Run Build Task` menu option in VS Code 8 | - Run both Compile and npm:WatchReact tasks 9 | 10 | ## How to run 11 | - From the terminal, go to the repo's folder 12 | - From the debug panel of the new VS Code instance, run the `Launch Extension` option or press F5. This will open a new instance of VS Code in dev mode 13 | - See [VS Code Docs](https://code.visualstudio.com/docs/extensions/developing-extensions#_launching-your-extension) for additional details 14 | 15 | ## Storybook 16 | - `npm run storybook` 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 DonJayamanne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git History, Search and More (including _git log_) 2 | 3 | * View and search git log along with the graph and details. 4 | * View a previous copy of the file. 5 | * View and search the history 6 | * View the history of one or all branches (git log) 7 | * View the history of a file 8 | * View the history of a line in a file (Git Blame). 9 | * View the history of an author 10 | * Compare: 11 | * Compare branches 12 | * Compare commits 13 | * Compare files across commits 14 | * Miscellaneous features: 15 | * Github avatars 16 | * Cherry-picking commits 17 | * Create Tag 18 | * Create Branch 19 | * Reset commit (soft and hard) 20 | * Reverting commits 21 | * Create branches from a commits 22 | * View commit information in a treeview (snapshot of all changes) 23 | * Merge and rebase 24 | 25 | Open the file to view the history, and then 26 | Press F1 and select/type "Git: View History", "Git: View File History" or "Git: View Line History". 27 | 28 | ## Available Commands 29 | * View Git History (git log) (git.viewHistory) 30 | * View File History (git.viewFileHistory) 31 | * View Line History (git.viewLineHistory) 32 | 33 | ## Keyboard Shortcuts 34 | You can add keyboard short cuts for the above commands by following the directions on the website [customization documentation](https://code.visualstudio.com/docs/customization/keybindings). 35 | 36 | NOTE: The file for which the history is to be viewed, must already be opened. 37 | 38 | ![Image of Git Log](https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/main/images/gitLogv3.gif) 39 | 40 | ![Image of File History](https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/main/images/fileHistoryCommandv3.gif) 41 | 42 | ![Image of Line History](https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/main/images/lineHistoryCommandv3.gif) 43 | 44 | ![Image of Compare](https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/main/images/compareCommits.gif) 45 | 46 | 47 | ## Source 48 | 49 | [GitHub](https://github.com/DonJayamanne/gitHistoryVSCode) 50 | 51 | ## Big thanks to [ole](https://github.com/ole1986) 52 | 53 | ## License 54 | 55 | [MIT](https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/main/LICENSE) 56 | -------------------------------------------------------------------------------- /browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "tests": "npm test", 6 | "build": "cd .. && npx webpack -p --progress --colors", 7 | "watch": "cd .. && npx webpack --progress --colors --watch" 8 | } 9 | } -------------------------------------------------------------------------------- /browser/src/actions/copyText.ts: -------------------------------------------------------------------------------- 1 | export default function(event: React.MouseEvent, textToCopy: string) { 2 | event.preventDefault(); 3 | event.stopPropagation(); 4 | navigator.clipboard.writeText(textToCopy); 5 | 6 | const currentElement = event.currentTarget; 7 | const prevLabel = currentElement.getAttribute('aria-label'); 8 | 9 | currentElement.setAttribute('aria-label', 'Copied!'); 10 | setTimeout(() => currentElement.setAttribute('aria-label', prevLabel), 1000); 11 | } 12 | -------------------------------------------------------------------------------- /browser/src/actions/messagebus.ts: -------------------------------------------------------------------------------- 1 | const vsc = { 2 | postMessage: (message: any) => { 3 | /*Noop*/ 4 | }, 5 | }; 6 | 7 | function uuid() { 8 | return ( 9 | '_' + 10 | Math.random() 11 | .toString(36) 12 | .substr(2, 9) 13 | ); 14 | } 15 | 16 | function createPromiseFromMessageEvent(requestId): Promise { 17 | return new Promise((resolve, reject) => { 18 | const handleEvent = (e: MessageEvent) => { 19 | if (requestId === e.data.requestId) { 20 | window.removeEventListener('message', handleEvent); 21 | 22 | if (e.data.error) { 23 | reject(e.data.error); 24 | } else { 25 | resolve(e.data.payload); 26 | } 27 | } 28 | }; 29 | 30 | window.addEventListener('message', handleEvent); 31 | }); 32 | } 33 | 34 | export function post(cmd: string, payload: any): Promise { 35 | const requestId = uuid(); 36 | 37 | vsc.postMessage({ 38 | requestId, 39 | cmd, 40 | payload, 41 | }); 42 | 43 | return createPromiseFromMessageEvent(requestId); 44 | } 45 | 46 | export function initialize(vscodeApi: any) { 47 | vsc.postMessage = vscodeApi.postMessage.bind(); 48 | } 49 | -------------------------------------------------------------------------------- /browser/src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | 4 | type FooterProps = { 5 | canGoForward: boolean; 6 | canGoBack: boolean; 7 | goForward: () => void; 8 | goBack: () => void; 9 | }; 10 | export default function Footer(props: FooterProps) { 11 | return ( 12 |
13 | 21 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /browser/src/components/LogView/BranchGraph/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { RootState } from '../../../reducers'; 4 | import { BranchGrapProps, drawGitGraph } from './svgGenerator'; 5 | 6 | class BrachGraph extends React.Component { 7 | componentDidUpdate(prevProps: BranchGrapProps) { 8 | if (this.props.hideGraph) { 9 | drawGitGraph(this.svg, this.svg.nextSibling as HTMLElement, 0, this.props.itemHeight, [], true); 10 | return; 11 | } 12 | if (prevProps.updateTick === this.props.updateTick) { 13 | return; 14 | } 15 | 16 | // Hack, first clear before rebuilding. 17 | // Remember, we will need to support apending results, as opposed to clearing page 18 | drawGitGraph(this.svg, this.svg.nextSibling as HTMLElement, 0, this.props.itemHeight, []); 19 | drawGitGraph(this.svg, this.svg.nextSibling as HTMLElement, 0, this.props.itemHeight, this.props.logEntries); 20 | } 21 | 22 | private svg: SVGSVGElement; 23 | render() { 24 | return (this.svg = ref)} xmlns="http://www.w3.org/2000/svg">; 25 | } 26 | } 27 | 28 | function mapStateToProps(state: RootState): BranchGrapProps { 29 | const hideGraph = 30 | state && 31 | state.logEntries && 32 | ((state.settings.searchText && state.settings.searchText.length > 0) || 33 | (state.settings.file && state.settings.file.length > 0) || 34 | (state.settings.authorFilter && state.settings.authorFilter.length > 0) || 35 | state.logEntries.isLoading); 36 | 37 | return { 38 | logEntries: state.logEntries.items, 39 | hideGraph, 40 | itemHeight: state.graph.itemHeight, 41 | updateTick: state.graph.updateTick, 42 | }; 43 | } 44 | 45 | export default connect(mapStateToProps)(BrachGraph); 46 | -------------------------------------------------------------------------------- /browser/src/components/LogView/Commit/Author/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { ActionedDetails } from '../../../../definitions'; 4 | import { RootState } from '../../../../reducers/index'; 5 | import { ResultActions } from '../../../../actions/results'; 6 | import { GoEye } from 'react-icons/go'; 7 | 8 | type AuthorProps = { 9 | result: ActionedDetails; 10 | locale: string; 11 | selectAuthor(author: string): void; 12 | }; 13 | 14 | export function Author(props: AuthorProps) { 15 | function selectAuthor(event: React.MouseEvent) { 16 | event.preventDefault(); 17 | event.stopPropagation(); 18 | props.selectAuthor(props.result.name); 19 | } 20 | return ( 21 |
22 | 28 | 29 | 30 | 31 | 32 | 33 | {props.result.name} 34 | 35 | on {formatDateTime(props.locale, props.result.date)} 36 |
37 | ); 38 | } 39 | 40 | function formatDateTime(locale: string, date?: Date) { 41 | if (date && typeof date.toLocaleDateString !== 'function') { 42 | return ''; 43 | } 44 | 45 | const dateOptions = { 46 | weekday: 'short', 47 | day: 'numeric', 48 | month: 'short', 49 | year: 'numeric', 50 | hour: 'numeric', 51 | minute: 'numeric', 52 | }; 53 | try { 54 | locale = typeof locale === 'string' ? locale.replace('_', '-') : locale; 55 | return date.toLocaleString(locale); 56 | } catch { 57 | // @ts-ignore 58 | return date.toLocaleString(undefined, dateOptions); 59 | } 60 | } 61 | 62 | function mapStateToProps(state: RootState, wrapper: { result: ActionedDetails }) { 63 | return { 64 | result: wrapper.result, 65 | locale: state.vscode.locale, 66 | }; 67 | } 68 | 69 | function mapDispatchToProps(dispatch) { 70 | return { 71 | selectAuthor: (text: string) => dispatch(ResultActions.selectAuthor(text)), 72 | }; 73 | } 74 | 75 | export default connect(mapStateToProps, mapDispatchToProps)(Author); 76 | -------------------------------------------------------------------------------- /browser/src/components/LogView/Commit/Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { ActionedDetails } from '../../../../definitions'; 4 | import { AvatarsState, RootState } from '../../../../reducers/index'; 5 | 6 | type AvatarProps = { 7 | result: ActionedDetails; 8 | avatars: AvatarsState; 9 | }; 10 | 11 | function Avatar(props: AvatarProps) { 12 | let avatarUrl = ''; 13 | if (props.result) { 14 | const avatar = props.avatars.find( 15 | item => 16 | item.name === props.result.name || 17 | item.login === props.result.name || 18 | item.email === props.result.email, 19 | ); 20 | avatarUrl = avatar ? avatar.avatarUrl : ''; 21 | } 22 | if (avatarUrl) { 23 | return User; 24 | } else { 25 | return null; 26 | } 27 | } 28 | 29 | function mapStateToProps(state: RootState, wrapper: { result: ActionedDetails }) { 30 | return { 31 | avatars: state.avatars, 32 | result: wrapper.result, 33 | }; 34 | } 35 | 36 | export default connect(mapStateToProps)(Avatar); 37 | -------------------------------------------------------------------------------- /browser/src/components/LogView/LogEntryList/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { LogEntry, Ref } from '../../../definitions'; 3 | import LogEntryView from '../LogEntry'; 4 | 5 | interface ResultProps { 6 | logEntries: LogEntry[]; 7 | onViewCommit(entry: LogEntry): void; 8 | onAction(entry: LogEntry, name: string): void; 9 | onRefAction(logEntry: LogEntry, ref: Ref, name: string): void; 10 | } 11 | 12 | export default class LogEntryList extends React.Component { 13 | public ref: HTMLDivElement; 14 | public render() { 15 | if (!Array.isArray(this.props.logEntries)) { 16 | return null; 17 | } 18 | 19 | const results = this.props.logEntries.map(entry => ( 20 | 27 | )); 28 | return
(this.ref = ref)}>{results}
; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /browser/src/components/LogView/Refs/Head.tsx: -------------------------------------------------------------------------------- 1 | import { Ref } from '../../../definitions'; 2 | import * as React from 'react'; 3 | import { GoGitBranch, GoX } from 'react-icons/go'; 4 | 5 | export default function HeadRef(props: Ref) { 6 | return ( 7 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /browser/src/components/LogView/Refs/Remote.tsx: -------------------------------------------------------------------------------- 1 | import { Ref } from '../../../definitions'; 2 | import * as React from 'react'; 3 | import { GoGitBranch, GoX } from 'react-icons/go'; 4 | 5 | export default function RemoteRef(props: Ref) { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 | {props.name} 13 | props.onRemove()} role="button"> 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /browser/src/components/LogView/Refs/Tag.tsx: -------------------------------------------------------------------------------- 1 | import { Ref } from '../../../definitions'; 2 | import * as React from 'react'; 3 | import { GoTag, GoX } from 'react-icons/go'; 4 | 5 | export default function TagRef(props: Ref) { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 | {props.name} 13 | props.onRemove()} role="button"> 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /browser/src/components/LogView/gitmojify.ts: -------------------------------------------------------------------------------- 1 | import * as emoji from 'node-emoji'; 2 | 3 | export function gitmojify(message: string): string { 4 | return emoji.emojify(message); 5 | } 6 | -------------------------------------------------------------------------------- /browser/src/constants/actions.ts: -------------------------------------------------------------------------------- 1 | export * from './resultActions'; 2 | -------------------------------------------------------------------------------- /browser/src/constants/resultActions.ts: -------------------------------------------------------------------------------- 1 | export const COMMITS_RENDERED = 'COMMITS_RENDERED'; 2 | export const SELECT_COMMITTED_FILE = 'SELECT_COMMITTED_FILE'; 3 | export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; 4 | export const FETCHED_COMMITS = 'FETCHED_COMMITS'; 5 | export const FETCH_COMMIT = 'FETCH_COMMIT'; 6 | export const FETCHED_COMMIT = 'FETCHED_COMMIT'; 7 | export const UPDATE_COMMIT_IN_LIST = 'UPDATE_COMMIT_IN_LIST'; 8 | export const GO_TO_NEXT_PAGE = 'GO_TO_NEXT_PAGE'; 9 | export const GO_TO_PREVIOUS_PAGE = 'GO_TO_PREVIOUS_PAGE'; 10 | export const FETCH_LOG_ALL_BRANCHES_ENTRIES = 'FETCH_LOG_ALL_BRANCHES_ENTRIES'; 11 | export const IS_LOADING_COMMITS = 'IS_LOADING_COMMITS'; 12 | export const IS_FETCHING_COMMIT = 'IS_LOADING_COMMIT'; 13 | export const CLEAR_SELECTED_COMMIT = 'CLEAR_SELECTED_COMMIT'; 14 | export const IS_FETCHING_BRANCHES = 'IS_FETCHING_BRANCHES'; 15 | export const FETCHED_BRANCHES = 'FETCHED_BRANCHES'; 16 | export const FETCH_BRANCHES = 'FETCH_BRANCHES'; 17 | export const FETCHED_AVATARS = 'FETCHED_AVATARS'; 18 | export const FETCHED_AUTHORS = 'FETCHED_AUTHORS'; 19 | -------------------------------------------------------------------------------- /browser/src/definitions.ts: -------------------------------------------------------------------------------- 1 | export * from '../src/types'; 2 | -------------------------------------------------------------------------------- /browser/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /** Global definitions for developement **/ 2 | 3 | // for style loader 4 | declare module '*.css' { 5 | const styles: any; 6 | export = styles; 7 | } 8 | 9 | // for redux devtools extension 10 | declare interface Window { 11 | devToolsExtension?(): (args?: any) => any; 12 | } 13 | 14 | declare let module: any; 15 | declare let require: any; 16 | -------------------------------------------------------------------------------- /browser/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 5 | import { ResultActions } from './actions/results'; 6 | import { initialize } from './actions/messagebus'; 7 | import App from './containers/App'; 8 | import { ISettings } from './definitions'; 9 | import configureStore from './store'; 10 | 11 | const defaultSettings: ISettings = window['settings']; 12 | 13 | const store = configureStore({ 14 | settings: defaultSettings, 15 | graph: {}, 16 | vscode: { 17 | theme: document.body.className, 18 | locale: window['locale'], 19 | configuration: window['configuration'], 20 | }, 21 | }); 22 | 23 | ReactDOM.render( 24 |
25 | 26 | 27 | 28 | 29 | 30 |
, 31 | document.getElementById('root'), 32 | ); 33 | 34 | initialize(window['vscode']); 35 | 36 | store.dispatch(ResultActions.getCommits()); 37 | store.dispatch(ResultActions.getBranches()); 38 | store.dispatch(ResultActions.getAuthors()); 39 | store.dispatch(ResultActions.fetchAvatars()); 40 | -------------------------------------------------------------------------------- /browser/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | 3 | export { logger }; 4 | -------------------------------------------------------------------------------- /browser/src/middleware/logger.ts: -------------------------------------------------------------------------------- 1 | export default function loggerMiddleware(store) { 2 | return next => action => { 3 | console.log(action); 4 | return next(action); 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /browser/src/reducers/authors.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as Actions from '../constants/actions'; 3 | import { AuthorsState } from './'; 4 | 5 | const initialState: AuthorsState = []; 6 | 7 | export default handleActions( 8 | { 9 | [Actions.FETCHED_AUTHORS]: (state, action: ReduxActions.Action) => { 10 | return [...action.payload]; 11 | }, 12 | }, 13 | initialState, 14 | ); 15 | -------------------------------------------------------------------------------- /browser/src/reducers/avatars.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as Actions from '../constants/actions'; 3 | import { Avatar } from '../definitions'; 4 | import { AvatarsState } from './index'; 5 | 6 | const initialState: AvatarsState = []; 7 | 8 | export default handleActions( 9 | { 10 | [Actions.FETCHED_AVATARS]: (state, action: ReduxActions.Action) => { 11 | return [...state, ...action.payload]; 12 | }, 13 | }, 14 | initialState, 15 | ); 16 | -------------------------------------------------------------------------------- /browser/src/reducers/branches.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as Actions from '../constants/actions'; 3 | import { BranchesState } from './'; 4 | 5 | const initialState: BranchesState = []; 6 | 7 | export default handleActions( 8 | { 9 | [Actions.FETCHED_BRANCHES]: (state, action: ReduxActions.Action) => { 10 | return [...action.payload]; 11 | }, 12 | 13 | [Actions.IS_FETCHING_BRANCHES]: (state, action) => { 14 | return [...state]; 15 | }, 16 | 17 | [Actions.FETCH_BRANCHES]: (state, action: ReduxActions.Action) => { 18 | return [...state]; 19 | }, 20 | }, 21 | initialState, 22 | ); 23 | -------------------------------------------------------------------------------- /browser/src/reducers/graph.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as Actions from '../constants/actions'; 3 | 4 | export interface IGraphState { 5 | height?: string; 6 | width?: string; 7 | itemHeight?: number; 8 | updateTick?: number; 9 | } 10 | const initialState: IGraphState = {}; 11 | 12 | export default handleActions( 13 | { 14 | [Actions.COMMITS_RENDERED]: (state, action: ReduxActions.Action) => { 15 | return { ...state, updateTick: new Date().getTime(), itemHeight: action.payload } as IGraphState; 16 | }, 17 | }, 18 | initialState, 19 | ); 20 | -------------------------------------------------------------------------------- /browser/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { routerReducer as routing } from 'react-router-redux'; 2 | import { combineReducers } from 'redux'; 3 | import { ActionedUser, Avatar, ISettings, LogEntriesResponse, RefType } from '../definitions'; 4 | import authors from './authors'; 5 | import avatars from './avatars'; 6 | import branches from './branches'; 7 | import settings from './settings'; 8 | import { default as graph, IGraphState } from './graph'; 9 | import logEntries from './logEntries'; 10 | import vscode, { IVSCodeSettings } from './vscode'; 11 | 12 | export type LogEntriesState = LogEntriesResponse & { 13 | isLoading: boolean; 14 | isLoadingCommit?: string; 15 | }; 16 | 17 | export type BranchesState = { name: string; type: RefType; current: boolean; remote: string; remoteType: number }[]; 18 | export type AuthorsState = ActionedUser[]; 19 | export type AvatarsState = Avatar[]; 20 | export type RootState = { 21 | vscode: IVSCodeSettings; 22 | logEntries?: LogEntriesState; 23 | branches?: BranchesState; 24 | avatars?: AvatarsState; 25 | authors?: AuthorsState; 26 | settings?: ISettings; 27 | graph: IGraphState; 28 | }; 29 | 30 | export default combineReducers({ 31 | routing, 32 | avatars, 33 | authors, 34 | logEntries, 35 | branches, 36 | settings, 37 | graph, 38 | vscode, 39 | } as any); 40 | -------------------------------------------------------------------------------- /browser/src/reducers/logEntries.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as Actions from '../constants/actions'; 3 | import { LogEntry } from '../definitions'; 4 | import { LogEntriesResponse } from '../types'; 5 | import { LogEntriesState } from './'; 6 | 7 | const initialState: LogEntriesState = { 8 | count: 0, 9 | isLoading: false, 10 | isLoadingCommit: undefined, 11 | items: [], 12 | pageIndex: 0, 13 | }; 14 | 15 | function fixDates(logEntry: LogEntry) { 16 | if (logEntry.author && typeof logEntry.author.date === 'string') { 17 | logEntry.author.date = new Date(logEntry.author.date); 18 | } 19 | if (logEntry.committer && typeof logEntry.committer.date === 'string') { 20 | logEntry.committer.date = new Date(logEntry.committer.date); 21 | } 22 | } 23 | 24 | export default handleActions( 25 | { 26 | [Actions.FETCHED_COMMITS]: (state, action: ReduxActions.Action) => { 27 | action.payload!.items.forEach(x => { 28 | fixDates(x); 29 | }); 30 | 31 | return { 32 | ...state, 33 | ...action.payload!, 34 | selected: undefined, 35 | isLoading: false, 36 | isLoadingCommit: undefined, 37 | }; 38 | }, 39 | 40 | [Actions.UPDATE_COMMIT_IN_LIST]: (state, action: ReduxActions.Action) => { 41 | const index = state.items.findIndex(item => item.hash.full === action.payload.hash.full); 42 | 43 | if (index >= 0) { 44 | const logEntry = JSON.parse(JSON.stringify(action.payload)); 45 | fixDates(logEntry); 46 | state.items.splice(index, 1, logEntry); 47 | } 48 | return { 49 | ...state, 50 | isLoadingCommit: undefined, 51 | }; 52 | }, 53 | [Actions.FETCHED_COMMIT]: (state, action: ReduxActions.Action) => { 54 | fixDates(action.payload); 55 | 56 | return { 57 | ...state, 58 | isLoadingCommit: undefined, 59 | selected: action.payload, 60 | }; 61 | }, 62 | 63 | [Actions.IS_FETCHING_COMMIT]: (state, action: ReduxActions.Action) => { 64 | return { ...state, isLoadingCommit: action.payload } as LogEntriesState; 65 | }, 66 | [Actions.CLEAR_SELECTED_COMMIT]: (state, action: any) => { 67 | return { ...state, selected: undefined } as LogEntriesState; 68 | }, 69 | 70 | [Actions.IS_LOADING_COMMITS]: (state, action) => { 71 | return { ...state, isLoading: true } as LogEntriesState; 72 | }, 73 | }, 74 | initialState, 75 | ); 76 | -------------------------------------------------------------------------------- /browser/src/reducers/settings.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import * as Actions from '../constants/actions'; 3 | 4 | const initialState = {}; 5 | 6 | export default handleActions( 7 | { 8 | [Actions.UPDATE_SETTINGS]: (state, action) => { 9 | return { ...state, ...action.payload }; 10 | }, 11 | }, 12 | initialState, 13 | ); 14 | -------------------------------------------------------------------------------- /browser/src/reducers/vscode.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | 3 | export type IConfiguration = { 4 | sideBySide: boolean; 5 | logLevel: string; 6 | pageSize: number; 7 | }; 8 | 9 | export type IVSCodeSettings = { 10 | theme?: string; 11 | locale?: string; 12 | configuration?: IConfiguration; 13 | }; 14 | 15 | const initialState = {}; 16 | 17 | export default handleActions( 18 | { 19 | xxx: (state, action: ReduxActions.Action) => { 20 | return { ...state }; 21 | }, 22 | }, 23 | initialState, 24 | ); 25 | -------------------------------------------------------------------------------- /browser/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, Store } from 'redux'; 2 | import { logger } from '../middleware'; 3 | import rootReducer, { RootState } from '../reducers'; 4 | import thunk from 'redux-thunk'; 5 | 6 | export default function configureStore(initialState?: RootState): Store { 7 | const store = createStore(rootReducer, initialState, applyMiddleware(thunk, logger)); 8 | 9 | return store; 10 | } 11 | -------------------------------------------------------------------------------- /browser/src/types.ts: -------------------------------------------------------------------------------- 1 | export * from '../../src/types'; 2 | -------------------------------------------------------------------------------- /browser/stories/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Author as HeaderAuthors } from '../src/components/Header/author'; 3 | import { Author } from '../src/components/LogView/Commit/Author'; 4 | import { storiesOf } from '@storybook/react'; 5 | import { action } from '@storybook/addon-actions'; 6 | import { linkTo } from '@storybook/addon-links'; 7 | import { Welcome } from '@storybook/react/demo'; 8 | 9 | storiesOf('Welcome', module).add('to Storybook', () => ); 10 | 11 | storiesOf('RoundedButton', module) 12 | .add('with text', () =>
Hello Button12341234
, { info: { inline: true } }) 13 | .add( 14 | 'Header Authors (dropdown)', 15 | () => , 16 | { 17 | info: { inline: true }, 18 | }, 19 | ) 20 | .add( 21 | 'Author', 22 | () => ( 23 | 28 | ), 29 | { 30 | info: { inline: true }, 31 | }, 32 | ) 33 | .add( 34 | 'with some emoji', 35 | () => ( 36 |
37 | 38 | 😀 😎 👍 💯 39 | 40 |
41 | ), 42 | { info: { inline: true } }, 43 | ); 44 | -------------------------------------------------------------------------------- /browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es6", 5 | "jsx": "react", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "declaration": false, 11 | "noImplicitAny": false, 12 | "noImplicitReturns": false, 13 | "removeComments": true, 14 | "strictNullChecks": false, 15 | "outDir": "build", 16 | "lib": [ 17 | "es6", 18 | "dom" 19 | ], 20 | "types": [], 21 | "strict": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitThis": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": false, 26 | "suppressImplicitAnyIndexErrors": true, 27 | "baseUrl": ".", 28 | "rootDirs": [ 29 | "src", 30 | "stories" 31 | ], 32 | }, 33 | "include": [ 34 | "src/**/*" 35 | ], 36 | "exclude": [ 37 | "dist", 38 | "build", 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /gitHistory.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | "name": "Root" 6 | }, 7 | { 8 | "path": "browser", 9 | "name": "browser" 10 | }, 11 | { 12 | "path": "src", 13 | "name": "server" 14 | } 15 | ], 16 | "settings": { 17 | "typescript.tsdk": "./node_modules/typescript/lib" 18 | } 19 | } -------------------------------------------------------------------------------- /githistoryvscode_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": [] 3 | } -------------------------------------------------------------------------------- /images/compareCommits.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/9f06b485d3382413b4fd4129b1b7a68fd0f9d96b/images/compareCommits.gif -------------------------------------------------------------------------------- /images/fileHistoryCommandv3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/9f06b485d3382413b4fd4129b1b7a68fd0f9d96b/images/fileHistoryCommandv3.gif -------------------------------------------------------------------------------- /images/gitLogv3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/9f06b485d3382413b4fd4129b1b7a68fd0f9d96b/images/gitLogv3.gif -------------------------------------------------------------------------------- /images/icon-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/icon-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/9f06b485d3382413b4fd4129b1b7a68fd0f9d96b/images/icon.png -------------------------------------------------------------------------------- /images/lineHistoryCommandv3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/9f06b485d3382413b4fd4129b1b7a68fd0f9d96b/images/lineHistoryCommandv3.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path'); 3 | module.exports = { 4 | // We don't want any other mocks from other places impacting this. 5 | roots: [path.join(__dirname, 'test', 'non-extension')], 6 | preset: 'ts-jest', 7 | testEnvironment: 'node', 8 | setupFiles: ['./test/setup.ts'], 9 | testMatch: [path.join(__dirname, 'test/non-extension/adapter/**/*.test.ts')], 10 | collectCoverage: true, 11 | }; 12 | -------------------------------------------------------------------------------- /jest.extension.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | testEnvironment: 'node', 6 | // Be specific, as we don't want mocks from other test suites messing this up. 7 | roots: [path.join(__dirname, 'dist/test/extension')], 8 | moduleFileExtensions: ['js'], 9 | testMatch: [path.join(__dirname, 'dist/test/extension/**/*.test.js')], 10 | setupFiles: [path.join(__dirname, 'dist/test/setup.js')], 11 | collectCoverage: true, 12 | verbose: true, 13 | debug: true, 14 | // Must for debugging and VSC integration. 15 | runInBand: true, 16 | cache: false, 17 | // Custom reporter so we can override where messages are written. 18 | // We want output written into console, not process.stdout streams. 19 | // See ./build/postInstall.js 20 | reporters: ['jest-standard-reporter'], 21 | useStderr: true, 22 | // Ensure it dies properly on CI (test step didn't exit). 23 | forceExit: true, 24 | // Ensure it dies properly on CI (test step didn't exit). 25 | detectOpenHandles: true, 26 | }; 27 | -------------------------------------------------------------------------------- /resources/fileicons/images/Document_16x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/fileicons/images/Document_16x_inverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/fileicons/images/FolderOpen_16x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/fileicons/images/FolderOpen_16x_inverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/fileicons/images/Folder_16x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/fileicons/images/Folder_16x_inverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/fileicons/vs_minimal_icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "iconDefinitions": { 3 | "_folder_dark": { 4 | "iconPath": "./images/Folder_16x_inverse.svg" 5 | }, 6 | "_folder_open_dark": { 7 | "iconPath": "./images/FolderOpen_16x_inverse.svg" 8 | }, 9 | "_file_dark": { 10 | "iconPath": "./images/Document_16x_inverse.svg" 11 | }, 12 | "_folder_light": { 13 | "iconPath": "./images/Folder_16x.svg" 14 | }, 15 | "_folder_open_light": { 16 | "iconPath": "./images/FolderOpen_16x.svg" 17 | }, 18 | "_file_light": { 19 | "iconPath": "./images/Document_16x.svg" 20 | } 21 | }, 22 | 23 | "folderExpanded": "_folder_open_dark", 24 | "folder": "_folder_dark", 25 | "file": "_file_dark", 26 | "fileExtensions": { 27 | // icons by file extension 28 | }, 29 | "fileNames": { 30 | // icons by file name 31 | }, 32 | "languageIds": { 33 | // icons by language id 34 | }, 35 | "light": { 36 | "folderExpanded": "_folder_open_light", 37 | "folder": "_folder_light", 38 | "file": "_file_light", 39 | "fileExtensions": { 40 | // icons by file extension 41 | }, 42 | "fileNames": { 43 | // icons by file name 44 | }, 45 | "languageIds": { 46 | // icons by language id 47 | } 48 | }, 49 | "highContrast": { 50 | // overrides for high contrast 51 | } 52 | } -------------------------------------------------------------------------------- /resources/icons/dark/status-added.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/dark/status-conflict.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/dark/status-copied.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/dark/status-deleted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | D 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/dark/status-ignored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | I 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/dark/status-modified.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | M 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/dark/status-renamed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | R 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-added.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-conflict.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-copied.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-deleted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | D 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-ignored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | I 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-modified.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | M 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/light/status-renamed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | R 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/misc/dialog-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | i 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/icons/misc/dialog-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ! 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/adapter/avatar/base.ts: -------------------------------------------------------------------------------- 1 | import { injectable, unmanaged } from 'inversify'; 2 | import { IStateStore, IStateStoreFactory } from '../../application/types/stateStore'; 3 | import { IWorkspaceService } from '../../application/types/workspace'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { Avatar, AvatarResponse, IGitService } from '../../types'; 6 | import { GitOriginType } from '../repository/types'; 7 | import { IAvatarProvider } from './types'; 8 | 9 | @injectable() 10 | export abstract class BaseAvatarProvider implements IAvatarProvider { 11 | protected readonly httpProxy: string; 12 | private readonly avatarStateStore: IStateStore; 13 | public constructor( 14 | protected serviceContainer: IServiceContainer, 15 | @unmanaged() private remoteRepoType: GitOriginType, 16 | ) { 17 | const workspace = this.serviceContainer.get(IWorkspaceService); 18 | this.httpProxy = workspace.getConfiguration('http').get('proxy', ''); 19 | const stateStoreFactory = this.serviceContainer.get(IStateStoreFactory); 20 | this.avatarStateStore = stateStoreFactory.createStore(); 21 | } 22 | 23 | public async getAvatars(repository: IGitService): Promise { 24 | const workspace = this.serviceContainer.get(IWorkspaceService); 25 | const cacheExpiration = workspace.getConfiguration('gitHistory').get('avatarCacheExpiration', 60); // in minutes (zero to disable cache) 26 | 27 | const remoteUrl = await repository.getOriginUrl(); 28 | const key = `Git:Avatars:${remoteUrl}`; 29 | 30 | const cachedAvatars = this.avatarStateStore.get(key); 31 | 32 | const retry = 33 | cacheExpiration === 0 || 34 | !cachedAvatars || 35 | (cachedAvatars && 36 | cachedAvatars.timestamp && 37 | cachedAvatars.timestamp + cacheExpiration * 60 * 1000 < new Date().getTime()); 38 | 39 | if (retry) { 40 | const avatars = await this.getAvatarsImplementation(repository); 41 | await this.avatarStateStore.set(key, { timestamp: new Date().getTime(), items: avatars }); 42 | return avatars; 43 | } else if (cachedAvatars) { 44 | return cachedAvatars.items; 45 | } 46 | 47 | return []; 48 | } 49 | public supported(remoteRepo: GitOriginType): boolean { 50 | return remoteRepo === this.remoteRepoType; 51 | } 52 | protected abstract getAvatarsImplementation(repository: IGitService): Promise; 53 | } 54 | -------------------------------------------------------------------------------- /src/adapter/avatar/gravatar.ts: -------------------------------------------------------------------------------- 1 | import * as gravatar from 'gravatar'; 2 | import { inject, injectable } from 'inversify'; 3 | import { IServiceContainer } from '../../ioc/types'; 4 | import { Avatar, IGitService } from '../../types'; 5 | import { GitOriginType } from '../repository/types'; 6 | import { BaseAvatarProvider } from './base'; 7 | import { IAvatarProvider } from './types'; 8 | 9 | @injectable() 10 | export class GravatarAvatarProvider extends BaseAvatarProvider implements IAvatarProvider { 11 | public constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { 12 | super(serviceContainer, GitOriginType.any); 13 | } 14 | protected async getAvatarsImplementation(repository: IGitService): Promise { 15 | const authors = await repository.getAuthors(); 16 | 17 | return authors.map(user => { 18 | return { 19 | login: user.name, 20 | url: '', 21 | avatarUrl: gravatar.url(user.email, { protocol: 'https' }), 22 | name: user.name, 23 | email: user.email, 24 | }; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/adapter/avatar/types.ts: -------------------------------------------------------------------------------- 1 | import { Avatar, IGitService } from '../../types'; 2 | import { GitOriginType } from '../repository/types'; 3 | 4 | export const IAvatarProvider = Symbol.for('IAvatarProvider'); 5 | 6 | export interface IAvatarProvider { 7 | supported(remoteRepo: GitOriginType): boolean; 8 | getAvatars(repository: IGitService): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/adapter/exec/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './gitCommandExec'; 3 | -------------------------------------------------------------------------------- /src/adapter/exec/types.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from 'stream'; 2 | import { API } from '../repository/git.d'; 3 | 4 | export const IGitCommandExecutor = Symbol.for('IGitCommandExecutor'); 5 | 6 | export interface IGitCommandExecutor { 7 | readonly gitApi: Promise; 8 | exec(cwd: string, ...args: string[]): Promise; 9 | exec(options: { cwd: string; shell?: boolean }, ...args: string[]): Promise; 10 | exec( 11 | options: { cwd: string; shell?: boolean; encoding: 'binary' }, 12 | destination: Writable, 13 | ...args: string[] 14 | ): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/adapter/helpers.ts: -------------------------------------------------------------------------------- 1 | import { EnumEx } from '../common/enumHelper'; 2 | import { CommitInfo } from '../types'; 3 | export class Helpers { 4 | public static GetLogArguments() { 5 | const args: string[] = []; 6 | for (const item of EnumEx.getValues(CommitInfo)) { 7 | if (item !== CommitInfo.NewLine) { 8 | args.push(Helpers.GetCommitInfoFormatCode(item)); 9 | } 10 | } 11 | 12 | return args; 13 | } 14 | public static GetCommitInfoFormatCode(info: CommitInfo): string { 15 | switch (info) { 16 | case CommitInfo.FullHash: { 17 | return '%H'; 18 | } 19 | case CommitInfo.ShortHash: { 20 | return '%h'; 21 | } 22 | case CommitInfo.TreeFullHash: { 23 | return '%T'; 24 | } 25 | case CommitInfo.TreeShortHash: { 26 | return '%t'; 27 | } 28 | case CommitInfo.ParentFullHash: { 29 | return '%P'; 30 | } 31 | case CommitInfo.ParentShortHash: { 32 | return '%p'; 33 | } 34 | case CommitInfo.AuthorName: { 35 | return '%an'; 36 | } 37 | case CommitInfo.AuthorEmail: { 38 | return '%ae'; 39 | } 40 | case CommitInfo.AuthorDateUnixTime: { 41 | return '%at'; 42 | } 43 | case CommitInfo.CommitterName: { 44 | return '%cn'; 45 | } 46 | case CommitInfo.CommitterEmail: { 47 | return '%ce'; 48 | } 49 | case CommitInfo.CommitterDateUnixTime: { 50 | return '%ct'; 51 | } 52 | case CommitInfo.RefsNames: { 53 | return '%D'; 54 | } 55 | case CommitInfo.Subject: { 56 | return '%s'; 57 | } 58 | case CommitInfo.Body: { 59 | return '%b'; 60 | } 61 | case CommitInfo.Notes: { 62 | return '%N'; 63 | } 64 | case CommitInfo.NewLine: { 65 | return '%n'; 66 | } 67 | default: { 68 | throw new Error(`Unrecognized Commit Info type ${info}`); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './avatar/types'; 3 | -------------------------------------------------------------------------------- /src/adapter/parsers/actionDetails/parser.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { ActionedDetails } from '../../../types'; 3 | import { IActionDetailsParser } from '../types'; 4 | 5 | @injectable() 6 | export class ActionDetailsParser implements IActionDetailsParser { 7 | public parse(name: string, email: string, unixTime: string): ActionedDetails | undefined { 8 | name = (name || '').trim(); 9 | unixTime = (unixTime || '').trim(); 10 | if (unixTime.length === 0) { 11 | return; 12 | } 13 | 14 | const time = parseInt(unixTime, 10); 15 | const date = new Date(time * 1000); 16 | return { 17 | date, 18 | email, 19 | name, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/adapter/parsers/fileStatStatus/parser.ts: -------------------------------------------------------------------------------- 1 | import { injectable, multiInject } from 'inversify'; 2 | import { ILogService } from '../../../common/types'; 3 | import { Status } from '../../../types'; 4 | import { IFileStatStatusParser } from '../types'; 5 | 6 | @injectable() 7 | export class FileStatStatusParser implements IFileStatStatusParser { 8 | constructor(@multiInject(ILogService) private loggers: ILogService[]) {} 9 | public canParse(status: string): boolean { 10 | const parsedStatus = this.parse(status); 11 | return parsedStatus !== undefined && parsedStatus !== null; 12 | } 13 | public parse(status: string): Status | undefined { 14 | status = status || ''; 15 | status = status.length === 0 ? '' : status.trim().substring(0, 1); 16 | switch (status) { 17 | case 'A': 18 | return Status.Added; 19 | case 'M': 20 | return Status.Modified; 21 | case 'D': 22 | return Status.Deleted; 23 | case 'C': 24 | return Status.Copied; 25 | case 'R': 26 | return Status.Renamed; 27 | case 'T': 28 | return Status.TypeChanged; 29 | case 'X': 30 | return Status.Unknown; 31 | case 'U': 32 | return Status.Unmerged; 33 | case 'B': 34 | return Status.Broken; 35 | default: { 36 | this.loggers.forEach(logger => logger.error(`Unrecognized file stat status '${status}`)); 37 | return; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/adapter/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /src/adapter/parsers/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../../ioc/types'; 2 | import { ActionDetailsParser } from './actionDetails/parser'; 3 | import { FileStatParser } from './fileStat/parser'; 4 | import { FileStatStatusParser } from './fileStatStatus/parser'; 5 | import { LogParser } from './log/parser'; 6 | import { IActionDetailsParser, IFileStatParser, IFileStatStatusParser, ILogParser } from './types'; 7 | 8 | export function registerTypes(serviceManager: IServiceManager) { 9 | serviceManager.add(IActionDetailsParser, ActionDetailsParser); 10 | serviceManager.add(IFileStatStatusParser, FileStatStatusParser); 11 | serviceManager.add(IFileStatParser, FileStatParser); 12 | serviceManager.add(ILogParser, LogParser); 13 | } 14 | -------------------------------------------------------------------------------- /src/adapter/parsers/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionedDetails, CommittedFile, LogEntry, Status } from '../../types'; 2 | 3 | export const IFileStatParser = 'IFileStatParser'; // Symbol.for('IFileStatParser'); 4 | 5 | export interface IFileStatParser { 6 | parse(gitRootPath: string, filesWithNumStat: string[], filesWithStats: string[]): CommittedFile[]; 7 | } 8 | export const IFileStatStatusParser = Symbol.for('IFileStatStatusParser'); 9 | export interface IFileStatStatusParser { 10 | canParse(status: string): boolean; 11 | parse(status: string): Status | undefined; 12 | } 13 | 14 | export const IActionDetailsParser = Symbol.for('IActionDetailsParser'); 15 | export interface IActionDetailsParser { 16 | parse(name: string, email: string, unixTime: string): ActionedDetails | undefined; 17 | } 18 | export const ILogParser = Symbol.for('ILogParser'); 19 | export interface ILogParser { 20 | parse( 21 | gitRepoPath: string, 22 | summaryEntry: string, 23 | itemEntrySeparator: string, 24 | logFormatArgs: string[], 25 | filesWithNumStat?: string, 26 | filesWithNameStatus?: string, 27 | ): LogEntry; 28 | } 29 | -------------------------------------------------------------------------------- /src/adapter/repository/constants.ts: -------------------------------------------------------------------------------- 1 | import { CommitInfo } from '../../types'; 2 | import { Helpers } from '../helpers'; 3 | 4 | export const LOG_ENTRY_SEPARATOR = '95E9659B-27DC-43C4-A717-D75969757EA5'; 5 | export const ITEM_ENTRY_SEPARATOR = '95E9659B-27DC-43C4-A717-D75969757EA6'; 6 | export const STATS_SEPARATOR = '95E9659B-27DC-43C4-A717-D75969757EA7'; 7 | // const LOG_FORMAT_ARGS = ['%D', '%H', '%h', '%T', '%t', '%P', '%p', '%an', '%ae', '%at', '%c', '%ce', '%ct', '%s', '%b', '%N']; 8 | export const LOG_FORMAT_ARGS = Helpers.GetLogArguments(); 9 | export const newLineFormatCode = Helpers.GetCommitInfoFormatCode(CommitInfo.NewLine); 10 | export const LOG_FORMAT = `--format=${LOG_ENTRY_SEPARATOR}${[ 11 | ...LOG_FORMAT_ARGS, 12 | STATS_SEPARATOR, 13 | ITEM_ENTRY_SEPARATOR, 14 | ].join(ITEM_ENTRY_SEPARATOR)}`; 15 | -------------------------------------------------------------------------------- /src/adapter/repository/gitRemoteService.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from './git.d'; 2 | import { IGitCommandExecutor } from '..'; 3 | import { GitOriginType } from '.'; 4 | import { captureTelemetry } from '../../common/telemetry'; 5 | 6 | export class GitRemoteService { 7 | constructor(private readonly repo: Repository, private readonly gitCmdExecutor: IGitCommandExecutor) {} 8 | private get currentBranch(): string { 9 | return this.repo.state.HEAD!.name || ''; 10 | } 11 | 12 | public async getBranchesConfiguredForPullForRemote(remoteName: string): Promise { 13 | const gitShowRemoteOutput = await this.gitCmdExecutor.exec( 14 | this.repo.rootUri.fsPath, 15 | ...['remote', 'show', remoteName, '-n'], 16 | ); 17 | 18 | const lines = gitShowRemoteOutput 19 | .split(/\r?\n/g) 20 | .map(line => line.trim()) 21 | .filter(line => line.length > 0); 22 | 23 | const startLineIndex = lines.findIndex(line => line.startsWith('Local branches configured for')); 24 | const endLineIndex = lines.findIndex(line => line.startsWith('Local ref configured for')); 25 | 26 | if (startLineIndex === -1 || endLineIndex == -1) { 27 | // TODO: Capture telemetry, something is wrong. 28 | return []; 29 | } 30 | if (startLineIndex > endLineIndex) { 31 | // TODO: Capture telemetry, something is wrong. 32 | return []; 33 | } 34 | 35 | // Branch name is first word in the line 36 | return lines.slice(startLineIndex + 1, endLineIndex).map(line => line.split(' ')[0]); 37 | } 38 | public async getOriginType(url?: string): Promise { 39 | if (!url) { 40 | url = await this.getOriginUrl(); 41 | } 42 | 43 | if (url.indexOf('github.com') > 0) { 44 | return GitOriginType.github; 45 | } else if (url.indexOf('bitbucket') > 0) { 46 | return GitOriginType.bitbucket; 47 | } else if (url.indexOf('visualstudio') > 0) { 48 | return GitOriginType.vsts; 49 | } 50 | return undefined; 51 | } 52 | 53 | @captureTelemetry() 54 | public async getOriginUrl(branchName?: string): Promise { 55 | branchName = branchName || this.currentBranch; 56 | 57 | const branch = await this.repo.getBranch(branchName); 58 | 59 | if (branch.upstream) { 60 | const remoteIndex = this.repo.state.remotes.findIndex(x => x.name === branch.upstream!.remote); 61 | return this.repo.state.remotes[remoteIndex].fetchUrl || ''; 62 | } 63 | 64 | return ''; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/adapter/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/adapter/repository/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../../ioc/types'; 2 | import { IGitService, IGitServiceFactory } from '../../types'; 3 | import { GitServiceFactory } from './factory'; 4 | import { Git } from './git'; 5 | import { GitArgsService } from './gitArgsService'; 6 | import { IGitArgsService } from './types'; 7 | 8 | export function registerTypes(serviceManager: IServiceManager) { 9 | serviceManager.add(IGitService, Git); 10 | serviceManager.add(IGitArgsService, GitArgsService); 11 | serviceManager.add(IGitServiceFactory, GitServiceFactory); 12 | } 13 | -------------------------------------------------------------------------------- /src/adapter/repository/types.ts: -------------------------------------------------------------------------------- 1 | // import { FsUri } from '../../types'; 2 | 3 | export type GitLogArgs = { 4 | logArgs: string[]; 5 | fileStatArgs: string[]; 6 | counterArgs: string[]; 7 | }; 8 | export const IGitArgsService = Symbol.for('IGitArgsService'); 9 | 10 | export interface IGitArgsService { 11 | getAuthorsArgs(): string[]; 12 | getCommitArgs(hash: string): string[]; 13 | getCommitParentHashesArgs(hash: string): string[]; 14 | getCommitWithNumStatArgs(hash: string): string[]; 15 | getCommitNameStatusArgs(hash: string): string[]; 16 | getCommitWithNumStatArgsForMerge(hash: string): string[]; 17 | getCommitNameStatusArgsForMerge(hash: string): string[]; 18 | getObjectHashArgs(object: string): string[]; 19 | getLogArgs( 20 | pageIndex?: number, 21 | pageSize?: number, 22 | branches?: string[], 23 | searchText?: string, 24 | relativeFilePath?: string, 25 | lineNumber?: number, 26 | author?: string, 27 | ): GitLogArgs; 28 | getDiffCommitWithNumStatArgs(hash1: string, hash2: string): string[]; 29 | getDiffCommitNameStatusArgs(hash1: string, hash2: string): string[]; 30 | getPreviousCommitHashForFileArgs(hash: string, file: string): string[]; 31 | } 32 | 33 | export enum GitOriginType { 34 | any = 1, 35 | github = 2, 36 | bitbucket = 3, 37 | tfs = 4, 38 | vsts = 5, 39 | } 40 | -------------------------------------------------------------------------------- /src/adapter/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../ioc/types'; 2 | import { GithubAvatarProvider } from './avatar/github'; 3 | import { GravatarAvatarProvider } from './avatar/gravatar'; 4 | import { IAvatarProvider } from './avatar/types'; 5 | import { GitCommandExecutor, IGitCommandExecutor } from './exec/index'; 6 | 7 | export function registerTypes(serviceManager: IServiceManager) { 8 | serviceManager.add(IGitCommandExecutor, GitCommandExecutor); 9 | serviceManager.add(IAvatarProvider, GithubAvatarProvider); 10 | serviceManager.add(IAvatarProvider, GravatarAvatarProvider); 11 | } 12 | -------------------------------------------------------------------------------- /src/adapter/types.ts: -------------------------------------------------------------------------------- 1 | export * from './exec/types'; 2 | -------------------------------------------------------------------------------- /src/application/applicationShell.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 'use strict'; 4 | 5 | import { injectable } from 'inversify'; 6 | import * as vscode from 'vscode'; 7 | import { IApplicationShell } from './types'; 8 | 9 | @injectable() 10 | export class ApplicationShell implements IApplicationShell { 11 | public showInformationMessage(message: string, ...items: string[]): Thenable; 12 | public showInformationMessage( 13 | message: string, 14 | options: vscode.MessageOptions, 15 | ...items: string[] 16 | ): Thenable; 17 | public showInformationMessage(message: string, ...items: T[]): Thenable; 18 | public showInformationMessage( 19 | message: string, 20 | options: vscode.MessageOptions, 21 | ...items: T[] 22 | ): Thenable; 23 | public showInformationMessage(message: string, options?: any, ...items: any[]): Thenable { 24 | return vscode.window.showInformationMessage(message, options, ...items); 25 | } 26 | 27 | public showWarningMessage(message: string, ...items: string[]): Thenable; 28 | public showWarningMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; 29 | public showWarningMessage(message: string, ...items: T[]): Thenable; 30 | public showWarningMessage( 31 | message: string, 32 | options: vscode.MessageOptions, 33 | ...items: T[] 34 | ): Thenable; 35 | public showWarningMessage(message: any, options?: any, ...items: any[]) { 36 | return vscode.window.showWarningMessage(message, options, ...items); 37 | } 38 | 39 | public showErrorMessage(message: string, ...items: string[]): Thenable; 40 | public showErrorMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; 41 | public showErrorMessage(message: string, ...items: T[]): Thenable; 42 | public showErrorMessage( 43 | message: string, 44 | options: vscode.MessageOptions, 45 | ...items: T[] 46 | ): Thenable; 47 | public showErrorMessage(message: any, options?: any, ...items: any[]) { 48 | return vscode.window.showErrorMessage(message, options, ...items); 49 | } 50 | 51 | public showQuickPick( 52 | items: string[] | Thenable, 53 | options?: vscode.QuickPickOptions, 54 | token?: vscode.CancellationToken, 55 | ): Thenable; 56 | public showQuickPick( 57 | items: T[] | Thenable, 58 | options?: vscode.QuickPickOptions, 59 | token?: vscode.CancellationToken, 60 | ): Thenable; 61 | public showQuickPick(items: any, options?: any, token?: any): Thenable { 62 | return vscode.window.showQuickPick(items, options, token); 63 | } 64 | 65 | public showOpenDialog(options: vscode.OpenDialogOptions) { 66 | return vscode.window.showOpenDialog(options); 67 | } 68 | public showSaveDialog(options: vscode.SaveDialogOptions) { 69 | return vscode.window.showSaveDialog(options); 70 | } 71 | public showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { 72 | return vscode.window.showInputBox(options, token); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/application/commandManager.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { commands, Disposable, TextEditor, TextEditorEdit } from 'vscode'; 3 | import { ICommandManager } from './types/commandManager'; 4 | 5 | @injectable() 6 | export class CommandManager implements ICommandManager { 7 | public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { 8 | return commands.registerCommand(command, callback, thisArg); 9 | } 10 | public registerTextEditorCommand( 11 | command: string, 12 | callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, 13 | thisArg?: any, 14 | ): Disposable { 15 | return commands.registerTextEditorCommand(command, callback, thisArg); 16 | } 17 | public executeCommand(command: string, ...rest: any[]): Thenable { 18 | return commands.executeCommand(command, ...rest); 19 | } 20 | public getCommands(filterInternal?: boolean | undefined): Thenable { 21 | return commands.getCommands(filterInternal); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/application/disposableRegistry.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { Disposable } from 'vscode'; 3 | import { IDisposableRegistry } from './types/disposableRegistry'; 4 | 5 | @injectable() 6 | export class DisposableRegistry implements IDisposableRegistry { 7 | private disposables: Disposable[] = []; 8 | public register(disposable: Disposable): void { 9 | this.disposables.push(disposable); 10 | } 11 | public dispose() { 12 | this.disposables.forEach(disposable => disposable.dispose()); 13 | this.disposables = []; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/application/documentManager.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { TextDocument, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace } from 'vscode'; 3 | import { IDocumentManager } from './types/documentManager'; 4 | 5 | @injectable() 6 | export class DocumentManager implements IDocumentManager { 7 | public openTextDocument(uri: Uri): Thenable; 8 | public openTextDocument(fileName: string): Thenable; 9 | public openTextDocument( 10 | options?: { language?: string | undefined; content?: string | undefined } | undefined, 11 | ): Thenable; 12 | public openTextDocument(options?: any); 13 | public openTextDocument(...args: any[]) { 14 | return workspace.openTextDocument.call(window, ...args); 15 | } 16 | public showTextDocument( 17 | document: TextDocument, 18 | column?: ViewColumn | undefined, 19 | preserveFocus?: boolean | undefined, 20 | ): Thenable; 21 | public showTextDocument( 22 | document: TextDocument, 23 | options?: TextDocumentShowOptions | undefined, 24 | ): Thenable; 25 | public showTextDocument(uri: Uri, options?: TextDocumentShowOptions | undefined): Thenable; 26 | public showTextDocument(document: any, column?: any, preserveFocus?: any); 27 | public showTextDocument(...args: any[]) { 28 | // @ts-ignore 29 | return window.showTextDocument.call(window, ...args); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/application/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../ioc/types'; 2 | import { ApplicationShell } from './applicationShell'; 3 | import { CommandManager } from './commandManager'; 4 | import { DisposableRegistry } from './disposableRegistry'; 5 | import { DocumentManager } from './documentManager'; 6 | import { WorkspaceStateStoreFactory } from './stateStore'; 7 | import { IApplicationShell } from './types'; 8 | import { ICommandManager } from './types/commandManager'; 9 | import { IDisposableRegistry } from './types/disposableRegistry'; 10 | import { IDocumentManager } from './types/documentManager'; 11 | import { IStateStoreFactory } from './types/stateStore'; 12 | import { IWorkspaceService } from './types/workspace'; 13 | import { WorkspaceService } from './workspace'; 14 | 15 | export function registerTypes(serviceManager: IServiceManager) { 16 | serviceManager.addSingleton(IApplicationShell, ApplicationShell); 17 | serviceManager.addSingleton(ICommandManager, CommandManager); 18 | serviceManager.addSingleton(IDisposableRegistry, DisposableRegistry); 19 | serviceManager.addSingleton(IDocumentManager, DocumentManager); 20 | serviceManager.addSingleton(IWorkspaceService, WorkspaceService); 21 | serviceManager.add(IStateStoreFactory, WorkspaceStateStoreFactory); 22 | } 23 | -------------------------------------------------------------------------------- /src/application/stateStore.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { Memento } from 'vscode'; 3 | import { IServiceContainer } from '../ioc/types'; 4 | import { IStateStore, IStateStoreFactory } from './types/stateStore'; 5 | 6 | export class WorkspaceMementoStore implements IStateStore { 7 | constructor(private store: Memento) {} 8 | public has(key: string): boolean { 9 | return this.store.get(key) !== undefined; 10 | } 11 | public async set(key: string, data: T): Promise { 12 | await this.store.update(key, data); 13 | } 14 | public get(key: string): T | undefined { 15 | return this.store.get(key); 16 | } 17 | } 18 | 19 | @injectable() 20 | export class WorkspaceStateStoreFactory implements IStateStoreFactory { 21 | constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} 22 | 23 | public createStore(): IStateStore { 24 | return new WorkspaceMementoStore(this.serviceContainer.get('workspaceMementoStore')); 25 | } 26 | } 27 | 28 | @injectable() 29 | export class GlobalStateStoreFactory implements IStateStoreFactory { 30 | constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} 31 | 32 | public createStore(): IStateStore { 33 | return new WorkspaceMementoStore(this.serviceContainer.get('globalMementoStore')); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/application/types/commandManager.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, TextEditor, TextEditorEdit } from 'vscode'; 2 | 3 | export const ICommandManager = Symbol.for('ICommandManager'); 4 | 5 | export interface ICommandManager { 6 | /** 7 | * Registers a command that can be invoked via a keyboard shortcut, 8 | * a menu item, an action, or directly. 9 | * 10 | * Registering a command with an existing command identifier twice 11 | * will cause an error. 12 | * 13 | * @param command A unique identifier for the command. 14 | * @param callback A command handler function. 15 | * @param thisArg The `this` context used when invoking the handler function. 16 | * @return Disposable which unregisters this command on disposal. 17 | */ 18 | registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable; 19 | 20 | /** 21 | * Registers a text editor command that can be invoked via a keyboard shortcut, 22 | * a menu item, an action, or directly. 23 | * 24 | * Text editor commands are different from ordinary [commands](#commands.registerCommand) as 25 | * they only execute when there is an active editor when the command is called. Also, the 26 | * command handler of an editor command has access to the active editor and to an 27 | * [edit](#TextEditorEdit)-builder. 28 | * 29 | * @param command A unique identifier for the command. 30 | * @param callback A command handler function with access to an [editor](#TextEditor) and an [edit](#TextEditorEdit). 31 | * @param thisArg The `this` context used when invoking the handler function. 32 | * @return Disposable which unregisters this command on disposal. 33 | */ 34 | registerTextEditorCommand( 35 | command: string, 36 | callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, 37 | thisArg?: any, 38 | ): Disposable; 39 | 40 | /** 41 | * Executes the command denoted by the given command identifier. 42 | * 43 | * * *Note 1:* When executing an editor command not all types are allowed to 44 | * be passed as arguments. Allowed are the primitive types `string`, `boolean`, 45 | * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`Uri`](#Uri) and [`Location`](#Location). 46 | * * *Note 2:* There are no restrictions when executing commands that have been contributed 47 | * by extensions. 48 | * 49 | * @param command Identifier of the command to execute. 50 | * @param rest Parameters passed to the command function. 51 | * @return A thenable that resolves to the returned value of the given command. `undefined` when 52 | * the command handler function doesn't return anything. 53 | */ 54 | executeCommand(command: string, ...rest: any[]): Thenable; 55 | 56 | /** 57 | * Retrieve the list of all available commands. Commands starting an underscore are 58 | * treated as internal commands. 59 | * 60 | * @param filterInternal Set `true` to not see internal commands (starting with an underscore) 61 | * @return Thenable that resolves to a list of command ids. 62 | */ 63 | getCommands(filterInternal?: boolean): Thenable; 64 | } 65 | -------------------------------------------------------------------------------- /src/application/types/disposableRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | 3 | export const IDisposableRegistry = Symbol.for('IDisposableRegistry'); 4 | 5 | export interface IDisposableRegistry extends Disposable { 6 | register(disposable: Disposable): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/application/types/stateStore.ts: -------------------------------------------------------------------------------- 1 | export interface IStateStore { 2 | has(key: string): boolean; 3 | set(key: string, data: T): Promise; 4 | get(key: string): T | undefined; 5 | } 6 | 7 | export const IStateStoreFactory = Symbol.for('IStateStoreFactory'); 8 | export const GlobalStateStore = 'global'; 9 | export const WorkspaceStateStore = 'workspace'; 10 | 11 | export interface IStateStoreFactory { 12 | createStore(): IStateStore; 13 | } 14 | -------------------------------------------------------------------------------- /src/application/workspace.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { injectable } from 'inversify'; 5 | import { 6 | CancellationToken, 7 | ConfigurationChangeEvent, 8 | Event, 9 | FileSystemWatcher, 10 | GlobPattern, 11 | Uri, 12 | workspace, 13 | WorkspaceConfiguration, 14 | WorkspaceFolder, 15 | WorkspaceFoldersChangeEvent, 16 | } from 'vscode'; 17 | import { IWorkspaceService } from './types/workspace'; 18 | 19 | @injectable() 20 | export class WorkspaceService implements IWorkspaceService { 21 | public get onDidChangeConfiguration(): Event { 22 | return workspace.onDidChangeConfiguration; 23 | } 24 | public get workspaceFolders(): ReadonlyArray | undefined { 25 | return workspace.workspaceFolders; 26 | } 27 | public get onDidChangeWorkspaceFolders(): Event { 28 | return workspace.onDidChangeWorkspaceFolders; 29 | } 30 | public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { 31 | return workspace.getConfiguration(section, resource); 32 | } 33 | public asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string { 34 | return workspace.asRelativePath(pathOrUri, includeWorkspaceFolder); 35 | } 36 | public createFileSystemWatcher( 37 | globPattern: GlobPattern, 38 | ignoreCreateEvents?: boolean, 39 | ignoreChangeEvents?: boolean, 40 | ignoreDeleteEvents?: boolean, 41 | ): FileSystemWatcher { 42 | return workspace.createFileSystemWatcher( 43 | globPattern, 44 | ignoreCreateEvents, 45 | ignoreChangeEvents, 46 | ignoreDeleteEvents, 47 | ); 48 | } 49 | public findFiles( 50 | include: GlobPattern, 51 | exclude?: GlobPattern, 52 | maxResults?: number, 53 | token?: CancellationToken, 54 | ): Thenable { 55 | return workspace.findFiles(include, exclude, maxResults, token); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commandFactories/commitFactory.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { 3 | IGitCheckoutCommandHandler, 4 | IGitCherryPickCommandHandler, 5 | IGitCommitViewDetailsCommandHandler, 6 | IGitCompareCommandHandler, 7 | IGitMergeCommandHandler, 8 | IGitRebaseCommandHandler, 9 | IGitRevertCommandHandler, 10 | } from '../commandHandlers/types'; 11 | import { CheckoutCommand } from '../commands/commit/checkout'; 12 | import { CherryPickCommand } from '../commands/commit/cherryPick'; 13 | import { CompareCommand } from '../commands/commit/compare'; 14 | import { MergeCommand } from '../commands/commit/merge'; 15 | import { RebaseCommand } from '../commands/commit/rebase'; 16 | import { RevertCommand } from '../commands/commit/revert'; 17 | import { SelectForComparison } from '../commands/commit/selectForComparion'; 18 | import { ViewDetailsCommand } from '../commands/commit/viewDetails'; 19 | import { CommitDetails, ICommand } from '../common/types'; 20 | import { ICommitCommandFactory } from './types'; 21 | 22 | @injectable() 23 | export class CommitCommandFactory implements ICommitCommandFactory { 24 | constructor( 25 | @inject(IGitCherryPickCommandHandler) private cherryPickHandler: IGitCherryPickCommandHandler, 26 | @inject(IGitCheckoutCommandHandler) private checkoutHandler: IGitCheckoutCommandHandler, 27 | @inject(IGitCompareCommandHandler) private compareHandler: IGitCompareCommandHandler, 28 | @inject(IGitMergeCommandHandler) private mergeHandler: IGitMergeCommandHandler, 29 | @inject(IGitRebaseCommandHandler) private rebaseHandler: IGitRebaseCommandHandler, 30 | @inject(IGitRevertCommandHandler) private revertHandler: IGitRevertCommandHandler, 31 | @inject(IGitCommitViewDetailsCommandHandler) private viewChangeLogHandler: IGitCommitViewDetailsCommandHandler, 32 | ) {} 33 | public async createCommands(commit: CommitDetails): Promise[]> { 34 | const commands: ICommand[] = [ 35 | new CherryPickCommand(commit, this.cherryPickHandler), 36 | new CheckoutCommand(commit, this.checkoutHandler), 37 | new ViewDetailsCommand(commit, this.viewChangeLogHandler), 38 | new SelectForComparison(commit, this.compareHandler), 39 | new RevertCommand(commit, this.revertHandler), 40 | new CompareCommand(commit, this.compareHandler), 41 | new MergeCommand(commit, this.mergeHandler), 42 | new RebaseCommand(commit, this.rebaseHandler), 43 | ]; 44 | 45 | return ( 46 | await Promise.all( 47 | commands.map(async cmd => { 48 | const result = cmd.preExecute(); 49 | const available = typeof result === 'boolean' ? result : await result; 50 | 51 | return available ? cmd : undefined; 52 | }), 53 | ) 54 | ) 55 | .filter(cmd => !!cmd) 56 | .map(cmd => cmd!); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/commandFactories/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../ioc/types'; 2 | import { CommitCommandFactory } from './commitFactory'; 3 | import { FileCommitCommandFactory } from './fileCommitFactory'; 4 | import { ICommitCommandFactory, IFileCommitCommandFactory } from './types'; 5 | 6 | export function registerTypes(serviceManager: IServiceManager) { 7 | serviceManager.add(IFileCommitCommandFactory, FileCommitCommandFactory); 8 | serviceManager.add(ICommitCommandFactory, CommitCommandFactory); 9 | } 10 | -------------------------------------------------------------------------------- /src/commandFactories/types.ts: -------------------------------------------------------------------------------- 1 | import { BranchDetails, CommitDetails, FileCommitDetails, ICommand } from '../common/types'; 2 | 3 | export const IFileCommitCommandFactory = Symbol.for('IFileCommitCommandFactory'); 4 | 5 | export interface IFileCommitCommandFactory { 6 | createCommands(data: FileCommitDetails): Promise[]>; 7 | getDefaultFileCommand(fileCommitDetails: FileCommitDetails): Promise | undefined>; 8 | } 9 | 10 | export const ICommitCommandFactory = Symbol.for('ICommitCommandFactory'); 11 | 12 | export interface ICommitCommandFactory { 13 | createCommands(data: CommitDetails): Promise[]>; 14 | } 15 | 16 | export const IBranchCommandFactory = Symbol.for('IBranchCommandFactory'); 17 | 18 | export interface IBranchCommandFactory { 19 | createCommands(data: BranchDetails): Promise[]>; 20 | } 21 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/commitViewExplorer.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { ICommandManager } from '../../application/types/commandManager'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { ICommitViewerFactory } from '../../viewers/types'; 5 | import { command } from '../registration'; 6 | import { IGitCommitViewExplorerCommandHandler } from '../types'; 7 | 8 | @injectable() 9 | export class GitCommitViewExplorerCommandHandler implements IGitCommitViewExplorerCommandHandler { 10 | constructor( 11 | @inject(ICommandManager) private commandManager: ICommandManager, 12 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 13 | ) {} 14 | 15 | @command('git.commit.view.hide', IGitCommitViewExplorerCommandHandler) 16 | public async hideCommitView(_commit: CommitDetails) { 17 | await this.commandManager.executeCommand('setContext', 'git.commit.view.show', false); 18 | } 19 | 20 | @command('git.commit.view.show', IGitCommitViewExplorerCommandHandler) 21 | public async showCommitView(_commit: CommitDetails) { 22 | await this.commandManager.executeCommand('setContext', 'git.commit.view.show', true); 23 | } 24 | 25 | @command('git.commit.view.showFilesOnly', IGitCommitViewExplorerCommandHandler) 26 | public async showFilesView(_commit: CommitDetails) { 27 | this.commitViewerFactory.getCommitViewer().showFilesView(); 28 | } 29 | 30 | @command('git.commit.view.showFolderView', IGitCommitViewExplorerCommandHandler) 31 | public async showFolderView(_commit: CommitDetails) { 32 | this.commitViewerFactory.getCommitViewer().showFolderView(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/compare.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell, ICommandManager } from '../../application/types'; 3 | import { CommitDetails, CompareCommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IGitServiceFactory } from '../../types'; 6 | import { ICommitViewerFactory } from '../../viewers/types'; 7 | import { command } from '../registration'; 8 | import { IGitCompareCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitCompareCommitCommandHandler implements IGitCompareCommandHandler { 12 | private _previouslySelectedCommit?: CommitDetails; 13 | 14 | constructor( 15 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 16 | @inject(ICommandManager) private commandManager: ICommandManager, 17 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 18 | @inject(IApplicationShell) private application: IApplicationShell, 19 | ) {} 20 | 21 | public get selectedCommit(): CommitDetails | undefined { 22 | return this._previouslySelectedCommit; 23 | } 24 | 25 | @command('git.commit.compare.selectForComparison', IGitCompareCommandHandler) 26 | public async select(commit: CommitDetails): Promise { 27 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.selectedForComparison', true); 28 | this._previouslySelectedCommit = commit; 29 | } 30 | 31 | @command('git.commit.compare', IGitCompareCommandHandler) 32 | public async compare(commit: CommitDetails): Promise { 33 | if (!this.selectedCommit) { 34 | await this.application.showErrorMessage('Please select another file to compare with'); 35 | return; 36 | } 37 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.compared', true); 38 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.view.show', true); 39 | // display explorer view when running compare 40 | await this.commandManager.executeCommand('workbench.view.explorer'); 41 | const gitService = await this.serviceContainer 42 | .get(IGitServiceFactory) 43 | .createGitService(commit.workspaceFolder); 44 | const fileDiffs = await gitService.getDifferences( 45 | this.selectedCommit.logEntry.hash.full, 46 | commit.logEntry.hash.full, 47 | ); 48 | const compareCommit = new CompareCommitDetails(this.selectedCommit, commit, fileDiffs); 49 | this.commitViewerFactory.getCompareCommitViewer().showCommitTree(compareCommit); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/compareViewExplorer.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { ICommandManager } from '../../application/types/commandManager'; 3 | import { ICommitViewerFactory } from '../../viewers/types'; 4 | import { command } from '../registration'; 5 | import { IGitCompareCommitViewExplorerCommandHandler } from '../types'; 6 | 7 | @injectable() 8 | export class GitCompareCommitViewExplorerCommandHandler implements IGitCompareCommitViewExplorerCommandHandler { 9 | constructor( 10 | @inject(ICommandManager) private commandManager: ICommandManager, 11 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 12 | ) {} 13 | 14 | @command('git.commit.compare.view.hide', IGitCompareCommitViewExplorerCommandHandler) 15 | public async hide() { 16 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.view.show', false); 17 | } 18 | 19 | @command('git.commit.compare.view.show', IGitCompareCommitViewExplorerCommandHandler) 20 | public async show() { 21 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.view.show', true); 22 | } 23 | 24 | @command('git.commit.compare.view.showFilesOnly', IGitCompareCommitViewExplorerCommandHandler) 25 | public async showFilesView() { 26 | this.commitViewerFactory.getCommitViewer().showFilesView(); 27 | } 28 | 29 | @command('git.commit.compare.view.showFolderView', IGitCompareCommitViewExplorerCommandHandler) 30 | public async showFolderView() { 31 | this.commitViewerFactory.getCommitViewer().showFolderView(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/gitCheckout.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell } from '../../application/types'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IGitServiceFactory } from '../../types'; 6 | import { ICommitViewerFactory } from '../../viewers/types'; 7 | import { command } from '../registration'; 8 | import { IGitCheckoutCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitCheckoutCommandHandler implements IGitCheckoutCommandHandler { 12 | constructor( 13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 15 | @inject(IApplicationShell) private applicationShell: IApplicationShell, 16 | ) {} 17 | 18 | @command('git.commit.checkout', IGitCheckoutCommandHandler) 19 | public async checkoutCommit(commit: CommitDetails) { 20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit; 21 | const gitService = await this.serviceContainer 22 | .get(IGitServiceFactory) 23 | .createGitService(commit.workspaceFolder); 24 | 25 | gitService.checkout(commit.logEntry.hash.full).catch(err => { 26 | if (typeof err === 'string') { 27 | this.applicationShell.showErrorMessage(err); 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/gitCherryPick.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell } from '../../application/types'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IGitServiceFactory } from '../../types'; 6 | import { ICommitViewerFactory } from '../../viewers/types'; 7 | import { command } from '../registration'; 8 | import { IGitCherryPickCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitCherryPickCommandHandler implements IGitCherryPickCommandHandler { 12 | constructor( 13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 15 | @inject(IApplicationShell) private applicationShell: IApplicationShell, 16 | ) {} 17 | 18 | @command('git.commit.cherryPick', IGitCherryPickCommandHandler) 19 | public async cherryPickCommit(commit: CommitDetails, showPrompt = true) { 20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit; 21 | const gitService = await this.serviceContainer 22 | .get(IGitServiceFactory) 23 | .createGitService(commit.workspaceFolder); 24 | const currentBranch = gitService.getCurrentBranch(); 25 | 26 | const msg = `Cherry pick ${commit.logEntry.hash.short} into ${currentBranch}?`; 27 | const yesNo = showPrompt 28 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg }) 29 | : 'Yes'; 30 | 31 | if (yesNo === undefined || yesNo === 'No') { 32 | return; 33 | } 34 | 35 | gitService.cherryPick(commit.logEntry.hash.full).catch(err => { 36 | if (typeof err === 'string') { 37 | this.applicationShell.showErrorMessage(err); 38 | } 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/gitCommitDetails.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { ICommandManager } from '../../application/types/commandManager'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { ICommitViewerFactory } from '../../viewers/types'; 6 | import { IGitCommitViewDetailsCommandHandler } from '../types'; 7 | 8 | @injectable() 9 | export class GitCommitViewDetailsCommandHandler implements IGitCommitViewDetailsCommandHandler { 10 | constructor( 11 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 12 | @inject(ICommandManager) private commandManager: ICommandManager, 13 | ) {} 14 | 15 | public async viewDetails(commit: CommitDetails) { 16 | await this.commandManager.executeCommand('setContext', 'git.commit.selected', true); 17 | this.serviceContainer 18 | .get(ICommitViewerFactory) 19 | .getCommitViewer() 20 | .showCommit(commit); 21 | } 22 | 23 | public async viewCommitTree(commit: CommitDetails) { 24 | await this.commandManager.executeCommand('setContext', 'git.commit.selected', true); 25 | this.serviceContainer 26 | .get(ICommitViewerFactory) 27 | .getCommitViewer() 28 | .showCommitTree(commit); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/gitMerge.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell } from '../../application/types'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IGitServiceFactory } from '../../types'; 6 | import { ICommitViewerFactory } from '../../viewers/types'; 7 | import { command } from '../registration'; 8 | import { IGitMergeCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitMergeCommandHandler implements IGitMergeCommandHandler { 12 | constructor( 13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 15 | @inject(IApplicationShell) private applicationShell: IApplicationShell, 16 | ) {} 17 | 18 | @command('git.commit.merge', IGitMergeCommandHandler) 19 | public async merge(commit: CommitDetails, showPrompt = true) { 20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit; 21 | const gitService = await this.serviceContainer 22 | .get(IGitServiceFactory) 23 | .createGitService(commit.workspaceFolder); 24 | const currentBranch = gitService.getCurrentBranch(); 25 | 26 | const commitBranches = (await gitService.getRefsContainingCommit(commit.logEntry.hash.full)) 27 | .filter(value => value.name!.length > 0) 28 | .map(x => x.name!); 29 | 30 | const branchMsg = `Choose the commit/branch to merge into ${currentBranch}`; 31 | const rev = await this.applicationShell.showQuickPick([commit.logEntry.hash.full, ...commitBranches], { 32 | placeHolder: branchMsg, 33 | }); 34 | 35 | let type: string; 36 | if (rev === undefined || rev.length === 0) { 37 | return; 38 | } 39 | if (rev === commit.logEntry.hash.full) { 40 | type = 'commit'; 41 | } else { 42 | type = 'branch'; 43 | } 44 | 45 | const msg = `Merge ${type} '${rev}' into ${currentBranch}?`; 46 | const yesNo = showPrompt 47 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg }) 48 | : 'Yes'; 49 | 50 | if (yesNo === undefined || yesNo === 'No') { 51 | return; 52 | } 53 | 54 | gitService.merge(rev).catch(err => { 55 | if (typeof err === 'string') { 56 | this.applicationShell.showErrorMessage(err); 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/gitRebase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell } from '../../application/types'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IGitServiceFactory } from '../../types'; 6 | import { ICommitViewerFactory } from '../../viewers/types'; 7 | import { command } from '../registration'; 8 | import { IGitRebaseCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitRebaseCommandHandler implements IGitRebaseCommandHandler { 12 | constructor( 13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 15 | @inject(IApplicationShell) private applicationShell: IApplicationShell, 16 | ) {} 17 | 18 | @command('git.commit.rebase', IGitRebaseCommandHandler) 19 | public async rebase(commit: CommitDetails, showPrompt = true) { 20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit; 21 | const gitService = await this.serviceContainer 22 | .get(IGitServiceFactory) 23 | .createGitService(commit.workspaceFolder); 24 | const currentBranch = gitService.getCurrentBranch(); 25 | 26 | const msg = `Rebase ${currentBranch} onto '${commit.logEntry.hash.short}'?`; 27 | const yesNo = showPrompt 28 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg }) 29 | : 'Yes'; 30 | 31 | if (yesNo === undefined || yesNo === 'No') { 32 | return; 33 | } 34 | 35 | gitService.rebase(commit.logEntry.hash.full).catch(err => { 36 | if (typeof err === 'string') { 37 | this.applicationShell.showErrorMessage(err); 38 | } 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commandHandlers/commit/revert.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell } from '../../application/types'; 3 | import { CommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IGitServiceFactory } from '../../types'; 6 | import { ICommitViewerFactory } from '../../viewers/types'; 7 | import { command } from '../registration'; 8 | import { IGitRevertCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitRevertCommandHandler implements IGitRevertCommandHandler { 12 | constructor( 13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory, 15 | @inject(IApplicationShell) private applicationShell: IApplicationShell, 16 | ) {} 17 | 18 | @command('git.commit.revert', IGitRevertCommandHandler) 19 | public async revertCommit(commit: CommitDetails, showPrompt = true) { 20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit; 21 | const gitService = await this.serviceContainer 22 | .get(IGitServiceFactory) 23 | .createGitService(commit.workspaceFolder); 24 | 25 | const msg = `Are you sure you want to revert this '${commit.logEntry.hash.short}' commit?`; 26 | const yesNo = showPrompt 27 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg }) 28 | : 'Yes'; 29 | 30 | if (yesNo === undefined || yesNo === 'No') { 31 | return; 32 | } 33 | 34 | gitService.revertCommit(commit.logEntry.hash.full).catch(err => { 35 | if (typeof err === 'string') { 36 | this.applicationShell.showErrorMessage(err); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commandHandlers/file/file.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { Uri } from 'vscode'; 3 | import { ICommandManager, IDocumentManager } from '../../application/types'; 4 | import { command } from '../registration'; 5 | import { IFileCommandHandler } from '../types'; 6 | import { isTextFile } from './mimeTypes'; 7 | 8 | @injectable() 9 | export class FileCommandHandler implements IFileCommandHandler { 10 | constructor( 11 | @inject(ICommandManager) private commandManager: ICommandManager, 12 | @inject(IDocumentManager) private documentManager: IDocumentManager, 13 | ) {} 14 | 15 | @command('git.openFileInViewer', IFileCommandHandler) 16 | public async openFile(file: Uri): Promise { 17 | if (isTextFile(file)) { 18 | const doc = await this.documentManager.openTextDocument(file); 19 | await this.documentManager.showTextDocument(doc, { preview: true }); 20 | } else { 21 | await this.commandManager.executeCommand('vscode.open', file); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commandHandlers/file/mimeTypes.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Uri } from 'vscode'; 3 | 4 | type MapExtToMediaMimes = { 5 | [index: string]: string; 6 | }; 7 | 8 | // Known media mimes that we can handle 9 | const mapExtToMediaMimes: MapExtToMediaMimes = { 10 | '.bmp': 'image/bmp', 11 | '.gif': 'image/gif', 12 | '.jpg': 'image/jpg', 13 | '.jpeg': 'image/jpg', 14 | '.jpe': 'image/jpg', 15 | '.png': 'image/png', 16 | '.tiff': 'image/tiff', 17 | '.tif': 'image/tiff', 18 | '.ico': 'image/x-icon', 19 | '.tga': 'image/x-tga', 20 | '.psd': 'image/vnd.adobe.photoshop', 21 | '.webp': 'image/webp', 22 | '.mid': 'audio/midi', 23 | '.midi': 'audio/midi', 24 | '.mp4a': 'audio/mp4', 25 | '.mpga': 'audio/mpeg', 26 | '.mp2': 'audio/mpeg', 27 | '.mp2a': 'audio/mpeg', 28 | '.mp3': 'audio/mpeg', 29 | '.m2a': 'audio/mpeg', 30 | '.m3a': 'audio/mpeg', 31 | '.oga': 'audio/ogg', 32 | '.ogg': 'audio/ogg', 33 | '.spx': 'audio/ogg', 34 | '.aac': 'audio/x-aac', 35 | '.wav': 'audio/x-wav', 36 | '.wma': 'audio/x-ms-wma', 37 | '.mp4': 'video/mp4', 38 | '.mp4v': 'video/mp4', 39 | '.mpg4': 'video/mp4', 40 | '.mpeg': 'video/mpeg', 41 | '.mpg': 'video/mpeg', 42 | '.mpe': 'video/mpeg', 43 | '.m1v': 'video/mpeg', 44 | '.m2v': 'video/mpeg', 45 | '.ogv': 'video/ogg', 46 | '.qt': 'video/quicktime', 47 | '.mov': 'video/quicktime', 48 | '.webm': 'video/webm', 49 | '.mkv': 'video/x-matroska', 50 | '.mk3d': 'video/x-matroska', 51 | '.mks': 'video/x-matroska', 52 | '.wmv': 'video/x-ms-wmv', 53 | '.flv': 'video/x-flv', 54 | '.avi': 'video/x-msvideo', 55 | '.movie': 'video/x-sgi-movie', 56 | }; 57 | 58 | export function isTextFile(file: Uri): boolean { 59 | const extension = path.extname(file.fsPath); 60 | return !mapExtToMediaMimes[extension]; 61 | } 62 | -------------------------------------------------------------------------------- /src/commandHandlers/fileCommit/fileCompare.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { IApplicationShell, ICommandManager } from '../../application/types'; 3 | import { FileCommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { FileNode } from '../../nodes/types'; 6 | import { IGitServiceFactory } from '../../types'; 7 | import { command } from '../registration'; 8 | import { IGitCompareFileCommandHandler } from '../types'; 9 | 10 | @injectable() 11 | export class GitCompareFileCommitCommandHandler implements IGitCompareFileCommandHandler { 12 | private _previouslySelectedCommit?: FileCommitDetails; 13 | 14 | constructor( 15 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 16 | @inject(ICommandManager) private commandManager: ICommandManager, 17 | @inject(IApplicationShell) private application: IApplicationShell, 18 | ) {} 19 | 20 | public get selectedCommit(): FileCommitDetails | undefined { 21 | return this._previouslySelectedCommit; 22 | } 23 | 24 | @command('git.commit.FileEntry.selectForComparison', IGitCompareFileCommandHandler) 25 | public async select(nodeOrFileCommit: FileNode | FileCommitDetails): Promise { 26 | const fileCommit = nodeOrFileCommit instanceof FileCommitDetails ? nodeOrFileCommit : nodeOrFileCommit.data!; 27 | await this.commandManager.executeCommand('setContext', 'git.commit.FileEntry.selectForComparison', true); 28 | this._previouslySelectedCommit = fileCommit; 29 | } 30 | 31 | @command('git.commit.FileEntry.compare', IGitCompareFileCommandHandler) 32 | public async compare(nodeOrFileCommit: FileNode | FileCommitDetails): Promise { 33 | if (!this.selectedCommit) { 34 | await this.application.showErrorMessage('Please select another file to compare with'); 35 | return; 36 | } 37 | const fileCommit = nodeOrFileCommit instanceof FileCommitDetails ? nodeOrFileCommit : nodeOrFileCommit.data!; 38 | const gitService = await this.serviceContainer 39 | .get(IGitServiceFactory) 40 | .createGitService(fileCommit.workspaceFolder); 41 | const fileDiffs = await gitService.getDifferences( 42 | this.selectedCommit.logEntry.hash.full, 43 | fileCommit.logEntry.hash.full, 44 | ); 45 | await this.commandManager.executeCommand('git.commit.diff.view', this.selectedCommit!, fileCommit, fileDiffs); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commandHandlers/handlerManager.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { ICommandManager } from '../application/types/commandManager'; 3 | import { IDisposableRegistry } from '../application/types/disposableRegistry'; 4 | import { IServiceContainer } from '../ioc/types'; 5 | import { CommandHandlerRegister } from './registration'; 6 | import { ICommandHandler, ICommandHandlerManager } from './types'; 7 | import { sendTelemetryEvent, sendTelemetryWhenDone } from '../common/telemetry'; 8 | import { StopWatch } from '../common/stopWatch'; 9 | 10 | @injectable() 11 | export class CommandHandlerManager implements ICommandHandlerManager { 12 | constructor( 13 | @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, 14 | @inject(ICommandManager) private commandManager: ICommandManager, 15 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 16 | ) {} 17 | 18 | public registerHandlers() { 19 | for (const item of CommandHandlerRegister.getHandlers()) { 20 | const serviceIdentifier = item[0]; 21 | const handlers = item[1]; 22 | const target = this.serviceContainer.get(serviceIdentifier); 23 | handlers.forEach(handlerInfo => 24 | this.registerCommand(handlerInfo.commandName, handlerInfo.handlerMethodName, target), 25 | ); 26 | } 27 | } 28 | 29 | private registerCommand(commandName: string, handlerMethodName: string, target: ICommandHandler) { 30 | const handler = target[handlerMethodName] as (...args: any[]) => void; 31 | const disposable = this.commandManager.registerCommand(commandName, (...args: any[]) => { 32 | const stopWatch = new StopWatch(); 33 | let result: Promise | undefined | any; 34 | try { 35 | result = handler.apply(target, args); 36 | } finally { 37 | // Track commands that we have created and attached to, no PII here hence cast `commandName` to any. 38 | if (result && result.then && typeof result.then === 'function') { 39 | // This is an async handler. 40 | sendTelemetryWhenDone(commandName as any, result); 41 | } else { 42 | // This is a synchronous handler. 43 | sendTelemetryEvent(commandName as any, stopWatch.elapsedTime); 44 | } 45 | } 46 | }); 47 | this.disposableRegistry.register(disposable); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commandHandlers/registration.ts: -------------------------------------------------------------------------------- 1 | import { interfaces } from 'inversify'; 2 | import { Disposable } from 'vscode'; 3 | import { ICommandHandler } from './types'; 4 | 5 | type CommandHandler = (...args: any[]) => any; 6 | type CommandHandlerInfo = { commandName: string; handlerMethodName: string }; 7 | 8 | export class CommandHandlerRegister implements Disposable { 9 | private static Handlers = new Map, CommandHandlerInfo[]>(); 10 | public static register( 11 | commandName: string, 12 | handlerMethodName: string, 13 | serviceIdentifier: interfaces.ServiceIdentifier, 14 | ) { 15 | if (!CommandHandlerRegister.Handlers.has(serviceIdentifier)) { 16 | CommandHandlerRegister.Handlers.set(serviceIdentifier, []); 17 | } 18 | const commandList = CommandHandlerRegister.Handlers.get(serviceIdentifier)!; 19 | commandList.push({ commandName, handlerMethodName }); 20 | CommandHandlerRegister.Handlers.set(serviceIdentifier, commandList); 21 | } 22 | public static getHandlers(): IterableIterator< 23 | [interfaces.ServiceIdentifier, CommandHandlerInfo[]] 24 | > { 25 | return CommandHandlerRegister.Handlers.entries(); 26 | } 27 | public dispose() { 28 | CommandHandlerRegister.Handlers.clear(); 29 | } 30 | } 31 | 32 | export function command(commandName: string, serviceIdentifier: interfaces.ServiceIdentifier) { 33 | return function( 34 | _target: ICommandHandler, 35 | propertyKey: string, 36 | descriptor: TypedPropertyDescriptor, 37 | ) { 38 | CommandHandlerRegister.register(commandName, propertyKey, serviceIdentifier); 39 | 40 | return descriptor; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/baseCommand.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../common/types'; 2 | 3 | export abstract class BaseCommand implements ICommand { 4 | private _arguments: any[] = []; 5 | public get arguments() { 6 | return this._arguments; 7 | } 8 | private _command = ''; 9 | private _title = ''; 10 | private _description = ''; 11 | private _detail?: string; 12 | private _tooltip?: string; 13 | public get command() { 14 | return this._command; 15 | } 16 | public get title() { 17 | return this._title; 18 | } 19 | public get label() { 20 | return this._title; 21 | } 22 | public get description() { 23 | return this._description; 24 | } 25 | public get detail() { 26 | return this._detail; 27 | } 28 | public get tooltip() { 29 | return this._tooltip; 30 | } 31 | constructor(public readonly data: T) {} 32 | public abstract execute(); 33 | public async preExecute(): Promise { 34 | return true; 35 | } 36 | protected setTitle(value: string) { 37 | this._title = value; 38 | } 39 | protected setCommand(value: string) { 40 | this._command = value; 41 | } 42 | protected setCommandArguments(args: any[]) { 43 | this._arguments = args; 44 | } 45 | protected setDescription(value: string) { 46 | this._description = value; 47 | } 48 | protected setDetail(value: string) { 49 | this._detail = value; 50 | } 51 | protected setTooltip(value: string) { 52 | this._tooltip = value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/baseCommitCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommitDetails } from '../common/types'; 2 | import { BaseCommand } from './baseCommand'; 3 | 4 | export abstract class BaseCommitCommand extends BaseCommand { 5 | constructor(data: CommitDetails) { 6 | super(data); 7 | } 8 | public abstract execute(); 9 | public async preExecute(): Promise { 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/baseFileCommitCommand.ts: -------------------------------------------------------------------------------- 1 | import { FileCommitDetails } from '../common/types'; 2 | import { BaseCommand } from './baseCommand'; 3 | 4 | export abstract class BaseFileCommitCommand extends BaseCommand { 5 | constructor(data: FileCommitDetails) { 6 | super(data); 7 | } 8 | public abstract execute(); 9 | public async preExecute(): Promise { 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/commit/checkout.ts: -------------------------------------------------------------------------------- 1 | import { IGitCheckoutCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class CheckoutCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCheckoutCommandHandler) { 7 | super(commit); 8 | // const committer = `${commit.logEntry.author!.name} <${commit.logEntry.author!.email}> on ${commit.logEntry.author!.date!.toLocaleString()}`; 9 | this.setTitle(`$(git-pull-request) Checkout (${commit.logEntry.hash.short}) commit`); 10 | // this.setDescription(committer); 11 | // this.setDetail(commit.logEntry.subject); 12 | this.setCommand('git.commit.checkout'); 13 | this.setCommandArguments([commit]); 14 | } 15 | public execute() { 16 | this.handler.checkoutCommit(this.data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/commit/cherryPick.ts: -------------------------------------------------------------------------------- 1 | import { IGitCherryPickCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class CherryPickCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCherryPickCommandHandler) { 7 | super(commit); 8 | // const committer = `${commit.logEntry.author!.name} <${commit.logEntry.author!.email}> on ${commit.logEntry.author!.date!.toLocaleString()}`; 9 | this.setTitle( 10 | `$(git-pull-request) Cherry pick this (${commit.logEntry.hash.short}) commit into current branch`, 11 | ); 12 | // this.setDescription(committer); 13 | // this.setDetail(commit.logEntry.subject); 14 | this.setCommand('git.commit.cherryPick'); 15 | this.setCommandArguments([commit]); 16 | } 17 | public execute() { 18 | this.handler.cherryPickCommit(this.data); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/commit/compare.ts: -------------------------------------------------------------------------------- 1 | import { IGitCompareCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class CompareCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCompareCommandHandler) { 7 | super(commit); 8 | if (handler.selectedCommit) { 9 | const committer = `${commit.logEntry.author!.name} <${ 10 | commit.logEntry.author!.email 11 | }> on ${commit.logEntry.author!.date!.toLocaleString()}`; 12 | this.setTitle(`$(git-compare) Compare with ${handler.selectedCommit!.logEntry.hash.short}, ${committer}`); 13 | this.setDetail(commit.logEntry.subject); 14 | } 15 | this.setCommand('git.commit.compare'); 16 | this.setCommandArguments([commit]); 17 | } 18 | public async preExecute(): Promise { 19 | return !!this.handler.selectedCommit; 20 | } 21 | public execute() { 22 | this.handler.compare(this.data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/commit/createBranch.ts: -------------------------------------------------------------------------------- 1 | import { IGitCommitCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class CreateBranchCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCommitCommandHandler) { 7 | super(commit); 8 | // const committer = `${commit.logEntry.author!.name} <${commit.logEntry.author!.email}> on ${commit.logEntry.author!.date!.toLocaleString()}`; 9 | // this.setTitle(`$(git-branch) Branch from here ${commit.logEntry.hash.short} (${committer})`); 10 | this.setTitle('$(git-branch) Branch from here'); 11 | // this.setDetail(commit.logEntry.subject); 12 | this.setCommand('git.commit.createBranch'); 13 | this.setCommandArguments([commit]); 14 | } 15 | public execute() { 16 | this.handler.createBranchFromCommit(this.data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/commit/createTag.ts: -------------------------------------------------------------------------------- 1 | import { IGitCommitCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class CreateTagCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCommitCommandHandler) { 7 | super(commit); 8 | this.setTitle(`$(tag) Tag commit ${commit.logEntry!.hash!.short}`); 9 | this.setCommand('git.commit.createTag'); 10 | this.setCommandArguments([commit]); 11 | } 12 | public execute() { 13 | this.handler.createTagFromCommit(this.data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/commit/hideCommitViewExplorer.ts: -------------------------------------------------------------------------------- 1 | import { ICommandManager } from '../../application/types/commandManager'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class HideCommitViewExplorerCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private commandManager: ICommandManager) { 7 | super(commit); 8 | this.setTitle('Hide Commit View Explorer'); 9 | this.setCommand('git.commit.view.hide'); 10 | } 11 | public execute() { 12 | this.commandManager.executeCommand(this.command); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/commit/merge.ts: -------------------------------------------------------------------------------- 1 | import { IGitMergeCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class MergeCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitMergeCommandHandler) { 7 | super(commit); 8 | this.setTitle(`$(git-merge) Merge this (${commit.logEntry.hash.short}) commit into current branch`); 9 | this.setCommand('git.commit.merge'); 10 | this.setCommandArguments([commit]); 11 | } 12 | public execute() { 13 | this.handler.merge(this.data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/commit/rebase.ts: -------------------------------------------------------------------------------- 1 | import { IGitRebaseCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class RebaseCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitRebaseCommandHandler) { 7 | super(commit); 8 | this.setTitle(`$(git-merge) Rebase current branch onto this (${commit.logEntry.hash.short}) commit`); 9 | this.setCommand('git.commit.rebase'); 10 | this.setCommandArguments([commit]); 11 | } 12 | public execute() { 13 | this.handler.rebase(this.data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/commit/revert.ts: -------------------------------------------------------------------------------- 1 | import { IGitRevertCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class RevertCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitRevertCommandHandler) { 7 | super(commit); 8 | this.setTitle(`$(x) Revert this (${commit.logEntry.hash.short}) commit`); 9 | this.setCommand('git.commit.revert'); 10 | this.setCommandArguments([commit]); 11 | } 12 | public execute() { 13 | this.handler.revertCommit(this.data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/commit/selectForComparion.ts: -------------------------------------------------------------------------------- 1 | import { IGitCompareCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class SelectForComparison extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCompareCommandHandler) { 7 | super(commit); 8 | const committer = `${commit.logEntry.author!.name} <${ 9 | commit.logEntry.author!.email 10 | }> on ${commit.logEntry.author!.date!.toLocaleString()}`; 11 | this.setTitle(`$(git-compare) Select this commit (${committer}) for comparison`); 12 | // this.setDetail(commit.logEntry.subject); 13 | this.setCommand('git.commit.compare.selectForComparison'); 14 | this.setCommandArguments([CommitDetails]); 15 | } 16 | public execute() { 17 | this.handler.select(this.data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/commit/viewDetails.ts: -------------------------------------------------------------------------------- 1 | import { IGitCommitViewDetailsCommandHandler } from '../../commandHandlers/types'; 2 | import { CommitDetails } from '../../common/types'; 3 | import { BaseCommitCommand } from '../baseCommitCommand'; 4 | 5 | export class ViewDetailsCommand extends BaseCommitCommand { 6 | constructor(commit: CommitDetails, private handler: IGitCommitViewDetailsCommandHandler) { 7 | super(commit); 8 | this.setTitle('$(eye) View Change log'); 9 | } 10 | public async preExecute(): Promise { 11 | // Disable for now, useless command. 12 | return false; 13 | } 14 | public execute() { 15 | this.handler.viewDetails(this.data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/fileCommit/compareFile.ts: -------------------------------------------------------------------------------- 1 | import { IGitCompareFileCommandHandler } from '../../commandHandlers/types'; 2 | import { FileCommitDetails } from '../../common/types'; 3 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 4 | 5 | export class CompareFileCommand extends BaseFileCommitCommand { 6 | constructor(fileCommit: FileCommitDetails, private handler: IGitCompareFileCommandHandler) { 7 | super(fileCommit); 8 | if (handler.selectedCommit) { 9 | this.setTitle(`$(git-compare) Compare with ${handler.selectedCommit.logEntry.hash.short}`); 10 | } 11 | this.setCommand('git.commit.FileEntry.compare'); 12 | this.setCommandArguments([fileCommit]); 13 | } 14 | public async preExecute(): Promise { 15 | // TODO: Not completed 16 | return false; 17 | // return !!this.handler.selectedCommit; 18 | } 19 | public execute() { 20 | this.handler.compare(this.data); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/fileCommit/compareFileAcrossCommits.ts: -------------------------------------------------------------------------------- 1 | import { IGitFileHistoryCommandHandler } from '../../commandHandlers/types'; 2 | import { CompareFileCommitDetails } from '../../common/types'; 3 | import { Status } from '../../types'; 4 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 5 | 6 | export class CompareFileWithAcrossCommitCommand extends BaseFileCommitCommand { 7 | constructor(fileCommit: CompareFileCommitDetails, private handler: IGitFileHistoryCommandHandler) { 8 | super(fileCommit); 9 | this.setTitle('$(git-compare) Compare'); 10 | this.setCommand('git.commit.compare.file.compare'); 11 | this.setCommandArguments([fileCommit]); 12 | } 13 | public async preExecute(): Promise { 14 | return this.data.committedFile.status === Status.Modified; 15 | } 16 | public execute() { 17 | this.handler.compareFileAcrossCommits(this.data as CompareFileCommitDetails); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/fileCommit/compareFileWithPrevious.ts: -------------------------------------------------------------------------------- 1 | import { IGitFileHistoryCommandHandler } from '../../commandHandlers/types'; 2 | import { FileCommitDetails } from '../../common/types'; 3 | import { Status } from '../../types'; 4 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 5 | 6 | export class CompareFileWithPreviousCommand extends BaseFileCommitCommand { 7 | constructor(fileCommit: FileCommitDetails, private handler: IGitFileHistoryCommandHandler) { 8 | super(fileCommit); 9 | this.setTitle('$(git-compare) Compare against previous version'); 10 | this.setCommand('git.commit.FileEntry.CompareAgainstPrevious'); 11 | this.setCommandArguments([fileCommit]); 12 | } 13 | public async preExecute(): Promise { 14 | return this.data.committedFile.status === Status.Modified; 15 | } 16 | public execute() { 17 | this.handler.compareFileWithPrevious(this.data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/fileCommit/compareFileWithWorkspace.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { IGitFileHistoryCommandHandler } from '../../commandHandlers/types'; 3 | import { FileCommitDetails } from '../../common/types'; 4 | import { IServiceContainer } from '../../ioc/types'; 5 | import { IFileSystem } from '../../platform/types'; 6 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 7 | 8 | export class CompareFileWithWorkspaceCommand extends BaseFileCommitCommand { 9 | constructor( 10 | fileCommit: FileCommitDetails, 11 | private handler: IGitFileHistoryCommandHandler, 12 | private serviceContainer: IServiceContainer, 13 | ) { 14 | super(fileCommit); 15 | this.setTitle('$(git-compare) Compare against workspace file'); 16 | this.setCommand('git.commit.FileEntry.CompareAgainstWorkspace'); 17 | this.setCommandArguments([fileCommit]); 18 | } 19 | public async preExecute(): Promise { 20 | const localFile = path.join(this.data.workspaceFolder, this.data.committedFile.relativePath); 21 | const fileSystem = this.serviceContainer.get(IFileSystem); 22 | return fileSystem.fileExistsAsync(localFile); 23 | } 24 | public execute() { 25 | this.handler.compareFileWithWorkspace(this.data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/fileCommit/fileHistory.ts: -------------------------------------------------------------------------------- 1 | import { ICommandManager } from '../../application/types'; 2 | import { FileCommitDetails } from '../../common/types'; 3 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 4 | 5 | export class ViewFileHistoryCommand extends BaseFileCommitCommand { 6 | constructor(fileCommit: FileCommitDetails, private commandManager: ICommandManager) { 7 | super(fileCommit); 8 | this.setTitle('$(history) View file history'); 9 | this.setCommand('git.viewFileHistory'); 10 | this.setCommandArguments([fileCommit]); 11 | } 12 | public execute() { 13 | this.commandManager.executeCommand(this.command, ...this.arguments); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/fileCommit/selectFileForComparison.ts: -------------------------------------------------------------------------------- 1 | import { IGitCompareFileCommandHandler } from '../../commandHandlers/types'; 2 | import { FileCommitDetails } from '../../common/types'; 3 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 4 | 5 | export class SelectFileForComparison extends BaseFileCommitCommand { 6 | constructor(fileCommit: FileCommitDetails, private handler: IGitCompareFileCommandHandler) { 7 | super(fileCommit); 8 | this.setTitle('$(git-compare) Select for comparison'); 9 | this.setCommand('git.commit.FileEntry.selectForComparison'); 10 | this.setCommandArguments([fileCommit]); 11 | } 12 | public async preExecute(): Promise { 13 | // TODO: Not completed 14 | return false; 15 | } 16 | public execute() { 17 | this.handler.select(this.data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/fileCommit/viewFile.ts: -------------------------------------------------------------------------------- 1 | import { IGitFileHistoryCommandHandler } from '../../commandHandlers/types'; 2 | import { FileCommitDetails } from '../../common/types'; 3 | import { Status } from '../../types'; 4 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 5 | 6 | export class ViewFileCommand extends BaseFileCommitCommand { 7 | constructor(fileCommit: FileCommitDetails, private handler: IGitFileHistoryCommandHandler) { 8 | super(fileCommit); 9 | this.setTitle('$(eye) View file contents'); 10 | this.setCommand('git.commit.FileEntry.ViewFileContents'); 11 | this.setCommandArguments([fileCommit]); 12 | } 13 | public async preExecute(): Promise { 14 | return this.data.committedFile.status !== Status.Deleted; 15 | } 16 | public execute() { 17 | this.handler.viewFile(this.data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/fileCommit/viewPreviousFile.ts: -------------------------------------------------------------------------------- 1 | import { IGitFileHistoryCommandHandler } from '../../commandHandlers/types'; 2 | import { FileCommitDetails } from '../../common/types'; 3 | import { Status } from '../../types'; 4 | import { BaseFileCommitCommand } from '../baseFileCommitCommand'; 5 | 6 | export class ViewPreviousFileCommand extends BaseFileCommitCommand { 7 | constructor(fileCommit: FileCommitDetails, private handler: IGitFileHistoryCommandHandler) { 8 | super(fileCommit); 9 | this.setTitle('$(eye) View previous file contents'); 10 | this.setCommand('git.commit.FileEntry.ViewPreviousFileContents'); 11 | this.setCommandArguments([fileCommit]); 12 | } 13 | public async preExecute(): Promise { 14 | return this.data.committedFile.status === Status.Deleted; 15 | } 16 | public execute() { 17 | this.handler.viewFile(this.data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export function isTestEnvironment() { 2 | return process.env.VSC_GITHISTORY_CI_TEST === '1' || process.env.IS_TEST_MODE; 3 | } 4 | 5 | export const disableCaching = process.env.IS_TEST_MODE && !!process.env.GIT_DISABLE_CACHING; 6 | -------------------------------------------------------------------------------- /src/common/enumHelper.ts: -------------------------------------------------------------------------------- 1 | export class EnumEx { 2 | public static getNamesAndValues(e: any) { 3 | return EnumEx.getNames(e).map(n => ({ name: n, value: e[n] as T })); 4 | } 5 | 6 | public static getNames(e: any) { 7 | return EnumEx.getObjValues(e).filter(v => typeof v === 'string') as string[]; 8 | } 9 | 10 | public static getValues(e: any) { 11 | return EnumEx.getObjValues(e).filter(v => typeof v === 'number') as T[]; 12 | } 13 | 14 | private static getObjValues(e: any): (number | string)[] { 15 | return Object.keys(e).map(k => e[k]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as tmp from 'tmp'; 2 | 3 | export async function createTemporaryFile( 4 | extension: string, 5 | temporaryDirectory?: string, 6 | ): Promise<{ filePath: string; cleanupCallback: Function }> { 7 | const options: { postfix: string; dir?: string } = { postfix: extension }; 8 | if (temporaryDirectory) { 9 | options.dir = temporaryDirectory; 10 | } 11 | 12 | return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => { 13 | tmp.file(options, (err: Error, tmpFile: string, _fd: any, cleanupCallback: any) => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | resolve({ filePath: tmpFile, cleanupCallback: cleanupCallback }); 18 | }); 19 | }); 20 | } 21 | 22 | export function formatDate(date: Date) { 23 | const lang = process.env.language; 24 | const dateOptions = { 25 | weekday: 'short', 26 | day: 'numeric', 27 | month: 'short', 28 | year: 'numeric', 29 | hour: 'numeric', 30 | minute: 'numeric', 31 | }; 32 | 33 | // @ts-ignore 34 | return date.toLocaleString(lang, dateOptions); 35 | } 36 | 37 | export async function asyncFilter(arr: T[], callback): Promise { 38 | return (await Promise.all(arr.map(async item => ((await callback(item)) ? item : undefined)))).filter( 39 | i => i !== undefined, 40 | ) as T[]; 41 | } 42 | 43 | export const sleep = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)); 44 | 45 | export function noop() { 46 | //Noop. 47 | } 48 | -------------------------------------------------------------------------------- /src/common/log.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { Disposable, workspace } from 'vscode'; 3 | import { ILogService } from './types'; 4 | 5 | @injectable() 6 | export class Logger implements ILogService { 7 | private enabled = true; 8 | private traceEnabled = false; 9 | private disposable: Disposable; 10 | constructor() { 11 | this.updateEnabledFlag(); 12 | this.disposable = workspace.onDidChangeConfiguration(() => this.updateEnabledFlag()); 13 | } 14 | public dispose() { 15 | this.disposable.dispose(); 16 | } 17 | public log(...args: any[]): void { 18 | if (!this.enabled) { 19 | return; 20 | } 21 | console.log(...args); 22 | } 23 | public error(...args: any[]): void { 24 | console.error(...args); 25 | } 26 | public trace(...args: any[]): void { 27 | if (!this.enabled) { 28 | return; 29 | } 30 | if (!this.traceEnabled) { 31 | return; 32 | } 33 | console.warn(...args); 34 | } 35 | private updateEnabledFlag() { 36 | const logLevel = workspace.getConfiguration('gitHistory').get('logLevel', 'None'); 37 | this.traceEnabled = logLevel === 'Debug'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/common/stopWatch.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | 'use strict'; 5 | 6 | export class StopWatch { 7 | private started = new Date().getTime(); 8 | public get elapsedTime() { 9 | return new Date().getTime() - this.started; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'vscode'; 2 | import { BranchSelection, CommittedFile, LogEntry } from '../types'; 3 | 4 | export const ILogService = Symbol.for('ILogService'); 5 | 6 | export interface ILogService { 7 | log(...args: any[]): void; 8 | trace(...args: any[]): void; 9 | error(...args: any[]): void; 10 | } 11 | 12 | export const IUiService = Symbol.for('IUiService'); 13 | 14 | export interface IUiService { 15 | getBranchSelection(): Promise; 16 | selectFileCommitCommandAction(fileCommit: FileCommitDetails): Promise | undefined>; 17 | selectCommitCommandAction(commit: CommitDetails): Promise | undefined>; 18 | } 19 | 20 | export enum CallContextSource { 21 | viewer = 0, 22 | commandPalette = 1, 23 | } 24 | 25 | export class CallContext { 26 | constructor(public readonly source: CallContextSource, public readonly data: T) {} 27 | } 28 | 29 | export class BranchDetails { 30 | constructor(public readonly workspaceFolder: string, public readonly branch: string) {} 31 | } 32 | 33 | export class CommitDetails extends BranchDetails { 34 | constructor(workspaceFolder: string, branch: string, public readonly logEntry: Readonly) { 35 | super(workspaceFolder, branch); 36 | } 37 | } 38 | 39 | export class CompareCommitDetails extends CommitDetails { 40 | constructor( 41 | leftCommit: CommitDetails, 42 | public readonly rightCommit: CommitDetails, 43 | public readonly committedFiles: CommittedFile[], 44 | ) { 45 | super(leftCommit.workspaceFolder, leftCommit.branch, leftCommit.logEntry); 46 | } 47 | } 48 | 49 | export class FileCommitDetails extends CommitDetails { 50 | constructor( 51 | workspaceFolder: string, 52 | branch: string, 53 | logEntry: LogEntry, 54 | public readonly committedFile: Readonly, 55 | ) { 56 | super(workspaceFolder, branch, logEntry); 57 | } 58 | } 59 | 60 | export class CompareFileCommitDetails extends FileCommitDetails { 61 | constructor( 62 | leftCommit: CommitDetails, 63 | public readonly rightCommit: CommitDetails, 64 | public readonly committedFile: Readonly, 65 | ) { 66 | super(leftCommit.workspaceFolder, leftCommit.branch, leftCommit.logEntry, committedFile); 67 | } 68 | } 69 | 70 | export interface ICommand extends Command { 71 | data: T; 72 | /** 73 | * Title of the command, like `save`. 74 | */ 75 | label: string; 76 | /** 77 | * A human readable string which is rendered less prominent. 78 | */ 79 | description: string; 80 | /** 81 | * A human readable string which is rendered less prominent. 82 | */ 83 | detail?: string; 84 | preExecute(): Promise; 85 | execute(): void | Promise | Thenable; 86 | } 87 | -------------------------------------------------------------------------------- /src/common/uiLogger.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { Disposable, OutputChannel, workspace } from 'vscode'; 3 | import { getLogChannel } from '../logger'; 4 | import { ILogService } from './types'; 5 | 6 | @injectable() 7 | export class OutputPanelLogger implements ILogService { 8 | private readonly outputChannel: OutputChannel; 9 | private enabled: boolean; 10 | private traceEnabled: boolean; 11 | private disposable: Disposable; 12 | constructor() { 13 | this.outputChannel = getLogChannel(); 14 | this.enabled = false; 15 | this.traceEnabled = false; 16 | 17 | this.updateEnabledFlag(); 18 | this.disposable = workspace.onDidChangeConfiguration(() => this.updateEnabledFlag()); 19 | } 20 | public dispose() { 21 | this.disposable.dispose(); 22 | } 23 | public log(...args: any[]): void { 24 | if (!this.enabled) { 25 | return; 26 | } 27 | 28 | const formattedText = this.formatArgs(...args); 29 | this.outputChannel.appendLine(formattedText); 30 | } 31 | public error(...args: any[]): void { 32 | const formattedText = this.formatArgs(...args); 33 | this.outputChannel.appendLine(formattedText); 34 | this.outputChannel.show(); 35 | } 36 | public trace(...args: any[]): void { 37 | if (!this.traceEnabled) { 38 | return; 39 | } 40 | const formattedText = this.formatArgs(...args); 41 | this.outputChannel.appendLine(formattedText); 42 | } 43 | public formatArgs(...args: any[]): string { 44 | return args 45 | .map(arg => { 46 | if (arg instanceof Error) { 47 | const error: { [key: string]: any } = {}; 48 | Object.getOwnPropertyNames(arg).forEach(key => { 49 | error[key] = arg[key]; 50 | }); 51 | 52 | return JSON.stringify(error); 53 | } else if (arg !== null && arg !== undefined && typeof arg === 'object') { 54 | return JSON.stringify(arg); 55 | } else if (typeof arg === 'string' && arg.startsWith('--format=')) { 56 | return '--pretty=oneline'; 57 | } else { 58 | return `${arg}`; 59 | } 60 | }) 61 | .join(' '); 62 | } 63 | 64 | private updateEnabledFlag() { 65 | const logLevel = workspace.getConfiguration('gitHistory').get('logLevel', 'None'); 66 | this.enabled = logLevel !== 'None'; 67 | this.traceEnabled = logLevel === 'Debug'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/common/uiService.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { CancellationTokenSource, QuickPickItem } from 'vscode'; 3 | import { IApplicationShell } from '../application/types'; 4 | import { ICommitCommandFactory, IFileCommitCommandFactory } from '../commandFactories/types'; 5 | import { IServiceContainer } from '../ioc/types'; 6 | import { BranchSelection } from '../types'; 7 | import { CommitDetails, FileCommitDetails, ICommand, IUiService } from './types'; 8 | 9 | const allBranches = '$(git-branch) All branches'; 10 | const currentBranch = '$(git-branch) Current branch'; 11 | 12 | @injectable() 13 | export class UiService implements IUiService { 14 | private selectionActionToken?: CancellationTokenSource; 15 | constructor( 16 | @inject(IServiceContainer) private serviceContainer: IServiceContainer, 17 | @inject(IApplicationShell) private application: IApplicationShell, 18 | ) {} 19 | 20 | public async getBranchSelection(): Promise { 21 | const itemPickList: QuickPickItem[] = []; 22 | itemPickList.push({ label: currentBranch, description: '' }); 23 | itemPickList.push({ label: allBranches, description: '' }); 24 | const modeChoice = await this.application.showQuickPick(itemPickList, { 25 | placeHolder: 'Show history for...', 26 | matchOnDescription: true, 27 | }); 28 | if (!modeChoice) { 29 | return; 30 | } 31 | 32 | return modeChoice.label === allBranches ? BranchSelection.All : BranchSelection.Current; 33 | } 34 | public async selectFileCommitCommandAction( 35 | fileCommit: FileCommitDetails, 36 | ): Promise | undefined> { 37 | if (this.selectionActionToken) { 38 | this.selectionActionToken.cancel(); 39 | } 40 | this.selectionActionToken = new CancellationTokenSource(); 41 | const commands = await this.serviceContainer 42 | .get(IFileCommitCommandFactory) 43 | .createCommands(fileCommit); 44 | const options = { matchOnDescription: true, matchOnDetail: true, token: this.selectionActionToken.token }; 45 | 46 | return this.application.showQuickPick(commands, options); 47 | } 48 | public async selectCommitCommandAction(commit: CommitDetails): Promise | undefined> { 49 | if (this.selectionActionToken) { 50 | this.selectionActionToken.cancel(); 51 | } 52 | this.selectionActionToken = new CancellationTokenSource(); 53 | const commands = await this.serviceContainer 54 | .get(ICommitCommandFactory) 55 | .createCommands(commit); 56 | const options = { matchOnDescription: true, matchOnDetail: true, token: this.selectionActionToken.token }; 57 | 58 | return this.application.showQuickPick(commands, options); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | 3 | export const gitHistorySchema = 'git-history-viewer'; 4 | export const previewUri = Uri.parse(`${gitHistorySchema}://authority/git-history`); 5 | export const extensionId = 'donjayamanne.githistory'; 6 | export const appinsightsKey = 'd6a4de02-e0fa-4515-be3d-7fe0ffa2f488'; 7 | 8 | // Will be set in extension activation. 9 | type ExtensionRoot = { path?: string }; 10 | export const extensionRoot: ExtensionRoot = { path: '' }; 11 | -------------------------------------------------------------------------------- /src/formatters/commitFormatter.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { EOL } from 'os'; 3 | import { LogEntry } from '../types'; 4 | import { ICommitViewFormatter } from './types'; 5 | 6 | @injectable() 7 | export class CommitViewFormatter implements ICommitViewFormatter { 8 | public format(item: LogEntry): string { 9 | const sb: string[] = []; 10 | 11 | if (item.hash && item.hash.full) { 12 | sb.push(`sha1 : ${item.hash.full}`); 13 | } 14 | if (item.author) { 15 | sb.push(this.formatAuthor(item)); 16 | } 17 | if (item.author && item.author.date) { 18 | const authorDate = item.author.date.toLocaleString(); 19 | sb.push(`Author Date : ${authorDate}`); 20 | } 21 | if (item.committer) { 22 | sb.push(`Committer : ${item.committer.name} <${item.committer.email}>`); 23 | } 24 | if (item.committer && item.committer.date) { 25 | const committerDate = item.committer.date.toLocaleString(); 26 | sb.push(`Commit Date : ${committerDate}`); 27 | } 28 | if (item.subject) { 29 | sb.push(`Subject : ${item.subject}`); 30 | } 31 | if (item.body) { 32 | sb.push(`Body : ${item.body}`); 33 | } 34 | if (item.notes) { 35 | sb.push(`Notes : ${item.notes}`); 36 | } 37 | 38 | return sb.join(EOL); 39 | } 40 | public formatAuthor(logEntry: LogEntry): string { 41 | return `Author : ${logEntry.author!.name} <${logEntry.author!.email}>`; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/formatters/types.ts: -------------------------------------------------------------------------------- 1 | import { LogEntry } from '../types'; 2 | 3 | export const ICommitViewFormatter = Symbol.for('ICommitViewFormatter'); 4 | 5 | export interface ICommitViewFormatter { 6 | format(logEntry: LogEntry): string; 7 | formatAuthor(logEntry: LogEntry): string; 8 | } 9 | -------------------------------------------------------------------------------- /src/ioc/container.ts: -------------------------------------------------------------------------------- 1 | import { Container, injectable, interfaces } from 'inversify'; 2 | import { Abstract, IServiceContainer, Newable } from './types'; 3 | 4 | @injectable() 5 | export class ServiceContainer implements IServiceContainer { 6 | constructor(private container: Container) {} 7 | public get(serviceIdentifier: interfaces.ServiceIdentifier): T { 8 | return this.container.get(serviceIdentifier); 9 | } 10 | public getAll( 11 | serviceIdentifier: string | symbol | Newable | Abstract, 12 | name?: string | number | symbol | undefined, 13 | ): T[] { 14 | return name 15 | ? this.container.getAllNamed(serviceIdentifier, name) 16 | : this.container.getAll(serviceIdentifier); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ioc/index.ts: -------------------------------------------------------------------------------- 1 | import { IServiceContainer } from './types'; 2 | 3 | let container: IServiceContainer; 4 | export function getServiceContainer() { 5 | return container; 6 | } 7 | export function setServiceContainer(serviceContainer: IServiceContainer) { 8 | container = serviceContainer; 9 | } 10 | -------------------------------------------------------------------------------- /src/ioc/serviceManager.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify'; 2 | import { Abstract, IServiceManager, Newable } from './types'; 3 | export class ServiceManager implements IServiceManager { 4 | constructor(private container: Container) {} 5 | public add( 6 | serviceIdentifier: string | symbol | Newable | Abstract, 7 | constructor: new (...args: any[]) => T, 8 | name?: string | number | symbol | undefined, 9 | ): void { 10 | if (name) { 11 | this.container 12 | .bind(serviceIdentifier) 13 | .to(constructor) 14 | .inSingletonScope() 15 | .whenTargetNamed(name); 16 | } else { 17 | this.container 18 | .bind(serviceIdentifier) 19 | .to(constructor) 20 | .inSingletonScope(); 21 | } 22 | } 23 | public addSingleton( 24 | serviceIdentifier: string | symbol | Newable | Abstract, 25 | constructor: new (...args: any[]) => T, 26 | name?: string | number | symbol | undefined, 27 | ): void { 28 | if (name) { 29 | this.container 30 | .bind(serviceIdentifier) 31 | .to(constructor) 32 | .inSingletonScope() 33 | .whenTargetNamed(name); 34 | } else { 35 | this.container 36 | .bind(serviceIdentifier) 37 | .to(constructor) 38 | .inSingletonScope(); 39 | } 40 | } 41 | public addSingletonInstance( 42 | serviceIdentifier: string | symbol | Newable | Abstract, 43 | instance: T, 44 | name?: string | number | symbol | undefined, 45 | ): void { 46 | if (name) { 47 | this.container 48 | .bind(serviceIdentifier) 49 | .toConstantValue(instance) 50 | .whenTargetNamed(name); 51 | } else { 52 | this.container.bind(serviceIdentifier).toConstantValue(instance); 53 | } 54 | } 55 | public get( 56 | serviceIdentifier: string | symbol | Newable | Abstract, 57 | name?: string | number | symbol | undefined, 58 | ): T { 59 | return name ? this.container.getNamed(serviceIdentifier, name) : this.container.get(serviceIdentifier); 60 | } 61 | public getAll( 62 | serviceIdentifier: string | symbol | Newable | Abstract, 63 | name?: string | number | symbol | undefined, 64 | ): T[] { 65 | return name 66 | ? this.container.getAllNamed(serviceIdentifier, name) 67 | : this.container.getAll(serviceIdentifier); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ioc/types.ts: -------------------------------------------------------------------------------- 1 | export interface Newable { 2 | new (...args: any[]): T; 3 | } 4 | export interface Abstract { 5 | prototype: T; 6 | } 7 | export type ServiceIdentifier = string | symbol | Newable | Abstract; 8 | 9 | export type ClassType = new (...args: any[]) => T; 10 | 11 | export const IServiceManager = Symbol.for('IServiceManager'); 12 | 13 | export interface IServiceManager { 14 | add(serviceIdentifier: ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void; 15 | addSingleton( 16 | serviceIdentifier: ServiceIdentifier, 17 | constructor: ClassType, 18 | name?: string | number | symbol, 19 | ): void; 20 | addSingletonInstance( 21 | serviceIdentifier: ServiceIdentifier, 22 | instance: T, 23 | name?: string | number | symbol, 24 | ): void; 25 | get(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T; 26 | getAll(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T[]; 27 | } 28 | 29 | export const IServiceContainer = Symbol.for('IServiceContainer'); 30 | export interface IServiceContainer { 31 | get(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T; 32 | getAll(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T[]; 33 | } 34 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | let outInfoChannel: vscode.OutputChannel; 4 | let outLogChannel: vscode.OutputChannel; 5 | const logLevel = vscode.workspace.getConfiguration('gitHistory').get('logLevel') as string; 6 | 7 | function getInfoChannel() { 8 | if (outInfoChannel === undefined) { 9 | outInfoChannel = vscode.window.createOutputChannel('Git History Info'); 10 | } 11 | return outInfoChannel; 12 | } 13 | 14 | export function getLogChannel() { 15 | if (outLogChannel === undefined) { 16 | outLogChannel = vscode.window.createOutputChannel('Git History'); 17 | } 18 | return outLogChannel; 19 | } 20 | 21 | export function logError(error: any) { 22 | getLogChannel().appendLine(`[Error-${getTimeAndms()}] ${error.toString()}`.replace(/(\r\n|\n|\r)/gm, '')); 23 | getLogChannel().show(); 24 | vscode.window.showErrorMessage("There was an error, please view details in the 'Git History Log' output window"); 25 | } 26 | 27 | export function logInfo(message: string) { 28 | if (logLevel === 'Info' || logLevel === 'Debug') { 29 | getLogChannel().appendLine(`[Info -${getTimeAndms()}] ${message}`); 30 | } 31 | } 32 | 33 | export function logDebug(message: string) { 34 | if (logLevel === 'Debug') { 35 | getLogChannel().appendLine(`[Debug-${getTimeAndms()}] ${message}`); 36 | } 37 | } 38 | 39 | function getTimeAndms(): string { 40 | const time = new Date(); 41 | const hours = `0${time.getHours()}`.slice(-2); 42 | const minutes = `0${time.getMinutes()}`.slice(-2); 43 | const seconds = `0${time.getSeconds()}`.slice(-2); 44 | const milliSeconds = `00${time.getMilliseconds()}`.slice(-3); 45 | return `${hours}:${minutes}:${seconds}.${milliSeconds}`; 46 | } 47 | 48 | export function showInfo(message: string) { 49 | getInfoChannel().clear(); 50 | getInfoChannel().appendLine(message); 51 | getInfoChannel().show(); 52 | } 53 | -------------------------------------------------------------------------------- /src/nodes/factory.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { CommitDetails, CompareCommitDetails, CompareFileCommitDetails, FileCommitDetails } from '../common/types'; 3 | import { CommittedFile } from '../types'; 4 | import { DirectoryNode, FileNode, INodeFactory } from './types'; 5 | 6 | @injectable() 7 | export class StandardNodeFactory implements INodeFactory { 8 | public createDirectoryNode(commit: CommitDetails, relativePath: string) { 9 | return new DirectoryNode(commit, relativePath); 10 | } 11 | public createFileNode(commit: CommitDetails, committedFile: CommittedFile) { 12 | return new FileNode( 13 | new FileCommitDetails(commit.workspaceFolder, commit.branch, commit.logEntry, committedFile), 14 | ); 15 | } 16 | } 17 | 18 | @injectable() 19 | export class ComparisonNodeFactory implements INodeFactory { 20 | public createDirectoryNode(commit: CommitDetails, relativePath: string) { 21 | return new DirectoryNode(commit, relativePath); 22 | } 23 | public createFileNode(commit: CommitDetails, committedFile: CommittedFile) { 24 | const compareCommit = commit as CompareCommitDetails; 25 | 26 | return new FileNode(new CompareFileCommitDetails(compareCommit, compareCommit.rightCommit, committedFile)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/nodes/nodeIcons.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | const resourcesPath = path.join(__dirname, '..', '..', 'resources'); 4 | 5 | export const AddedIcon = { 6 | light: path.join(resourcesPath, 'icons', 'light', 'status-added.svg'), 7 | dark: path.join(resourcesPath, 'icons', 'dark', 'status-added.svg'), 8 | }; 9 | export const RemovedIcon = { 10 | light: path.join(resourcesPath, 'icons', 'light', 'status-deleted.svg'), 11 | dark: path.join(resourcesPath, 'icons', 'dark', 'status-deleted.svg'), 12 | }; 13 | export const ModifiedIcon = { 14 | light: path.join(resourcesPath, 'icons', 'light', 'status-modified.svg'), 15 | dark: path.join(resourcesPath, 'icons', 'dark', 'status-modified.svg'), 16 | }; 17 | export const RenameIcon = { 18 | light: path.join(resourcesPath, 'icons', 'light', 'status-renamed.svg'), 19 | dark: path.join(resourcesPath, 'icons', 'dark', 'status-renamed.svg'), 20 | }; 21 | -------------------------------------------------------------------------------- /src/nodes/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../ioc/types'; 2 | import { ComparisonNodeFactory, StandardNodeFactory } from './factory'; 3 | import { INodeFactory } from './types'; 4 | 5 | export function registerTypes(serviceManager: IServiceManager) { 6 | serviceManager.addSingleton(INodeFactory, StandardNodeFactory, 'standard'); 7 | serviceManager.addSingleton(INodeFactory, ComparisonNodeFactory, 'comparison'); 8 | } 9 | -------------------------------------------------------------------------------- /src/nodes/treeNodes.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { DirectoryNode, FileNode } from '../nodes/types'; 3 | 4 | export class DirectoryTreeItem extends TreeItem { 5 | constructor(public readonly data: DirectoryNode) { 6 | super(data.label, TreeItemCollapsibleState.Collapsed); 7 | this.contextValue = 'folder'; 8 | } 9 | } 10 | 11 | export class FileTreeItem extends TreeItem { 12 | constructor(public readonly data: FileNode) { 13 | super(data.label, TreeItemCollapsibleState.None); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/nodes/types.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Uri } from 'vscode'; 3 | import { CommitDetails, FileCommitDetails } from '../common/types'; 4 | import { CommittedFile } from '../types'; 5 | import { DirectoryTreeItem, FileTreeItem } from './treeNodes'; 6 | 7 | export interface INode { 8 | /** 9 | * A human readable string which is rendered prominent. 10 | */ 11 | readonly label: string; 12 | readonly resource: Uri; 13 | readonly data?: T; 14 | readonly children: INode[]; 15 | } 16 | 17 | export abstract class AbstractCommitNode implements INode { 18 | public children: AbstractCommitNode[] = []; 19 | // @ts-ignore 20 | private _label: string; 21 | // @ts-ignore 22 | private _resource: Uri; 23 | public get label() { 24 | return this._label; 25 | } 26 | public get resource() { 27 | return this._resource; 28 | } 29 | constructor(public readonly data: T | undefined) {} 30 | protected setLabel(value: string) { 31 | this._label = value; 32 | } 33 | protected setResoruce(value: string | Uri) { 34 | this._resource = typeof value === 'string' ? Uri.file(value) : value; 35 | } 36 | } 37 | 38 | export class DirectoryNode extends AbstractCommitNode { 39 | constructor(commit: CommitDetails, relativePath: string) { 40 | super(commit); 41 | const folderName = path.basename(relativePath); 42 | this.setLabel(folderName); 43 | this.setResoruce(relativePath); 44 | } 45 | } 46 | 47 | export class FileNode extends AbstractCommitNode { 48 | constructor(commit: FileCommitDetails) { 49 | super(commit); 50 | const fileName = path.basename(commit.committedFile.relativePath); 51 | this.setLabel(fileName); 52 | this.setResoruce(commit.committedFile.relativePath); 53 | } 54 | } 55 | 56 | export const INodeFactory = Symbol.for('INodeFactory'); 57 | 58 | export interface INodeFactory { 59 | createDirectoryNode(commit: CommitDetails, relativePath: string): DirectoryNode; 60 | createFileNode(commit: CommitDetails, committedFile: CommittedFile): FileNode; 61 | } 62 | 63 | export const INodeBuilder = Symbol.for('INodeBuilder'); 64 | 65 | export interface INodeBuilder { 66 | buildTree(commit: CommitDetails, committedFiles: CommittedFile[]): (DirectoryNode | FileNode)[]; 67 | buildList(commit: CommitDetails, committedFiles: CommittedFile[]): FileNode[]; 68 | getTreeItem(node: DirectoryNode | FileNode): Promise; 69 | } 70 | -------------------------------------------------------------------------------- /src/platform/fileSystem.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | 'use strict'; 5 | 6 | import * as fs from 'fs'; 7 | import * as fse from 'fs-extra'; 8 | import { inject, injectable } from 'inversify'; 9 | import * as path from 'path'; 10 | import { IFileSystem, IPlatformService } from './types'; 11 | 12 | @injectable() 13 | export class FileSystem implements IFileSystem { 14 | constructor(@inject(IPlatformService) private platformService: IPlatformService) {} 15 | 16 | public get directorySeparatorChar(): string { 17 | return path.sep; 18 | } 19 | 20 | public objectExistsAsync(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { 21 | return new Promise(resolve => { 22 | fse.stat(filePath, (error, stats) => { 23 | if (error) { 24 | return resolve(false); 25 | } 26 | 27 | return resolve(statCheck(stats)); 28 | }); 29 | }); 30 | } 31 | 32 | public fileExistsAsync(filePath: string): Promise { 33 | return this.objectExistsAsync(filePath, stats => stats.isFile()); 34 | } 35 | 36 | public directoryExistsAsync(filePath: string): Promise { 37 | return this.objectExistsAsync(filePath, stats => stats.isDirectory()); 38 | } 39 | 40 | public createDirectoryAsync(directoryPath: string): Promise { 41 | return fse.mkdirp(directoryPath); 42 | } 43 | 44 | public getSubDirectoriesAsync(rootDir: string): Promise { 45 | return new Promise(resolve => { 46 | fs.readdir(rootDir, (error, files) => { 47 | if (error) { 48 | return resolve([]); 49 | } 50 | const subDirs: string[] = []; 51 | files.forEach(name => { 52 | const fullPath = path.join(rootDir, name); 53 | try { 54 | if (fs.statSync(fullPath).isDirectory()) { 55 | subDirs.push(fullPath); 56 | } 57 | } catch (ex) {} 58 | }); 59 | resolve(subDirs); 60 | }); 61 | }); 62 | } 63 | 64 | public arePathsSame(path1: string, path2: string): boolean { 65 | const path1ToCompare = path.normalize(path1); 66 | const path2ToCompare = path.normalize(path2); 67 | if (this.platformService.isWindows) { 68 | return path1ToCompare.toUpperCase() === path2ToCompare.toUpperCase(); 69 | } else { 70 | return path1ToCompare === path2ToCompare; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/platform/platformService.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 'use strict'; 4 | 5 | import { injectable } from 'inversify'; 6 | import { arch } from 'os'; 7 | import { IPlatformService } from './types'; 8 | 9 | @injectable() 10 | export class PlatformService implements IPlatformService { 11 | private _isWindows: boolean; 12 | private _isMac: boolean; 13 | 14 | constructor() { 15 | this._isWindows = /^win/.test(process.platform); 16 | this._isMac = /^darwin/.test(process.platform); 17 | } 18 | public get isWindows(): boolean { 19 | return this._isWindows; 20 | } 21 | public get isMac(): boolean { 22 | return this._isMac; 23 | } 24 | public get isLinux(): boolean { 25 | return !(this.isWindows || this.isMac); 26 | } 27 | public get is64bit(): boolean { 28 | return arch() === 'x64'; 29 | } 30 | public get pathVariableName() { 31 | return this.isWindows ? 'Path' : 'PATH'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/platform/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../ioc/types'; 2 | import { FileSystem } from './fileSystem'; 3 | import { PlatformService } from './platformService'; 4 | import { IFileSystem, IPlatformService } from './types'; 5 | 6 | export function registerTypes(serviceManager: IServiceManager) { 7 | serviceManager.add(IPlatformService, PlatformService); 8 | serviceManager.add(IFileSystem, FileSystem); 9 | } 10 | -------------------------------------------------------------------------------- /src/platform/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fs from 'fs'; 5 | 6 | export enum Architecture { 7 | Unknown = 1, 8 | x86 = 2, 9 | x64 = 3, 10 | } 11 | 12 | export const IPlatformService = Symbol.for('IPlatformService'); 13 | export interface IPlatformService { 14 | isWindows: boolean; 15 | isMac: boolean; 16 | isLinux: boolean; 17 | is64bit: boolean; 18 | pathVariableName: 'Path' | 'PATH'; 19 | } 20 | 21 | export const IFileSystem = Symbol.for('IFileSystem'); 22 | export interface IFileSystem { 23 | directorySeparatorChar: string; 24 | objectExistsAsync(path: string, statCheck: (s: fs.Stats) => boolean): Promise; 25 | fileExistsAsync(path: string): Promise; 26 | directoryExistsAsync(path: string): Promise; 27 | createDirectoryAsync(path: string): Promise; 28 | getSubDirectoriesAsync(rootDir: string): Promise; 29 | arePathsSame(path1: string, path2: string): boolean; 30 | } 31 | -------------------------------------------------------------------------------- /src/viewers/commitViewerFactory.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable, named } from 'inversify'; 2 | import { OutputChannel } from 'vscode'; 3 | import { ICommandManager } from '../application/types/commandManager'; 4 | import { IFileCommitCommandFactory } from '../commandFactories/types'; 5 | import { ICommitViewFormatter } from '../formatters/types'; 6 | import { NodeBuilder } from '../nodes/nodeBuilder'; 7 | import { INodeFactory } from '../nodes/types'; 8 | import { IPlatformService } from '../platform/types'; 9 | import { IOutputChannel } from '../types'; 10 | import { CommitViewer } from './commitViewer'; 11 | import { ICommitViewer, ICommitViewerFactory } from './types'; 12 | 13 | @injectable() 14 | export class CommitViewerFactory implements ICommitViewerFactory { 15 | // @ts-ignore 16 | private commitViewer: ICommitViewer; 17 | // @ts-ignore 18 | private compareViewer: ICommitViewer; 19 | constructor( 20 | @inject(IOutputChannel) private outputChannel: OutputChannel, 21 | @inject(ICommitViewFormatter) private commitFormatter: ICommitViewFormatter, 22 | @inject(ICommandManager) private commandManager: ICommandManager, 23 | @inject(IPlatformService) private platformService: IPlatformService, 24 | @inject(IFileCommitCommandFactory) private fileCommitFactory: IFileCommitCommandFactory, 25 | @inject(INodeFactory) @named('standard') private standardNodeFactory: INodeFactory, 26 | @inject(INodeFactory) @named('comparison') private compareNodeFactory: INodeFactory, 27 | ) {} 28 | public getCommitViewer(): ICommitViewer { 29 | if (this.commitViewer) { 30 | return this.commitViewer; 31 | } 32 | 33 | return (this.commitViewer = new CommitViewer( 34 | this.outputChannel, 35 | this.commitFormatter, 36 | this.commandManager, 37 | new NodeBuilder(this.fileCommitFactory, this.standardNodeFactory, this.platformService), 38 | 'commitViewProvider', 39 | 'git.commit.view.show', 40 | )); 41 | } 42 | public getCompareCommitViewer(): ICommitViewer { 43 | if (this.compareViewer) { 44 | return this.compareViewer; 45 | } 46 | 47 | return (this.compareViewer = new CommitViewer( 48 | this.outputChannel, 49 | this.commitFormatter, 50 | this.commandManager, 51 | new NodeBuilder(this.fileCommitFactory, this.compareNodeFactory, this.platformService), 52 | 'compareCommitViewProvider', 53 | 'git.commit.compare.view.show', 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/viewers/serviceRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IServiceManager } from '../ioc/types'; 2 | import { CommitViewerFactory } from './commitViewerFactory'; 3 | import { ICommitViewerFactory } from './types'; 4 | 5 | export function registerTypes(serviceManager: IServiceManager) { 6 | serviceManager.addSingleton(ICommitViewerFactory, CommitViewerFactory); 7 | } 8 | -------------------------------------------------------------------------------- /src/viewers/types.ts: -------------------------------------------------------------------------------- 1 | import { CommitDetails } from '../common/types'; 2 | import { CommitComparison } from '../types'; 3 | 4 | export const ICommitViewer = Symbol.for('ICommitViewer'); 5 | 6 | export interface ICommitViewer { 7 | readonly selectedCommit: Readonly; 8 | showCommit(commit: CommitDetails): void; 9 | showCommitTree(commit: CommitDetails): void; 10 | showFilesView(): void; 11 | showFolderView(): void; 12 | } 13 | 14 | export const ICompareCommitViewer = Symbol.for('ICompareCommitViewer'); 15 | 16 | export interface ICompareCommitViewer { 17 | readonly comparison: Readonly; 18 | showComparisonTree(comparison: CommitComparison): void; 19 | showFilesView(): void; 20 | showFolderView(): void; 21 | } 22 | 23 | export const ICommitViewerFactory = Symbol.for('ICommitViewerFactory'); 24 | 25 | export interface ICommitViewerFactory { 26 | getCommitViewer(): ICommitViewer; 27 | getCompareCommitViewer(): ICommitViewer; 28 | } 29 | -------------------------------------------------------------------------------- /test/common.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | 4 | export const extensionRootPath = path.resolve(__dirname, '..', '..'); 5 | export const tempFolder = path.join(extensionRootPath, 'temp'); 6 | export const tempRepoFolder = path.join(extensionRootPath, 'temp', 'testing'); 7 | export const noop = () => { 8 | //Noop. 9 | }; 10 | export const sleep = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)); 11 | 12 | /** 13 | * Wait for a condition to be fulfilled within a timeout. 14 | */ 15 | export async function waitForCondition( 16 | condition: () => Promise, 17 | timeoutMs: number, 18 | errorMessage: string, 19 | ): Promise { 20 | return new Promise(async (resolve, reject) => { 21 | const timeout = setTimeout(() => { 22 | clearTimeout(timeout); 23 | // tslint:disable-next-line: no-use-before-declare 24 | clearTimeout(timer); 25 | reject(new Error(errorMessage)); 26 | }, timeoutMs); 27 | const timer = setInterval(async () => { 28 | if (!(await condition().catch(() => false))) { 29 | return; 30 | } 31 | clearTimeout(timeout); 32 | clearTimeout(timer); 33 | resolve(); 34 | }, 10); 35 | }); 36 | } 37 | 38 | fs.mkdirpSync(tempFolder); 39 | fs.mkdirpSync(tempRepoFolder); 40 | -------------------------------------------------------------------------------- /test/extension/__mocks__/vscode.ts: -------------------------------------------------------------------------------- 1 | // jest doesn't seem to provide a way to inject global/dynamic imports. 2 | // Basically if we have a `require`, jest assumes that it is a module on disc. 3 | // However `vscode` isn't a module in disc, its provided by vscode host environment. 4 | // Hack - put vscode namespace into global variable `process`, and re-export as as mock in jest. 5 | // Using global variables don't work, as jest seems to fiddle with that as well. 6 | module.exports = (process as any).__VSCODE as typeof import('vscode'); 7 | -------------------------------------------------------------------------------- /test/extension/setup.ts: -------------------------------------------------------------------------------- 1 | // Initialization script for jest. 2 | // Used by both VS Code tests and non-vscode tests. 3 | 4 | // This line should always be right on top. 5 | // Other extensions use this, re-importing will cause data stored by this to wipe out data in other extensions using this same package. 6 | if ((Reflect as any).metadata === undefined) { 7 | require('reflect-metadata'); 8 | } 9 | -------------------------------------------------------------------------------- /test/extension/testRunner.ts: -------------------------------------------------------------------------------- 1 | import * as jest from 'jest'; 2 | import * as fs from 'fs-extra'; 3 | import * as path from 'path'; 4 | import { AggregatedResult } from '@jest/test-result'; 5 | import { tempRepoFolder } from '../common'; 6 | 7 | const extensionRoot = path.join(__dirname, '..', '..', '..'); 8 | type Output = { results: AggregatedResult }; 9 | export function run(): Promise { 10 | // jest doesn't seem to provide a way to inject global/dynamic imports. 11 | // Basically if we have a `require`, jest assumes that it is a module on disc. 12 | // However `vscode` isn't a module in disc, its provided by vscode host environment. 13 | // Hack - put vscode namespace into global variable `process`, and re-export as as mock in jest (see __mocks__/vscode.ts). 14 | // Using global variables don't work, as jest seems to fiddle with that as well. 15 | (process as any).__VSCODE = require('vscode'); 16 | 17 | const jestConfigFile = path.join(extensionRoot, 'jest.extension.config.js'); 18 | // eslint-disable-next-line @typescript-eslint/no-var-requires 19 | const jestConfig = require(jestConfigFile); 20 | return new Promise((resolve, reject) => { 21 | jest.runCLI(jestConfig, [extensionRoot]) 22 | .catch(error => { 23 | delete (process as any).__VSCODE; 24 | console.error('Calling jest.runCLI failed', error); 25 | reject(error); 26 | }) 27 | .then(output => { 28 | delete (process as any).__VSCODE; 29 | if (!output) { 30 | return resolve(); 31 | } 32 | const results = output as Output; 33 | if (results.results.numFailedTestSuites || results.results.numFailedTests) { 34 | // Do not reject, VSC does not exit gracefully, hence test job hangs. 35 | // We don't want that, specially on CI. 36 | fs.appendFileSync(path.join(tempRepoFolder, 'tests.failed'), 'failed'); 37 | } 38 | resolve(); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/mocks.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify'; 2 | import { Abstract, IServiceContainer, IServiceManager, Newable } from '../src/ioc/types'; 3 | 4 | export class TestServiceContainer implements IServiceContainer, IServiceManager { 5 | private cont: Container; 6 | constructor() { 7 | this.cont = new Container(); 8 | } 9 | public add( 10 | serviceIdentifier: string | symbol | Newable | Abstract, 11 | constructor: new (...args: any[]) => T, 12 | name?: string | number | symbol | undefined, 13 | ): void { 14 | if (name) { 15 | this.cont 16 | .bind(serviceIdentifier) 17 | .to(constructor) 18 | .whenTargetNamed(name); 19 | } else { 20 | this.cont.bind(serviceIdentifier).to(constructor); 21 | } 22 | } 23 | public addSingleton( 24 | serviceIdentifier: string | symbol | Newable | Abstract, 25 | constructor: new (...args: any[]) => T, 26 | name?: string | number | symbol | undefined, 27 | ): void { 28 | if (name) { 29 | this.cont 30 | .bind(serviceIdentifier) 31 | .to(constructor) 32 | .whenTargetNamed(name); 33 | } else { 34 | this.cont.bind(serviceIdentifier).to(constructor); 35 | } 36 | } 37 | public addSingletonInstance( 38 | serviceIdentifier: string | symbol | Newable | Abstract, 39 | instance: T, 40 | name?: string | number | symbol | undefined, 41 | ): void { 42 | if (name) { 43 | this.cont 44 | .bind(serviceIdentifier) 45 | .toConstantValue(instance) 46 | .whenTargetNamed(name); 47 | } else { 48 | this.cont.bind(serviceIdentifier).toConstantValue(instance); 49 | } 50 | } 51 | public get( 52 | serviceIdentifier: string | symbol | Newable | Abstract, 53 | name?: string | number | symbol | undefined, 54 | ): T { 55 | return name ? this.cont.getNamed(serviceIdentifier, name) : this.cont.get(serviceIdentifier); 56 | } 57 | public getAll( 58 | serviceIdentifier: string | symbol | Newable | Abstract, 59 | name?: string | number | symbol | undefined, 60 | ): T[] { 61 | return name ? this.cont.getAllNamed(serviceIdentifier, name) : this.cont.getAll(serviceIdentifier); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/non-extension/__mocks__/vsc/README.md: -------------------------------------------------------------------------------- 1 | # This folder contains classes exposed by VS Code required in running the unit tests. 2 | * These classes are only used when running unit tests that are not hosted by VS Code. 3 | * So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. 4 | * The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. 5 | * Everything in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. 6 | This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. 7 | 8 | # Source borrowed from https://github.com/microsoft/vscode-python/tree/master/src/test/mocks/vsc 9 | -------------------------------------------------------------------------------- /test/non-extension/__mocks__/vsc/strings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | /* eslint-disable */ 8 | 9 | export namespace vscMockStrings { 10 | /** 11 | * Determines if haystack starts with needle. 12 | */ 13 | export function startsWith(haystack: string, needle: string): boolean { 14 | if (haystack.length < needle.length) { 15 | return false; 16 | } 17 | 18 | for (let i = 0; i < needle.length; i++) { 19 | if (haystack[i] !== needle[i]) { 20 | return false; 21 | } 22 | } 23 | 24 | return true; 25 | } 26 | 27 | /** 28 | * Determines if haystack ends with needle. 29 | */ 30 | export function endsWith(haystack: string, needle: string): boolean { 31 | let diff = haystack.length - needle.length; 32 | if (diff > 0) { 33 | return haystack.indexOf(needle, diff) === diff; 34 | } else if (diff === 0) { 35 | return haystack === needle; 36 | } else { 37 | return false; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/non-extension/__mocks__/vsc/telemetryReporter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | 'use strict'; 5 | 6 | /* eslint-disable */ 7 | export class vscMockTelemetryReporter { 8 | constructor() { 9 | // 10 | } 11 | 12 | public sendTelemetryEvent(): void { 13 | // 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/non-extension/__mocks__/vscode.ts: -------------------------------------------------------------------------------- 1 | import { initialize } from './vscode-mock'; 2 | 3 | module.exports = initialize(); 4 | -------------------------------------------------------------------------------- /test/non-extension/adapter/parsers/actionDetails/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ActionDetailsParser } from '../../../../../src/adapter/parsers/actionDetails/parser'; 3 | 4 | describe('Adapter Parser ActionDetails', () => { 5 | test('Information is returned correctly', () => { 6 | const date = new Date(); 7 | const name = `Don Jayamanne ${date.getTime()}`; 8 | const email = `don.jayamanne@yahoo.com ${date.getTime()}`; 9 | const unixTime = (date.getTime() / 1000).toString(); 10 | const info = new ActionDetailsParser().parse(name, email, unixTime)!; 11 | expect(info).to.have.property('email', email, 'email property not in parsed details'); 12 | expect(info).to.have.property('name', name, 'name property not in parsed details'); 13 | expect(info.date.toLocaleDateString()).is.equal(date.toLocaleDateString(), 'time is incorrect'); 14 | }); 15 | 16 | test('Undefined is returned if information is empty', () => { 17 | const info = new ActionDetailsParser().parse('', '', '')!; 18 | expect(info).to.be.an('undefined', 'Action details must be undefined'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/non-extension/adapter/parsers/actionedDetailsParser.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { ActionDetailsParser } from '../../../../src/adapter/parsers/actionDetails/parser'; 3 | 4 | describe('Adapter ActionedDetails Parser', () => { 5 | test('Information is returned correctly', () => { 6 | const name = `Don Jayamanne ${new Date().getTime()}`; 7 | const email = `don.jayamanne@yahoo.com ${new Date().getTime()}`; 8 | const date = new Date(); 9 | const unixTime = (date.getTime() / 1000).toString(); 10 | // const localisedDate = ''; // formatDate(date); 11 | const info = new ActionDetailsParser().parse(name, email, unixTime); 12 | if (info) { 13 | assert.equal(info.name, name, 'name is incorrect'); 14 | assert.equal(info.email, email, 'email is incorrect'); 15 | assert.equal(info.date.toLocaleDateString(), date.toLocaleDateString(), 'time is incorrect'); 16 | } else { 17 | assert.fail(info, {}, 'Expected non empty info', '='); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/non-extension/adapter/parsers/fileStatus/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as TypeMoq from 'typemoq'; 3 | import { FileStatStatusParser } from '../../../../../src/adapter/parsers/fileStatStatus/parser'; 4 | import { ILogService } from '../../../../../src/common/types'; 5 | import { Status } from '../../../../../src/types'; 6 | 7 | describe('Adapter Parser File Status', () => { 8 | test('Ensure status can be parsed correctly', () => { 9 | const parser = new FileStatStatusParser([TypeMoq.Mock.ofType().object]); 10 | ['A', 'M', 'D', 'C', 'R', 'C1234', 'R1234', 'U', 'X', 'B', 'T'].forEach(status => { 11 | assert.isTrue(parser.canParse(status), `Status '${status}' must be parseable`); 12 | }); 13 | ['a', 'm', 'd', 'C', 'R', 'C1234', 'R1234', 'U', 'X', 'B', 'T', 'Z', '1', '2', 'x', 'fc1234', 'eR1234'].forEach( 14 | status => { 15 | assert.isFalse( 16 | parser.canParse(status.toLocaleLowerCase()), 17 | `Status '${status.toLocaleLowerCase()}' must not be parseable`, 18 | ); 19 | }, 20 | ); 21 | }); 22 | 23 | test('Ensure status is parsed correctly', () => { 24 | const parser = new FileStatStatusParser([TypeMoq.Mock.ofType().object]); 25 | const statuses = [ 26 | ['A', Status.Added], 27 | ['M', Status.Modified], 28 | ['D', Status.Deleted], 29 | ['C', Status.Copied], 30 | ['R', Status.Renamed], 31 | ['C1234', Status.Copied], 32 | ['U', Status.Unmerged], 33 | ['X', Status.Unknown], 34 | ['B', Status.Broken], 35 | ['T', Status.TypeChanged], 36 | ['R1234', Status.Renamed], 37 | ]; 38 | statuses.forEach(status => { 39 | assert.equal( 40 | parser.parse((status[0] as any) as string), 41 | status[1] as Status, 42 | `Status '${status[0]}' not parsed correctly`, 43 | ); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import { runTests } from 'vscode-test'; 4 | import { extensionRootPath, tempRepoFolder } from './common'; 5 | import { setupDefaultRepo } from './extension/repoSetup'; 6 | 7 | async function main() { 8 | try { 9 | // The folder containing the Extension Manifest package.json 10 | // Passed to `--extensionDevelopmentPath` 11 | const extensionDevelopmentPath = extensionRootPath; 12 | 13 | // The path to the extension test script 14 | // Passed to --extensionTestsPath 15 | const extensionTestsPath = path.resolve(__dirname, './extension/testRunner'); 16 | fs.mkdirpSync(path.join(tempRepoFolder, 'test_gitHistory')); 17 | 18 | // We'll check if this file exists, if it does, then tests failed. 19 | const testResultFailedFile = path.join(tempRepoFolder, 'tests.failed'); 20 | if (fs.existsSync(testResultFailedFile)) { 21 | fs.unlinkSync(testResultFailedFile); 22 | } 23 | 24 | await setupDefaultRepo(); 25 | 26 | // Download VS Code, unzip it and run the integration test 27 | await runTests({ 28 | extensionDevelopmentPath, 29 | extensionTestsPath, 30 | launchArgs: [path.join(tempRepoFolder, 'test_gitHistory'), '--disable-extensions'], 31 | extensionTestsEnv: { IS_TEST_MODE: '1' }, 32 | }); 33 | 34 | if (fs.existsSync(testResultFailedFile)) { 35 | console.error('Failed to run tests'); 36 | process.exit(1); 37 | } 38 | } catch (err) { 39 | console.error('Failed to run tests'); 40 | process.exit(1); 41 | } 42 | } 43 | 44 | main(); 45 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | // Initialization script for jest. 2 | // Used by both VS Code tests and non-vscode tests. 3 | 4 | // This line should always be right on top. 5 | // Other extensions use this, re-importing will cause data stored by this to wipe out data in other extensions using this same package. 6 | if ((Reflect as any).metadata === undefined) { 7 | require('reflect-metadata'); 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "lib": [ 5 | "dom", 6 | "es6" 7 | ], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "sourceMap": true, 12 | "target": "es6", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": false, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": false, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "strictNullChecks": true, 22 | "suppressImplicitAnyIndexErrors": true 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "./node_modules", 27 | "./node_modules/@types", 28 | "/node_modules/@types/**/*", 29 | "./temp/**/*", 30 | ".vscode-test", 31 | "browser", 32 | "browser/src/**/*" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | // variables 7 | const isProduction = process.argv.indexOf('-p') >= 0; 8 | 9 | const browserSourcePath = path.join(__dirname, './browser/src'); 10 | const serverSourcePath = path.join(__dirname, './src'); 11 | 12 | const outPath = path.join(__dirname, './dist'); 13 | 14 | // cleanup dist directory 15 | fs.emptyDirSync(outPath); 16 | 17 | const browser = { 18 | mode: isProduction ? 'production' : 'development', 19 | context: browserSourcePath, 20 | entry: ['./index.tsx', './main.css'], 21 | output: { 22 | path: path.join(outPath, 'browser'), 23 | filename: 'bundle.js', 24 | }, 25 | target: 'web', 26 | resolve: { 27 | extensions: ['.js', '.ts', '.tsx'], 28 | mainFields: ['main'], 29 | }, 30 | devtool: isProduction ? false : 'source-map', 31 | module: { 32 | rules: [ 33 | // .ts, .tsx 34 | { 35 | test: /\.tsx?$/, 36 | loader: 'ts-loader', 37 | options: { 38 | configFile: 'browser/tsconfig.json', 39 | }, 40 | }, 41 | // scss 42 | { 43 | test: /\.css$/, 44 | use: [ 45 | { 46 | loader: 'file-loader', 47 | options: { 48 | name: 'bundle.css', 49 | }, 50 | }, 51 | 'extract-loader', 52 | 'css-loader?-url', 53 | ], 54 | }, 55 | ], 56 | }, 57 | plugins: [ 58 | new CopyWebpackPlugin({ 59 | patterns: [ 60 | { from: 'index.ejs', to: path.join(outPath, 'browser', 'src', 'index.ejs') }, 61 | ] 62 | }), 63 | ], 64 | }; 65 | 66 | const server = { 67 | mode: isProduction ? 'production' : 'development', 68 | context: serverSourcePath, 69 | entry: ['./extension.ts'], 70 | output: { 71 | path: path.join(outPath, 'src'), 72 | filename: 'extension.js', 73 | libraryTarget: 'commonjs2', 74 | devtoolModuleFilenameTemplate: '[absoluteResourcePath]', 75 | }, 76 | target: 'node', 77 | node: { 78 | __dirname: false, 79 | }, 80 | resolve: { 81 | extensions: ['.js', '.ts'], 82 | }, 83 | devtool: isProduction ? false : 'source-map', 84 | externals: { 85 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 86 | }, 87 | module: { 88 | rules: [ 89 | { 90 | test: /\.ts$/, 91 | exclude: /node_modules/, 92 | use: [ 93 | { 94 | loader: 'ts-loader', 95 | }, 96 | ], 97 | }, 98 | ], 99 | }, 100 | }; 101 | 102 | module.exports = [browser, server]; 103 | --------------------------------------------------------------------------------