├── .eslintrc ├── .github └── workflows │ └── publish-extension.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── launch.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── Icon.svg ├── docs │ ├── branch_warn.png │ ├── check_config_variables.png │ ├── close_ide.png │ ├── detached_head.png │ ├── git-assistant.png │ ├── message_auto_resolve.png │ ├── no_remote.png │ ├── publish_branch.png │ ├── pull_commits.png │ ├── push_commits.png │ ├── push_submodules_first.png │ └── update_submodules.png └── icon.png ├── package-lock.json ├── package.json ├── src ├── UI │ ├── InformationMessage.ts │ ├── InputBox.ts │ ├── Logger.ts │ ├── MessageOption.ts │ ├── QuickPick.ts │ ├── QuickPickOption.ts │ ├── Status.ts │ ├── StatusBar.ts │ └── StatusItem.ts ├── application │ ├── CMD.ts │ ├── Commands.ts │ ├── Config.ts │ ├── Features.ts │ ├── GitRepository.ts │ ├── Helper.ts │ ├── VsCode.ts │ └── Watcher.ts ├── commands │ ├── Command.ts │ ├── PushBeforeClosingIDE.command.ts │ ├── ShowOutput.command.ts │ ├── StartExtension.command.ts │ └── StopExtension.command.ts ├── extension.ts ├── handlers │ ├── ChangeHandler.ts │ ├── EventHandler.ts │ ├── exit │ │ └── PushBeforeClosingIDE.handler.ts │ ├── git │ │ ├── GitHandler.handler.ts │ │ ├── branch_changed │ │ │ ├── BranchWarn.handler.ts │ │ │ ├── CheckForRemote.handler.ts │ │ │ └── DetectDetachedHead.handler.ts │ │ ├── commits │ │ │ ├── MergeCommits.handler.ts │ │ │ ├── PullCommits.handler.ts │ │ │ └── PushCommit.handler.ts │ │ └── push │ │ │ └── PushSubmodulesFirst.handler.ts │ ├── start │ │ ├── CheckConfigVariables.handler.ts │ │ ├── CheckRemoteChanges.handler.ts │ │ ├── PerformStartupCheckOfRepositories.handler.ts │ │ ├── UpdateInitSubmodules.handler.ts │ │ └── WatcherStart.handler.ts │ └── submodule │ │ ├── SubmoduleHandler.handler.ts │ │ └── update │ │ └── HandleSubmoduleUpdate.handler.ts └── models │ ├── Branch.ts │ ├── Event.ts │ ├── Git.ts │ └── Submodule.ts ├── tsconfig.json ├── tslint.json ├── version-bump.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": false, 5 | "es6": true 6 | }, 7 | "extends": ["prettier/@typescript-eslint"], 8 | "globals": {}, 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 2018, 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["@typescript-eslint", "prettier"], 15 | "rules": { 16 | "prettier/prettier": [ 17 | "error", 18 | { 19 | "endOfLine": "auto" 20 | } 21 | ], 22 | "node/no-unpublished-require": "off", 23 | "node/no-unsupported-features/es-syntax": "off", 24 | "import/no-unresolved": "off", 25 | "import/prefer-default-export": "off", 26 | "no-unused-vars": "off", 27 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], 28 | "arrow-body-style": ["warn", "as-needed"], 29 | "no-async-promise-executor": "off", 30 | "func-names": "off" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-extension.yml: -------------------------------------------------------------------------------- 1 | name: build and publish extension 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: # dont run when changes made to these folders 8 | - '.vscode/**' 9 | 10 | jobs: 11 | publish: 12 | name: build & publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout repo 16 | uses: actions/checkout@v2 17 | 18 | - name: setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '14.x' 22 | 23 | - name: install dependencies 24 | run: npm i 25 | 26 | - name: 'Automated Version Bump' 27 | uses: 'phips28/gh-action-bump-version@master' 28 | with: 29 | tag-prefix: 'release' 30 | 31 | - name: build extension 32 | run: npm run vscode:package 33 | 34 | - name: publish to marketplace 35 | run: npx vsce publish -p $VSCE_TOKEN 36 | env: 37 | VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }} 38 | 39 | - name: get-npm-version 40 | id: package-version 41 | uses: martinbeentjes/npm-get-version-action@master 42 | 43 | - uses: actions/upload-artifact@v2 44 | with: 45 | name: git-assistant-${{ steps.package-version.outputs.current-version }}.vsix 46 | path: git-assistant-*.vsix 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode/settings.json 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 120, 4 | "tabWidth": 3, 5 | "singleQuote": true, 6 | "useTabs": true, 7 | "arrowParens": "always", 8 | "trailingComma": "all" 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.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 11 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 12 | "preLaunchTask": "npm: webpack" 13 | }, 14 | { 15 | "name": "Extension Tests", 16 | "type": "extensionHost", 17 | "request": "launch", 18 | "runtimeExecutable": "${execPath}", 19 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test"], 20 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 21 | "preLaunchTask": "npm: test-compile" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | images/docs/* 3 | images/icon.svg 4 | node_modules/** 5 | src/ 6 | .gitignore 7 | .vscodeignore 8 | package.lock.json 9 | package.json 10 | tsconfig.json 11 | tslint.json 12 | webpack.config.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "git-assistant" extension will be documented in this file. 4 | 5 | ## 1.0.0 6 | 7 | - Initial release of extension 8 | 9 | ## 1.1.0 10 | 11 | - Use webpack for bundling 12 | 13 | ## 1.2.0 14 | 15 | - Configured branch in .gitmodules is considered when looking for correct branch after a detached HEAD 16 | 17 | ## 1.2.2 18 | 19 | - fix security issue of a dependency 20 | 21 | ## 1.3.0 22 | 23 | - fix #8: closing IDE with keybinding works when no git-repository is opened in workspace 24 | - update all dependencies 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hofer Ivan 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 (Submodule) Assistant 2 | 3 | > Not under active development! Feel free to contribute to the project if you like seeing it improve. I (the original maintainer) don't use submodules and this extension anymore. 4 | 5 | 'Git (Submodule) Assistant' is an extension for VS Code that helps you preventing common problems when handling with Git-repositories. Specially the use of Submodules in a project, when done wrong, can introduce some unintended problems. This extension detects these problems, notifies and assists you with fixes. The fixes can be also applied automatically as soon as the problem is detected. 6 | 7 | [See extension in the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ivanhofer.git-assistant) 8 | 9 | ![image - 'Git (Submodule) Assistant'](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/git-assistant.png "image - 'Git (Submodule) Assistant'") 10 | 11 | ## Usage 12 | 13 | Simply install this extension in VS Code and open a Git-repository\*. Then the extension will enable itself and assist you. 14 | 15 | ##### \* the ".git" Folder must be located in the root of the opened Workspace-Folder 16 | 17 | ## Features 18 | 19 | The 'Git (Submodule) Assistant' runs as a background service and integrates in the VS Code taskbar. It offers a few features, that can detect common mistakes when handling with Git-repositories. You'll get a notification and the possibility to fix the problem with a simple click. Every feature can be disabled and configured in the VS Code settings. For many features it is possible to enable an auto-fix (see section SETTINGS): as soon as the problem is detected 'Git (Submodule) Assistant' will solve it for you. 20 | 21 | 'Git (Submodule) Assistant' helps you with the following things: 22 | 23 | ### It will warn you if you work on a branch where you should not commit (e.g. "master"-branch) 24 | 25 | Whenever you are on a branch, you should not commit, 'Git (Submodule) Assistant' will display a warning. The names of the branches can be configured in the VS Code settings panel. You can interact with the notification and easily switch to another branch. 26 | 27 | ![image - branch warn](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/branch_warn.png 'image - branch warn') 28 | 29 | ### It will warn you if you close VS Code and not all commits were pushed to the remote 30 | 31 | If you have changes that are not pushed to the remote and you try to close VS Code, 'Git (Submodule) Assistant' will prevent VS Code from closing and let you choose if you wish to push the changes or close VS Code. When you choose to push the changes, all commits will be pushed to the remote and VS Code will close itself. Unfurtunately, this will only work if you close VS Code by keyboard shortcut. If you close VS Code with your mouse by pressing the 'x'-button, the check fur unpushed commits will not perform. 32 | 33 | ![image - close IDE](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/close_ide.png 'image - close IDE') 34 | 35 | ### It will allow you to push every commit immediately to the remote 36 | 37 | You'll get an Notification, that allows you to push your changes as soon as you commit them. You can configure the behaviour to let 'Git (Submodule) Assistant' push all your changes automatically as soon you commit something. 38 | 39 | ![image - push commits](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/push_commits.png 'image - push commits') 40 | 41 | ### It will check for new commits on the remote in a certain intervall and notify you if you'r not up-to-date 42 | 43 | You can specify an intervall, where 'Git (Submodule) Assistant' will look for new changes on the remote. If changes were found you'll get a notification. It is also possible to let 'Git (Submodule) Assistant' download the commits automatically if there are no cahnges in the current workspace. The check for new commits will also be applied when you open VS Code. 44 | 45 | ![image - pull commits](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/pull_commits.png 'image - pull commits') 46 | 47 | ### It will check your Submodules, initialize and update them 48 | 49 | Ever had the problem you had to update every submodule by hand? From now on 'Git (Submodule) Assistant' will handle that for you. It will check for new submodules, update all existing submodules in your repository and also checkout the correct branch for each of your submodules\*. 50 | 51 | ##### \* depending on how many submodules you have, it may take a few seconds until every submodule is up-to-date 52 | 53 | ![image - update submodules](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/update_submodules.png 'image - update submodules') 54 | 55 | ### It will detect an “detached HEAD” and allows to checkout the corresponding branch 56 | 57 | 'Git (Submodule) Assistant' will detect if the HEAD of your repository is detached. If that happens, you'll get a notification, that informs you about the status of your repository, to prevent you from unitended data loss. In the VS Code settings you can configure that 'Git (Submodule) Assistant' should checkout the correct branch for you as soon as detached HEAD is detected. 58 | 59 | ![image - detached head](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/detached_head.png 'image - push submodules first') 60 | 61 | ### It will also push your Submodules if the main-repository is pushed 62 | 63 | To prevent that you forget to push your submodules, and nobody can clone the main repository anymore, 'Git (Submodule) Assistant' will push your submodules before it will push the commits from your main repositroy. 64 | 65 | ![image - push submodules first](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/push_submodules_first.png 'image - detached head') 66 | 67 | ### It will warn you if you don't have configured a remote in your repository 68 | 69 | If you start a new Git project and forgot to add a remote, 'Git (Submodule) Assistant' will show a notification to inform you about the missing remote. This should prevent accidential data loss. 70 | 71 | ![image - publish branch](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/no_remote.png 'image - no remote') 72 | 73 | ### It will warn you if you forgot to publish a branch to the remote 74 | 75 | If you create a new brnach and forgot to publish it, 'Git (Submodule) Assistant' will show a notification. If you want to publish the branch, just choose your desired remote. 'Git (Submodule) Assistant' can also publish every branch automatically, if you configure it in the settings. In the settings you can also specify a default remote where new branches are automatically published. 76 | 77 | ![image - check config variables](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/publish_branch.png 'image - publish branch') 78 | 79 | ### It will detect if some Git-configurations are missing and prompt you to fill them in 80 | 81 | In case you forgot to configure essential Git settings, 'Git (Submodule) Assistant' will inform you and you can simply enter the value of the variable in VS Code. No command line is required. In the settings, you can choose variables 'Git (Submodule) Assistant' should look for in you Git config file. 82 | 83 | ![image - no remote](https://raw.githubusercontent.com/ivanhofer/git-assistant/main/images/docs/check_config_variables.png 'image - check config variables') 84 | 85 | ## Settings 86 | 87 | If you get too many notifications or if you always click on "yes" to let 'Git (Submodule) Assistant' resolve the problem for you, there is an option for most features to save you some time: if a feature is set to "auto", it will not display a notification and handle the problem automatically for you. 88 | 89 | It is possible to disable the extension in the Settings. This is useful if you only want the extension to assist you in some of your Git-repositories. You can configure project-specific features in your "workspace-settings". 90 | 91 | A feature, if not needed, can be disabled in the settings panel. 92 | 93 | ## Feedback 94 | 95 | Please share your thougts on the [GitHub page](https://github.com/ivanhofer/git-assistant) of the project. 96 | 97 | ## Dependencies 98 | 99 | - [deep-diff](https://github.com/flitbit/diff) 100 | - [simple-git](https://github.com/steveukx/git-js) 101 | -------------------------------------------------------------------------------- /images/Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 15 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /images/docs/branch_warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/branch_warn.png -------------------------------------------------------------------------------- /images/docs/check_config_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/check_config_variables.png -------------------------------------------------------------------------------- /images/docs/close_ide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/close_ide.png -------------------------------------------------------------------------------- /images/docs/detached_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/detached_head.png -------------------------------------------------------------------------------- /images/docs/git-assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/git-assistant.png -------------------------------------------------------------------------------- /images/docs/message_auto_resolve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/message_auto_resolve.png -------------------------------------------------------------------------------- /images/docs/no_remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/no_remote.png -------------------------------------------------------------------------------- /images/docs/publish_branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/publish_branch.png -------------------------------------------------------------------------------- /images/docs/pull_commits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/pull_commits.png -------------------------------------------------------------------------------- /images/docs/push_commits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/push_commits.png -------------------------------------------------------------------------------- /images/docs/push_submodules_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/push_submodules_first.png -------------------------------------------------------------------------------- /images/docs/update_submodules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/docs/update_submodules.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanhofer/git-assistant/ad69624e170bfe6abae3381e02ec8ec4e38d1db7/images/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-assistant", 3 | "displayName": "Git (Submodule) Assistant", 4 | "description": "VS Code extension that detects common git (and submodule) issues and helps to solve them", 5 | "version": "1.3.14", 6 | "publisher": "ivanhofer", 7 | "repository": "https://github.com/ivanhofer/git-assistant", 8 | "license": "MIT", 9 | "scripts": { 10 | "vscode:prepublish": "webpack --mode none", 11 | "vscode:package": "npm run vscode:prepublish && vsce package", 12 | "vscode:publish": "npm run vscode:prepublish && vsce package && vsce publish", 13 | "webpack": "webpack --mode none", 14 | "webpack-dev": "webpack --mode development --watch", 15 | "test-compile": "tsc -p ./", 16 | "version-bump": "node version-bump", 17 | "lint:fix": "eslint --fix src/**/*.ts" 18 | }, 19 | "devDependencies": { 20 | "@types/deep-diff": "^1.0.0", 21 | "@types/glob": "^7.1.2", 22 | "@types/node": "^14.0.13", 23 | "@typescript-eslint/eslint-plugin": "^3.2.0", 24 | "@typescript-eslint/parser": "^3.2.0", 25 | "copy-webpack-plugin": "^6.0.2", 26 | "eslint": "^7.2.0", 27 | "eslint-config-prettier": "^6.11.0", 28 | "eslint-plugin-prettier": "^3.1.3", 29 | "prettier": "^2.0.5", 30 | "ts-loader": "^7.0.5", 31 | "typescript": "^3.9.5", 32 | "vsce": "^1.76.1", 33 | "vscode": "^1.1.37", 34 | "webpack": "^4.43.0", 35 | "webpack-cli": "^3.3.11" 36 | }, 37 | "dependencies": { 38 | "deep-diff": "^1.0.2", 39 | "simple-git": "^1.132.0" 40 | }, 41 | "engines": { 42 | "vscode": "^1.33.0" 43 | }, 44 | "categories": [ 45 | "Other" 46 | ], 47 | "keywords": [ 48 | "git", 49 | "submodule", 50 | "assistant", 51 | "helper", 52 | "workflow" 53 | ], 54 | "icon": "images/icon.png", 55 | "galleryBanner": { 56 | "color": "#F2A52A", 57 | "theme": "light" 58 | }, 59 | "activationEvents": [ 60 | "*" 61 | ], 62 | "main": "./dist/extension", 63 | "contributes": { 64 | "commands": [ 65 | { 66 | "command": "git-assistant.startGitAssisitant", 67 | "title": "Git Assistant - Start Watching" 68 | }, 69 | { 70 | "command": "git-assistant.stopGitAssisitant", 71 | "title": "Git Assistant - Stop Watching" 72 | }, 73 | { 74 | "command": "git-assistant.showOutput", 75 | "title": "Git Assistant - Show Logging Output" 76 | } 77 | ], 78 | "keybindings": [ 79 | { 80 | "command": "git-assistant.pushBeforeClosingIDEhard", 81 | "title": "Quit VSCode", 82 | "key": "ctrl+Q", 83 | "mac": "cmd+Q" 84 | }, 85 | { 86 | "command": "git-assistant.pushBeforeClosingIDE", 87 | "title": "Close Editor", 88 | "key": "ctrl+W", 89 | "mac": "cmd+W" 90 | } 91 | ], 92 | "configuration": { 93 | "type": "object", 94 | "title": "Git Assistant", 95 | "properties": { 96 | "git-assistant.enabled": { 97 | "type": "boolean", 98 | "default": true, 99 | "description": "enables git-assistant on startup" 100 | }, 101 | "git-assistant.message-wait-time": { 102 | "type": "number", 103 | "default": 60000, 104 | "description": "time in ms to wait for an interaction with a message" 105 | }, 106 | "git-assistant.watcher-excludePaths": { 107 | "type": "array", 108 | "default": [ 109 | "dist", 110 | "node_modules", 111 | "out" 112 | ], 113 | "description": "defines wich folders should not be included to watch for changes" 114 | }, 115 | "git-assistant.checkConfigVariables": { 116 | "type": "string", 117 | "enum": [ 118 | "enabled", 119 | "disabled" 120 | ], 121 | "default": "enabled", 122 | "description": "performs a check for missing git-config variables on startup" 123 | }, 124 | "git-assistant.checkConfigVariables-variablesToCheck": { 125 | "type": "array", 126 | "default": [ 127 | "user.email", 128 | "user.name", 129 | "user.username" 130 | ], 131 | "description": "defines wich git-config variables should be checked" 132 | }, 133 | "git-assistant.checkConfigVariables-scope": { 134 | "type": "string", 135 | "enum": [ 136 | "global", 137 | "local" 138 | ], 139 | "default": "global", 140 | "description": "defines if the git-config-variables should be set global or local" 141 | }, 142 | "git-assistant.branchWarn": { 143 | "type": "string", 144 | "enum": [ 145 | "enabled", 146 | "disabled" 147 | ], 148 | "default": "enabled", 149 | "description": "checks for branches the developer should not commit" 150 | }, 151 | "git-assistant.branchWarn-illegalBranches": { 152 | "type": "array", 153 | "default": [ 154 | "master" 155 | ], 156 | "description": "defines the branches the developer should not commit" 157 | }, 158 | "git-assistant.branchWarn-stashChanges": { 159 | "type": "string", 160 | "enum": [ 161 | "enabled", 162 | "auto", 163 | "disabled" 164 | ], 165 | "default": "enabled", 166 | "description": "defines if changes should be stashed it checkout fails" 167 | }, 168 | "git-assistant.pushBeforeClosingIDE": { 169 | "type": "string", 170 | "enum": [ 171 | "enabled", 172 | "disabled" 173 | ], 174 | "default": "enabled", 175 | "description": "warns if there are unpushed commits when closing the IDE" 176 | }, 177 | "git-assistant.pushSubmodulesFirst": { 178 | "type": "string", 179 | "enum": [ 180 | "enabled", 181 | "disabled" 182 | ], 183 | "default": "enabled", 184 | "description": "pushes commits in submodules before commits in main-repository" 185 | }, 186 | "git-assistant.checkForRemote": { 187 | "type": "string", 188 | "enum": [ 189 | "enabled", 190 | "auto", 191 | "disabled" 192 | ], 193 | "default": "enabled", 194 | "description": "warns if a branch was not pushed to the remote" 195 | }, 196 | "git-assistant.checkForRemote-defaultRemote": { 197 | "type": "string", 198 | "default": "", 199 | "description": "if remote is set, branches will be pushed automatically to this remote" 200 | }, 201 | "git-assistant.pullCommits": { 202 | "type": "string", 203 | "enum": [ 204 | "enabled", 205 | "auto", 206 | "disabled" 207 | ], 208 | "default": "enabled", 209 | "description": "pulls commits" 210 | }, 211 | "git-assistant.pushCommits": { 212 | "type": "string", 213 | "enum": [ 214 | "enabled", 215 | "auto", 216 | "disabled" 217 | ], 218 | "default": "enabled", 219 | "description": "pushes commits" 220 | }, 221 | "git-assistant.mergeCommits": { 222 | "type": "string", 223 | "enum": [ 224 | "enabled", 225 | "auto", 226 | "disabled" 227 | ], 228 | "default": "enabled", 229 | "description": "merges commits" 230 | }, 231 | "git-assistant.detectDetachedHead": { 232 | "type": "string", 233 | "enum": [ 234 | "enabled", 235 | "auto", 236 | "disabled" 237 | ], 238 | "default": "enabled", 239 | "description": "checks repository and submodule for detachedHEAD-status" 240 | }, 241 | "git-assistant.updateSubmodules": { 242 | "type": "string", 243 | "enum": [ 244 | "enabled", 245 | "auto", 246 | "disabled" 247 | ], 248 | "default": "enabled", 249 | "description": "updates Submodules automatically if main-repository changes" 250 | }, 251 | "git-assistant.checkRemoteChanges": { 252 | "type": "string", 253 | "enum": [ 254 | "enabled", 255 | "disabled" 256 | ], 257 | "default": "enabled", 258 | "description": "checks periodically for new commits on server" 259 | }, 260 | "git-assistant.checkRemoteChanges-checkEveryNMinutes": { 261 | "type": "number", 262 | "default": 60, 263 | "description": "checks periodically for new commits on server" 264 | } 265 | } 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/UI/InformationMessage.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { window } from 'vscode' 4 | import Logger from './Logger' 5 | import MessageOption from './MessageOption' 6 | import StatusBar from './StatusBar' 7 | import Status from './Status' 8 | import Config from '../application/Config' 9 | 10 | /** 11 | * this class is a wrapper for the VS Code InfromationMessage 12 | */ 13 | export default class InformationMessage { 14 | /** 15 | * displays a InformationMessage and some Options 16 | * @param message message to show 17 | * @param options options to display 18 | */ 19 | static showInformationMessage(message: string, ...options: MessageOption[]): Promise { 20 | return new Promise(async (resolve) => { 21 | let resolved = false 22 | 23 | const timeToWait = (Config.getValue('message-wait-time') as number) || 0 24 | // auto-resolve message after a certain time so the Extension will not be blocked 25 | setTimeout(() => { 26 | if (!resolved) { 27 | resolved = true 28 | resolve('') 29 | } 30 | 31 | return 32 | }, timeToWait) 33 | 34 | // display the InformationMessage 35 | const data: MessageOption | undefined = await window.showInformationMessage(message, {}, ...options) 36 | if (resolved) { 37 | StatusBar.addStatus(Status.messageAutoResolved()) 38 | 39 | return 40 | } 41 | 42 | if (data) { 43 | resolve(data.action) 44 | resolved = true 45 | 46 | return 47 | } 48 | 49 | // user dismissed message without clicking on an action 50 | Logger.showMessage(`Message not resolved: '${message}' `) 51 | resolve('') 52 | 53 | return 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/UI/InputBox.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { window } from 'vscode' 4 | import Logger from './Logger' 5 | 6 | export class InputValidation { 7 | static NOTEMPTY(text: string): string | undefined { 8 | if (!text || text.length === 0) { 9 | return 'You must enter something' 10 | } else { 11 | return undefined 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * this class is a wrapper for the VS Code InputBox 18 | */ 19 | export default class InputBox { 20 | /** 21 | * shows a InputBox in the VS Code Window 22 | * @param message message to display 23 | * @param validationFunction a function that validates the Input 24 | */ 25 | static async showInputBox(message: string, validationFunction: any = InputValidation.NOTEMPTY): Promise { 26 | const input = await window.showInputBox({ 27 | prompt: message, 28 | validateInput: validationFunction, 29 | }) 30 | if (input) { 31 | return input 32 | } 33 | Logger.showMessage(`Message not resolved: '${message}'`) 34 | 35 | return '' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/UI/Logger.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { OutputChannel, window } from 'vscode' 4 | import InformationMessage from './InformationMessage' 5 | 6 | /** 7 | * this class handles the logging-output 8 | */ 9 | export default class Logger { 10 | private static output: OutputChannel 11 | 12 | /** 13 | * registers a OutputChannel in VS Code 14 | */ 15 | static init(): void { 16 | Logger.output = window.createOutputChannel('git-assistant') 17 | } 18 | 19 | /** 20 | * shows the Output-Panel 21 | */ 22 | static showOutput(): void { 23 | Logger.output.show() 24 | } 25 | 26 | /** 27 | * logs a message to the OutputChannel 28 | * @param message message to show 29 | * @param notification display message as InformationMessage or not 30 | */ 31 | static showMessage(message: string, notification: boolean = false): void { 32 | Logger.output.appendLine(getCurrentTimestamp() + message) 33 | if (notification) { 34 | InformationMessage.showInformationMessage(message) 35 | } 36 | } 37 | 38 | /** 39 | * logs an error to the OutputChannel 40 | * @param error error to show 41 | * @param notification display error as InformationMessage or not 42 | */ 43 | static showError(error: string, notification: boolean = false): void { 44 | Logger.output.appendLine(`[ERROR] ${error}`) 45 | Logger.output.append('' + new Error().stack) 46 | if (notification) { 47 | InformationMessage.showInformationMessage(error) 48 | } 49 | } 50 | } 51 | 52 | const getCurrentTimestamp = (): string => { 53 | const now = new Date() 54 | const hour = prefixWithZeros(now.getHours()) 55 | const minute = prefixWithZeros(now.getMinutes()) 56 | const second = prefixWithZeros(now.getSeconds()) 57 | 58 | return `[${hour}:${minute}:${second}] ` 59 | } 60 | 61 | const prefixWithZeros = (value: number): string => (value < 10 ? '0' : '') + value 62 | -------------------------------------------------------------------------------- /src/UI/MessageOption.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { MessageItem } from 'vscode' 4 | 5 | /** 6 | * this class is a wrapper for VS Code MessageOptions 7 | */ 8 | export default class MessageOption implements MessageItem { 9 | title: string 10 | action: string 11 | 12 | constructor(title: string, action: string = title) { 13 | this.title = title 14 | this.action = action 15 | } 16 | 17 | static YES = 'y' 18 | static NO = 'n' 19 | static NEVER = 'never' 20 | static ALWAYS = 'always' 21 | static optionYES = new MessageOption('yes', MessageOption.YES) 22 | static optionNO = new MessageOption('no', MessageOption.NO) 23 | static optionNEVERASKAGAIN = new MessageOption('never ask again', MessageOption.NEVER) 24 | static optionALWAYS = new MessageOption('always', MessageOption.ALWAYS) 25 | } 26 | -------------------------------------------------------------------------------- /src/UI/QuickPick.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { window } from 'vscode' 4 | import Logger from './Logger' 5 | import QuickPickOption from './QuickPickOption' 6 | 7 | /** 8 | * this class is a wrapper for the VS Code QuickPick-Panel 9 | */ 10 | export default class QuickPick { 11 | /** 12 | * shows a QuickPick-Panel in the VS Code Window 13 | * @param placeHolder text to display when nothing was chosen 14 | * @param options options to choose from 15 | */ 16 | static async showQuickPick(placeHolder: string, ...options: QuickPickOption[]): Promise { 17 | Logger.showMessage('Push-QuickPick shown') 18 | 19 | const option = await window.showQuickPick(options, { placeHolder: placeHolder }) 20 | if (option) { 21 | return option.command 22 | } 23 | 24 | Logger.showMessage(`No option chosen`) 25 | 26 | return '' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/UI/QuickPickOption.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { QuickPickItem } from 'vscode' 4 | 5 | /** 6 | * this class is a wrapper for the VS Code QuickPickOptions 7 | */ 8 | export default class QuickPickOption implements QuickPickItem { 9 | label: string 10 | command: string 11 | description: string 12 | 13 | constructor(label: string, command: string, description: string = '') { 14 | this.label = label 15 | this.command = command 16 | this.description = description 17 | } 18 | 19 | static optionCANCEL = new QuickPickOption('Cancel', 'search.action.focusActiveEditor', 'ESC') 20 | static optionQUIT = new QuickPickOption('Close Window', 'workbench.action.quit') 21 | } 22 | -------------------------------------------------------------------------------- /src/UI/Status.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import StatusItem from './StatusItem' 4 | import { getRepositoryName } from '../application/Helper' 5 | 6 | /** 7 | * this class generates all Status Messages 8 | */ 9 | export default class Status { 10 | static startingExtension(): StatusItem { 11 | return StatusItem.newAnimatedStatusItem(`loading git-assistant`) 12 | } 13 | static watcherRunning(): StatusItem { 14 | return new StatusItem('git-assistant running') 15 | } 16 | static watcherRestarted(): StatusItem { 17 | return StatusItem.newTemporaryStatusItem('git-assistant restarted') 18 | } 19 | static configVariableSet(variable: string, input: string): StatusItem { 20 | return StatusItem.newTemporaryStatusItem(`git-config variable '${variable}' set to '${input}'`) 21 | } 22 | static allConfigVariablesChecked(): StatusItem { 23 | return StatusItem.newTemporaryStatusItem(`all git-config variables checked`) 24 | } 25 | 26 | static autoCheckoutForDetachedHead(repositoryPath: string, branch: string) { 27 | const message = `DetachedHEAD detected: auto-checked out '${branch}'` 28 | 29 | return StatusItem.newTemporaryStatusItem(addSubmoduleText(message, repositoryPath)) 30 | } 31 | 32 | static updatingGitModel(repositoryPath: string): StatusItem { 33 | return StatusItem.newAnimatedStatusItem(`checking repository '${getRepositoryName(repositoryPath)}'`) 34 | } 35 | 36 | static publishBranch(repositoryPath: string, remote: string, branch: string): StatusItem { 37 | return StatusItem.newAnimatedStatusItem( 38 | `publishing branch '${branch}' to remote '${remote}' for '${getRepositoryName(repositoryPath)}'`, 39 | ) 40 | } 41 | 42 | static branchIsUpToDate(repositoryPath: string, remote: string, branch: string): StatusItem { 43 | const message = `branch '${getBranchName(remote, branch)}' is up-to-date` 44 | 45 | return StatusItem.newTemporaryStatusItem(addSubmoduleText(message, repositoryPath)) 46 | } 47 | static commitsPushed(repositoryPath: string, remote: string, branch: string, ahead: number = 0): StatusItem { 48 | const message = `${ahead} commits pushed to '${getBranchName(remote, branch)}'` 49 | 50 | return StatusItem.newTemporaryStatusItem(addSubmoduleText(message, repositoryPath)) 51 | } 52 | static commitsPushing(repositoryPath: string, remote: string, branch: string, ahead: number = 0): StatusItem { 53 | const message = `pushing ${ahead} commits to '${getBranchName(remote, branch)}'` 54 | 55 | return StatusItem.newAnimatedStatusItem(addSubmoduleText(message, repositoryPath)) 56 | } 57 | static commitsPulled(repositoryPath: string, remote: string, branch: string, behind: number = 0): StatusItem { 58 | const message = `${behind} commits pulled from '${getBranchName(remote, branch)}'` 59 | 60 | return StatusItem.newTemporaryStatusItem(addSubmoduleText(message, repositoryPath)) 61 | } 62 | static commitsPulling(repositoryPath: string, remote: string, branch: string, behind: number = 0): StatusItem { 63 | const message = `pulling ${behind} commits from '${getBranchName(remote, branch)}'` 64 | 65 | return StatusItem.newAnimatedStatusItem(addSubmoduleText(message, repositoryPath)) 66 | } 67 | static commitsMerged( 68 | repositoryPath: string, 69 | remote: string, 70 | branch: string, 71 | ahead: number = 0, 72 | behind: number = 0, 73 | ): StatusItem { 74 | const message = `${ahead} / ${behind} commits merged on '${getBranchName(remote, branch)}'` 75 | 76 | return StatusItem.newTemporaryStatusItem(addSubmoduleText(message, repositoryPath)) 77 | } 78 | static commitsMerging( 79 | repositoryPath: string, 80 | remote: string, 81 | branch: string, 82 | ahead: number = 0, 83 | behind: number = 0, 84 | ): StatusItem { 85 | const message = `merging ${ahead} / ${behind} commits on '${getBranchName(remote, branch)}'` 86 | 87 | return StatusItem.newAnimatedStatusItem(addSubmoduleText(message, repositoryPath)) 88 | } 89 | 90 | static checkedOutRepositoryBranch(repositoryPath: string, branch: string): StatusItem { 91 | const message = `Checked out '${branch}'` 92 | 93 | return StatusItem.newTemporaryStatusItem(addSubmoduleText(message, repositoryPath)) 94 | } 95 | 96 | static stashSaveChanges(): StatusItem { 97 | return StatusItem.newTemporaryStatusItem(`changes saved to stash`) 98 | } 99 | static stashPopChanges(): StatusItem { 100 | return StatusItem.newTemporaryStatusItem(`changes popped from stash`) 101 | } 102 | 103 | static branchCreated(branch: string): StatusItem { 104 | return StatusItem.newTemporaryStatusItem(`new branch '${branch}' created`) 105 | } 106 | 107 | static submoduleUpdated(): StatusItem { 108 | return StatusItem.newTemporaryStatusItem(`Submodules updated`) 109 | } 110 | 111 | static messageAutoResolved(): StatusItem { 112 | return StatusItem.newTemporaryStatusItem(`message was already auto-resolved - no action performed`) 113 | } 114 | } 115 | 116 | const addSubmoduleText = (message: string, repositoryPath: string): string => { 117 | if (repositoryPath.length) { 118 | message += ` in Submodule '${repositoryPath}'` 119 | } 120 | 121 | return message 122 | } 123 | 124 | const getBranchName = (remote: string, branch: string): string => (remote ? `${remote}/` : '') + branch 125 | -------------------------------------------------------------------------------- /src/UI/StatusBar.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext, StatusBarAlignment, StatusBarItem, window } from 'vscode' 4 | import Logger from './Logger' 5 | import StatusItem from './StatusItem' 6 | 7 | export const DEFAULT_TIMEOUT: number = 3000 8 | export const TICK_TIME: number = 500 9 | const ANIMATION_TIMEOUT: number = 250 10 | const NR_OF_ANIMATION_DOTS: number = 5 11 | 12 | /** 13 | * this class handles the information displayed in the StatusBar 14 | */ 15 | export default class StatusBar { 16 | static statusbarItem: StatusBarItem 17 | static status: StatusItem[] 18 | static tickCount: number 19 | static iteration: number = 0 20 | 21 | /** 22 | * initializes the StatusBar 23 | * @param context VS Code ExtensionContext 24 | */ 25 | static initStatusBar(context: ExtensionContext): void { 26 | StatusBar.statusbarItem = window.createStatusBarItem(StatusBarAlignment.Left, 0) 27 | StatusBar.statusbarItem.command = 'git-assistant.showOutput' 28 | context.subscriptions.push(StatusBar.statusbarItem) 29 | StatusBar.status = [] 30 | StatusBar.tickCount = 0 31 | } 32 | 33 | /** 34 | * adds a status to the queue 35 | * @param status status to add 36 | */ 37 | static addStatus(status: StatusItem): void { 38 | Logger.showMessage('[status] ' + status.toStringText()) 39 | StatusBar.status.push(status) 40 | StatusBar.showLatestStatus() 41 | } 42 | 43 | /** 44 | * removes a status from the queue 45 | * @param status status to remove 46 | */ 47 | static removeStatus(status: StatusItem): void { 48 | StatusBar.status = StatusBar.status.filter((stat) => stat !== status) 49 | StatusBar.showLatestStatus() 50 | } 51 | 52 | /** 53 | * animates a status over time 54 | * @param status status to animate 55 | */ 56 | static animateStatus(status: StatusItem): void { 57 | if (!StatusBar.status.length || StatusBar.getPriorityStatusItem() !== status) { 58 | return 59 | } 60 | 61 | let text = `${status.toStringIcon()} ` 62 | text += new Array(StatusBar.iteration).fill('.').join('') 63 | text += status.toStringText() 64 | text += new Array(NR_OF_ANIMATION_DOTS - StatusBar.iteration).fill('.').join('') 65 | 66 | StatusBar.statusbarItem.text = text 67 | 68 | if (++StatusBar.iteration > NR_OF_ANIMATION_DOTS) { 69 | StatusBar.iteration = 0 70 | } 71 | 72 | setTimeout(() => { 73 | StatusBar.animateStatus(status) 74 | }, ANIMATION_TIMEOUT) 75 | } 76 | 77 | /** 78 | * displays the status with the highest priority 79 | */ 80 | private static showLatestStatus(): void { 81 | const length = StatusBar.status.length 82 | if (!length) { 83 | StatusBar.statusbarItem.hide() 84 | 85 | return 86 | } 87 | 88 | let status = StatusBar.status[length - 1] 89 | 90 | const temp = StatusBar.getPriorityStatusItem() 91 | if (temp) { 92 | status = temp 93 | } 94 | 95 | StatusBar.statusbarItem.text = status.toString() 96 | StatusBar.statusbarItem.show() 97 | if (status.isAnimated()) { 98 | StatusBar.animateStatus(status) 99 | } 100 | if (status.isTemporary()) { 101 | StatusBar.nextTick(status, ++StatusBar.tickCount) 102 | } 103 | } 104 | 105 | /** 106 | * returns a StatusItem with priority if it exists in the queue 107 | */ 108 | private static getPriorityStatusItem(): StatusItem | undefined { 109 | const animated = StatusBar.status.filter((statusItem: StatusItem) => statusItem.isAnimated()) 110 | if (animated.length) { 111 | return animated.reverse()[0] 112 | } 113 | const temporary = StatusBar.status.filter((statusItem: StatusItem) => statusItem.isTemporary()) 114 | if (temporary.length) { 115 | return temporary[0] 116 | } 117 | 118 | return undefined 119 | } 120 | 121 | /** 122 | * displays a status for a temporary time 123 | * @param status status to display 124 | * @param tickCount incrementing tick count 125 | */ 126 | private static nextTick(status: StatusItem, tickCount: number): void { 127 | if (StatusBar.tickCount !== tickCount || StatusBar.getPriorityStatusItem() !== status) { 128 | return 129 | } 130 | const finished = status.reduceDisplayTime() 131 | 132 | if (!finished) { 133 | setTimeout(() => { 134 | StatusBar.nextTick(status, tickCount) 135 | }, TICK_TIME) 136 | 137 | return 138 | } 139 | StatusBar.removeStatus(status) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/UI/StatusItem.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { DEFAULT_TIMEOUT, TICK_TIME } from './StatusBar' 4 | 5 | export enum Octicon { 6 | 'comment' = '$(comment)', 7 | 'sync' = '$(sync)', 8 | 'code' = '$(code)', 9 | } 10 | 11 | /** 12 | * this class is a wrapper for a message displayed in the StatusBar 13 | */ 14 | export default class StatusItem { 15 | private text: string 16 | private icon: Octicon 17 | private animated: boolean 18 | private temporary: boolean 19 | private displayTime: number 20 | 21 | constructor(text: string = '', icon: Octicon = Octicon.comment, animated: boolean = false, displayTime: number = 0) { 22 | this.icon = icon 23 | this.text = text 24 | this.animated = animated 25 | this.temporary = displayTime > 0 26 | this.displayTime = displayTime 27 | } 28 | 29 | static newTemporaryStatusItem(text: string): StatusItem { 30 | return new StatusItem(text, Octicon.comment, false, DEFAULT_TIMEOUT) 31 | } 32 | 33 | static newAnimatedStatusItem(text: string): StatusItem { 34 | return new StatusItem(text, Octicon.sync, true) 35 | } 36 | 37 | isAnimated(): boolean { 38 | return this.animated 39 | } 40 | 41 | isTemporary(): boolean { 42 | return this.temporary 43 | } 44 | 45 | reduceDisplayTime(): boolean { 46 | this.displayTime -= TICK_TIME 47 | 48 | return this.displayTime < 0 49 | } 50 | 51 | toString(): string { 52 | return `${this.toStringIcon()} ${this.toStringText()}` 53 | } 54 | 55 | toStringIcon(): string { 56 | return `${this.icon}` 57 | } 58 | 59 | toStringText(): string { 60 | return this.text 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/application/CMD.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { exec } from 'child_process' 4 | import Logger from '../UI/Logger' 5 | import { getWorkspacePath } from './Helper' 6 | 7 | /** 8 | * this class allows to execute commands in the OS-shell 9 | */ 10 | export default class CMD { 11 | // filter to validate the input 12 | private static commandInjectionRegexp = new RegExp(/^[a-zA-Z 0-9\.\-\@]*$/) 13 | 14 | /** 15 | * executes a given command in the shell 16 | * @param command command to execute 17 | * @param workspace path where the command should be executed 18 | */ 19 | static executeCommand(command: string, workspace: string = getWorkspacePath()): Promise { 20 | // input is validated to prevent command-injection 21 | return new Promise((resolve, reject) => { 22 | if (!CMD.commandInjectionRegexp.test(command)) { 23 | Logger.showError(`Command '${command}' not allowed`, true) 24 | 25 | return reject() 26 | } 27 | // execute the passed command 28 | exec(command, { cwd: workspace }, (error: any, stdout: any, _stderr: any) => { 29 | if (error !== null) { 30 | return reject(error) 31 | } 32 | resolve(stdout.trim()) 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/application/Commands.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext } from 'vscode' 4 | import PushBeforeClosingIDECommand from '../commands/PushBeforeClosingIDE.command' 5 | import ShowOutputCommand from '../commands/ShowOutput.command' 6 | import StartExtension from '../commands/StartExtension.command' 7 | import StopExtension from '../commands/StopExtension.command' 8 | 9 | const COMMANDS = [PushBeforeClosingIDECommand, ShowOutputCommand, StartExtension, StopExtension] 10 | /** 11 | * this class registers all commands displayed in the VS Code Command Pallette 12 | */ 13 | export default class Commands { 14 | /** 15 | * registers all Commands as a Command in VS Code 16 | * @param context VS Code ExtensionContext 17 | */ 18 | static registerCommands(context: ExtensionContext): void { 19 | COMMANDS.forEach((command) => { 20 | command.registerCommand(context) 21 | }) 22 | } 23 | 24 | /** 25 | * registers dummy Commands as a Command in VS Code so it will not throw an Exception when the Extension 26 | * is not loaded 27 | * @param context VS Code ExtensionContext 28 | */ 29 | static registerDummyCommands(context: ExtensionContext): void { 30 | COMMANDS.forEach((command) => { 31 | command.registerDummyCommand(context) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/application/Config.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { WorkspaceConfiguration, ConfigurationTarget, workspace } from 'vscode' 3 | 4 | export enum ConfigOptions { 5 | 'enabled' = 'enabled', 6 | 'auto' = 'auto', 7 | 'disabled' = 'disabled', 8 | } 9 | 10 | /** 11 | * this class is responsible for reading the Configuration-Settings of the Extension 12 | */ 13 | export default class Config { 14 | private static config: WorkspaceConfiguration 15 | 16 | /** 17 | * loads the current config of the extension 18 | */ 19 | static loadConfig() { 20 | this.config = workspace.getConfiguration('git-assistant') 21 | } 22 | 23 | /** 24 | * gets the value of a specific config-key 25 | * @param value config-key 26 | */ 27 | static getValue(value: string): any { 28 | return Config.config?.get(value) 29 | } 30 | 31 | /** 32 | * checks if a feature is enabled 33 | * @param value config-key 34 | */ 35 | static isEnabled(value: string): boolean { 36 | const val = Config.getValue(value) 37 | if (!val || val === ConfigOptions.disabled) { 38 | return false 39 | } 40 | 41 | return true 42 | } 43 | 44 | /** 45 | * disables a feature in the global-configs 46 | * @param value config-key 47 | */ 48 | static disable(value: string) { 49 | Config.config.update(value, 'disabled', ConfigurationTarget.Global) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/application/Features.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import PushBeforeClosingIDE from '../handlers/exit/PushBeforeClosingIDE.handler' 4 | import GitHandler from '../handlers/git/GitHandler.handler' 5 | import BranchWarn from '../handlers/git/branch_changed/BranchWarn.handler' 6 | import CheckForRemote from '../handlers/git/branch_changed/CheckForRemote.handler' 7 | import DetectDetachedHead from '../handlers/git/branch_changed/DetectDetachedHead.handler' 8 | import MergeCommits from '../handlers/git/commits/MergeCommits.handler' 9 | import PullCommits from '../handlers/git/commits/PullCommits.handler' 10 | import PushCommits from '../handlers/git/commits/PushCommit.handler' 11 | import PushSubmodulesFirst from '../handlers/git/push/PushSubmodulesFirst.handler' 12 | import CheckConfigVariables from '../handlers/start/CheckConfigVariables.handler' 13 | import CheckRemoteChanges from '../handlers/start/CheckRemoteChanges.handler' 14 | import PerformStartupCheckOfRepositories from '../handlers/start/PerformStartupCheckOfRepositories.handler' 15 | import UpdateInitSubmodules from '../handlers/start/UpdateInitSubmodules.handler' 16 | import WatcherStart from '../handlers/start/WatcherStart.handler' 17 | import HandleSubmoduleUpdate from '../handlers/submodule/update/HandleSubmoduleUpdate.handler' 18 | import SubmoduleHandler from '../handlers/submodule/SubmoduleHandler.handler' 19 | 20 | const HANDLERS = [ 21 | PushBeforeClosingIDE, 22 | BranchWarn, 23 | CheckForRemote, 24 | DetectDetachedHead, 25 | MergeCommits, 26 | PullCommits, 27 | PushCommits, 28 | GitHandler, 29 | PushSubmodulesFirst, 30 | CheckRemoteChanges, 31 | PerformStartupCheckOfRepositories, 32 | UpdateInitSubmodules, 33 | WatcherStart, 34 | SubmoduleHandler, 35 | HandleSubmoduleUpdate, 36 | CheckConfigVariables, 37 | ] 38 | /** 39 | * this class registers all feature-handler in the extension 40 | */ 41 | export default class Features { 42 | /** 43 | * registers all files matching the "*.handler.js"-name-pattern to the Event-Handler 44 | */ 45 | static enableFeatures(): void { 46 | HANDLERS.forEach((handler) => { 47 | handler.registerEventHandler() 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/application/GitRepository.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { join } from 'path' 4 | import { readFile } from 'fs' 5 | import Git from '../models/Git' 6 | import Submodule from '../models/Submodule' 7 | import Logger from '../UI/Logger' 8 | import Status from '../UI/Status' 9 | import StatusBar from '../UI/StatusBar' 10 | import StatusItem from '../UI/StatusItem' 11 | import CMD from './CMD' 12 | import { diff as deepdiff } from 'deep-diff' 13 | import { getWorkspacePath, deepmerge } from './Helper' 14 | 15 | const unimportantGitFiles: string[] = ['lock', 'FETCH_HEAD', '.git/objects/'] 16 | 17 | /** 18 | * this class handles the communication with the Git-Repository 19 | */ 20 | export default class GitRepository { 21 | private static gitModels: Map = new Map() 22 | private static simplegits: Map = new Map() 23 | 24 | private static isUpdating: boolean = false 25 | 26 | /** 27 | * deletes all Git-Models and simple-git instances 28 | */ 29 | static reset(): void { 30 | GitRepository.gitModels = new Map() 31 | GitRepository.simplegits = new Map() 32 | } 33 | 34 | /** 35 | * creates a new simple-git instance or returns a existing one 36 | * @param repositoryPath relative path of the Repsoitory 37 | */ 38 | private static getSimplegit(repositoryPath: string = ''): any { 39 | if (GitRepository.simplegits.has(repositoryPath)) { 40 | return GitRepository.simplegits.get(repositoryPath) 41 | } 42 | const simplegit = require('simple-git/promise')(join(getWorkspacePath(), repositoryPath)) 43 | GitRepository.simplegits.set(repositoryPath, simplegit) 44 | 45 | return simplegit 46 | } 47 | 48 | /** 49 | * returns the current status as Git-Model 50 | * @param repositoryPath relative path of the Repsoitory 51 | */ 52 | static async getGitModel(repositoryPath: string = '', mainRepositoryPath: string = ''): Promise { 53 | repositoryPath = repositoryPath.replace(/\\/gi, '/') 54 | 55 | let gitModel = GitRepository.gitModels.get(repositoryPath) 56 | if (gitModel) { 57 | return gitModel 58 | } 59 | 60 | // no Git-Model was created yet => create a new one 61 | gitModel = await GitRepository.updateGitModel(repositoryPath, mainRepositoryPath) 62 | 63 | return gitModel 64 | } 65 | 66 | /** 67 | * sets a Git-Model as current status of the Repository 68 | * @param repositoryPath relative path of the Repsoitory 69 | * @param gitModel Git-Model to set as current 70 | */ 71 | private static setGitModel(repositoryPath: string = '', gitModel: Git): void { 72 | repositoryPath = repositoryPath.replace(/\\/gi, '/') 73 | const oldModel = GitRepository.gitModels.get(repositoryPath) 74 | GitRepository.gitModels.set(repositoryPath, gitModel) 75 | 76 | // store the old Git-Model in the new one 77 | if (oldModel) { 78 | gitModel.setOldModel(oldModel) 79 | } 80 | } 81 | 82 | /** 83 | * checks if a given file is an unimportant file that contains no Status-change-infomrmation 84 | * @param filename file to check 85 | */ 86 | static async isUnimportantGitFile(filename: string): Promise { 87 | // root git folder 88 | if (filename.length === 4) { 89 | return true 90 | } 91 | // unimportant file 92 | if (unimportantGitFiles.some((file: string) => new RegExp(file, 'gi').test(filename))) { 93 | return true 94 | } 95 | // submodule references 96 | const subModuleRoots = (await GitRepository.getGitModel()) 97 | .getSubmodules() 98 | .filter((submodule) => '.git/modules/' + submodule.getPath() === filename) 99 | if (subModuleRoots.length) { 100 | return true 101 | } 102 | 103 | return false 104 | } 105 | 106 | /** 107 | * checks if a change occured in a Submodule-Repsoitory 108 | * @param filename file to check 109 | */ 110 | static isChangeInSubmodule(filename: string): boolean { 111 | return new RegExp('.git/modules/', 'gi').test(filename) 112 | } 113 | 114 | /** 115 | * checks if a file is located in a Submodule 116 | * @param filename file to check 117 | */ 118 | static async isFileInSubmodule(filename: string): Promise { 119 | const submodule = (await GitRepository.getGitModel()) 120 | .getSubmodules() 121 | .find((submodule: Submodule) => new RegExp(submodule.getPath(), 'gi').test(filename)) 122 | 123 | return submodule ? submodule.getPath() : '' 124 | } 125 | 126 | /** 127 | * checks if the Extension is currently executing a Git-Command 128 | */ 129 | static isCurrentlyUpdating(): boolean { 130 | return GitRepository.isUpdating 131 | } 132 | 133 | /** 134 | * marks the begin of a Git-Command 135 | */ 136 | private static updatingStart(): void { 137 | GitRepository.isUpdating = true 138 | } 139 | 140 | /** 141 | * marks the end of a Git-Command 142 | * @param repositoryPath relative path of the Repsoitory 143 | * @param branch current Branch of the Repository 144 | */ 145 | private static updatingEnd(repositoryPath: string, branch?: string): void { 146 | GitRepository.updateGUI(repositoryPath, branch) 147 | GitRepository.isUpdating = false 148 | } 149 | 150 | /** 151 | * hack for updating the VS Code GUI 152 | * @param repositoryPath relative path of the Repsoitory 153 | * @param branch current Branch of the repository 154 | */ 155 | static async updateGUI(repositoryPath: string, branch?: string): Promise { 156 | if (!repositoryPath.length) { 157 | return 158 | } 159 | 160 | const simplegit = GitRepository.getSimplegit(repositoryPath) 161 | 162 | const oldBranch = branch ? branch : (await GitRepository.getGitModel(repositoryPath)).getBranch() 163 | const helperBranch = oldBranch !== 'master' ? 'master' : 'development' 164 | let helperBranchExists = false 165 | 166 | // checkout other Branch 167 | await simplegit.checkoutLocalBranch(helperBranch).catch(() => (helperBranchExists = true)) 168 | if (helperBranchExists) { 169 | await simplegit.checkout(helperBranch) 170 | } 171 | 172 | // wait a short time to switch back to the original Branch 173 | setTimeout(async () => { 174 | await simplegit.checkout(oldBranch) 175 | 176 | if (!helperBranchExists) { 177 | simplegit.deleteLocalBranch(helperBranch) 178 | } 179 | 180 | Logger.showMessage('VS Code SCM-GUI updated') 181 | }, 10) 182 | } 183 | 184 | /*******************************************************************************************/ 185 | /* UPDATE GIT MODEL */ 186 | /*******************************************************************************************/ 187 | 188 | /** 189 | * reads the current Status of the Repsoitory 190 | * @param repositoryPath relative path of the Repsoitory 191 | */ 192 | static async updateGitModel(repositoryPath: string = '', mainRepositoryPath: string = ''): Promise { 193 | let simplegit 194 | try { 195 | simplegit = GitRepository.getSimplegit(repositoryPath) 196 | } catch (error) { 197 | Logger.showMessage(`Repository path ${repositoryPath} not found`) 198 | 199 | return new Git() 200 | } 201 | const gitModel = new Git(repositoryPath) 202 | 203 | try { 204 | // fetch latest changes from Remote 205 | await simplegit.fetch() 206 | } catch (error) { 207 | if (error.message.indexOf('unable to access')) { 208 | Logger.showMessage(`[repository] could not connect to Server`) 209 | } else { 210 | Logger.showMessage(`[repository] No remote repository found for ${repositoryPath}`) 211 | } 212 | } 213 | 214 | if (mainRepositoryPath) { 215 | gitModel.setMainRepositoryPath(mainRepositoryPath) 216 | } 217 | 218 | const stat = Status.updatingGitModel(repositoryPath) 219 | StatusBar.addStatus(stat) 220 | 221 | const status = await simplegit.status() 222 | 223 | gitModel.setBehind(status.behind) 224 | gitModel.setAhead(status.ahead) 225 | gitModel.setBranch(status.current) 226 | if (status.tracking) { 227 | gitModel.setRemote(status.tracking.split('/')[0]) 228 | } 229 | 230 | const branches = await simplegit.branch() 231 | if (gitModel.getBranch() === 'HEAD') { 232 | gitModel.setBranch(branches.current) 233 | } 234 | gitModel.setDetachedHEAD(branches.detached) 235 | 236 | gitModel.setBranches(branches.branches) 237 | 238 | const remotes = await simplegit.getRemotes() 239 | gitModel.setRemotes(remotes) 240 | 241 | const result = await CMD.executeCommand('git submodule', gitModel.getPath()) 242 | gitModel.setSubmodules(result.replace(/\r?\n|\r/g, ' ').replace(' ', ' ')) 243 | 244 | GitRepository.setGitModel(repositoryPath, gitModel) 245 | 246 | StatusBar.removeStatus(stat) 247 | 248 | return gitModel 249 | } 250 | 251 | /*******************************************************************************************/ 252 | /* GIT MODEL DIFF */ 253 | /*******************************************************************************************/ 254 | 255 | /** 256 | * returns a diff of the last two Git-Models 257 | * @param repositoryPath relative path of the Repsoitory 258 | */ 259 | static async getModelDiff(repositoryPath: string = ''): Promise { 260 | const newModel = await GitRepository.getGitModel(repositoryPath) 261 | const oldModel = newModel.getOldModel() 262 | 263 | if (!oldModel) { 264 | return newModel 265 | } 266 | 267 | const tempDiff = deepdiff(newModel, oldModel) || [] 268 | 269 | let diff: any = {} 270 | // converts the deep-diff object to a Git-Model 271 | tempDiff.forEach((object: any) => { 272 | if (object.path[0] === 'oldModel') { 273 | return 274 | } 275 | 276 | let pathPointer = object.lhs 277 | object.path.reverse().forEach((pathSegment: string) => { 278 | const obj: any = {} 279 | obj[pathSegment] = pathPointer 280 | pathPointer = obj 281 | }) 282 | 283 | if (pathPointer.branches) { 284 | pathPointer.branches = undefined 285 | } 286 | 287 | diff = deepmerge(diff, pathPointer) 288 | }) 289 | 290 | return diff 291 | } 292 | 293 | /*******************************************************************************************/ 294 | /* INIT */ 295 | /*******************************************************************************************/ 296 | 297 | /** 298 | * inits a new Git-Repository 299 | */ 300 | static async init(): Promise { 301 | const gitInstance = GitRepository.getSimplegit() 302 | await gitInstance.init() 303 | await gitInstance.add('./*') 304 | await gitInstance.commit('first commit') 305 | } 306 | 307 | /*******************************************************************************************/ 308 | /* CONFIG-VARIABLES */ 309 | /*******************************************************************************************/ 310 | 311 | /** 312 | * returns the value of a given config-variable-name 313 | * @param variable name of the config-variable 314 | */ 315 | static getConfigVariable(variable: string): Promise { 316 | return new Promise((resolve, reject) => { 317 | CMD.executeCommand(`git config --get ${variable}`) 318 | .then((result) => { 319 | if (!result.length) { 320 | return reject() 321 | } 322 | resolve(result) 323 | }) 324 | .catch(reject) 325 | }) 326 | } 327 | 328 | /** 329 | * sets the value of a Git config-variable 330 | * @param variable name of the config-variable 331 | * @param value value to set 332 | * @param scope global or local scope 333 | */ 334 | static async setConfigVariable( 335 | variable: string, 336 | value: string, 337 | scope: 'global' | 'local' = 'global', 338 | ): Promise { 339 | await CMD.executeCommand(`git config --${scope} ${variable} ${value}`).catch(() => { 340 | Logger.showError(`An Error occured while trying to set '${variable}'`) 341 | 342 | return 343 | }) 344 | 345 | StatusBar.addStatus(Status.configVariableSet(variable, value)) 346 | } 347 | 348 | /*******************************************************************************************/ 349 | /* PUBLISH BRANCH */ 350 | /*******************************************************************************************/ 351 | 352 | /** 353 | * publishes a Branch to a given remote 354 | * @param repositoryPath relative path of the Repsoitory 355 | * @param remote remote to publish 356 | * @param branch branch to publish 357 | */ 358 | static async publishBranch(repositoryPath: string, remote: string, branch: string): Promise { 359 | const status = Status.publishBranch(repositoryPath, remote, branch) 360 | StatusBar.addStatus(status) 361 | GitRepository.updatingStart() 362 | 363 | await GitRepository.getSimplegit(repositoryPath).push(remote, branch, { '-u': null }).catch() 364 | 365 | GitRepository.updatingEnd(repositoryPath) 366 | StatusBar.removeStatus(status) 367 | } 368 | 369 | /*******************************************************************************************/ 370 | /* BRANCHES */ 371 | /*******************************************************************************************/ 372 | 373 | /** 374 | * creates a new Branch 375 | * @param repositoryPath relative path of the Repsoitory 376 | * @param branch Branch to create 377 | */ 378 | static async createNewBranch(repositoryPath: string = '', branch: string): Promise { 379 | await GitRepository.getSimplegit(repositoryPath).checkoutLocalBranch(branch) 380 | Status.branchCreated(branch) 381 | } 382 | 383 | /** 384 | * checks out a branch 385 | * @param repositoryPath relative path of the Repsoitory 386 | * @param branch name of the Branch 387 | */ 388 | static async checkoutBranchForRepository(repositoryPath: string, branch: string): Promise { 389 | GitRepository.updatingStart() 390 | 391 | await GitRepository.getSimplegit(repositoryPath) 392 | .checkout(branch) 393 | .catch(() => { 394 | let message = `could not checkout branch '${branch}'` 395 | if (repositoryPath.length) { 396 | message += ` in Submodule '${repositoryPath}'` 397 | } 398 | Logger.showError(message) 399 | GitRepository.updatingEnd(repositoryPath, branch) 400 | 401 | return 402 | }) 403 | 404 | StatusBar.addStatus(Status.checkedOutRepositoryBranch(repositoryPath, branch)) 405 | GitRepository.updatingEnd(repositoryPath, branch) 406 | } 407 | 408 | /** 409 | * add changes to Stash 410 | * @param repositoryPath relative path of the Repsoitory 411 | */ 412 | static async stashSaveChanges(repositoryPath: string): Promise { 413 | GitRepository.updatingStart() 414 | await CMD.executeCommand('git stash save', repositoryPath).catch(() => { 415 | GitRepository.updatingEnd(repositoryPath) 416 | Logger.showError(`An Error occured while stash changes`, true) 417 | 418 | return 419 | }) 420 | 421 | StatusBar.addStatus(Status.stashSaveChanges()) 422 | GitRepository.updatingEnd(repositoryPath) 423 | } 424 | 425 | /** 426 | * restores changes from Stash 427 | * @param repositoryPath relative path of the Repsoitory 428 | */ 429 | static async stashPopChanges(repositoryPath: string): Promise { 430 | GitRepository.updatingStart() 431 | await CMD.executeCommand('git stash pop', repositoryPath).catch(() => { 432 | GitRepository.updatingEnd(repositoryPath) 433 | Logger.showError(`An Error occured while trying to pop from stash`, true) 434 | 435 | return 436 | }) 437 | 438 | StatusBar.addStatus(Status.stashPopChanges()) 439 | GitRepository.updatingEnd(repositoryPath) 440 | } 441 | 442 | /** 443 | * checks the '.gitmodules' file for a configured branch 444 | * @param gitModel Git-Model of Submodule 445 | */ 446 | static async getConfiguredBranchForSubmodule(gitModel: Git): Promise { 447 | return new Promise((resolve) => { 448 | const mainRepsoitoryPath = gitModel.getMainRepositoryPath() 449 | const submodulePath = gitModel.getRelativePath() 450 | 451 | readFile(join(mainRepsoitoryPath, '/.gitmodules'), 'utf8', (err, data: string) => { 452 | if (!data) { 453 | return resolve('') 454 | } 455 | 456 | const lines = data.match(/[^\r\n]+/g) || ([] as RegExpMatchArray) 457 | let foundSubmodule = false 458 | lines.forEach((line) => { 459 | if (line.includes('[submodule')) { 460 | foundSubmodule = line.includes(submodulePath) 461 | } else if (foundSubmodule) { 462 | if (line.includes('branch')) { 463 | return resolve(line.replace('branch =', '').trim()) 464 | } 465 | } 466 | }) 467 | 468 | resolve('') 469 | }) 470 | }) 471 | } 472 | 473 | /*******************************************************************************************/ 474 | /* PULL */ 475 | /*******************************************************************************************/ 476 | 477 | /** 478 | * pulls changes from Remote 479 | * @param repositoryPath relative path of the Repsoitory 480 | * @param remote name of the Remote 481 | * @param branch name of the Branch 482 | * @param behind number of commits behind 483 | * @param silent iff true => don't show messages 484 | */ 485 | static async pullRepository( 486 | repositoryPath: string, 487 | remote: string, 488 | branch: string, 489 | behind: number = 0, 490 | silent: boolean = false, 491 | ): Promise { 492 | const status = Status.commitsPulling(repositoryPath, remote, branch, behind) 493 | if (!silent) { 494 | GitRepository.updatingStart() 495 | StatusBar.addStatus(status) 496 | } 497 | await GitRepository.getSimplegit(repositoryPath) 498 | .pull(remote, branch) 499 | .catch((error: any) => { 500 | Logger.showError(`Failed to Pull changes from '${remote}/${branch}'`, true) 501 | Logger.showError(error, true) 502 | if (!silent) { 503 | StatusBar.removeStatus(status) 504 | GitRepository.updatingEnd(repositoryPath) 505 | } 506 | 507 | return 508 | }) 509 | 510 | if (!silent) { 511 | StatusBar.addStatus(Status.commitsPulled(repositoryPath, remote, branch, behind)) 512 | StatusBar.removeStatus(status) 513 | GitRepository.updatingEnd(repositoryPath) 514 | } 515 | } 516 | 517 | /*******************************************************************************************/ 518 | /* PUSH */ 519 | /*******************************************************************************************/ 520 | 521 | /** 522 | * pushes changes to Remote 523 | * @param repositoryPath relative path of the Repsoitory 524 | * @param remote name of the Remote 525 | * @param branch name of the Branch 526 | * @param ahead number of commits ahead 527 | * @param silent iff true => don't show messages 528 | */ 529 | static async pushRepository( 530 | repositoryPath: string, 531 | remote: string, 532 | branch: string, 533 | ahead: number = 0, 534 | silent: boolean = false, 535 | ): Promise { 536 | const status = Status.commitsPushing(repositoryPath, remote, branch, ahead) 537 | if (!silent) { 538 | GitRepository.updatingStart() 539 | StatusBar.addStatus(status) 540 | } 541 | await GitRepository.getSimplegit(repositoryPath) 542 | .push(remote, branch) 543 | .catch((error: any) => { 544 | Logger.showError(`Failed to Push changes to '${remote}/${branch}'`, true) 545 | Logger.showError(error, true) 546 | if (!silent) { 547 | StatusBar.removeStatus(status) 548 | GitRepository.updatingEnd(repositoryPath) 549 | } 550 | 551 | return 552 | }) 553 | 554 | if (!silent) { 555 | StatusBar.addStatus(Status.commitsPushed(repositoryPath, remote, branch, ahead)) 556 | StatusBar.removeStatus(status) 557 | GitRepository.updatingEnd(repositoryPath) 558 | } 559 | } 560 | 561 | /** 562 | * pushes the changes from the root-Repository 563 | */ 564 | static async pushRootRepository(): Promise { 565 | const gitModel = await GitRepository.getGitModel() 566 | 567 | return GitRepository.pushRepository( 568 | gitModel.getRelativePath(), 569 | gitModel.getRemote(), 570 | gitModel.getBranch(), 571 | gitModel.getAhead(), 572 | ) 573 | } 574 | 575 | /*******************************************************************************************/ 576 | /* MERGE */ 577 | /*******************************************************************************************/ 578 | 579 | /** 580 | * pulls, merges and pushes changes to the Remote 581 | * @param repositoryPath relative path of the Repsoitory 582 | * @param remote name of the Remote 583 | * @param branch name of the Branch 584 | * @param ahead number of commits ahead 585 | * @param behind number of commits behind 586 | */ 587 | static async pullAndPushRepository( 588 | repositoryPath: string, 589 | remote: string, 590 | branch: string, 591 | ahead: number = 0, 592 | behind: number = 0, 593 | ): Promise { 594 | GitRepository.updatingStart() 595 | const status = Status.commitsMerging(repositoryPath, remote, branch, ahead, behind) 596 | StatusBar.addStatus(status) 597 | 598 | await GitRepository.pullRepository(repositoryPath, remote, branch, behind, true).catch((error: any) => 599 | GitRepository.catchPushAndPullRepositoryError(error, repositoryPath, remote, branch, status), 600 | ) 601 | 602 | await GitRepository.pushRepository(repositoryPath, remote, branch, ahead, true).catch((error: any) => 603 | GitRepository.catchPushAndPullRepositoryError(error, repositoryPath, remote, branch, status), 604 | ) 605 | 606 | StatusBar.addStatus(Status.commitsMerged(repositoryPath, remote, branch, ahead, behind)) 607 | StatusBar.removeStatus(status) 608 | GitRepository.updatingEnd(repositoryPath) 609 | } 610 | 611 | /** 612 | * handles Errors that occure when merging changes 613 | * @param error Error that occurred 614 | * @param repositoryPath relative path of the Repsoitory 615 | * @param remote name of the Remote 616 | * @param branch name of the Branch 617 | * @param status StatusItem 618 | */ 619 | private static catchPushAndPullRepositoryError( 620 | error: any, 621 | repositoryPath: string, 622 | remote: string, 623 | branch: string, 624 | status: StatusItem, 625 | ): void { 626 | Logger.showError(`Failed to Merge changes on '${remote}/${branch}'`, true) 627 | Logger.showError(error, true) 628 | StatusBar.removeStatus(status) 629 | GitRepository.updatingEnd(repositoryPath) 630 | } 631 | 632 | /*******************************************************************************************/ 633 | /* SUBMODULE UPDATE INIT */ 634 | /*******************************************************************************************/ 635 | 636 | /** 637 | * updates and inits Submodules 638 | */ 639 | static async submoduleUpdateInit(): Promise { 640 | await CMD.executeCommand('git submodule update --init').catch(() => { 641 | Logger.showError(`An Error occured while trying to update submodules`, true) 642 | 643 | return 644 | }) 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /src/application/Helper.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { workspace } from 'vscode' 4 | 5 | /** 6 | * returns the path opf the current opened Workspace 7 | */ 8 | export const getWorkspacePath = (): string => { 9 | if (!workspace.workspaceFolders) { 10 | return '' 11 | } 12 | 13 | return workspace.workspaceFolders[0].uri.fsPath 14 | } 15 | 16 | /** 17 | * returns the name of the repository folder 18 | * @param repositoryPath relative path of the repository 19 | */ 20 | export const getRepositoryName = (repositoryPath: string): string => { 21 | let name = repositoryPath 22 | // if the root-folder is passed, find and return its name 23 | if (!name.length) { 24 | if (workspace.workspaceFolders) { 25 | name = workspace.workspaceFolders[0].name 26 | } 27 | } 28 | 29 | return name 30 | } 31 | 32 | /** 33 | * creates a diff of two objects 34 | * @param target target-object 35 | * @param source source-object 36 | */ 37 | export const deepmerge = (target: any, source: any) => { 38 | // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties 39 | for (const key of Object.keys(source)) { 40 | if (source[key] instanceof Object && key in target) { 41 | Object.assign(source[key], deepmerge(target[key], source[key])) 42 | } 43 | } 44 | // Join `target` and modified `source` 45 | Object.assign(target || {}, source) 46 | 47 | return target 48 | } 49 | -------------------------------------------------------------------------------- /src/application/VsCode.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { commands } from 'vscode' 4 | 5 | /** 6 | * this file is a wrapper for VS Code specific methods 7 | */ 8 | export default class VsCode { 9 | /** 10 | * executes a Command registered to VS Code 11 | * @param command Command to execute 12 | */ 13 | static executeCommand(command: string): void { 14 | commands.executeCommand(command) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/application/Watcher.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { FSWatcher, watch } from 'fs' 4 | import Event from '../models/Event' 5 | import EventHandler from '../handlers/EventHandler' 6 | import Logger from '../UI/Logger' 7 | import Status from '../UI/Status' 8 | import StatusBar from '../UI/StatusBar' 9 | import GitRepository from './GitRepository' 10 | import { getWorkspacePath } from './Helper' 11 | import Config from './Config' 12 | 13 | const TIME_TO_WAIT_FOR_ALL_CHANGES = 1000 14 | 15 | /** 16 | * this class handles changes in the curretnt Workspace-folder 17 | */ 18 | export default class Watcher { 19 | private static fsWatcher: FSWatcher 20 | private static lastChange: Map 21 | private static changedFiles: Map> 22 | private static excludePaths: string[] 23 | private static status = Status.watcherRunning() 24 | 25 | /** 26 | * starts listening in the current Workspace-folder 27 | */ 28 | static async start(): Promise { 29 | Watcher.fsWatcher = watch(getWorkspacePath(), { recursive: true }, (_event: string, filename: any) => { 30 | Watcher.handleFileChange(filename) 31 | }) 32 | 33 | Watcher.lastChange = new Map() 34 | Watcher.changedFiles = new Map() 35 | Watcher.excludePaths = Config.getValue('watcher-excludePaths') || [] 36 | 37 | StatusBar.addStatus(Watcher.status) 38 | } 39 | 40 | /** 41 | * restarts the watcher 42 | */ 43 | static restart(): void { 44 | Watcher.stop() 45 | GitRepository.reset() 46 | StatusBar.addStatus(Status.watcherRestarted()) 47 | Logger.showMessage('Watcher restarted') 48 | Watcher.start() 49 | } 50 | 51 | /** 52 | * stops listening for file changes 53 | */ 54 | static stop(): void { 55 | if (Watcher.fsWatcher) { 56 | Watcher.fsWatcher.close() 57 | } 58 | StatusBar.removeStatus(Watcher.status) 59 | } 60 | 61 | /*******************************************************************************************/ 62 | /* HANDLE FILE CHANGES */ 63 | /*******************************************************************************************/ 64 | 65 | /** 66 | * handles what to do next when a file was changed 67 | * @param filename file that was changed 68 | */ 69 | private static async handleFileChange(filename: string): Promise { 70 | if (!filename) { 71 | return 72 | } 73 | // do nothing when the extension currently executes some Git-Commands 74 | if (GitRepository.isCurrentlyUpdating()) { 75 | return 76 | } 77 | 78 | // relpace Windows- with Unix-paths 79 | filename = filename.replace(/\\/gi, '/') 80 | 81 | let event 82 | if (filename.substring(0, 4) === '.git') { 83 | if (!(await GitRepository.isUnimportantGitFile(filename))) { 84 | // file in .git changed 85 | event = GitRepository.isChangeInSubmodule(filename) ? (event = Event.SUBMODULE) : (event = Event.GIT) 86 | } 87 | } else if (Watcher.isExcludedFile(filename)) { 88 | // something changed that was excluded 89 | return 90 | } else if ((await GitRepository.isFileInSubmodule(filename)).length) { 91 | // a file in a submodule has changed 92 | event = Event.FILE_SUBMODULE 93 | } else { 94 | // some otehr file has changed 95 | event = Event.FILE 96 | } 97 | if (event) { 98 | Watcher.fireChange(event, filename) 99 | } 100 | } 101 | 102 | /** 103 | * waits for multiple changed files in a short period and calls the Event-Handler 104 | * @param event Event the changed file belongs to 105 | * @param filename name of the changed file 106 | */ 107 | private static async fireChange(event: Event, filename: string): Promise { 108 | const changedFiles = await Watcher.waitForLastChange(event, filename) 109 | EventHandler.handle(event, changedFiles) 110 | } 111 | 112 | /** 113 | * checks if the file is excluded to trigger a change 114 | * @param filename filename to check 115 | */ 116 | private static isExcludedFile(filename: string): boolean { 117 | return Watcher.excludePaths.some((path: string) => new RegExp(path, 'gi').test(filename)) 118 | } 119 | 120 | /** 121 | * returns all changed files for a specific Event 122 | * @param event Event to get changed files 123 | */ 124 | private static getChangedFiles(event: Event): Set { 125 | const ret = Watcher.changedFiles.get(event) 126 | if (ret) { 127 | return ret 128 | } 129 | 130 | // nothing registered yet for this event => create new Set for it 131 | const set = new Set() 132 | Watcher.changedFiles.set(event, set) 133 | 134 | return set 135 | } 136 | 137 | /** 138 | * delays the return of the changed files for a specific Event 139 | * @param event Event the changed file belongs to 140 | * @param filename name of the changed file 141 | */ 142 | private static waitForLastChange(event: Event, filename: string): Promise { 143 | const increased = Watcher.addChange(event, filename) 144 | 145 | return new Promise((resolve) => { 146 | setTimeout(() => { 147 | const lastUpdate = Watcher.getLastChange(event) 148 | // if the file of this call was the last changed file for this Event => return all changed files 149 | if (lastUpdate === increased) { 150 | let changedFiles: string[] = [] 151 | changedFiles = [...Watcher.getChangedFiles(event)] 152 | Watcher.changedFiles.set(event, new Set()) 153 | Logger.showMessage(`[changedFiles] ${event}: ${changedFiles.length} files changed`) 154 | 155 | resolve(changedFiles) 156 | } 157 | }, TIME_TO_WAIT_FOR_ALL_CHANGES) 158 | }) 159 | } 160 | 161 | /** 162 | * add a file to the changed files and update its lastChanged-counter 163 | * @param event Event the changed file belongs to 164 | * @param filename name of the changed file 165 | */ 166 | private static addChange(event: Event, filename: string): number { 167 | Watcher.getChangedFiles(event).add(filename) 168 | 169 | const incremented = Watcher.getLastChange(event) + 1 170 | Watcher.lastChange.set(event, incremented) 171 | 172 | return incremented 173 | } 174 | 175 | /** 176 | * returns the number of the last iteration a file was changed for a specific Event 177 | * @param event Event to get the last iteration from 178 | */ 179 | private static getLastChange(event: Event): number { 180 | const ret = Watcher.lastChange.get(event) 181 | 182 | return ret ? ret : 0 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext, commands } from 'vscode' 4 | import Logger from '../UI/Logger' 5 | 6 | /** 7 | * this class is the template for a Command 8 | */ 9 | export default abstract class Command { 10 | /** 11 | * registers a Command in VS Code 12 | * @param context VS Code ExtensionContext 13 | * @param commandString name of the Command to register 14 | * @param command command that should be executed 15 | */ 16 | protected static register(context: ExtensionContext, commandString: string, command: any): void { 17 | context.subscriptions.push(commands.registerCommand('git-assistant.' + commandString, command)) 18 | Logger.showMessage(`[command] ${commandString} registered`) 19 | } 20 | 21 | /** 22 | * this method is called when the Command should register 23 | * @param context VS Code ExtensionContext 24 | */ 25 | static registerCommand(_context: ExtensionContext): void { 26 | throw new TypeError('Must override method') 27 | } 28 | 29 | /** 30 | * this method is called when the dummy Command should register 31 | * @param context VS Code ExtensionContext 32 | */ 33 | static registerDummyCommand(_context: ExtensionContext): void { 34 | throw new TypeError('Must override method') 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/PushBeforeClosingIDE.command.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext, commands } from 'vscode' 4 | import Event from '../models/Event' 5 | import EventHandler from '../handlers/EventHandler' 6 | import Command from './Command' 7 | 8 | /** 9 | * this class registers a Command that is activated when the user tries to close 10 | * the VS Code Window with a keyboard-shortcut 11 | */ 12 | export default class PushBeforeClosingIDECommand extends Command { 13 | static registerCommand(context: ExtensionContext): void { 14 | Command.register(context, 'pushBeforeClosingIDE', PushBeforeClosingIDECommand.pushBeforeClosingIDE) 15 | Command.register(context, 'pushBeforeClosingIDEhard', PushBeforeClosingIDECommand.pushBeforeClosingIDEhard) 16 | } 17 | 18 | static registerDummyCommand(context: ExtensionContext): void { 19 | Command.register( 20 | context, 21 | 'pushBeforeClosingIDE', 22 | commands.executeCommand.bind(null, 'workbench.action.closeActiveEditor'), 23 | ) 24 | Command.register(context, 'pushBeforeClosingIDEhard', commands.executeCommand.bind(null, 'workbench.action.quit')) 25 | } 26 | 27 | static async pushBeforeClosingIDE(): Promise { 28 | await EventHandler.handle(Event.EXIT, false) 29 | } 30 | 31 | static async pushBeforeClosingIDEhard(): Promise { 32 | await EventHandler.handle(Event.EXIT, true) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/ShowOutput.command.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext } from 'vscode' 4 | import Logger from '../UI/Logger' 5 | import Command from './Command' 6 | 7 | /** 8 | * this class registers a Command to show the Output of the Logger 9 | */ 10 | export default class ShowOutputCommand extends Command { 11 | static registerCommand(context: ExtensionContext): void { 12 | Command.register(context, 'showOutput', ShowOutputCommand.executeCommand) 13 | } 14 | 15 | static registerDummyCommand(context: ExtensionContext): void { 16 | Command.register( 17 | context, 18 | 'showOutput', 19 | Logger.showMessage.bind(null, 'you must open a git-repository in your workspace root', true), 20 | ) 21 | } 22 | 23 | static executeCommand(): void { 24 | Logger.showOutput() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/StartExtension.command.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext } from 'vscode' 4 | import Command from './Command' 5 | import { startExtension } from '../extension' 6 | import Logger from '../UI/Logger' 7 | import EventHandler from '../handlers/EventHandler' 8 | import Config from '../application/Config' 9 | 10 | /** 11 | * this class registers a Command to start the Extension 12 | */ 13 | export default class StartExtension extends Command { 14 | static registerCommand(context: ExtensionContext): void { 15 | Command.register(context, 'startGitAssisitant', StartExtension.startExtension) 16 | } 17 | 18 | static registerDummyCommand(context: ExtensionContext): void { 19 | Command.register( 20 | context, 21 | 'startGitAssisitant', 22 | Logger.showMessage.bind(null, 'you must open a git-repository in your workspace root', true), 23 | ) 24 | } 25 | 26 | static async startExtension(): Promise { 27 | Logger.showMessage('Git Assistant started manually') 28 | 29 | Config.loadConfig() 30 | EventHandler.clearAllHandlers() 31 | startExtension() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/StopExtension.command.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext } from 'vscode' 4 | import Command from './Command' 5 | import Watcher from '../application/Watcher' 6 | import Logger from '../UI/Logger' 7 | 8 | /** 9 | * this class registers a Command to stop the Extension 10 | */ 11 | export default class StopExtension extends Command { 12 | static registerCommand(context: ExtensionContext): void { 13 | Command.register(context, 'stopGitAssisitant', StopExtension.stopExtension) 14 | } 15 | 16 | static registerDummyCommand(context: ExtensionContext): void { 17 | Command.register( 18 | context, 19 | 'stopGitAssisitant', 20 | Logger.showMessage.bind(null, 'you must open a git-repository in your workspace root', true), 21 | ) 22 | } 23 | 24 | static async stopExtension(): Promise { 25 | Logger.showMessage('Git Assistant stopped manually') 26 | Watcher.stop() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { ExtensionContext } from 'vscode' 4 | import Commands from './application/Commands' 5 | import Features from './application/Features' 6 | import Event from './models/Event' 7 | import EventHandler from './handlers/EventHandler' 8 | import Logger from './UI/Logger' 9 | import Status from './UI/Status' 10 | import StatusBar from './UI/StatusBar' 11 | import Config from './application/Config' 12 | import { exists } from 'fs' 13 | import { getWorkspacePath } from './application/Helper' 14 | 15 | let context: ExtensionContext 16 | 17 | // this function is called when the extension is activated for the first time 18 | export const activate = (ctx: ExtensionContext): void => { 19 | context = ctx 20 | 21 | Logger.init() 22 | 23 | exists(getWorkspacePath() + '/.git', (exists) => { 24 | if (exists) { 25 | initExtension() 26 | } else { 27 | Commands.registerDummyCommands(context) 28 | } 29 | }) 30 | } 31 | 32 | // this method is called when your extension is deactivated 33 | export const deactivate = (): void => Logger.showMessage('Extension deactivated') 34 | 35 | const initExtension = async (): Promise => { 36 | Config.loadConfig() 37 | 38 | await Commands.registerCommands(context) 39 | 40 | if (!Config.isEnabled('enabled')) { 41 | Logger.showMessage('Extension is not enabled in settings') 42 | 43 | return 44 | } 45 | startExtension() 46 | } 47 | 48 | export const startExtension = async (): Promise => { 49 | StatusBar.initStatusBar(context) 50 | 51 | await Features.enableFeatures() 52 | 53 | const status = Status.startingExtension() 54 | StatusBar.addStatus(status) 55 | 56 | await EventHandler.handle(Event.START) 57 | 58 | StatusBar.removeStatus(status) 59 | } 60 | -------------------------------------------------------------------------------- /src/handlers/ChangeHandler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this class is the template for a ChangeHandler 3 | */ 4 | export default abstract class ChangeHandler { 5 | /** 6 | * function that is called when the Handler should register itself 7 | */ 8 | static registerEventHandler(): void { 9 | throw new TypeError('Must override method') 10 | } 11 | 12 | /** 13 | * function that is called when a Event was fired 14 | * @param payload some additional information for the Handlers 15 | */ 16 | static async handle(_payload?: any): Promise { 17 | throw new TypeError('Must override method') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/handlers/EventHandler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Logger from '../UI/Logger' 4 | import Event from '../models/Event' 5 | import ChangeHandler from './ChangeHandler' 6 | 7 | /** 8 | * this class handles the registration and calling of Features of the Extension 9 | */ 10 | export default class EventHandler { 11 | private static changeHandlers: Map = new Map() 12 | private static level = 0 13 | 14 | /** 15 | * gets all registered Handlers for a given Event 16 | * @param changeEvent Event to search for 17 | */ 18 | private static getChangeHandlers(changeEvent: Event): typeof ChangeHandler[] { 19 | if (!EventHandler.changeHandlers.has(changeEvent)) { 20 | EventHandler.changeHandlers.set(changeEvent, []) 21 | } 22 | const ret = EventHandler.changeHandlers.get(changeEvent) 23 | if (ret) { 24 | return ret 25 | } 26 | 27 | return [] 28 | } 29 | 30 | /** 31 | * resets everything and deletes all Handlers 32 | */ 33 | static clearAllHandlers(): void { 34 | EventHandler.changeHandlers = new Map() 35 | EventHandler.level = 0 36 | Logger.showMessage(`[event] ChangeHandlers reset`) 37 | } 38 | 39 | /** 40 | * registers an Handler for a specific Event 41 | * @param changeEvent Event, when the Handler should be called 42 | * @param Handler Handler that is called when the Event was fired 43 | */ 44 | static registerHandler(changeEvent: Event, Handler: typeof ChangeHandler): void { 45 | const Handlers = EventHandler.getChangeHandlers(changeEvent) 46 | Handlers.push(Handler) 47 | Logger.showMessage(`[event][register] '${Handler.name}' added to ${changeEvent}-Handlers`) 48 | } 49 | 50 | /** 51 | * calls all Handlers for a specific Event 52 | * @param changeEvent Event to fire 53 | * @param payload some additional information for the Handlers 54 | */ 55 | static async handle(changeEvent: Event, payload?: any): Promise { 56 | const Handlers = EventHandler.getChangeHandlers(changeEvent) 57 | if (!Handlers.length) { 58 | return 59 | } 60 | 61 | EventHandler.level++ 62 | 63 | const levelStringUp = EventHandler.levelString(true) 64 | const levelStringDown = EventHandler.levelString(false) 65 | 66 | const timeStartHandler = Date.now() 67 | for (const Handler of Handlers) { 68 | const timeStartEvent = Date.now() 69 | Logger.showMessage(`[event][call]${levelStringUp} '${Handler.name}'`) 70 | // call Handler and wait until its finished 71 | await Handler.handle(payload) 72 | const timeEndEvent = Date.now() 73 | const timeDiffEvent = (timeEndEvent - timeStartEvent) / 1000 74 | Logger.showMessage(`[event][call]${levelStringDown} '${Handler.name}' [${timeDiffEvent}s]`) 75 | } 76 | const timeEndHandler = Date.now() 77 | const timeDiffHandler = (timeEndHandler - timeStartHandler) / 1000 78 | Logger.showMessage( 79 | `[event] ${Handlers.length} function${ 80 | Handlers.length === 1 ? '' : 's' 81 | } called for '${changeEvent}' [${timeDiffHandler}s]`, 82 | ) 83 | 84 | EventHandler.level-- 85 | } 86 | 87 | /** 88 | * generates the depth-info of the current called function 89 | * @param up wheter the Event was started or ended 90 | */ 91 | private static levelString(up: boolean): string { 92 | return `${new Array(EventHandler.level - 1).fill(' -').join('')} ${up ? '>' : '<'}` 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/handlers/exit/PushBeforeClosingIDE.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { commands, workspace } from 'vscode' 4 | import Logger from '../../UI/Logger' 5 | import QuickPick from '../../UI/QuickPick' 6 | import QuickPickOption from '../../UI/QuickPickOption' 7 | import GitRepository from '../../application/GitRepository' 8 | import ChangeHandler from '../ChangeHandler' 9 | import Event from '../../models/Event' 10 | import EventHandler from '../EventHandler' 11 | import Config from '../../application/Config' 12 | 13 | /** 14 | * this Handler is responsible for informing the user he hasn't pushed all changes when closing VS Code 15 | */ 16 | export default class PushBeforeClosingIDE extends ChangeHandler { 17 | static registerEventHandler(): void { 18 | if (Config.isEnabled('pushBeforeClosingIDE')) { 19 | EventHandler.registerHandler(Event.EXIT, this) 20 | } 21 | } 22 | 23 | static async handle(hardQuit: boolean): Promise { 24 | if (!hardQuit) { 25 | commands.executeCommand('workbench.action.closeActiveEditor') 26 | let fileOpened = false 27 | const rootPath = 28 | (workspace.workspaceFolders && 29 | workspace.workspaceFolders.length && 30 | workspace.workspaceFolders[0].uri.path) || 31 | '' 32 | 33 | // checks if no more files are opened in the current Window 34 | workspace.textDocuments.forEach((doc) => { 35 | if (doc.fileName.includes(rootPath)) { 36 | fileOpened = true 37 | } 38 | }) 39 | 40 | if (fileOpened) { 41 | return 42 | } 43 | } 44 | 45 | if (!(await GitRepository.getGitModel()).getAhead()) { 46 | Logger.showMessage('Editor closed') 47 | commands.executeCommand(hardQuit ? 'workbench.action.quit' : 'workbench.action.closeWindow') 48 | 49 | return 50 | } 51 | 52 | const command = await QuickPick.showQuickPick( 53 | 'choose an option', 54 | new QuickPickOption('Push all changes and close Window', 'pushChanges'), 55 | QuickPickOption.optionQUIT, 56 | QuickPickOption.optionCANCEL, 57 | ) 58 | if (command) { 59 | if (command === 'pushChanges') { 60 | await GitRepository.pushRootRepository() 61 | commands.executeCommand(QuickPickOption.optionQUIT.command) 62 | 63 | return 64 | } 65 | 66 | commands.executeCommand(command) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/handlers/git/GitHandler.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Git from '../../models/Git' 4 | import GitRepository from '../../application/GitRepository' 5 | import ChangeHandler from '../ChangeHandler' 6 | import Event from '../../models/Event' 7 | import EventHandler from '../EventHandler' 8 | 9 | /** 10 | * this Handler is responsible for changes in the Git-Repository 11 | */ 12 | export default class GitHandler extends ChangeHandler { 13 | static registerEventHandler(): void { 14 | EventHandler.registerHandler(Event.GIT, this) 15 | } 16 | 17 | static async handle(): Promise { 18 | const gitModel = await GitRepository.updateGitModel() 19 | await GitHandler.handleRepositoryChange(gitModel) 20 | } 21 | 22 | /** 23 | * checks for changes in a given Git-Repository 24 | * @param gitModel gitModel to check for changes 25 | */ 26 | static async handleRepositoryChange(gitModel: Git): Promise { 27 | const repositoryPath = gitModel.getRelativePath() 28 | const modelDiff = await GitRepository.getModelDiff(repositoryPath) 29 | 30 | // nothing important changed (except timestamp and oldGitModel) 31 | if (Object.keys(modelDiff).length < 2) { 32 | return gitModel 33 | } 34 | 35 | const ahead = modelDiff.ahead > 0 36 | const behind = modelDiff.behind > 0 37 | if (!modelDiff.detachedHEAD) { 38 | if (ahead && behind) { 39 | await EventHandler.handle(Event.GIT_COMMITS, repositoryPath) 40 | } else if (ahead) { 41 | await EventHandler.handle(Event.GIT_COMMITS_LOCAL, repositoryPath) 42 | } else if (behind) { 43 | await EventHandler.handle(Event.GIT_COMMITS_REMOTE, repositoryPath) 44 | } 45 | } 46 | 47 | if (modelDiff.branch !== undefined) { 48 | await EventHandler.handle(Event.GIT_BRANCH_CHANGED, repositoryPath) 49 | } 50 | 51 | if (modelDiff.modifiedSubmodules && modelDiff.modifiedSubmodules.length) { 52 | await EventHandler.handle(Event.SUBMODULE_UPDATE) 53 | } 54 | 55 | return gitModel 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/handlers/git/branch_changed/BranchWarn.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Git from '../../../models/Git' 4 | import InformationMessage from '../../../UI/InformationMessage' 5 | import InputBox from '../../../UI/InputBox' 6 | import MessageOption from '../../../UI/MessageOption' 7 | import QuickPick from '../../../UI/QuickPick' 8 | import QuickPickOption from '../../../UI/QuickPickOption' 9 | import GitRepository from '../../../application/GitRepository' 10 | import ChangeHandler from '../../ChangeHandler' 11 | import Event from '../../../models/Event' 12 | import EventHandler from '../../EventHandler' 13 | import Config, { ConfigOptions } from '../../../application/Config' 14 | 15 | /** 16 | * this Handler is responsible to check if the user works on a wrong Branch 17 | */ 18 | export default class BranchWarn extends ChangeHandler { 19 | static registerEventHandler(): void { 20 | if (Config.isEnabled('branchWarn')) { 21 | EventHandler.registerHandler(Event.GIT_BRANCH_CHANGED, this) 22 | } 23 | } 24 | 25 | static async handle(repositoryPath: string): Promise { 26 | const gitModel = await GitRepository.getGitModel(repositoryPath) 27 | const currentBranch = gitModel.getBranch() 28 | 29 | if (gitModel.isHeadDetached()) { 30 | return 31 | } 32 | 33 | if (Config.getValue('branchWarn-illegalBranches').indexOf(currentBranch) < 0) { 34 | return 35 | } 36 | 37 | let message = `You are currently on branch "${currentBranch}"` 38 | if (!gitModel.isRootGit()) { 39 | message += ` in Submodule "${repositoryPath}"` 40 | } 41 | message += `. You should not commit on this branch. Would you like to switch branch?` 42 | const action = await InformationMessage.showInformationMessage( 43 | message, 44 | MessageOption.optionYES, 45 | MessageOption.optionNO, 46 | ) 47 | 48 | if (action !== MessageOption.YES) { 49 | return 50 | } 51 | 52 | // let the user choose a branch to checkout 53 | const localBranches = gitModel.getLocalBranches() 54 | const createNewBranchCommand = '[git-assistant][create-new-branch]' 55 | let branch = '' 56 | if (localBranches.length > 1) { 57 | const options: QuickPickOption[] = [] 58 | localBranches.forEach((branch) => { 59 | const branchName = branch.getName() 60 | if (branchName !== currentBranch) { 61 | options.push(new QuickPickOption(branchName, branchName)) 62 | } 63 | }) 64 | options.push(new QuickPickOption('+ create a new branch', createNewBranchCommand)) 65 | 66 | branch = await QuickPick.showQuickPick('choose the branch to checkout', ...options) 67 | } 68 | 69 | if (!branch.length || branch === createNewBranchCommand) { 70 | branch = await InputBox.showInputBox('enter name of the new branch') 71 | await GitRepository.createNewBranch(repositoryPath, branch) 72 | } 73 | 74 | BranchWarn.checkoutWithoutStash(gitModel, branch) 75 | } 76 | 77 | // tries to checkout a branch 78 | // iff checkout fails => try to stash before checkout 79 | private static checkoutWithoutStash = async (gitModel: Git, branch: string): Promise => { 80 | GitRepository.checkoutBranchForRepository(gitModel.getRelativePath(), branch).catch(() => 81 | BranchWarn.checkoutWithStash(gitModel, branch), 82 | ) 83 | } 84 | 85 | // stashes changes => then checkout 86 | // asks user in the end if it should pop the latest changes from stash 87 | private static checkoutWithStash = async (gitModel: Git, branch: string): Promise => { 88 | const stashed = await BranchWarn.stashBeforeCheckout(gitModel.getPath(), branch) 89 | if (!stashed) { 90 | return 91 | } 92 | 93 | await GitRepository.checkoutBranchForRepository(gitModel.getRelativePath(), branch) 94 | 95 | BranchWarn.stashPopAfterCheckout(gitModel.getPath()) 96 | } 97 | 98 | private static stashBeforeCheckout = async (repositoryPath: string, branch: string): Promise => { 99 | const stashChanges = Config.getValue('branchWarn-stashChanges') 100 | if (stashChanges === ConfigOptions.disabled) { 101 | return false 102 | } 103 | 104 | if (stashChanges === ConfigOptions.auto) { 105 | GitRepository.stashSaveChanges(repositoryPath) 106 | .then(() => true) 107 | .catch(() => false) 108 | } 109 | 110 | const action = await InformationMessage.showInformationMessage( 111 | `would you like to stash the current changes before checking out branch '${branch}'? The current changes will be lost`, 112 | MessageOption.optionYES, 113 | MessageOption.optionNO, 114 | ) 115 | 116 | if (action === MessageOption.NO) { 117 | return false 118 | } 119 | 120 | await GitRepository.stashSaveChanges(repositoryPath) 121 | 122 | return true 123 | } 124 | 125 | private static stashPopAfterCheckout = async (repositoryPath: string): Promise => { 126 | const action = await InformationMessage.showInformationMessage( 127 | `would you like to unstash the changes?`, 128 | MessageOption.optionYES, 129 | MessageOption.optionNO, 130 | ) 131 | if (action !== MessageOption.YES) { 132 | return 133 | } 134 | await GitRepository.stashPopChanges(repositoryPath) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/handlers/git/branch_changed/CheckForRemote.handler.ts: -------------------------------------------------------------------------------- 1 | 'use static' 2 | 3 | import InformationMessage from '../../../UI/InformationMessage' 4 | import MessageOption from '../../../UI/MessageOption' 5 | import QuickPick from '../../../UI/QuickPick' 6 | import QuickPickOption from '../../../UI/QuickPickOption' 7 | import GitRepository from '../../../application/GitRepository' 8 | import ChangeHandler from '../../ChangeHandler' 9 | import Event from '../../../models/Event' 10 | import EventHandler from '../../EventHandler' 11 | import Config, { ConfigOptions } from '../../../application/Config' 12 | import { getRepositoryName } from '../../../application/Helper' 13 | 14 | /** 15 | * this Handler is responsible for checking if a Remote exists for the current Branch 16 | */ 17 | export default class CheckForRemote extends ChangeHandler { 18 | static registerEventHandler(): void { 19 | if (Config.isEnabled('checkForRemote')) { 20 | EventHandler.registerHandler(Event.GIT_BRANCH_CHANGED, this) 21 | } 22 | } 23 | 24 | static async handle(repositoryPath: string): Promise { 25 | const gitModel = await GitRepository.getGitModel(repositoryPath) 26 | const branch = gitModel.getBranch() 27 | let remote = gitModel.getRemote() 28 | 29 | if (gitModel.isHeadDetached() || remote || remote === null) { 30 | return 31 | } 32 | 33 | const remotes = gitModel.getRemotes() 34 | 35 | if (remotes.length && Config.getValue('checkForRemote') !== ConfigOptions.auto) { 36 | const option = await InformationMessage.showInformationMessage( 37 | `No remote found for '${getRepositoryName( 38 | repositoryPath, 39 | )}' on branch '${branch}' Would you like to publish this branch to the remote Server?`, 40 | MessageOption.optionYES, 41 | MessageOption.optionNO, 42 | ) 43 | 44 | if (option !== MessageOption.YES) { 45 | return 46 | } 47 | } 48 | 49 | // only one Remote exists => choose it 50 | if (remotes.length === 1) { 51 | remote = remotes[0] 52 | } else { 53 | // a default-Remote is set => choose it 54 | remote = Config.getValue('checkForRemote-defaultRemote') 55 | if (!remote) { 56 | // let the user decide wich Remote he wants to publish the Branch 57 | const options = remotes.map((remote: string) => new QuickPickOption(remote, remote)) 58 | 59 | if (!options.length) { 60 | return CheckForRemote.noRemoteUpstreamSet(repositoryPath) 61 | } 62 | remote = await QuickPick.showQuickPick(`choose a remote to publish the branch '${branch}'`, ...options) 63 | } 64 | } 65 | 66 | if (!remote) { 67 | return 68 | } 69 | 70 | await GitRepository.publishBranch(repositoryPath, remote, branch) 71 | } 72 | 73 | private static noRemoteUpstreamSet(repositoryPath: string): void { 74 | InformationMessage.showInformationMessage( 75 | `No Remotes found for your Repository '${getRepositoryName( 76 | repositoryPath, 77 | )}'. You should add a Remote to have a backup in case of data loss.`, 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/handlers/git/branch_changed/DetectDetachedHead.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Branch from '../../../models/Branch' 4 | import Git from '../../../models/Git' 5 | import InformationMessage from '../../../UI/InformationMessage' 6 | import Logger from '../../../UI/Logger' 7 | import MessageOption from '../../../UI/MessageOption' 8 | import Status from '../../../UI/Status' 9 | import StatusBar from '../../../UI/StatusBar' 10 | import GitRepository from '../../../application/GitRepository' 11 | import ChangeHandler from '../../ChangeHandler' 12 | import Event from '../../../models/Event' 13 | import EventHandler from '../../EventHandler' 14 | import Config, { ConfigOptions } from '../../../application/Config' 15 | import QuickPickOption from '../../../UI/QuickPickOption' 16 | import QuickPick from '../../../UI/QuickPick' 17 | 18 | /** 19 | * this Handler is responsible for resolving the "detached HEAD" status 20 | */ 21 | export default class DetectDetachedHead extends ChangeHandler { 22 | static registerEventHandler(): void { 23 | if (Config.isEnabled('detectDetachedHead')) { 24 | EventHandler.registerHandler(Event.GIT_BRANCH_CHANGED, this) 25 | } 26 | } 27 | 28 | static async handle(repositoryPath: string): Promise { 29 | const gitModel = await GitRepository.getGitModel(repositoryPath) 30 | if (!gitModel.isHeadDetached()) { 31 | return 32 | } 33 | 34 | const branch = await DetectDetachedHead.getRealBranchForHash(gitModel) 35 | 36 | if (!branch.length) { 37 | return 38 | } 39 | 40 | if (Config.getValue('detectDetachedHead') === ConfigOptions.auto) { 41 | await GitRepository.checkoutBranchForRepository(gitModel.getRelativePath(), branch) 42 | StatusBar.addStatus(Status.autoCheckoutForDetachedHead(gitModel.getRelativePath(), branch)) 43 | 44 | return 45 | } 46 | 47 | let message = `the HEAD of your Repository is detached. Would you like to checkout its corresponding branch '${branch}'?` 48 | 49 | if (!gitModel.isRootGit()) { 50 | message = `The HEAD of the Submodule '${gitModel.getRelativePath()}' in your Repisotory is detached. Would you like to checkout its corresponding branch '${branch}'?` 51 | } 52 | 53 | const action = await InformationMessage.showInformationMessage( 54 | message, 55 | MessageOption.optionYES, 56 | MessageOption.optionNO, 57 | ) 58 | if (action !== MessageOption.YES) { 59 | return 60 | } 61 | 62 | await GitRepository.checkoutBranchForRepository(gitModel.getRelativePath(), branch) 63 | } 64 | 65 | /** 66 | * finds the corresponding Branch for a Commit-Hash 67 | */ 68 | private static getRealBranchForHash = async (gitModel: Git): Promise => { 69 | // the first one in the list is the current "detached HEAD" 70 | const branches = gitModel.getLocalBranches().filter((branch, index) => index > 1) 71 | const current = gitModel.getBranch() 72 | 73 | const realBranches: string[] = [] 74 | branches.forEach((branch: Branch) => { 75 | if (current === branch.getCommit() || current === branch.getName()) { 76 | realBranches.push(branch.getName()) 77 | } 78 | }) 79 | 80 | if (realBranches.length) { 81 | if (realBranches.length === 1) { 82 | return realBranches[0] 83 | } 84 | 85 | if (gitModel.getMainRepositoryPath()) { 86 | const configuredBranch = await GitRepository.getConfiguredBranchForSubmodule(gitModel) 87 | if (configuredBranch && realBranches.includes(configuredBranch)) { 88 | return configuredBranch 89 | } 90 | 91 | const options: QuickPickOption[] = realBranches.map((branch) => new QuickPickOption(branch, branch)) 92 | 93 | const selectedBranch = await QuickPick.showQuickPick('choose the branch to check out', ...options) 94 | if (selectedBranch) { 95 | return selectedBranch 96 | } 97 | } 98 | } 99 | 100 | Logger.showError( 101 | `could not find branch for '${current}' ${ 102 | !gitModel.isRootGit() ? ` in Submodule '${gitModel.getRelativePath()}'` : '' 103 | }. You have to checkout the branch manually.`, 104 | true, 105 | ) 106 | 107 | return '' 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/handlers/git/commits/MergeCommits.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import InformationMessage from '../../../UI/InformationMessage' 4 | import MessageOption from '../../../UI/MessageOption' 5 | import GitRepository from '../../../application/GitRepository' 6 | import ChangeHandler from '../../ChangeHandler' 7 | import Event from '../../../models/Event' 8 | import EventHandler from '../../EventHandler' 9 | import Config, { ConfigOptions } from '../../../application/Config' 10 | import { getRepositoryName } from '../../../application/Helper' 11 | 12 | /** 13 | * this Handler is responsible to pull Commits from Remote, merge them and push them to the Remote 14 | */ 15 | export default class MergeCommits extends ChangeHandler { 16 | static registerEventHandler(): void { 17 | if (Config.isEnabled('mergeCommits')) { 18 | EventHandler.registerHandler(Event.GIT_COMMITS, this) 19 | } 20 | } 21 | 22 | static async handle(repositoryPath: string): Promise { 23 | const gitModel = await GitRepository.getGitModel(repositoryPath) 24 | const ahead = gitModel.getAhead() 25 | const behind = gitModel.getBehind() 26 | const remote = gitModel.getRemote() 27 | const branch = gitModel.getBranch() 28 | 29 | if (!(ahead && behind)) { 30 | return 31 | } 32 | 33 | if (Config.getValue('mergeCommits') === ConfigOptions.auto) { 34 | await GitRepository.pullAndPushRepository(repositoryPath, remote, branch, ahead, behind) 35 | 36 | return 37 | } 38 | 39 | const action = await InformationMessage.showInformationMessage( 40 | `You are currently behind ${behind} commits. But you also have ${ahead} changes that are currently not on the server for ${getRepositoryName( 41 | repositoryPath, 42 | )}. Would you like to merge the changes?`, 43 | MessageOption.optionYES, 44 | MessageOption.optionNO, 45 | ) 46 | 47 | if (action !== MessageOption.YES) { 48 | return 49 | } 50 | 51 | await GitRepository.pullAndPushRepository(repositoryPath, remote, branch, ahead, behind) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/handlers/git/commits/PullCommits.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import InformationMessage from '../../../UI/InformationMessage' 4 | import MessageOption from '../../../UI/MessageOption' 5 | import GitRepository from '../../../application/GitRepository' 6 | import ChangeHandler from '../../ChangeHandler' 7 | import Event from '../../../models/Event' 8 | import EventHandler from '../../EventHandler' 9 | import Config, { ConfigOptions } from '../../../application/Config' 10 | import { getRepositoryName } from '../../../application/Helper' 11 | 12 | /** 13 | * this Handler is responsible for pulling changes from a Remote 14 | */ 15 | export default class PullCommits extends ChangeHandler { 16 | static registerEventHandler(): void { 17 | if (Config.isEnabled('pullCommits')) { 18 | EventHandler.registerHandler(Event.GIT_COMMITS_REMOTE, this) 19 | } 20 | } 21 | 22 | static async handle(repositoryPath: string): Promise { 23 | const gitModel = await GitRepository.getGitModel(repositoryPath) 24 | const behind = gitModel.getBehind() 25 | const remote = gitModel.getRemote() 26 | const branch = gitModel.getBranch() 27 | 28 | if (!behind) { 29 | return 30 | } 31 | 32 | if (Config.getValue('pullCommits') === ConfigOptions.auto) { 33 | await GitRepository.pullRepository(repositoryPath, remote, branch, behind) 34 | 35 | return 36 | } 37 | 38 | const action = await InformationMessage.showInformationMessage( 39 | `You are currently ${behind} commits behind in '${getRepositoryName( 40 | repositoryPath, 41 | )}'. Would you like to pull these changes?`, 42 | MessageOption.optionYES, 43 | MessageOption.optionNO, 44 | ) 45 | 46 | if (action !== MessageOption.YES) { 47 | return 48 | } 49 | 50 | await GitRepository.pullRepository(repositoryPath, remote, branch, behind) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/handlers/git/commits/PushCommit.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import InformationMessage from '../../../UI/InformationMessage' 4 | import MessageOption from '../../../UI/MessageOption' 5 | import GitRepository from '../../../application/GitRepository' 6 | import ChangeHandler from '../../ChangeHandler' 7 | import Event from '../../../models/Event' 8 | import EventHandler from '../../EventHandler' 9 | import Config, { ConfigOptions } from '../../../application/Config' 10 | import { getRepositoryName } from '../../../application/Helper' 11 | 12 | /** 13 | * this Handler is responsible for pushing changes to the Remote 14 | */ 15 | export default class PushCommits extends ChangeHandler { 16 | static registerEventHandler(): void { 17 | if (Config.isEnabled('pushCommits')) { 18 | EventHandler.registerHandler(Event.GIT_COMMITS_LOCAL, this) 19 | } 20 | } 21 | 22 | static async handle(repositoryPath: string): Promise { 23 | const gitModel = await GitRepository.getGitModel(repositoryPath) 24 | const ahead = gitModel.getAhead() 25 | const remote = gitModel.getRemote() 26 | const branch = gitModel.getBranch() 27 | 28 | if (!ahead) { 29 | return 30 | } 31 | 32 | if (Config.getValue('pushCommits') === ConfigOptions.auto) { 33 | await PushCommits.push(repositoryPath, remote, branch, ahead) 34 | 35 | return 36 | } 37 | 38 | const action = await InformationMessage.showInformationMessage( 39 | `You have ${ahead} changes that are currently not on the server for ${getRepositoryName( 40 | repositoryPath, 41 | )}. Would you like to push the changes?`, 42 | MessageOption.optionYES, 43 | MessageOption.optionNO, 44 | ).catch(() => {}) 45 | 46 | if (action !== MessageOption.YES) { 47 | return 48 | } 49 | 50 | await PushCommits.push(repositoryPath, remote, branch, ahead) 51 | } 52 | 53 | private static async push(repositoryPath: string, remote: string, branch: string, ahead: number): Promise { 54 | await EventHandler.handle(Event['GIT_PUSH'], repositoryPath) 55 | await GitRepository.pushRepository(repositoryPath, remote, branch, ahead) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/handlers/git/push/PushSubmodulesFirst.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import GitRepository from '../../../application/GitRepository' 4 | import ChangeHandler from '../../ChangeHandler' 5 | import Event from '../../../models/Event' 6 | import EventHandler from '../../EventHandler' 7 | import PushCommits from '../commits/PushCommit.handler' 8 | import Config from '../../../application/Config' 9 | 10 | /** 11 | * this Handler is responsible to push all Submodules for a Repository 12 | */ 13 | export default class PushSubmodulesFirst extends ChangeHandler { 14 | static registerEventHandler(): void { 15 | if (Config.isEnabled('pushSubmodulesFirst')) { 16 | EventHandler.registerHandler(Event.GIT_PUSH, this) 17 | } 18 | } 19 | 20 | static async handle(repositoryPath: string): Promise { 21 | const gitModel = await GitRepository.getGitModel(repositoryPath) 22 | 23 | for (const submodule of gitModel.getSubmodules()) { 24 | await PushCommits.handle(submodule.getPath()) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/handlers/start/CheckConfigVariables.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import InputBox from '../../UI/InputBox' 4 | import Logger from '../../UI/Logger' 5 | import Status from '../../UI/Status' 6 | import StatusBar from '../../UI/StatusBar' 7 | import GitRepository from '../../application/GitRepository' 8 | import ChangeHandler from '../ChangeHandler' 9 | import Event from '../../models/Event' 10 | import EventHandler from '../EventHandler' 11 | import Config from '../../application/Config' 12 | 13 | /** 14 | * this Handler checks for missing config-Variables 15 | */ 16 | export default class CheckConfigVariables extends ChangeHandler { 17 | static registerEventHandler(): void { 18 | if (Config.isEnabled('checkConfigVariables')) { 19 | EventHandler.registerHandler(Event.START, this) 20 | } 21 | } 22 | 23 | static async handle(): Promise { 24 | // wich variables should be checked for 25 | const toCheck = Config.getValue('checkConfigVariables-variablesToCheck') 26 | 27 | let failed: boolean = false 28 | for (const variable of toCheck) { 29 | const result = await GitRepository.getConfigVariable(variable).catch(async () => { 30 | failed = true 31 | const input = await InputBox.showInputBox(`Config Variable '${variable}' not set. Please enter a value`) 32 | if (input.length) { 33 | await GitRepository.setConfigVariable(variable, input, Config.getValue('checkConfigVariables-scope')) 34 | } 35 | }) 36 | if (result) { 37 | Logger.showMessage(`Config Variable '${variable}' is '${result}'`) 38 | } 39 | } 40 | 41 | StatusBar.addStatus(Status.allConfigVariablesChecked()) 42 | // if all variables were set => disable this Feature 43 | if (!failed) { 44 | Config.disable('checkConfigVariables') 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/handlers/start/CheckRemoteChanges.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import ChangeHandler from '../ChangeHandler' 4 | import EventHandler from '../EventHandler' 5 | import Event from '../../models/Event' 6 | import GitRepository from '../../application/GitRepository' 7 | import StatusBar from '../../UI/StatusBar' 8 | import Status from '../../UI/Status' 9 | import Git from '../../models/Git' 10 | import GitHandler from '../git/GitHandler.handler' 11 | import Config from '../../application/Config' 12 | 13 | /** 14 | * this Handler checks periodically for new Commits on the Remote 15 | */ 16 | export default class CheckRemoteChanges extends ChangeHandler { 17 | static iterations = 0 18 | static registerEventHandler(): void { 19 | if (Config.isEnabled('checkRemoteChanges')) { 20 | EventHandler.registerHandler(Event.START, this) 21 | } 22 | } 23 | 24 | static async handle(repositoryPath: string = ''): Promise { 25 | const gitModel = !CheckRemoteChanges.iterations 26 | ? await GitRepository.getGitModel(repositoryPath) 27 | : await GitRepository.updateGitModel(repositoryPath) 28 | 29 | CheckRemoteChanges.nextCheck(gitModel) 30 | 31 | // do nothing on first call, because there exist other Handlers for that 32 | if (!CheckRemoteChanges.iterations++) { 33 | return 34 | } 35 | 36 | await GitHandler.handleRepositoryChange(gitModel) 37 | 38 | const ahead = gitModel.getAhead() 39 | const behind = gitModel.getBehind() 40 | const remote = gitModel.getRemote() 41 | const branch = gitModel.getBranch() 42 | 43 | if (!remote.length) { 44 | return CheckRemoteChanges.nextCheck(gitModel) 45 | } 46 | 47 | // display message that all is up-to-date 48 | if (!ahead && !behind) { 49 | StatusBar.addStatus(Status.branchIsUpToDate(gitModel.getRelativePath(), remote, branch)) 50 | } 51 | 52 | // check all Submodules 53 | for (const submodulePath of gitModel.getSubmodules().map((submodule) => submodule.getPath())) { 54 | await CheckRemoteChanges.handle(submodulePath) 55 | } 56 | } 57 | 58 | /** 59 | * plans the next execution of the check if it is the root-Repository 60 | * @param gitModel current Model of a Repository 61 | */ 62 | private static nextCheck(gitModel: Git): void { 63 | if (!gitModel.isRootGit()) { 64 | return 65 | } 66 | 67 | const checkEveryNMinutes = Config.getValue('checkRemoteChanges-checkEveryNMinutes') 68 | if (checkEveryNMinutes) { 69 | setTimeout(() => { 70 | if (!GitRepository.isCurrentlyUpdating()) { 71 | CheckRemoteChanges.handle() 72 | } 73 | }, checkEveryNMinutes * 1000 * 60) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/handlers/start/PerformStartupCheckOfRepositories.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import ChangeHandler from '../ChangeHandler' 4 | import EventHandler from '../EventHandler' 5 | import Event from '../../models/Event' 6 | import GitRepository from '../../application/GitRepository' 7 | import GitHandler from '../git/GitHandler.handler' 8 | import Submodule from '../../models/Submodule' 9 | 10 | /** 11 | * this Hanlder initializes all information for each Repository 12 | */ 13 | export default class PerformStartupCheckOfRepositories extends ChangeHandler { 14 | static registerEventHandler(): void { 15 | EventHandler.registerHandler(Event.START, this) 16 | } 17 | 18 | static async handle(): Promise { 19 | const gitModel = await GitRepository.getGitModel() 20 | await GitHandler.handleRepositoryChange(gitModel) 21 | 22 | const submodulePaths = gitModel.getSubmodules().map((submodule: Submodule) => submodule.getPath()) 23 | for (const submodulePath of submodulePaths) { 24 | const submoduleModel = await GitRepository.getGitModel(submodulePath, gitModel.getPath()) 25 | await GitHandler.handleRepositoryChange(submoduleModel) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/handlers/start/UpdateInitSubmodules.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import ChangeHandler from '../ChangeHandler' 4 | import EventHandler from '../EventHandler' 5 | import Event from '../../models/Event' 6 | import Config from '../../application/Config' 7 | import GitRepository from '../../application/GitRepository' 8 | 9 | /** 10 | * this Hanlder updates all Submodules on Extension-start 11 | */ 12 | export default class UpdateInitSubmodules extends ChangeHandler { 13 | static iterations = 0 14 | static registerEventHandler(): void { 15 | if (Config.isEnabled('updateSubmodules')) { 16 | EventHandler.registerHandler(Event.START, this) 17 | } 18 | } 19 | 20 | static async handle(): Promise { 21 | await GitRepository.submoduleUpdateInit() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/handlers/start/WatcherStart.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Watcher from '../../application/Watcher' 4 | import ChangeHandler from '../ChangeHandler' 5 | import Event from '../../models/Event' 6 | import EventHandler from '../EventHandler' 7 | 8 | /** 9 | * this Handler starts the Watcher on Extension-start 10 | */ 11 | export default class WatcherStart extends ChangeHandler { 12 | static registerEventHandler(): void { 13 | EventHandler.registerHandler(Event.START, this) 14 | } 15 | 16 | static async handle(): Promise { 17 | return Watcher.start() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/handlers/submodule/SubmoduleHandler.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import GitRepository from '../../application/GitRepository' 4 | import ChangeHandler from '../ChangeHandler' 5 | import Event from '../../models/Event' 6 | import EventHandler from '../EventHandler' 7 | import GitHandler from '../git/GitHandler.handler' 8 | 9 | /** 10 | * this Handler is responsible for whenever something changed in a Submodule 11 | */ 12 | export default class SubmoduleHandler extends ChangeHandler { 13 | static registerEventHandler(): void { 14 | EventHandler.registerHandler(Event.SUBMODULE, this) 15 | } 16 | 17 | static async handle(changedFiles: string[]): Promise { 18 | if (!changedFiles) { 19 | return 20 | } 21 | 22 | // get submodulePaths of files that have changed 23 | const gitModel = await GitRepository.getGitModel() 24 | const submodules = gitModel.getSubmodules() 25 | const submodulePaths: Set = new Set() 26 | changedFiles.forEach((changedFile) => { 27 | const founds = submodules 28 | .filter((submodule) => new RegExp(submodule.getPath(), 'gi').test(changedFile)) 29 | .map((submodule) => submodule.getPath()) 30 | founds.forEach((found) => submodulePaths.add(found)) 31 | }) 32 | 33 | for (const submodulePath of submodulePaths) { 34 | const submoduleModel = await GitRepository.updateGitModel(submodulePath, gitModel.getPath()) 35 | await GitHandler.handleRepositoryChange(submoduleModel) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/handlers/submodule/update/HandleSubmoduleUpdate.handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import InformationMessage from '../../../UI/InformationMessage' 4 | import MessageOption from '../../../UI/MessageOption' 5 | import GitRepository from '../../../application/GitRepository' 6 | import ChangeHandler from '../../ChangeHandler' 7 | import Event from '../../../models/Event' 8 | import EventHandler from '../../EventHandler' 9 | import Config, { ConfigOptions } from '../../../application/Config' 10 | import StatusBar from '../../../UI/StatusBar' 11 | import Status from '../../../UI/Status' 12 | 13 | /** 14 | * this Handler is reponsible for updating Submodule references 15 | */ 16 | export default class HandleSubmoduleUpdate extends ChangeHandler { 17 | static registerEventHandler(): void { 18 | EventHandler.registerHandler(Event.SUBMODULE_UPDATE, this) 19 | } 20 | 21 | static async handle(): Promise { 22 | if (Config.getValue('updateSubmodules') === ConfigOptions.auto) { 23 | await GitRepository.submoduleUpdateInit() 24 | StatusBar.addStatus(Status.submoduleUpdated()) 25 | 26 | return 27 | } 28 | 29 | const action = await InformationMessage.showInformationMessage( 30 | `Your submodues have updated. Would you like to checkout these commits?`, 31 | MessageOption.optionYES, 32 | MessageOption.optionNO, 33 | ) 34 | 35 | if (action !== MessageOption.YES) { 36 | return 37 | } 38 | 39 | await GitRepository.submoduleUpdateInit() 40 | 41 | StatusBar.addStatus(Status.submoduleUpdated()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/models/Branch.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default class Branch { 4 | private name: string 5 | private commit: string 6 | 7 | constructor(name: string, commit: string) { 8 | this.name = name 9 | this.commit = commit 10 | } 11 | 12 | getName(): string { 13 | return this.name.replace(/remotes\//, '') 14 | } 15 | 16 | getCommit(): string { 17 | return this.commit 18 | } 19 | 20 | isRemote(): boolean { 21 | return this.name.match(/remotes\//) ? true : false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/models/Event.ts: -------------------------------------------------------------------------------- 1 | enum Event { 2 | EXIT = 'EXIT', 3 | FILE = 'FILE', 4 | FILE_SUBMODULE = 'FILE_SUBMODULE', 5 | GIT = 'GIT', 6 | GIT_BRANCH_CHANGED = 'GIT_BRANCH_CHANGED', 7 | GIT_COMMITS = 'GIT_COMMITS', 8 | GIT_COMMITS_LOCAL = 'GIT_COMMITS_LOCAL', 9 | GIT_COMMITS_REMOTE = 'GIT_COMMITS_REMOTE', 10 | GIT_PUSH = 'GIT_PUSH', 11 | START = 'START', 12 | SUBMODULE = 'SUBMODULE', 13 | SUBMODULE_UPDATE = 'SUBMODULE_UPDATE', 14 | } 15 | 16 | export default Event 17 | -------------------------------------------------------------------------------- /src/models/Git.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { isAbsolute, join } from 'path' 4 | import Branch from './Branch' 5 | import Submodule from './Submodule' 6 | import { getWorkspacePath } from '../application/Helper' 7 | 8 | export default class Git { 9 | private dirpath: string 10 | private timestamp: number 11 | private branch: string 12 | private remote: string 13 | private detachedHEAD: boolean 14 | private branches: Branch[] 15 | private remotes: string[] 16 | private ahead: number 17 | private behind: number 18 | private submodules: Submodule[] 19 | private mainRepositoryPath: string 20 | private oldModel: Git | null 21 | 22 | constructor(dirpath: string = '') { 23 | dirpath = dirpath.replace(/\\/gi, '/') 24 | this.dirpath = join(getWorkspacePath(), dirpath) 25 | this.timestamp = Date.now() 26 | this.branch = '' 27 | this.remote = '' 28 | this.detachedHEAD = false 29 | this.branches = [] 30 | this.remotes = [] 31 | this.ahead = 0 32 | this.behind = 0 33 | this.submodules = [] 34 | this.mainRepositoryPath = '' 35 | this.oldModel = null 36 | } 37 | 38 | getPath(): string { 39 | return this.dirpath 40 | } 41 | 42 | getRelativePath(): string { 43 | const relativePath = this.dirpath.replace(getWorkspacePath(), '').replace(/\\/gi, '/') 44 | if (isAbsolute(relativePath)) { 45 | return relativePath.substr(1) 46 | } 47 | 48 | return relativePath 49 | } 50 | 51 | isRootGit(): boolean { 52 | return this.dirpath === getWorkspacePath() 53 | } 54 | 55 | getTimestamp(): number { 56 | return this.timestamp 57 | } 58 | 59 | getBranch(): string { 60 | return this.branch 61 | } 62 | 63 | setBranch(branch: string): void { 64 | this.branch = branch 65 | } 66 | 67 | getRemote(): string { 68 | return this.remote 69 | } 70 | 71 | setRemote(remote: string): void { 72 | this.remote = remote 73 | } 74 | 75 | isHeadDetached(): boolean { 76 | return this.detachedHEAD 77 | } 78 | 79 | setDetachedHEAD(detachedHEAD: boolean): void { 80 | this.detachedHEAD = detachedHEAD 81 | } 82 | 83 | setBranches(branches: any): void { 84 | this.branches = Object.keys(branches) 85 | .map((key) => branches[key]) 86 | .map((branch: any) => new Branch(branch.name, branch.commit)) 87 | } 88 | 89 | getBranches(): Branch[] { 90 | return this.branches 91 | } 92 | 93 | getLocalBranches(): Branch[] { 94 | return this.branches.filter((branch: Branch) => !branch.isRemote()) 95 | } 96 | 97 | getRemoteBranches(): Branch[] { 98 | return this.branches.filter((branch: Branch) => branch.isRemote()) 99 | } 100 | 101 | setRemotes(remotes: any): void { 102 | this.remotes = remotes.map((remote: any) => remote.name) 103 | } 104 | 105 | getRemotes(): string[] { 106 | return this.remotes 107 | } 108 | 109 | getAhead(): number { 110 | return this.ahead 111 | } 112 | 113 | setAhead(ahead: number): void { 114 | this.ahead = ahead 115 | } 116 | 117 | getBehind(): number { 118 | return this.behind 119 | } 120 | 121 | setBehind(behind: number): void { 122 | this.behind = behind 123 | } 124 | 125 | setSubmodules(CMDoutput: string): void { 126 | this.submodules = [] 127 | if (!CMDoutput.length) { 128 | return 129 | } 130 | const split = CMDoutput.split(' ') 131 | for (let i = 0; i < split.length; ) { 132 | if (!split[i].length) { 133 | i++ 134 | } 135 | const hash = split[i] 136 | const path = split[i + 1] 137 | let branch = split[i + 2] || '' 138 | 139 | if (branch.indexOf('(') < 0) { 140 | branch = '' 141 | i-- 142 | } 143 | i += 3 144 | this.submodules.push(new Submodule(hash, path, branch)) 145 | } 146 | } 147 | 148 | getSubmodules(): Submodule[] { 149 | return this.submodules 150 | } 151 | 152 | setMainRepositoryPath(mainRepositoryPath: string): void { 153 | this.mainRepositoryPath = mainRepositoryPath 154 | } 155 | 156 | getMainRepositoryPath(): string { 157 | return this.mainRepositoryPath 158 | } 159 | 160 | setOldModel(oldModel: Git): void { 161 | this.oldModel = oldModel 162 | } 163 | 164 | getOldModel(): Git | null { 165 | return this.oldModel 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/models/Submodule.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default class Submodule { 4 | private commitHash: string 5 | private path: string 6 | private branch: string 7 | 8 | constructor(commitHash: string, path: string, branch: string) { 9 | this.commitHash = commitHash 10 | this.path = path 11 | this.branch = branch 12 | } 13 | 14 | getCommitHash(): string { 15 | return this.commitHash 16 | } 17 | 18 | getPath(): string { 19 | return this.path 20 | } 21 | 22 | getBranch(): string { 23 | return this.branch 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "sourceMap": true, 7 | "strict": true, 8 | "rootDir": "src" 9 | }, 10 | "include": ["src/**/*.ts", "./node_modules/vscode/vscode.d.ts", "./node_modules/vscode/lib/*"] 11 | } 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [true, "always"], 9 | "triple-equals": true 10 | }, 11 | "defaultSeverity": "warning" 12 | } 13 | -------------------------------------------------------------------------------- /version-bump.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | // get information which type of version bump should be applied 4 | const type = process.argv[2]; 5 | 6 | const types = ["major", "minor", "patch"]; 7 | let toUpdate = types.findIndex(t => type === t); 8 | if (toUpdate < 0) { 9 | toUpdate = 2; 10 | } 11 | 12 | const packagePath = "package.json"; 13 | 14 | // get current version 15 | const packageFileString = fs.readFileSync(packagePath).toString(); 16 | const packageJson = JSON.parse(packageFileString); 17 | const { version } = packageJson; 18 | 19 | // gengerate new version 20 | const versionArray = version.split("."); 21 | const newVersion = versionArray 22 | .map((v, i) => { 23 | if (i < toUpdate) { 24 | return v; 25 | } 26 | if (i === toUpdate) { 27 | return parseInt(v, 10) + 1; 28 | } 29 | return 0; 30 | }) 31 | .join("."); 32 | 33 | // update version in each package 34 | packageJson.version = newVersion; 35 | fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 3)}\n`); 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 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 | 6 | //@ts-check 7 | 8 | 'use strict' 9 | 10 | const path = require('path') 11 | 12 | /**@type {import('webpack').Configuration}*/ 13 | const config = { 14 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 15 | 16 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 17 | output: { 18 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: 'extension.js', 21 | libraryTarget: 'commonjs2', 22 | devtoolModuleFilenameTemplate: '../[resource-path]', 23 | }, 24 | devtool: 'source-map', 25 | externals: { 26 | 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/ 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: ['.ts', '.js'], 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts$/, 36 | exclude: /node_modules/, 37 | use: [ 38 | { 39 | loader: 'ts-loader', 40 | options: { 41 | compilerOptions: { 42 | module: 'es6', // override `tsconfig.json` so that TypeScript emits native JavaScript modules. 43 | }, 44 | }, 45 | }, 46 | ], 47 | }, 48 | ], 49 | }, 50 | } 51 | 52 | module.exports = config 53 | --------------------------------------------------------------------------------