├── .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 |  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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------