├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ ├── commitlint.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── context-git.gif ├── docs └── docs.md ├── lib ├── Notifications.js ├── commands.js ├── commands │ ├── add-to-last-commit.js │ ├── commit-all.js │ ├── commit-staged.js │ ├── commit.js │ ├── create-branch.js │ ├── delete-branch.js │ ├── diff.js │ ├── discard-all-changes.js │ ├── discard-changes.js │ ├── fetch-all.js │ ├── fetch.js │ ├── ignore-changes.js │ ├── init.js │ ├── log.js │ ├── merge-branch.js │ ├── pull-all.js │ ├── pull.js │ ├── push-all.js │ ├── push.js │ ├── refresh.js │ ├── run-command.js │ ├── stage-changes.js │ ├── stash-changes.js │ ├── switch-branch.js │ ├── sync-all.js │ ├── sync.js │ ├── undo-last-commit.js │ ├── unignore-changes.js │ └── unstash-changes.js ├── config.js ├── dialogs │ ├── CommitDialog.js │ ├── CreateBranchDialog.js │ ├── DeleteBranchDialog.js │ ├── Dialog.js │ ├── LogDialog.js │ ├── MergeBranchDialog.js │ ├── RunCommandDialog.js │ └── SwitchBranchDialog.js ├── git-cmd.js ├── helper.js ├── main.js └── widgets │ ├── Autocomplete.js │ ├── FileTree.js │ └── StatusBarManager.js ├── package-lock.json ├── package.json ├── release.config.js ├── renovate.json ├── spec ├── commands │ ├── commit-all-spec.js │ ├── commit-spec.js │ ├── commit-staged-spec.js │ ├── delete-branch-spec.js │ ├── diff-spec.js │ ├── discard-all-changes-spec.js │ ├── discard-changes-spec.js │ ├── fetch-all-spec.js │ ├── fetch-spec.js │ ├── ignore-changes-spec.js │ ├── log-spec.js │ ├── merge-branch-spec.js │ ├── pull-all-spec.js │ ├── pull-spec.js │ ├── push-all-spec.js │ ├── push-spec.js │ ├── run-command-spec.js │ ├── stage-changes-spec.js │ ├── switch-branch-spec.js │ ├── sync-all-spec.js │ ├── sync-spec.js │ └── unignore-changes-spec.js ├── dialogs │ ├── commit-dialog-spec.js │ ├── create-branch-dialog-spec.js │ ├── delete-branch-dialog-spec.js │ ├── dialog-spec.js │ ├── log-dialog-spec.js │ ├── merge-branch-dialog-spec.js │ ├── run-command-dialog-spec.js │ └── switch-branch-dialog-spec.js ├── git-menu-spec.js ├── git │ ├── abort-spec.js │ ├── add-spec.js │ ├── cmd-spec.js │ ├── countCommits-spec.js │ ├── diff-spec.js │ ├── init-spec.js │ ├── log-spec.js │ ├── merge-spec.js │ ├── pull-spec.js │ ├── push-spec.js │ ├── rebase-spec.js │ ├── remove-spec.js │ ├── rootDir-spec.js │ └── status-spec.js ├── helper-spec.js ├── mocks.js ├── runner.js └── widgets │ ├── autocomplete-spec.js │ └── filetree-spec.js └── styles ├── Autocomplete.less ├── CommitDialog.less ├── CreateBranchDialog.less ├── FileTree.less ├── LogDialog.less ├── dialog.less └── status.less /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | coffeelint: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - javascript 9 | checks: 10 | Similar code: 11 | enabled: false 12 | eslint: 13 | enabled: true 14 | exclude_fingerprints: 15 | - acebaf69d448e7cbde3c0e917b449f46 16 | - 810ad086e2f36b1bedfda23d6f8ff252 17 | - 3b4956d7297a1a9c225a258261f9f59f 18 | - b64e6153a9945fce505a07cdf676a06f 19 | - 79736386bd9faa9f127610f004343af4 20 | - d82d7e425cacfda5e3bd28ffc462374c 21 | - 74f02d4a04401532068a6b80d93fda5e 22 | - 94b63b55b232ccc9aa7382f1d29fd5b4 23 | - b1fb94bde8ba423e4c4d5faa2c6ba5c7 24 | - 10e12d2b8d96e438747f157da0c6d37b 25 | - e1e7f744d8f275da32f848e6937ed7ad 26 | - 9fbf69879eb06897e8eab297c6220a11 27 | - b2ecba7bfe576177a0c3c99a00b5feed 28 | fixme: 29 | enabled: true 30 | ratings: 31 | paths: 32 | - "**.coffee" 33 | - "**.js" 34 | exclude_paths: 35 | - spec/ 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = tab 8 | # tab_width is not set to allow the readers preferred width 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true, 6 | "jasmine": true, 7 | "atomtest": true 8 | }, 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaVersion": 2018, 12 | "ecmaFeatures": { 13 | "jsx": true, 14 | "impliedStrict": true 15 | } 16 | }, 17 | "plugins": [ 18 | "react" 19 | ], 20 | "extends": [ 21 | "eslint:recommended" 22 | ], 23 | "rules": { 24 | "valid-jsdoc": "warn", 25 | 26 | "block-scoped-var": "error", 27 | "curly": "error", 28 | "default-case": "error", 29 | "dot-location": ["error", "property"], 30 | "eqeqeq": "error", 31 | "no-else-return": "warn", 32 | "no-eval": "error", 33 | "no-loop-func": "warn", 34 | "no-multi-spaces": "warn", 35 | "no-param-reassign": "error", 36 | "no-unused-expressions": "warn", 37 | "no-unused-vars": "error", 38 | "no-warning-comments": "warn", 39 | "no-with": "error", 40 | "strict": "warn", 41 | 42 | "no-restricted-globals": [ 43 | "error", 44 | { 45 | "name": "fit", 46 | "message": "Do not commit focused tests." 47 | }, 48 | { 49 | "name": "ffit", 50 | "message": "Do not commit focused tests." 51 | }, 52 | { 53 | "name": "fffit", 54 | "message": "Do not commit focused tests." 55 | }, 56 | { 57 | "name": "fdescribe", 58 | "message": "Do not commit focused tests." 59 | }, 60 | { 61 | "name": "ffdescribe", 62 | "message": "Do not commit focused tests." 63 | }, 64 | { 65 | "name": "fffdescribe", 66 | "message": "Do not commit focused tests." 67 | } 68 | ], 69 | "no-console": "warn", 70 | "no-shadow": "warn", 71 | "no-undef": "error", 72 | "no-undefined": "error", 73 | "no-use-before-define": "error", 74 | "no-sync": "warn", 75 | 76 | "array-bracket-spacing": "error", 77 | "block-spacing": "error", 78 | "brace-style": ["error", "1tbs"], 79 | "comma-spacing": "error", 80 | "comma-style": "error", 81 | "comma-dangle": ["error", "always-multiline"], 82 | "computed-property-spacing": "error", 83 | "eol-last": "warn", 84 | "func-call-spacing": "error", 85 | "indent": ["error", "tab"], 86 | "key-spacing": "error", 87 | "keyword-spacing": "error", 88 | "line-comment-position": "warn", 89 | "linebreak-style": "error", 90 | "lines-around-comment": "error", 91 | "lines-between-class-members": "error", 92 | "new-parens": "error", 93 | "no-array-constructor": "error", 94 | "no-whitespace-before-property": "error", 95 | "object-curly-newline": ["error", { 96 | "consistent": true 97 | }], 98 | "object-curly-spacing": "error", 99 | "quotes": "warn", 100 | "semi": "error", 101 | "space-before-blocks": "error", 102 | "space-before-function-paren": ["error", { 103 | "anonymous": "always", 104 | "named": "never", 105 | "asyncArrow": "always" 106 | }], 107 | "space-in-parens": "error", 108 | "space-infix-ops": "error", 109 | "space-unary-ops": "error", 110 | "spaced-comment": "warn", 111 | "switch-colon-spacing": "error", 112 | 113 | "arrow-spacing": "error", 114 | "prefer-const": "warn", 115 | "prefer-destructuring": "warn", 116 | "prefer-rest-params": "warn", 117 | "prefer-spread": "warn", 118 | "prefer-template": "warn", 119 | "rest-spread-spacing": "error", 120 | "template-curly-spacing": "error", 121 | 122 | "react/jsx-uses-react": "error", 123 | "react/jsx-uses-vars": "error", 124 | "react/jsx-indent": ["error", "tab"], 125 | "react/jsx-no-bind": "error" 126 | }, 127 | "globals": { 128 | "atom": "readonly", 129 | "pass": "readonly" 130 | }, 131 | "settings": { 132 | "react": { 133 | "version": "16" 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [UziTech] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ### Command Name 3 | 4 | 5 | 6 | 7 | ### Git Command(s) 8 | 9 | 10 | 16 | 17 | ### Dialog Input (if applicable) 18 | 19 | 20 | 27 | 28 | ### Input Validation 29 | 30 | 31 | 37 | 38 | 39 | ### Steps to Reproduce 40 | 1. 41 | 2. 42 | 43 | ### Expected Behavior 44 | 45 | 46 | ### Actual Behavior 47 | 48 | 49 | ### Environment 50 | - OS: 51 | - apm --version: 52 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ### Command 3 | 4 | 5 | - [ ] Command issue: # 6 | 7 | 8 | - [ ] Command added to docs 9 | - [ ] Tests added 10 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 1 * * 1' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v3 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | on: [pull_request] 3 | 4 | jobs: 5 | Commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v4 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | Test: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | channel: [stable, beta] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | - name: Install Atom 22 | uses: UziTech/action-setup-atom@v1 23 | with: 24 | channel: ${{ matrix.channel }} 25 | - name: Atom Version 26 | run: atom -v 27 | - name: APM Version 28 | run: apm -v 29 | - name: Install Dependencies 30 | run: apm ci 31 | - name: Test 👩🏾‍💻 32 | run: | 33 | git config --global user.email "test@example.com" 34 | git config --global user.name "test" 35 | git config --global pull.ff true 36 | atom --test spec 37 | 38 | Lint: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout Code 42 | uses: actions/checkout@v3 43 | - name: Install Node 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: 'lts/*' 47 | - name: Install Dependencies 48 | run: npm ci 49 | - name: Lint ✨ 50 | run: npm run lint 51 | 52 | Release: 53 | needs: [Test, Lint] 54 | if: | 55 | github.ref == 'refs/heads/master' && 56 | github.event.repository.fork == false 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout Code 60 | uses: actions/checkout@v3 61 | - name: Install Atom 62 | uses: UziTech/action-setup-atom@v1 63 | - name: Install Node 64 | uses: actions/setup-node@v3 65 | with: 66 | node-version: 'lts/*' 67 | - name: Install Dependencies 68 | run: npm ci 69 | - name: Release 🎉 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | ATOM_ACCESS_TOKEN: ${{ secrets.ATOM_ACCESS_TOKEN }} 73 | run: npx semantic-release 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | debug.log 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Tony Brix 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub is [sunsetting Atom](https://github.blog/2022-06-08-sunsetting-atom/) on Dec 15th, 2022 so I am archiving all of my Atom plugins. If someone wants to continue development please message @UziTech. 2 | 3 | --- 4 | 5 | [![Actions Status](https://github.com/UziTech/git-menu/workflows/CI/badge.svg)](https://github.com/UziTech/git-menu/actions) 6 | 7 | # git-menu package 8 | 9 | An Atom.io package to use git from the context menu. You can choose which commands show up in the context menu by enabling/disabling them in the settings. 10 | 11 | ![screenshot](./context-git.gif) 12 | 13 | ## Usage 14 | 15 | This package will add a "Git" item to the context menu when you right click on the tree-view, tabs, or the editor. 16 | 17 | ## Documentation 18 | 19 | [See /docs/docs.md](https://github.com/UziTech/git-menu/blob/master/docs/docs.md) 20 | 21 | ## Contributing 22 | 23 | ### Voting 24 | 25 | The easiest way to contribute to this package is to vote on new commmands. New commands are entered as issues with the [`command`](https://github.com/UziTech/git-menu/issues?q=is%3Aissue+is%3Aopen+label%3Acommand) label. I will prioritize commands with the most :+1:'s 26 | 27 | ### Submit Commands 28 | 29 | If there are commands you would like to see just create an issue with the command template and I will label it as a command. 30 | 31 | ### Submit An Issue 32 | 33 | If you find a bug or just have a question about something create an issue and I will be happy to help you out. 34 | 35 | ### Create A Pull Request 36 | 37 | If there is an issue you think you can fix or a command you think you can implement just create a pull request referencing that issue so I know that you are working on it 38 | -------------------------------------------------------------------------------- /context-git.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UziTech/git-menu/a02cd1c4d222ae2f837a10e8b64f535d44e83236/context-git.gif -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Commands 4 | 5 | - [Commit...](#commit) 6 | - [Commit All...](#commit-all) 7 | - [Commit Staged...](#commit-staged) 8 | - [Stage Changes](#stage-changes) 9 | - [Add To Last Commit](#add-to-last-commit) 10 | - [Undo Last Commit](#undo-last-commit) 11 | - [Discard Changes](#discard-changes) 12 | - [Discard All Changes](#discard-all-changes) 13 | - [Ignore Changes](#ignore-changes) 14 | - [Unignore Changes](#unignore-changes) 15 | - [Stash Changes](#stash-changes) 16 | - [Unstash Changes](#unstash-changes) 17 | - [Fetch](#fetch) 18 | - [Fetch All](#fetch-all) 19 | - [Pull](#pull) 20 | - [Pull All](#pull-all) 21 | - [Push](#push) 22 | - [Push All](#push-all) 23 | - [Sync](#sync) 24 | - [Sync All](#sync-all) 25 | - [Merge Branch...](#merge-branch) 26 | - [Switch Branch...](#switch-branch) 27 | - [Create Branch...](#create-branch) 28 | - [Delete Branch...](#delete-branch) 29 | - [Initialize](#initialize) 30 | - [Log](#log) 31 | - [Diff](#diff) 32 | - [Run Command...](#run-command) 33 | - [Refresh](#refresh) 34 | 35 | ## Commit... 36 | 37 | `git-menu:commit` 38 | 39 | You can commit a single file or multiple files or folders selected in the tree-view. 40 | This command will bring up a dialog window where you can unselect any files you do not want to commit. 41 | 42 | You can also choose to amend the last commit with the selected files and/or optionally change the last commit message. 43 | 44 | You then have the following options to commit the message/files: 45 | 46 | - "Commit" will just commit the files. 47 | - "Commit & Push" will commit the files then push them to origin. 48 | - "Commit & Sync" will commit the files, pull from origin then push to origin. 49 | 50 | ## Commit All... 51 | 52 | `git-menu:commit-all` 53 | 54 | Same as [Commit...](#commit) but will list all changed files in the dialog. 55 | 56 | ## Commit Staged... 57 | 58 | `git-menu:commit-staged` 59 | 60 | Same as [Commit...](#commit) but will list only staged changes in the dialog. 61 | 62 | ## Stage Changes 63 | 64 | `git-menu:stage-changes` 65 | 66 | Stage changes for committing later. 67 | 68 | ## Add To Last Commit 69 | 70 | `git-menu:add-to-last-commit` 71 | 72 | This will add the selected files to the last commit. 73 | 74 | If you want to change the message of the last commit you will have to choose [Commit...](#commit) or [Commit All...](#commit-all). 75 | 76 | ## Undo Last Commit 77 | 78 | `git-menu:undo-last-commit` 79 | 80 | This will undo the last commit but save the changes. Like `git reset --mixed HEAD~1`. 81 | 82 | ## Discard Changes 83 | 84 | `git-menu:discard-changes` 85 | 86 | This will discard changes to the selected files. 87 | 88 | ## Discard All Changes 89 | 90 | `git-menu:discard-all-changes` 91 | 92 | This will discard changes to the all files in the repo. 93 | 94 | ## Ignore Changes 95 | 96 | `git-menu:ignore-changes` 97 | 98 | Update the index with the changed version but don't commit the changes. Like `git update-index --assume-unchanged`. 99 | 100 | ## Unignore Changes 101 | 102 | `git-menu:unignore-changes` 103 | 104 | Opposite of [Ignore Changes](#ignore-changes). Like `git update-index --no-assume-unchanged`. 105 | 106 | ## Stash Changes 107 | 108 | `git-menu:stash-changes` 109 | 110 | Save changes and checkout last commit. 111 | 112 | ## Unstash Changes 113 | 114 | `git-menu:unstash-changes` 115 | 116 | Restore changes from last stash. 117 | 118 | ## Fetch 119 | 120 | `git-menu:fetch` 121 | 122 | Fetch from all tracked repos. 123 | 124 | ## Fetch All 125 | 126 | `git-menu:fetch-all` 127 | 128 | Fetch all project repos. 129 | 130 | ## Pull 131 | 132 | `git-menu:pull` 133 | 134 | Pull from tracked upstream. 135 | 136 | ## Pull All 137 | 138 | `git-menu:pull-all` 139 | 140 | Pull all project repos. 141 | 142 | ## Push 143 | 144 | `git-menu:push` 145 | 146 | Push to tracked upstream. 147 | 148 | ## Push All 149 | 150 | `git-menu:push-all` 151 | 152 | Push all project repos. 153 | 154 | ## Sync 155 | 156 | `git-menu:sync` 157 | 158 | Pull then Push. 159 | 160 | ## Sync All 161 | 162 | `git-menu:sync-all` 163 | 164 | Pull then Push all project repos. 165 | 166 | ## Merge Branch... 167 | 168 | `git-menu:merge-branch` 169 | 170 | Merge or rebase a branch. 171 | 172 | ## Switch Branch... 173 | 174 | `git-menu:switch-branch` 175 | 176 | Checkout a different branch in this repo. 177 | 178 | ## Create Branch... 179 | 180 | `git-menu:create-branch` 181 | 182 | Create a branch and optionally track/create a remote branch. 183 | 184 | ## Delete Branch... 185 | 186 | `git-menu:delete-branch` 187 | 188 | Delete a local and/or remote branch. 189 | 190 | ## Initialize 191 | 192 | `git-menu:init` 193 | 194 | Initialize a git repo for the current project. 195 | 196 | ## Log 197 | 198 | `git-menu:log` 199 | 200 | Show the git log. 201 | 202 | ## Diff 203 | 204 | `git-menu:diff` 205 | 206 | Open the diff patch in a new editor. 207 | 208 | ## Run Command... 209 | 210 | `git-menu:run-command` 211 | 212 | Run any `git` command with selected `%files%` as an argument. 213 | 214 | ## Refresh 215 | 216 | `git-menu:refresh` 217 | 218 | Refresh the git status in Atom. 219 | -------------------------------------------------------------------------------- /lib/Notifications.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import {Notification} from "atom"; 4 | 5 | export const CONFIG_YES = 0; 6 | export const CONFIG_NO = 1; 7 | export const CONFIG_ONLY_ERROR = 2; 8 | export const CONFIG_GIT = 3; 9 | export const CONFIG_GIT_VERBOSE = 4; 10 | 11 | export const NotificationsConfig = { 12 | default: CONFIG_YES, 13 | enum: [ 14 | {value: CONFIG_YES, description: "Simple"}, 15 | {value: CONFIG_NO, description: "None"}, 16 | {value: CONFIG_ONLY_ERROR, description: "Only Errors"}, 17 | {value: CONFIG_GIT, description: "Git Output"}, 18 | {value: CONFIG_GIT_VERBOSE, description: "Verbose Git Output"}, 19 | ], 20 | }; 21 | 22 | export function isVerbose() { 23 | return (atom.config.get("git-menu.notifications") === CONFIG_GIT_VERBOSE); 24 | } 25 | 26 | export default class Notifications { 27 | static addNotification(type, title, message, options) { 28 | switch (atom.config.get("git-menu.notifications")) { 29 | case CONFIG_NO: 30 | return; 31 | case CONFIG_ONLY_ERROR: 32 | if (type !== "error") { 33 | return; 34 | } 35 | break; 36 | case CONFIG_GIT: 37 | case CONFIG_GIT_VERBOSE: 38 | if (type === "git") { 39 | // eslint-disable-next-line no-param-reassign 40 | type = "info"; 41 | } else if (type !== "error") { 42 | return; 43 | } 44 | break; 45 | case CONFIG_YES: 46 | default: 47 | if (type === "git") { 48 | // eslint-disable-next-line no-param-reassign 49 | type = "info"; 50 | } 51 | } 52 | 53 | if (!title) { 54 | throw new Error("Notification title must be specified."); 55 | } 56 | 57 | if (typeof message === "object") { 58 | // eslint-disable-next-line no-param-reassign 59 | options = message; 60 | } else { 61 | if (typeof options !== "object") { 62 | // eslint-disable-next-line no-param-reassign 63 | options = {}; 64 | } 65 | options.detail = message; 66 | } 67 | 68 | if (options.detail) { 69 | atom.notifications.addNotification(new Notification(type, title, options)); 70 | } 71 | } 72 | 73 | static addSuccess(title, message, options) { 74 | Notifications.addNotification("success", title, message, options); 75 | } 76 | 77 | static addError(title, message, options) { 78 | 79 | // default dismissable to true 80 | if (typeof options !== "object") { 81 | if (typeof message === "object") { 82 | if (typeof message.dismissable === "undefined") { 83 | message.dismissable = true; 84 | } 85 | } else { 86 | // eslint-disable-next-line no-param-reassign 87 | options = { 88 | dismissable: true, 89 | }; 90 | } 91 | } else if (typeof options.dismissable === "undefined") { 92 | options.dismissable = true; 93 | } 94 | Notifications.addNotification("error", title, message, options); 95 | } 96 | 97 | static addInfo(title, message, options) { 98 | Notifications.addNotification("info", title, message, options); 99 | } 100 | 101 | static addGit(title, message, options) { 102 | if (Array.isArray(message)) { 103 | // eslint-disable-next-line no-param-reassign 104 | message = message.filter(m => !!m).join("\n\n"); 105 | } 106 | if (message !== "") { 107 | Notifications.addNotification("git", title, message, options); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/commands.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import commit from "./commands/commit"; 4 | import commitAll from "./commands/commit-all"; 5 | import commitStaged from "./commands/commit-staged"; 6 | import stageChanges from "./commands/stage-changes"; 7 | import addToLastCommit from "./commands/add-to-last-commit"; 8 | import undoLastCommit from "./commands/undo-last-commit"; 9 | import discardChanges from "./commands/discard-changes"; 10 | import discardAllChanges from "./commands/discard-all-changes"; 11 | import ignoreChanges from "./commands/ignore-changes"; 12 | import unignoreChanges from "./commands/unignore-changes"; 13 | import stashChanges from "./commands/stash-changes"; 14 | import unstashChanges from "./commands/unstash-changes"; 15 | import fetch from "./commands/fetch"; 16 | import fetchAll from "./commands/fetch-all"; 17 | import pull from "./commands/pull"; 18 | import pullAll from "./commands/pull-all"; 19 | import push from "./commands/push"; 20 | import pushAll from "./commands/push-all"; 21 | import sync from "./commands/sync"; 22 | import syncAll from "./commands/sync-all"; 23 | import mergeBranch from "./commands/merge-branch"; 24 | import switchBranch from "./commands/switch-branch"; 25 | import createBranch from "./commands/create-branch"; 26 | import deleteBranch from "./commands/delete-branch"; 27 | import init from "./commands/init"; 28 | import log from "./commands/log"; 29 | import diff from "./commands/diff"; 30 | import runCommand from "./commands/run-command"; 31 | import refresh from "./commands/refresh"; 32 | 33 | /** 34 | * These commands will be added to the context menu in the order they appear here. 35 | * They can include the following properties: 36 | * { 37 | * label: (required) The text to display on the context menu item 38 | * description: (optional) A description that will be displayed by the enable/disable setting 39 | * keymap: (optional) A key combination to add as a default keybinding 40 | * confirm: (optional) If the command requires a confirm dialog you can supply the `message` and `detail` parameters 41 | * message: (required) This is the question you are asking the user to confirm. 42 | * detail: (optional) This is where you can provide a more detailed list of the changes. 43 | * This can be a string or a function that will be called with the `filePaths` parameter that returns a string 44 | * This function can be asynchronous 45 | * command: (required) The asynchronous function to run when the command is called. 46 | * This function will be called with the parameters `filePaths` and `statusBar`. 47 | * This function should ultimately resolve to an object with the following properties: 48 | * .title: A title for the command 49 | * .message: A success message to display to the user 50 | * } 51 | */ 52 | export default { 53 | "commit": commit, 54 | "commit-all": commitAll, 55 | "commit-staged": commitStaged, 56 | "stage-changes": stageChanges, 57 | "add-to-last-commit": addToLastCommit, 58 | "undo-last-commit": undoLastCommit, 59 | "discard-changes": discardChanges, 60 | "discard-all-changes": discardAllChanges, 61 | "ignore-changes": ignoreChanges, 62 | "unignore-changes": unignoreChanges, 63 | "stash-changes": stashChanges, 64 | "unstash-changes": unstashChanges, 65 | "fetch": fetch, 66 | "fetch-all": fetchAll, 67 | "pull": pull, 68 | "pull-all": pullAll, 69 | "push": push, 70 | "push-all": pushAll, 71 | "sync": sync, 72 | "sync-all": syncAll, 73 | "merge-branch": mergeBranch, 74 | "switch-branch": switchBranch, 75 | "create-branch": createBranch, 76 | "delete-branch": deleteBranch, 77 | "init": init, 78 | "log": log, 79 | "diff": diff, 80 | "run-command": runCommand, 81 | "refresh": refresh, 82 | }; 83 | -------------------------------------------------------------------------------- /lib/commands/add-to-last-commit.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Add To Last Commit", 9 | description: "Amend the last commit with the changes", 10 | confirm: { 11 | message: "Are you sure you want to add these changes to the last commit?", 12 | detail: async (filePaths, git = gitCmd) => { 13 | const root = await helper.getRoot(filePaths, git); 14 | const lastCommit = await git.lastCommit(root); 15 | return `You are adding these files:\n${filePaths.join("\n")}\n\nTo this commit:\n${lastCommit}`; 16 | }, 17 | }, 18 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Add To Last Commit") { 19 | const [files, root] = await helper.getRootAndFiles(filePaths, git); 20 | await helper.checkGitLock(root); 21 | const lastCommit = await git.lastCommit(root); 22 | 23 | if (lastCommit === null) { 24 | throw "No commits yet"; 25 | } 26 | 27 | // commit files 28 | statusBar.show("Committing..."); 29 | const numFiles = `${files.length} File${files.length !== 1 ? "s" : ""}`; 30 | const results = []; 31 | results.push(await git.unstage(root)); 32 | results.push(await git.add(root, filePaths)); 33 | results.push(await git.commit(root, lastCommit, true, filePaths)); 34 | notifications.addGit(title, results); 35 | helper.refreshAtom(root); 36 | return { 37 | title, 38 | message: `${numFiles} committed.`, 39 | }; 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /lib/commands/commit-all.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | import CommitDialog from "../dialogs/CommitDialog"; 6 | 7 | import {command as commit} from "./commit"; 8 | 9 | export default { 10 | label: "Commit All...", 11 | description: "Commit all files", 12 | command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = CommitDialog, title = "Commit All") { 13 | // only get paths that are parents of filePaths files 14 | const paths = atom.project.getPaths().map(root => { 15 | const r = root.toLowerCase().replace(/\\/g, "/"); 16 | const hasPath = filePaths.some(file => { 17 | const p = file.toLowerCase().replace(/\\/g, "/"); 18 | return p.indexOf(r) === 0; 19 | }); 20 | return hasPath ? root : false; 21 | }).filter(r => r); 22 | 23 | return commit(paths, statusBar, git, notifications, dialog, title); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/commands/commit-staged.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | import {command as sync} from "./sync"; 8 | import {command as push} from "./push"; 9 | import CommitDialog from "../dialogs/CommitDialog"; 10 | 11 | export default { 12 | label: "Commit Staged...", 13 | description: "Commit staged files", 14 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = CommitDialog, title = "Commit Staged") { 15 | const [files, root] = await helper.getRootAndFilesStatuses(atom.project.getPaths(), git); 16 | const stagedFiles = files.filter(f => f.added); 17 | if (stagedFiles.length === 0) { 18 | throw "No Changes Staged"; 19 | } 20 | await helper.checkGitLock(root); 21 | const treeView = atom.config.get("git-menu.treeView"); 22 | const lastCommit = await git.lastCommit(root); 23 | const [ 24 | message, 25 | amend, 26 | shouldPush, 27 | shouldSync, 28 | ] = await new dialog({files: stagedFiles, lastCommit, filesSelectable: false, treeView}).activate(); 29 | 30 | if (!message) { 31 | throw "Message cannot be blank."; 32 | } 33 | 34 | // commit files 35 | statusBar.show("Committing..."); 36 | const numFiles = `${stagedFiles.length} File${stagedFiles.length !== 1 ? "s" : ""}`; 37 | await helper.checkGitLock(root); 38 | const results = await git.commit(root, message, amend, null); 39 | localStorage.removeItem("git-menu.commit-message"); 40 | notifications.addGit(title, results); 41 | helper.refreshAtom(root); 42 | const success = {title, message: `${numFiles} committed.`}; 43 | if (shouldSync) { 44 | await sync([root], statusBar, git, notifications); 45 | success.message = `${numFiles} committed & synced.`; 46 | } else if (shouldPush) { 47 | await push([root], statusBar, git, notifications); 48 | success.message = `${numFiles} committed & pushed.`; 49 | } 50 | return success; 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /lib/commands/commit.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | import {command as sync} from "./sync"; 8 | import {command as push} from "./push"; 9 | import CommitDialog from "../dialogs/CommitDialog"; 10 | 11 | export default { 12 | label: "Commit...", 13 | description: "Commit selected files", 14 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = CommitDialog, title = "Commit") { 15 | const [files, root] = await helper.getRootAndFilesStatuses(filePaths, git); 16 | await helper.checkGitLock(root); 17 | const treeView = atom.config.get("git-menu.treeView"); 18 | const lastCommit = await git.lastCommit(root); 19 | const [ 20 | message, 21 | amend, 22 | shouldPush, 23 | shouldSync, 24 | selectedFiles, 25 | ] = await new dialog({files, lastCommit, treeView}).activate(); 26 | 27 | if (!message) { 28 | throw "Message cannot be blank."; 29 | } 30 | 31 | // commit files 32 | statusBar.show("Committing..."); 33 | const changedFiles = (await helper.getStatuses([root], git)).map(status => status.file); 34 | const reducedFiles = helper.reduceFilesToCommonFolders(selectedFiles, changedFiles); 35 | const numFiles = `${selectedFiles.length} File${selectedFiles.length !== 1 ? "s" : ""}`; 36 | await helper.checkGitLock(root); 37 | const results = []; 38 | results.push(await git.unstage(root)); 39 | results.push(await git.add(root, reducedFiles)); 40 | results.push(await git.commit(root, message, amend, null)); 41 | localStorage.removeItem("git-menu.commit-message"); 42 | notifications.addGit(title, results); 43 | helper.refreshAtom(root); 44 | const success = {title, message: `${numFiles} committed.`}; 45 | if (shouldSync) { 46 | await sync([root], statusBar, git, notifications); 47 | success.message = `${numFiles} committed & synced.`; 48 | } else if (shouldPush) { 49 | await push([root], statusBar, git, notifications); 50 | success.message = `${numFiles} committed & pushed.`; 51 | } 52 | return success; 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /lib/commands/create-branch.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | import CreateBranchDialog from "../dialogs/CreateBranchDialog"; 7 | 8 | export default { 9 | label: "Create Branch...", 10 | description: "Create a new branch", 11 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = CreateBranchDialog, title = "Create Branch") { 12 | const root = await helper.getRoot(filePaths, git); 13 | await helper.checkGitLock(root); 14 | 15 | const branches = await git.branches(root); 16 | const [sourceBranch, newBranch, track] = await new dialog({branches, root}).activate(); 17 | 18 | statusBar.show("Creating Branch..."); 19 | 20 | await helper.checkGitLock(root); 21 | const results = []; 22 | results.push(await git.checkoutBranch(root, sourceBranch)); 23 | 24 | results.push(await git.createBranch(root, newBranch)); 25 | notifications.addGit(title, results); 26 | 27 | helper.refreshAtom(root); 28 | 29 | let tracking = ""; 30 | if (track) { 31 | const trackResult = await git.setUpstream(root, "origin", newBranch); 32 | notifications.addGit(title, trackResult); 33 | 34 | helper.refreshAtom(root); 35 | tracking = ` and tracking origin/${newBranch}`; 36 | } 37 | 38 | return { 39 | title, 40 | message: `Created ${newBranch} from ${sourceBranch}${tracking}.`, 41 | }; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /lib/commands/delete-branch.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | import DeleteBranchDialog from "../dialogs/DeleteBranchDialog"; 7 | 8 | export default { 9 | label: "Delete Branch...", 10 | description: "Delete a branch", 11 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = DeleteBranchDialog, title = "Delete Branch") { 12 | const root = await helper.getRoot(filePaths, git); 13 | await helper.checkGitLock(root); 14 | 15 | let branches = await git.branches(root); 16 | const [branch, local, remote, force] = await new dialog({branches, root}).activate(); 17 | 18 | if (!local && !remote) { 19 | return; 20 | } 21 | 22 | statusBar.show("Deleting Branch..."); 23 | 24 | await helper.checkGitLock(root); 25 | const results = []; 26 | if (branch.selected) { 27 | const defaultBranch = helper.getDefaultBranch(branches); 28 | if (defaultBranch) { 29 | // if branch is current branch then checkout master first 30 | if (branch.name === defaultBranch.name) { 31 | branches = await git.branches(root); 32 | const br = branches.find(b => !b.selected && b.local); 33 | if (br) { 34 | results.push(await git.checkoutBranch(root, br.name)); 35 | } 36 | } else { 37 | results.push(await git.checkoutBranch(root, defaultBranch.name)); 38 | } 39 | 40 | if (results.length > 0) { 41 | helper.refreshAtom(root); 42 | } 43 | } 44 | } 45 | 46 | if (local) { 47 | results.push(await git.deleteBranch(root, branch.name, false, force)); 48 | 49 | helper.refreshAtom(root); 50 | } 51 | 52 | if (remote) { 53 | results.push(await git.deleteBranch(root, branch.name, true, force)); 54 | 55 | helper.refreshAtom(root); 56 | } 57 | 58 | notifications.addGit(title, results); 59 | 60 | return { 61 | title, 62 | message: `Deleted branch ${branch.name}.`, 63 | }; 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /lib/commands/diff.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | 6 | export default { 7 | label: "Diff", 8 | description: "Open diff in a new file", 9 | async command(filePaths, statusBar, git = gitCmd, title = "Diff") { 10 | const root = await helper.getRoot(filePaths, git); 11 | await helper.checkGitLock(root); 12 | 13 | // commit files 14 | statusBar.show("Diffing..."); 15 | const result = await git.diff(root, filePaths); 16 | 17 | const textEditor = await atom.workspace.open("untitled.diff"); 18 | textEditor.setText(result); 19 | 20 | return { 21 | title, 22 | message: "Diff opened.", 23 | }; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/commands/discard-all-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | 6 | import {command as discardChanges} from "./discard-changes"; 7 | 8 | export default { 9 | label: "Discard All Changes", 10 | description: "Discard all changes", 11 | confirm: { 12 | message: "Are you sure you want to discard all uncommitted changes to all files in this repo?", 13 | }, 14 | command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Discard All Changes") { 15 | return discardChanges(atom.project.getPaths(), statusBar, git, notifications, title); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/commands/discard-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Discard Changes", 9 | description: "Discard file changes", 10 | confirm: { 11 | message: "Are you sure you want to discard all uncommitted changes to these files?", 12 | detail: filePaths => `You are discarding changes to:\n${filePaths.join("\n")}`, 13 | }, 14 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Discard Changes") { 15 | const [files, root] = await helper.getRootAndFilesStatuses(filePaths, git); 16 | await helper.checkGitLock(root); 17 | 18 | let results = []; 19 | results.push(await git.unstage(root)); 20 | 21 | let {untrackedFiles, trackedFiles} = files.reduce((prev, file) => { 22 | if (file.untracked) { 23 | prev.untrackedFiles.push(file.file); 24 | } else { 25 | prev.trackedFiles.push(file.file); 26 | } 27 | return prev; 28 | }, {untrackedFiles: [], trackedFiles: []}); 29 | 30 | const allFiles = (await helper.getStatuses([root], git)).reduce((prev, file) => { 31 | if (file.untracked) { 32 | prev.untracked.push(file.file); 33 | } else { 34 | prev.tracked.push(file.file); 35 | } 36 | return prev; 37 | }, {untracked: [], tracked: []}); 38 | 39 | const hasUntrackedFiles = (untrackedFiles.length > 0 && allFiles.untracked.length > 0); 40 | const hasTrackedFiles = (trackedFiles.length > 0 && allFiles.tracked.length > 0); 41 | 42 | untrackedFiles = helper.reduceFilesToCommonFolders(untrackedFiles, allFiles.untracked); 43 | trackedFiles = helper.reduceFilesToCommonFolders(trackedFiles, allFiles.tracked); 44 | 45 | statusBar.show("Discarding..."); 46 | 47 | // discard files 48 | results = results.concat(await Promise.all([ 49 | (hasUntrackedFiles ? git.clean(root, untrackedFiles) : ""), 50 | (hasTrackedFiles ? git.checkoutFiles(root, trackedFiles) : ""), 51 | ])); 52 | notifications.addGit(title, results); 53 | helper.refreshAtom(root); 54 | return { 55 | title, 56 | message: `${files.length} File${files.length !== 1 ? "s" : ""} Discarded.`, 57 | }; 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /lib/commands/fetch-all.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | import {command as fetch} from "./fetch"; 6 | 7 | export default { 8 | label: "Fetch All", 9 | description: "Fetch all project repos", 10 | confirm: { 11 | message: "Are you sure you want to fetch all project repos?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Fetch All") { 14 | const fetches = await Promise.all(atom.project.getPaths().map(root => { 15 | return fetch([root], statusBar, git, notifications).catch((err) => { 16 | const message = (err.stack ? err.stack : err.toString()); 17 | notifications.addError("Git Menu: Fetch", `${root}\n\n${message}`); 18 | return null; 19 | }); 20 | })); 21 | 22 | const failed = fetches.filter(p => !p); 23 | let num = "all"; 24 | if (failed.length > 0) { 25 | num = fetches.length - failed.length; 26 | } 27 | 28 | return { 29 | title, 30 | message: `Fetched ${num} repos.`, 31 | }; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/commands/fetch.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Fetch", 9 | description: "Fetch from all tracked repos", 10 | confirm: { 11 | message: "Are you sure you want to fetch all tracked repos?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Fetch") { 14 | const root = await helper.getRoot(filePaths, git); 15 | await helper.checkGitLock(root); 16 | statusBar.show("Fetching..."); 17 | const result = await git.fetch(root); 18 | notifications.addGit(title, result); 19 | helper.refreshAtom(root); 20 | return { 21 | title, 22 | message: "Fetched.", 23 | }; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/commands/ignore-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from "path"; 4 | import gitCmd from "../git-cmd"; 5 | import helper from "../helper"; 6 | import Notifications from "../Notifications"; 7 | 8 | export default { 9 | label: "Ignore Changes", 10 | description: "Ignore changes to selected files", 11 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, ignore = true, title = "Ignore Changes") { 12 | const [[files, root], statuses] = await Promise.all([ 13 | helper.getRootAndAllFiles(filePaths, git), 14 | helper.getStatuses(filePaths, git), 15 | ]); 16 | await helper.checkGitLock(root); 17 | 18 | const trackedFiles = files.filter(file => { 19 | return !statuses.some(status => { 20 | return status.untracked && path.resolve(root, status.file) === file; 21 | }); 22 | }); 23 | 24 | statusBar.show(`${ignore ? "I" : "Uni"}gnoring...`, null); 25 | 26 | const result = await git.updateIndex(root, trackedFiles, ignore); 27 | notifications.addGit(title, result); 28 | helper.refreshAtom(root); 29 | return { 30 | title, 31 | message: `${trackedFiles.length} File${trackedFiles.length !== 1 ? "s" : ""} ${ignore ? "I" : "Uni"}gnored.`, 32 | }; 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /lib/commands/init.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Initialize", 9 | description: "Inizialize a git repo", 10 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Initialize") { 11 | const roots = atom.project.getPaths().filter(dir => (!!dir && filePaths.some(filePath => filePath.startsWith(dir)))); 12 | if (roots.length === 0) { 13 | throw "No project directory."; 14 | } 15 | 16 | statusBar.show("Initializing..."); 17 | const results = await Promise.all(roots.map(root => git.init(root))); 18 | notifications.addGit(title, results); 19 | atom.project.setPaths(atom.project.getPaths()); 20 | roots.forEach(root => { 21 | helper.refreshAtom(root); 22 | }); 23 | return { 24 | title, 25 | message: `Git folder${results.length > 1 ? "s" : ""} initialized.`, 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /lib/commands/log.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import LogDialog from "../dialogs/LogDialog"; 6 | 7 | export default { 8 | label: "Log", 9 | description: "Show Git Log", 10 | // eslint-disable-next-line no-unused-vars 11 | async command(filePaths, statusBar, git = gitCmd, notifications = null, dialog = LogDialog) { 12 | const root = await helper.getRoot(filePaths, git); 13 | const format = atom.config.get("git-menu.logFormat"); 14 | await new dialog({root, gitCmd, format}).activate(); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /lib/commands/merge-branch.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | import MergeBranchDialog from "../dialogs/MergeBranchDialog"; 7 | 8 | export default { 9 | label: "Merge Branch...", 10 | description: "Merge a branch", 11 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = MergeBranchDialog, title = "Merge Branch") { 12 | const root = await helper.getRoot(filePaths, git); 13 | await helper.checkGitLock(root); 14 | 15 | const branches = await git.branches(root, false); 16 | const [rootBranch, mergeBranch, rebase, deleteBranch, abort] = await new dialog({branches, root}).activate(); 17 | 18 | if (rootBranch.name === mergeBranch.name) { 19 | throw "Branches cannot be the same."; 20 | } 21 | 22 | statusBar.show("Merging Branch..."); 23 | 24 | await helper.checkGitLock(root); 25 | 26 | const gitResults = []; 27 | if (!rootBranch.selected) { 28 | // if rootBranch is not current branch then checkout rootBranch first 29 | gitResults.push(await git.checkoutBranch(root, rootBranch.name)); 30 | 31 | helper.refreshAtom(root); 32 | } 33 | 34 | try { 35 | gitResults.push( 36 | rebase 37 | ? await git.rebase(root, mergeBranch.name) 38 | : await git.merge(root, mergeBranch.name), 39 | ); 40 | } catch (ex) { 41 | notifications.addGit(title, gitResults); 42 | if (abort) { 43 | await git.abort(root, !rebase); 44 | throw `Merge aborted:\n\n${ex}`; 45 | } else { 46 | throw ex; 47 | } 48 | } finally { 49 | helper.refreshAtom(root); 50 | } 51 | 52 | 53 | if (deleteBranch) { 54 | gitResults.push(await git.deleteBranch(root, mergeBranch.name)); 55 | 56 | helper.refreshAtom(root); 57 | } 58 | 59 | notifications.addGit(title, gitResults); 60 | 61 | return { 62 | title, 63 | message: `Merged branch ${mergeBranch.name} into ${rootBranch.name}.`, 64 | }; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /lib/commands/pull-all.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | import {command as pull} from "./pull"; 6 | 7 | export default { 8 | label: "Pull All", 9 | description: "Pull all project repos", 10 | confirm: { 11 | message: "Are you sure you want to pull all project repos?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Pull All") { 14 | const pulls = await Promise.all(atom.project.getPaths().map(root => { 15 | return pull([root], statusBar, git, notifications).catch((err) => { 16 | const message = (err.stack ? err.stack : err.toString()); 17 | notifications.addError("Git Menu: Pull", `${root}\n\n${message}`); 18 | return null; 19 | }); 20 | })); 21 | 22 | const failed = pulls.filter(p => !p); 23 | let num = "all"; 24 | if (failed.length > 0) { 25 | num = pulls.length - failed.length; 26 | } 27 | 28 | return { 29 | title, 30 | message: `Pulled ${num} repos.`, 31 | }; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/commands/pull.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Pull", 9 | description: "Pull from upstream", 10 | confirm: { 11 | message: "Are you sure you want to pull from upstream?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Pull") { 14 | const root = await helper.getRoot(filePaths, git); 15 | const rebase = atom.config.get("git-menu.rebaseOnPull"); 16 | await helper.checkGitLock(root); 17 | statusBar.show("Pulling..."); 18 | const result = await git.pull(root, rebase, false); 19 | notifications.addGit(title, result); 20 | helper.refreshAtom(root); 21 | return { 22 | title, 23 | message: "Pulled.", 24 | }; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/commands/push-all.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | import {command as push} from "./push"; 6 | 7 | export default { 8 | label: "Push All", 9 | description: "Push all project repos", 10 | confirm: { 11 | message: "Are you sure you want to push all projectrepos?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Push All") { 14 | const pushes = await Promise.all(atom.project.getPaths().map(root => { 15 | return push([root], statusBar, git, notifications).catch((err) => { 16 | const message = (err.stack ? err.stack : err.toString()); 17 | notifications.addError("Git Menu: Push", `${root}\n\n${message}`); 18 | return null; 19 | }); 20 | })); 21 | 22 | const failed = pushes.filter(p => !p); 23 | let num = "all"; 24 | if (failed.length > 0) { 25 | num = pushes.length - failed.length; 26 | } 27 | 28 | return { 29 | title, 30 | message: `Pushed ${num} repos.`, 31 | }; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/commands/push.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Push", 9 | description: "Push to upstream", 10 | confirm: { 11 | message: "Are you sure you want to push to upstream?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Push") { 14 | const root = await helper.getRoot(filePaths, git); 15 | await helper.checkGitLock(root); 16 | statusBar.show("Pushing..."); 17 | const result = await git.push(root, false); 18 | notifications.addGit(title, result); 19 | helper.refreshAtom(root); 20 | return { 21 | title, 22 | message: "Pushed.", 23 | }; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/commands/refresh.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import helper from "../helper"; 4 | 5 | export default { 6 | label: "Refresh", 7 | description: "Refresh Atom", 8 | async command(filePaths, statusBar, title = "Refresh") { 9 | statusBar.show("Refreshing..."); 10 | await helper.refreshAtom(); 11 | return { 12 | title, 13 | message: "Git Refreshed.", 14 | }; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /lib/commands/run-command.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from "path"; 4 | import gitCmd from "../git-cmd"; 5 | import helper from "../helper"; 6 | import Notifications from "../Notifications"; 7 | import RunCommandDialog from "../dialogs/RunCommandDialog"; 8 | import stringArgv from "string-argv"; 9 | 10 | export default { 11 | label: "Run Command...", 12 | description: "Run a git command", 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = RunCommandDialog, title = "Run Command") { 14 | const [files, root] = await helper.getRootAndFilesStatuses(filePaths, git); 15 | await helper.checkGitLock(root); 16 | const treeView = atom.config.get("git-menu.treeView"); 17 | const [gitCommand, selectedFiles] = await new dialog({files, treeView}).activate(); 18 | if (!gitCommand) { 19 | throw "Command cannot be blank."; 20 | } 21 | const trimmedGitCommand = gitCommand.trim().replace(/^git /, ""); 22 | 23 | statusBar.show("Running..."); 24 | const selectedFilePaths = selectedFiles.map(file => path.join(root, file)); 25 | const numFiles = `${selectedFiles.length} file${selectedFiles.length !== 1 ? "s" : ""}`; 26 | let includedFiles = false; 27 | const gitArgs = stringArgv(trimmedGitCommand).reduce((prev, arg) => { 28 | if (arg === "%files%") { 29 | includedFiles = true; 30 | selectedFilePaths.forEach(file => { 31 | prev.push(file); 32 | }); 33 | } else { 34 | prev.push(arg); 35 | } 36 | return prev; 37 | }, []); 38 | 39 | await helper.checkGitLock(root); 40 | const result = await git.cmd(root, gitArgs); 41 | notifications.addGit(gitCommand, result); 42 | helper.refreshAtom(root); 43 | return { 44 | title, 45 | message: `Ran 'git ${trimmedGitCommand}'${includedFiles ? ` with ${numFiles}.` : ""}`, 46 | }; 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /lib/commands/stage-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Stage Changes", 9 | description: "Stage the changes to commit later", 10 | confirm: { 11 | message: "Are you sure you want to stage these changes?", 12 | detail: (filePaths) => { 13 | return `You are staging these files:\n${filePaths.join("\n")}`; 14 | }, 15 | }, 16 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Stage Changes") { 17 | const [files, root] = await helper.getRootAndFiles(filePaths, git); 18 | await helper.checkGitLock(root); 19 | 20 | // commit files 21 | statusBar.show("Staging..."); 22 | const numFiles = `${files.length} File${files.length !== 1 ? "s" : ""}`; 23 | const results = await git.add(root, files); 24 | notifications.addGit(title, results); 25 | helper.refreshAtom(root); 26 | return { 27 | title, 28 | message: `${numFiles} staged.`, 29 | }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/commands/stash-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Stash Changes", 9 | description: "Stash and remove the current changes", 10 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, unstash = false, title = "Stash Changes") { 11 | const root = await helper.getRoot(filePaths, git); 12 | await helper.checkGitLock(root); 13 | statusBar.show(`${unstash ? "Uns" : "S"}tashing Changes...`, null); 14 | const result = await git.stash(root, unstash); 15 | notifications.addGit(title, result); 16 | helper.refreshAtom(root); 17 | return { 18 | title, 19 | message: `Changes ${unstash ? "un" : ""}stashed.`, 20 | }; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/commands/switch-branch.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | import SwitchBranchDialog from "../dialogs/SwitchBranchDialog"; 7 | 8 | export default { 9 | label: "Switch Branch...", 10 | description: "Checkout a different branch", 11 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, dialog = SwitchBranchDialog, title = "Switch Branch") { 12 | const root = await helper.getRoot(filePaths, git); 13 | await helper.checkGitLock(root); 14 | 15 | const branches = await git.branches(root); 16 | const [branchName, remote] = await new dialog({branches, root}).activate(); 17 | 18 | statusBar.show("Switching Branch..."); 19 | 20 | await helper.checkGitLock(root); 21 | const result = remote 22 | ? await git.createBranch(root, branchName, remote) 23 | : await git.checkoutBranch(root, branchName); 24 | notifications.addGit(title, result); 25 | 26 | helper.refreshAtom(root); 27 | return { 28 | title, 29 | message: `Switched to ${branchName}.`, 30 | }; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/commands/sync-all.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | import {command as sync} from "./sync"; 6 | 7 | export default { 8 | label: "Sync All", 9 | description: "Pull then push all project repos", 10 | confirm: { 11 | message: "Are you sure you want to sync all project repos?", 12 | }, 13 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Sync All") { 14 | const syncs = await Promise.all(atom.project.getPaths().map(root => { 15 | return sync([root], statusBar, git, notifications).catch((err) => { 16 | const message = (err.stack ? err.stack : err.toString()); 17 | notifications.addError("Git Menu: Sync", `${root}\n\n${message}`); 18 | return null; 19 | }); 20 | })); 21 | 22 | const failed = syncs.filter(p => !p); 23 | let num = "all"; 24 | if (failed.length > 0) { 25 | num = syncs.length - failed.length; 26 | } 27 | 28 | return { 29 | title, 30 | message: `Synced ${num} repos.`, 31 | }; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/commands/sync.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | import {command as pull} from "./pull"; 6 | import {command as push} from "./push"; 7 | 8 | export default { 9 | label: "Sync", 10 | description: "Pull then push from upstream", 11 | confirm: { 12 | message: "Are you sure you want to sync with upstream?", 13 | }, 14 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Sync") { 15 | await pull(filePaths, statusBar, git, notifications); 16 | await push(filePaths, statusBar, git, notifications); 17 | return { 18 | title, 19 | message: "Synced", 20 | }; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/commands/undo-last-commit.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import helper from "../helper"; 5 | import Notifications from "../Notifications"; 6 | 7 | export default { 8 | label: "Undo Last Commit", 9 | description: "Undo the last commit and save the current changes", 10 | confirm: { 11 | message: "Are you sure you want to undo the last commit?", 12 | detail: async (filePaths, git = gitCmd) => { 13 | const root = await helper.getRoot(filePaths, git); 14 | const lastCommit = await git.lastCommit(root); 15 | return `You are undoing the commit:\n${lastCommit}`; 16 | }, 17 | }, 18 | async command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Undo Last Commit") { 19 | const root = await helper.getRoot(filePaths, git); 20 | await helper.checkGitLock(root); 21 | statusBar.show("Resetting..."); 22 | try { 23 | const count = await git.countCommits(root); 24 | let result; 25 | if (count > 1) { 26 | result = await git.reset(root, false, 1); 27 | } else { 28 | await git.remove(root); 29 | result = await git.init(root); 30 | } 31 | notifications.addGit(title, result); 32 | helper.refreshAtom(root); 33 | return { 34 | title, 35 | message: "Last commit is reset.", 36 | }; 37 | } catch (error) { 38 | if (!error) { 39 | throw "Unknown Error."; 40 | } else if (error.includes("ambiguous argument")) { 41 | throw "No commits."; 42 | } 43 | throw error; 44 | } 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /lib/commands/unignore-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | 6 | import {command as ignoreChanges} from "./ignore-changes"; 7 | 8 | export default { 9 | label: "Unignore Changes", 10 | description: "Unignore changes to selected files", 11 | command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Unignore Changes") { 12 | return ignoreChanges(filePaths, statusBar, git, notifications, false, title); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /lib/commands/unstash-changes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import gitCmd from "../git-cmd"; 4 | import Notifications from "../Notifications"; 5 | 6 | import {command as stashChanges} from "./stash-changes"; 7 | 8 | export default { 9 | label: "Unstash Changes", 10 | description: "Restore the changes that were stashed", 11 | command(filePaths, statusBar, git = gitCmd, notifications = Notifications, title = "Unstash Changes") { 12 | return stashChanges(filePaths, statusBar, git, notifications, true, title); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import which from "which"; 4 | import commands from "./commands"; 5 | import {NotificationsConfig} from "./Notifications"; 6 | 7 | export default { 8 | gitPath: { 9 | type: "string", 10 | default: which.sync("git", {nothrow: true}) || "git", 11 | description: "Path to your git executable", 12 | order: 0, 13 | }, 14 | notifications: { 15 | type: "integer", 16 | enum: NotificationsConfig.enum, 17 | default: NotificationsConfig.default, 18 | order: 1, 19 | }, 20 | logFormat: { 21 | type: "string", 22 | title: "Default Log Format", 23 | description: "(see https://git-scm.com/docs/git-log#_pretty_formats)", 24 | default: "medium", 25 | order: 2, 26 | }, 27 | treeView: { 28 | type: "boolean", 29 | title: "Tree View", 30 | description: "Show files as tree view", 31 | default: true, 32 | order: 3, 33 | }, 34 | rebaseOnPull: { 35 | type: "boolean", 36 | title: "Rebase on Pull", 37 | description: "Rebase instead of merge on Pull and Sync", 38 | default: false, 39 | order: 4, 40 | }, 41 | confirmationDialogs: { 42 | type: "object", 43 | order: 5, 44 | properties: Object.keys(commands).reduce((prev, cmd, idx) => { 45 | if (commands[cmd].confirm) { 46 | const label = commands[cmd].label || commands[cmd].confirm.label; 47 | prev[cmd] = { 48 | title: label, 49 | description: `Show confirmation dialog on ${label}`, 50 | type: "boolean", 51 | default: true, 52 | order: idx, 53 | }; 54 | } 55 | return prev; 56 | }, { 57 | deleteRemote: { 58 | title: "Delete Remote", 59 | description: "Show confirmation dialog when deleting a remote branch", 60 | type: "boolean", 61 | default: true, 62 | order: Object.keys(commands).length, 63 | }, 64 | deleteAfterMerge: { 65 | title: "Delete After Merge", 66 | description: "Show confirmation dialog when deleting a branch after merging", 67 | type: "boolean", 68 | default: true, 69 | order: Object.keys(commands).length + 1, 70 | }, 71 | }), 72 | }, 73 | contextMenuItems: { 74 | type: "object", 75 | order: 6, 76 | properties: Object.keys(commands).reduce((prev, cmd, idx) => { 77 | if (commands[cmd].label) { 78 | prev[cmd] = { 79 | title: commands[cmd].label, 80 | description: commands[cmd].description, 81 | type: "boolean", 82 | default: true, 83 | order: idx, 84 | }; 85 | } 86 | return prev; 87 | }, {}), 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /lib/dialogs/CommitDialog.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | /** @jsx etch.dom */ 4 | 5 | import Dialog from "./Dialog"; 6 | import etch from "etch"; 7 | import FileTree from "../widgets/FileTree.js"; 8 | 9 | export default class CommitDialog extends Dialog { 10 | 11 | initialState(props) { 12 | const state = { 13 | files: props.files || [], 14 | message: localStorage.getItem("git-menu.commit-message") || "", 15 | lastCommit: props.lastCommit || "", 16 | amend: false, 17 | push: false, 18 | sync: false, 19 | filesSelectable: props.filesSelectable === false ? false : true, 20 | treeView: props.treeView, 21 | }; 22 | return state; 23 | } 24 | 25 | validate(state) { 26 | let error = false; 27 | if (!state.message) { 28 | error = true; 29 | this.refs.messageInput.classList.add("error"); 30 | } 31 | if (error) { 32 | return; 33 | } 34 | 35 | const files = this.refs.fileTree.getSelectedFiles(); 36 | 37 | return [ 38 | state.message, 39 | state.amend, 40 | state.push, 41 | state.sync, 42 | files, 43 | ]; 44 | } 45 | 46 | show() { 47 | this.refs.messageInput.focus(); 48 | } 49 | 50 | messageChange(e) { 51 | this.refs.messageInput.classList.remove("error"); 52 | localStorage.setItem("git-menu.commit-message", e.target.value); 53 | this.update({message: e.target.value}); 54 | } 55 | 56 | amendChange(e) { 57 | let {message} = this.state; 58 | const amend = e.target.checked; 59 | if (!message && amend) { 60 | message = this.state.lastCommit; 61 | } else if (message === this.state.lastCommit && !amend) { 62 | message = ""; 63 | } 64 | this.update({message, amend}); 65 | } 66 | 67 | pushClick() { 68 | this.update({push: true}); 69 | this.accept(); 70 | } 71 | 72 | syncClick() { 73 | this.update({push: true, sync: true}); 74 | this.accept(); 75 | } 76 | 77 | body() { 78 | const messageTooLong = this.state.message.split("\n").some((line, idx) => ((idx === 0 && line.length > 50) || line.length > 80)); 79 | const lastCommitLines = this.state.lastCommit !== null ? this.state.lastCommit.split("\n") : null; 80 | const firstLineOfLastCommit = lastCommitLines !== null ? lastCommitLines[0] + (lastCommitLines.length > 1 ? "..." : "") : null; 81 | 82 | return ( 83 |
84 | 85 |