├── .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 | [](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 | 
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 |
86 |
90 |
91 | );
92 | }
93 |
94 | title() {
95 | return "Commit";
96 | }
97 |
98 | buttons() {
99 | return (
100 |
101 |
104 |
107 |
110 |
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib/dialogs/CreateBranchDialog.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import gitCmd from "../git-cmd";
6 | import Dialog from "./Dialog";
7 | import etch from "etch";
8 | import Notifications from "../Notifications";
9 |
10 | export default class CreateBranchDialog extends Dialog {
11 |
12 | initialState(props) {
13 | if (!props.root) {
14 | throw new Error("Must specify a {root} property");
15 | }
16 |
17 | this.git = props.git || gitCmd;
18 | this.notifications = props.notifications || Notifications;
19 |
20 | const state = {
21 | branches: props.branches || [],
22 | sourceBranch: "",
23 | newBranch: "",
24 | track: false,
25 | root: props.root,
26 | fetching: false,
27 | };
28 |
29 | const branch = state.branches.find(b => b.selected);
30 | state.sourceBranch = branch ? branch.name : "";
31 |
32 | return state;
33 | }
34 |
35 | validate(state) {
36 | let error = false;
37 | if (!state.newBranch) {
38 | error = true;
39 | this.refs.newBranchInput.classList.add("error");
40 | }
41 | if (!state.sourceBranch) {
42 | error = true;
43 | this.refs.sourceBranchInput.classList.add("error");
44 | }
45 | if (error) {
46 | return;
47 | }
48 | const newBranch = this.removeIllegalChars(state.newBranch);
49 |
50 | return [state.sourceBranch, newBranch, state.track];
51 | }
52 |
53 | show() {
54 | this.refs.newBranchInput.focus();
55 | }
56 |
57 | async fetch() {
58 | this.update({fetching: true});
59 | try {
60 | await this.git.fetch(this.state.root);
61 | const branches = await this.git.branches(this.state.root);
62 | this.update({branches: branches, fetching: false});
63 | } catch (err) {
64 | this.notifications.addError("Fetch", err);
65 | this.update({fetching: false});
66 | }
67 | }
68 |
69 | sourceBranchChange(e) {
70 | this.refs.sourceBranchInput.classList.remove("error");
71 | this.update({sourceBranch: e.target.value});
72 | }
73 |
74 | newBranchChange(e) {
75 | this.refs.newBranchInput.classList.remove("error");
76 | this.update({newBranch: e.target.value});
77 | }
78 |
79 | trackChange(e) {
80 | this.update({track: e.target.checked});
81 | }
82 |
83 | removeIllegalChars(branchName) {
84 | // from https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html#_description
85 | // eslint-disable-next-line no-control-regex
86 | return branchName.replace(/^[./]|[./]$|^@$|[\s~^:[\\?*\x00-\x20\x7F]/g, "-").replace(/\.\.|@{/g, "--");
87 | }
88 |
89 | body() {
90 | const branchOptions = this.state.fetching ? (
91 |
92 | ) : this.state.branches.map(branch => (
93 |
94 | ));
95 |
96 | const actualName = this.removeIllegalChars(this.state.newBranch);
97 |
98 | return (
99 |
100 |
104 |
{this.state.newBranch !== actualName ? `Will be created as ${actualName}` : ""}
105 |
111 |
115 |
116 | );
117 | }
118 |
119 | title() {
120 | return "Create Branch";
121 | }
122 |
123 | buttons() {
124 | return (
125 |
126 |
129 |
132 |
133 | );
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/lib/dialogs/DeleteBranchDialog.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import gitCmd from "../git-cmd";
6 | import Dialog from "./Dialog";
7 | import etch from "etch";
8 | import Notifications from "../Notifications";
9 | import {promisify} from "promisificator";
10 |
11 | export default class DeleteBranchDialog extends Dialog {
12 |
13 | initialState(props) {
14 | if (!props.root) {
15 | throw new Error("Must specify a {root} property");
16 | }
17 |
18 | this.git = props.git || gitCmd;
19 | this.notifications = props.notifications || Notifications;
20 |
21 | const state = {
22 | branches: props.branches || [],
23 | branch: "",
24 | local: true,
25 | remote: false,
26 | force: false,
27 | root: props.root,
28 | fetching: false,
29 | };
30 |
31 | const branch = state.branches.find(b => b.selected);
32 | state.branch = branch ? branch.name : "";
33 |
34 | return state;
35 | }
36 |
37 | async validate(state) {
38 | if (!state.branch) {
39 | this.refs.branchInput.classList.add("error");
40 | return;
41 | }
42 |
43 | const branch = state.branches.find(b => b.name === state.branch) || {};
44 | const local = !!(branch.local && state.local);
45 | const remote = !!(branch.remote && state.remote);
46 |
47 | if (state.force && (!branch.local || state.local) && (!branch.remote || state.remote)) {
48 | const branches = [
49 | local ? state.branch : null,
50 | remote ? `origin/${state.branch}` : null,
51 | ].filter(i => i).join("\n");
52 | const [confirmButton, hideDialog] = await promisify(atom.confirm.bind(atom), {rejectOnError: false, alwaysReturnArray: true})({
53 | type: "warning",
54 | checkboxLabel: "Never Show This Dialog Again",
55 | message: "Are you sure you want to force delete the only version of this branch?",
56 | detail: `You are deleting:\n${branches}`,
57 | buttons: [
58 | "Delete Branches",
59 | "Cancel",
60 | ],
61 | });
62 |
63 | if (hideDialog) {
64 | atom.config.set("git-menu.confirmationDialogs.deleteRemote", false);
65 | }
66 | if (confirmButton === 1) {
67 | return;
68 | }
69 | }
70 |
71 | return [branch, local, remote, state.force];
72 | }
73 |
74 | show() {
75 | this.refs.branchInput.focus();
76 | }
77 |
78 | async fetch() {
79 | this.update({fetching: true});
80 | try {
81 | await this.git.fetch(this.state.root);
82 | const branches = await this.git.branches(this.state.root);
83 | this.update({branches: branches, fetching: false});
84 | } catch (err) {
85 | this.notifications.addError("Fetch", err);
86 | this.update({fetching: false});
87 | }
88 | }
89 |
90 | branchChange(e) {
91 | this.refs.branchInput.classList.remove("error");
92 | this.update({branch: e.target.value});
93 | }
94 |
95 | remoteChange(e) {
96 | this.update({remote: e.target.checked});
97 | }
98 |
99 | localChange(e) {
100 | this.update({local: e.target.checked});
101 | }
102 |
103 | forceChange(e) {
104 | this.update({force: e.target.checked});
105 | }
106 |
107 | body() {
108 | let branchOptions;
109 | if (this.state.fetching) {
110 | branchOptions = (
111 |
112 | );
113 | } else {
114 | branchOptions = this.state.branches.map(b => (
115 |
116 | ));
117 | }
118 |
119 | const branch = this.state.branches.find(b => b.name === this.state.branch);
120 | const local = branch ? branch.local : false;
121 | const remote = branch ? branch.remote : false;
122 |
123 | return (
124 |
125 |
130 |
134 |
138 |
142 |
143 | );
144 | }
145 |
146 | title() {
147 | return "Delete Branch";
148 | }
149 |
150 | buttons() {
151 | return (
152 |
153 |
156 |
159 |
160 | );
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/lib/dialogs/Dialog.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import etch from "etch";
6 | import {CompositeDisposable} from "atom";
7 |
8 | export default class Dialog {
9 | constructor(props = {}) {
10 |
11 | this.disposables = new CompositeDisposable();
12 |
13 | this.state = this.initialState(props);
14 |
15 | etch.initialize(this);
16 |
17 | this.disposables.add(atom.tooltips.add(this.refs.close, {
18 | title: "Close",
19 | keyBindingCommand: "core:cancel",
20 | }));
21 | }
22 |
23 | update(props) {
24 | if (props) {
25 | this.setState(props);
26 | }
27 |
28 | return etch.update(this);
29 | }
30 |
31 | destroy() {
32 | this.disposables.dispose();
33 | return etch.destroy(this);
34 | }
35 |
36 | setState(state) {
37 | this.state = Object.assign({}, this.state, state);
38 | }
39 |
40 | activate() {
41 | this.lastActiveItem = atom.document.activeElement;
42 | this.modalPanel = atom.workspace.addModalPanel({item: this});
43 | this.show();
44 |
45 | return new Promise((resolve, reject) => {
46 | this.resolve = resolve;
47 | this.reject = reject;
48 | });
49 | }
50 |
51 | deactivate() {
52 | this.hide();
53 | this.modalPanel.destroy();
54 | this.lastActiveItem.focus();
55 | this.destroy();
56 | }
57 |
58 | keyup(e) {
59 | switch (e.key) {
60 | case "Escape":
61 | this.cancel();
62 | break;
63 | default:
64 | // do nothing
65 | }
66 | }
67 |
68 | cancel() {
69 | this.reject();
70 | this.deactivate();
71 | }
72 |
73 | async accept() {
74 | const result = await this.validate(this.state);
75 | if (!Array.isArray(result)) {
76 | return;
77 | }
78 | this.resolve(result);
79 | this.deactivate();
80 | }
81 |
82 | render() {
83 |
84 | const title = this.title();
85 | const titleClass = title.toLowerCase()
86 | .replace(/\W/g, "-");
87 |
88 | return (
89 |
90 |
91 |
92 |
{title}
93 |
94 |
95 | {this.body()}
96 |
97 |
98 | {this.buttons()}
99 |
100 |
101 | );
102 | }
103 |
104 | initialState(props) {
105 | // Subclass can override this initialState() method
106 | return props;
107 | }
108 |
109 | // eslint-disable-next-line no-unused-vars
110 | validate(state) {
111 | throw new Error("Subclass must implement a validate(state) method");
112 | }
113 |
114 | title() {
115 | throw new Error("Subclass must implement a title() method");
116 | }
117 |
118 | body() {
119 | throw new Error("Subclass must implement a body() method");
120 | }
121 |
122 | buttons() {
123 | throw new Error("Subclass must implement a buttons() method");
124 | }
125 |
126 | show() {
127 | // Subclass can override this show() method
128 | }
129 |
130 | hide() {
131 | // Subclass can override this hide() method
132 | }
133 |
134 | beforeInitialize() {
135 | // Subclass can override this beforeInitialize() method
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/lib/dialogs/LogDialog.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import Dialog from "./Dialog";
6 | import etch from "etch";
7 |
8 | export default class LogDialog extends Dialog {
9 |
10 | initialState(props) {
11 | const state = {
12 | format: props.format,
13 | logs: "",
14 | offset: 0,
15 | loading: false,
16 | gitCmd: props.gitCmd,
17 | root: props.root,
18 | error: null,
19 | };
20 |
21 | return state;
22 | }
23 |
24 | async getLogs() {
25 | this.update({
26 | loading: true,
27 | error: null,
28 | });
29 | let {format} = this.state;
30 | format = format.replace(/\\n/g, "%n");
31 |
32 | // unescape slashes
33 | try {
34 | // add another escaped slash if the string ends with an odd
35 | // number of escaped slashes which will crash JSON.parse
36 | let parsedFormat = format.replace(/(?:^|[^\\])(?:\\\\)*\\$/, "$&\\");
37 | parsedFormat = JSON.parse(`"${parsedFormat}"`);
38 | format = parsedFormat;
39 | } catch (e) {
40 | // invalid json
41 | }
42 |
43 | try {
44 | const newLogs = await this.state.gitCmd.log(this.state.root, 10, this.state.offset, format);
45 | this.update({
46 | logs: `${this.state.logs}\n\n${newLogs}`,
47 | offset: this.state.offset + 10,
48 | loading: false,
49 | });
50 | if (this.state.format !== this.refs.formatInput.value) {
51 | this.formatChange({target: this.refs.formatInput});
52 | } else if (newLogs.trim() !== "") {
53 | this.scroll({target: this.refs.logs});
54 | }
55 | } catch (err) {
56 | this.update({
57 | loading: false,
58 | error: err,
59 | });
60 | }
61 | }
62 |
63 | scroll(e) {
64 | if (!this.state.loading && !this.state.error && e.target.scrollHeight - e.target.scrollTop - e.target.clientHeight < 100) {
65 | this.getLogs();
66 | }
67 | }
68 |
69 | formatChange(e) {
70 | if (!this.state.loading) {
71 | this.update({
72 | format: e.target.value,
73 | logs: "",
74 | offset: 0,
75 | });
76 | this.getLogs();
77 | }
78 | }
79 |
80 | show() {
81 | this.disposables.add(atom.tooltips.add(this.refs.info, {
82 | title: "Open Log Format Info",
83 | }));
84 |
85 | this.refs.formatInput.focus();
86 | this.refs.formatInput.select();
87 |
88 | this.getLogs();
89 | }
90 |
91 | body() {
92 | let message = this.state.logs;
93 | if (this.state.loading) {
94 | message += "\nLoading More...";
95 | }
96 | if (this.state.error) {
97 | message = this.state.error;
98 | }
99 |
100 | return (
101 |
102 |
103 |
108 |
109 | );
110 | }
111 |
112 | title() {
113 | return "Git Log";
114 | }
115 |
116 | buttons() {
117 | return null;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/lib/dialogs/RunCommandDialog.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import Dialog from "./Dialog";
6 | import etch from "etch";
7 | import Autocomplete from "../widgets/Autocomplete.js";
8 | import FileTree from "../widgets/FileTree.js";
9 |
10 | const RECENT_ITEM_KEY = "git-menu-run-command-recent";
11 |
12 | export default class RunCommandDialog extends Dialog {
13 |
14 | getRecentItems() {
15 | let recentItems = [];
16 | try {
17 | recentItems = JSON.parse(localStorage.getItem(RECENT_ITEM_KEY));
18 | } catch (ex) {
19 | // invalid json
20 | }
21 |
22 | if (!Array.isArray(recentItems)) {
23 | recentItems = [];
24 | }
25 |
26 | return recentItems;
27 | }
28 |
29 | addRecentItem(item) {
30 | let recentItems = this.getRecentItems();
31 |
32 | // remove item from list
33 | recentItems = recentItems.filter(recentItem => recentItem !== item);
34 |
35 | // add item to the top of the list
36 | recentItems.unshift(item);
37 |
38 | // maximum 100 items to prevent bloat
39 | recentItems.splice(100);
40 |
41 | try {
42 | localStorage.setItem(RECENT_ITEM_KEY, JSON.stringify(recentItems));
43 | } catch (ex) {
44 | // invalid json
45 | }
46 | }
47 |
48 | removeRecentItem(item) {
49 | let recentItems = this.getRecentItems();
50 | recentItems = recentItems.filter(recentItem => recentItem !== item);
51 |
52 | try {
53 | localStorage.setItem(RECENT_ITEM_KEY, JSON.stringify(recentItems));
54 | } catch (ex) {
55 | // invalid json
56 | }
57 | }
58 |
59 | initialState(props) {
60 | const state = {
61 | files: props.files || [],
62 | command: "",
63 | recentItems: this.getRecentItems(),
64 | treeView: props.treeView,
65 | };
66 |
67 | this.commandRemoveItem = this.commandRemoveItem.bind(this);
68 | this.commandChange = this.commandChange.bind(this);
69 |
70 | return state;
71 | }
72 |
73 | validate(state) {
74 | let error = false;
75 | if (!state.command) {
76 | error = true;
77 | this.refs.commandInput.refs.input.classList.add("error");
78 | }
79 | if (error) {
80 | return;
81 | }
82 |
83 | const files = this.refs.fileTree.getSelectedFiles();
84 |
85 | this.addRecentItem(state.command);
86 |
87 | return [
88 | state.command,
89 | files,
90 | ];
91 | }
92 |
93 | show() {
94 | this.refs.commandInput.focus();
95 | }
96 |
97 | commandChange(e, value) {
98 | this.refs.commandInput.refs.input.classList.remove("error");
99 | this.update({command: value});
100 | }
101 |
102 | commandRemoveItem(value, item) {
103 | this.removeRecentItem(item);
104 | this.update({recentItems: this.getRecentItems(), command: value});
105 | }
106 |
107 | body() {
108 |
109 | return (
110 |
111 |
112 |
116 |
117 | );
118 | }
119 |
120 | title() {
121 | return "Run Command";
122 | }
123 |
124 | buttons() {
125 | return (
126 |
127 |
130 |
131 | );
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/lib/dialogs/SwitchBranchDialog.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import gitCmd from "../git-cmd";
6 | import Dialog from "./Dialog";
7 | import etch from "etch";
8 | import Notifications from "../Notifications";
9 |
10 | export default class SwitchBranchDialog extends Dialog {
11 |
12 | initialState(props) {
13 | if (!props.root) {
14 | throw new Error("Must specify a {root} property");
15 | }
16 |
17 | this.git = props.git || gitCmd;
18 | this.notifications = props.notifications || Notifications;
19 |
20 | const state = {
21 | branches: props.branches || [],
22 | branch: "",
23 | root: props.root,
24 | fetching: false,
25 | };
26 |
27 | const branch = state.branches.find(b => b.selected);
28 | state.branch = branch ? branch.name : "";
29 |
30 | return state;
31 | }
32 |
33 | validate(state) {
34 | let error = false;
35 | if (!state.branch) {
36 | error = true;
37 | this.refs.branchInput.classList.add("error");
38 | }
39 | if (error) {
40 | return;
41 | }
42 |
43 | const branch = state.branches.find(b => b.name === state.branch);
44 | let name = state.branch;
45 | let remote = branch && branch.remote ? "origin" : null;
46 | if (!remote) {
47 | const isRemote = state.branch.match(/^remotes\/([^/]+)\/(.+)$/);
48 | if (isRemote) {
49 | [, remote, name] = isRemote;
50 | }
51 | }
52 |
53 | return [name, remote];
54 | }
55 |
56 | show() {
57 | this.refs.branchInput.focus();
58 | }
59 |
60 | async fetch() {
61 | this.update({fetching: true});
62 | try {
63 | await this.git.fetch(this.state.root);
64 | const branches = await this.git.branches(this.state.root);
65 | this.update({branches: branches, fetching: false});
66 | } catch (err) {
67 | this.notifications.addError("Fetch", err);
68 | this.update({fetching: false});
69 | }
70 | }
71 |
72 | branchChange(e) {
73 | this.refs.branchInput.classList.remove("error");
74 | this.update({branch: e.target.value});
75 | }
76 |
77 | body() {
78 | let branchOptions;
79 | if (this.state.fetching) {
80 | branchOptions = (
81 |
82 | );
83 | } else {
84 | branchOptions = this.state.branches.map(branch => (
85 |
86 | ));
87 | }
88 |
89 | return (
90 |
91 |
96 |
97 | );
98 | }
99 |
100 | title() {
101 | return "Switch Branch";
102 | }
103 |
104 | buttons() {
105 | return (
106 |
107 |
110 |
113 |
114 | );
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import {
4 | CompositeDisposable,
5 | Disposable,
6 | } from "atom";
7 | import commands from "./commands";
8 | import config from "./config";
9 | import helper from "./helper";
10 | import StatusBarManager from "./widgets/StatusBarManager";
11 | import Notifications from "./Notifications";
12 | import {promisify} from "promisificator";
13 |
14 | export default {
15 | config,
16 |
17 | /**
18 | * Activate package
19 | * @return {void}
20 | */
21 | activate() {
22 | this.updateConfig();
23 |
24 | this.disposables = new CompositeDisposable();
25 | this.contextMenuDisposables = {};
26 | this.confirmationDialogs = {};
27 |
28 | this.statusBarManager = new StatusBarManager();
29 | this.disposables.add(new Disposable(() => {
30 | this.statusBarManager.destroy();
31 | this.statusBarManager = null;
32 | }));
33 |
34 | for (const command in commands) {
35 | const cmd = commands[command];
36 |
37 | // observe confirm dialog settings
38 | if (cmd.confirm) {
39 | this.disposables.add(atom.config.observe(`git-menu.confirmationDialogs.${command}`, value => {
40 | this.confirmationDialogs[command] = value;
41 | }));
42 | } else {
43 | this.confirmationDialogs[command] = false;
44 | }
45 |
46 | // add command
47 | this.disposables.add(atom.commands.add("atom-workspace", `context-git:${command}`, {
48 | didDispatch: (event) => {
49 | Notifications.addError("Deprecated Command", "'context-git:*' commands are deprecated. Please use 'git-menu:*' instead.");
50 | return this.dispatchCommand(command, cmd)(event);
51 | },
52 | hiddenInCommandPalette: true,
53 | }));
54 | this.disposables.add(atom.commands.add("atom-workspace", `git-menu:${command}`, this.dispatchCommand(command, cmd)));
55 |
56 | if (cmd.label) {
57 | // add to context menu
58 | this.disposables.add(atom.config.observe(`git-menu.contextMenuItems.${command}`, value => {
59 | if (value) {
60 | this.contextMenuDisposables[command] = atom.contextMenu.add({
61 | "atom-workspace, atom-text-editor, .tree-view, .tab-bar": [{
62 | label: "Git",
63 | submenu: [{
64 | label: cmd.label.replace(/&/g, "&&"),
65 | command: `git-menu:${command}`,
66 | }],
67 | }],
68 | });
69 | this.disposables.add(this.contextMenuDisposables[command]);
70 | } else {
71 | if (this.contextMenuDisposables[command]) {
72 | this.disposables.remove(this.contextMenuDisposables[command]);
73 | this.contextMenuDisposables[command].dispose();
74 | delete this.contextMenuDisposables[command];
75 | }
76 | }
77 | }));
78 | }
79 |
80 | if (cmd.keymap) {
81 | // add key binding
82 | atom.keymaps.add("git-menu", {
83 | "atom-workspace": {
84 | [cmd.keymap]: `git-menu:${command}`,
85 | },
86 | });
87 | }
88 | }
89 | },
90 |
91 | /**
92 | * Deactivate package
93 | * @return {void}
94 | */
95 | deactivate() {
96 | this.disposables.dispose();
97 | },
98 |
99 | /**
100 | * Copy config from context-git to git-menu
101 | * @return {void}
102 | */
103 | updateConfig() {
104 | try {
105 | const hasGitMenuConfig = "git-menu" in atom.config.settings;
106 | const hasContextGitConfig = "context-git" in atom.config.settings;
107 | if (hasGitMenuConfig || !hasContextGitConfig) {
108 | return;
109 | }
110 |
111 | const contextGitConfig = atom.config.getAll("context-git");
112 | contextGitConfig.forEach((cfg) => {
113 | let {scopeSelector} = cfg;
114 | if (scopeSelector === "*") {
115 | scopeSelector = null;
116 | }
117 | for (const key in cfg.value) {
118 | const value = cfg.value[key];
119 | atom.config.set(`git-menu.${key}`, value, {scopeSelector});
120 | }
121 | });
122 | } catch (ex) {
123 | // fail silently
124 | }
125 | },
126 |
127 | /**
128 | * Consume the status bar service
129 | * @param {mixed} statusBar Status bar service
130 | * @return {void}
131 | */
132 | statusBarService(statusBar) {
133 | if (this.statusBarManager) {
134 | this.statusBarManager.addStatusBar(statusBar);
135 | }
136 | },
137 |
138 | /**
139 | * Consume the busy signal service
140 | * @param {mixed} busySignal Busy signal service
141 | * @return {void}
142 | */
143 | busySignalService(busySignal) {
144 | if (this.statusBarManager) {
145 | this.statusBarManager.addBusySignal(busySignal);
146 | }
147 | },
148 |
149 | dispatchCommand(command, cmd) {
150 | return async event => {
151 | try {
152 | this.statusBarManager.show(cmd.label, {revealTooltip: false});
153 | const filePaths = helper.getPaths(event.target);
154 |
155 | // show confirm dialog if applicable
156 | if (this.confirmationDialogs[command]) {
157 | const {message} = cmd.confirm;
158 | let {detail} = cmd.confirm;
159 | if (typeof detail === "function") {
160 | detail = await detail(filePaths);
161 | }
162 |
163 | const [confirmButton, hideDialog] = await promisify(atom.confirm.bind(atom), {rejectOnError: false, alwaysReturnArray: true})({
164 | type: "warning",
165 | checkboxLabel: "Never Show This Dialog Again",
166 | message,
167 | detail,
168 | buttons: [
169 | cmd.label,
170 | "Cancel",
171 | ],
172 | });
173 |
174 | if (hideDialog) {
175 | atom.config.set(`git-menu.confirmationDialogs.${command}`, false);
176 | }
177 | if (confirmButton === 1) {
178 | return;
179 | }
180 | }
181 |
182 | try {
183 | // run command
184 | const {title, message} = await cmd.command(filePaths, this.statusBarManager);
185 | if (message) {
186 | Notifications.addSuccess(title, message);
187 | }
188 | } catch (err) {
189 | if (err) {
190 | const message = (err.stack ? err.stack : err.toString());
191 | Notifications.addError(`Git Menu: ${cmd.label}`, message);
192 | }
193 | }
194 | } finally {
195 | this.statusBarManager.hide();
196 | }
197 | };
198 | },
199 | };
200 |
--------------------------------------------------------------------------------
/lib/widgets/StatusBarManager.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | export default class StatusBarManager {
4 |
5 | constructor() {
6 | this.busySignal = null;
7 | this.tile = null;
8 | this.onDidClick = null;
9 | this.element = null;
10 | this.progressBar = null;
11 | this.label = null;
12 | }
13 |
14 | addBusySignal(busySignal) {
15 | if (this.busySignal) {
16 | this.busySignal.dispose();
17 | }
18 | if (this.tile) {
19 | this.tile.destroy();
20 | this.tile = null;
21 | this.onDidClick = null;
22 | this.element = null;
23 | this.progressBar = null;
24 | this.label = null;
25 | }
26 |
27 | this.busySignal = busySignal;
28 | }
29 |
30 | addStatusBar(statusBar) {
31 | if (this.busySignal) {
32 | // prefer busy signal
33 | return;
34 | }
35 |
36 | if (this.tile) {
37 | this.tile.destroy();
38 | }
39 |
40 |
41 | this.progressBar = document.createElement("progress");
42 |
43 | this.label = document.createElement("span");
44 | this.label.innerHTML = "git-menu";
45 |
46 | this.element = document.createElement("div");
47 | this.element.classList.add("git-menu", "status", "hidden");
48 | this.element.appendChild(this.progressBar);
49 | this.element.appendChild(this.label);
50 |
51 | this.onDidClick = null;
52 |
53 | this.tile = statusBar.addRightTile({
54 | item: this.element,
55 | priority: Number.MAX_SAFE_INTEGER,
56 | });
57 | }
58 |
59 | destroy() {
60 | if (this.tile) {
61 | this.tile.destroy();
62 | this.tile = null;
63 | this.onDidClick = null;
64 | this.element = null;
65 | this.progressBar = null;
66 | this.label = null;
67 | }
68 | if (this.busySignal) {
69 | this.busySignal.dispose();
70 | this.busySignal = null;
71 | }
72 | }
73 |
74 | show(label, opts = null) {
75 | if (opts === null || typeof opts === "number") {
76 | // eslint-disable-next-line no-param-reassign
77 | opts = {
78 | progress: opts,
79 | };
80 | }
81 |
82 | if (opts instanceof Promise) {
83 | // eslint-disable-next-line no-param-reassign
84 | opts = {
85 | promise: opts,
86 | };
87 | } else if (typeof opts === "function" || (opts && opts.then)) {
88 | // eslint-disable-next-line no-param-reassign
89 | opts = {
90 | promise: Promise.resolve(opts),
91 | };
92 | }
93 |
94 | if (this.tile) {
95 | if (this.onDidClick) {
96 | this.element.removeEventListener("click", this.onDidClick);
97 | this.onDidClick = null;
98 | }
99 |
100 | if (typeof label !== "undefined") {
101 | this.setLabel(label);
102 | }
103 | if ("progress" in opts) {
104 | this.setProgress(opts.progress);
105 | }
106 | this.element.classList.remove("hidden");
107 | if (opts.onDidClick) {
108 | this.onDidClick = opts.onDidClick;
109 | this.element.addEventListener("click", this.onDidClick);
110 | }
111 | }
112 |
113 | if (this.busySignal) {
114 | const busySignalOpts = {
115 | revealTooltip: ("revealTooltip" in opts) ? !!opts.revealTooltip : true,
116 | };
117 | if (opts.waitingFor) {
118 | busySignalOpts.waitingFor = opts.waitingFor;
119 | }
120 | if (opts.onDidClick) {
121 | busySignalOpts.onDidClick = opts.onDidClick;
122 | }
123 | if (!opts.append && this.lastBusyMessage) {
124 | this.lastBusyMessage.dispose();
125 | this.lastBusyMessage = null;
126 | }
127 | if (opts.promise) {
128 | return this.busySignal.reportBusyWhile(label, () => opts.promise, busySignalOpts);
129 | }
130 |
131 | this.lastBusyMessage = this.busySignal.reportBusy(label, busySignalOpts);
132 | return this.lastBusyMessage;
133 | }
134 | }
135 |
136 | hide(busyMessage) {
137 | if (this.tile) {
138 | this.element.classList.add("hidden");
139 |
140 | if (this.onDidClick) {
141 | this.element.removeEventListener("click", this.onDidClick);
142 | this.onDidClick = null;
143 | }
144 | }
145 |
146 | if (this.busySignal) {
147 | if (busyMessage) {
148 | busyMessage.dispose();
149 | if (this.lastBusyMessage === busyMessage) {
150 | this.lastBusyMessage = null;
151 | }
152 | } else if (this.lastBusyMessage) {
153 | this.lastBusyMessage.dispose();
154 | this.lastBusyMessage = null;
155 | }
156 | }
157 | }
158 |
159 | setLabel(label, busyMessage) {
160 | if (this.tile) {
161 | this.label.innerHTML = label;
162 | }
163 |
164 | if (this.busySignal) {
165 | if (busyMessage) {
166 | busyMessage.setTitle(label);
167 | } else if (this.lastBusyMessage) {
168 | this.lastBusyMessage.setTitle(label);
169 | } else {
170 | return this.show(label);
171 | }
172 | }
173 | }
174 |
175 | setProgress(progress) {
176 | if (this.tile) {
177 | const prog = parseInt(progress, 10);
178 | // check if progress is a number
179 | if (isNaN(prog)) {
180 | // set progress to indeterminate
181 | this.progressBar.removeAttribute("value");
182 | } else {
183 | this.progressBar.value = prog;
184 | }
185 | }
186 | }
187 |
188 | setProgressMax(max) {
189 | if (this.tile) {
190 | this.progressBar.max = max;
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-menu",
3 | "main": "./lib/main",
4 | "version": "3.2.9",
5 | "description": "Use git from the context menu",
6 | "keywords": [
7 | "git",
8 | "menu",
9 | "context"
10 | ],
11 | "repository": "https://github.com/UziTech/git-menu",
12 | "license": "MIT",
13 | "engines": {
14 | "atom": ">=1.25.0 <2.0.0"
15 | },
16 | "scripts": {
17 | "test": "atom --test spec",
18 | "lint": "eslint ."
19 | },
20 | "atomTestRunner": "./spec/runner",
21 | "consumedServices": {
22 | "status-bar": {
23 | "versions": {
24 | "^1.0.0": "statusBarService"
25 | }
26 | },
27 | "atom-ide-busy-signal": {
28 | "versions": {
29 | "0.1.0": "busySignalService"
30 | }
31 | }
32 | },
33 | "dependencies": {
34 | "etch": "^0.14.1",
35 | "promisificator": "^4.2.0",
36 | "rimraf": "^3.0.2",
37 | "string-argv": "^0.3.1",
38 | "which": "^2.0.2"
39 | },
40 | "devDependencies": {
41 | "@semantic-release/apm-config": "^9.0.1",
42 | "atom-jasmine3-test-runner": "^5.2.13",
43 | "eslint": "^8.17.0",
44 | "eslint-plugin-react": "^7.30.0",
45 | "semantic-release": "^19.0.3",
46 | "temp": "^0.9.4"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "@semantic-release/apm-config",
3 | };
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "devDependencies": {
6 | "automerge": true,
7 | "commitMessageTopic": "devDependency {{depName}}"
8 | },
9 | "rangeStrategy": "bump"
10 | }
11 |
--------------------------------------------------------------------------------
/spec/commands/commit-all-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import commitAll from "../../lib/commands/commit-all";
4 | import commit from "../../lib/commands/commit";
5 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("commit-all", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot);
18 | });
19 |
20 | it("should call commit with project folders", async function () {
21 | spyOn(commit, "command").and.callFake(() => Promise.resolve());
22 | await commitAll.command(this.filePaths);
23 | expect(commit.command.calls.mostRecent().args[0]).toEqual(atom.project.getPaths());
24 | });
25 |
26 | it("should only send project folders that contain filePaths", async function () {
27 | spyOn(commit, "command").and.callFake(() => Promise.resolve());
28 | const [projectFolder] = atom.project.getPaths();
29 | spyOn(atom.project, "getPaths").and.callFake(() => [projectFolder, "test"]);
30 | await commitAll.command(this.filePaths);
31 | expect(commit.command.calls.mostRecent().args[0]).toEqual([projectFolder]);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/spec/commands/delete-branch-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import {Directory} from "atom";
4 | import deleteBranch from "../../lib/commands/delete-branch";
5 | import Notifications from "../../lib/Notifications";
6 | import {getFilePath, statusBar, mockGit, mockDialog, removeGitRoot, createGitRoot, fileStatus, files} from "../mocks";
7 |
8 | describe("delete-branch", function () {
9 |
10 | beforeEach(async function () {
11 | await atom.packages.activatePackage("git-menu");
12 | this.gitRoot = await createGitRoot();
13 |
14 | this.repo = await atom.project.repositoryForDirectory(new Directory(this.gitRoot));
15 |
16 | this.statuses = [fileStatus("M ", files.t1), fileStatus("M ", files.t2)];
17 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
18 | this.branches = [{name: "deleteBranch", selected: true}, {name: "master", selected: false}];
19 | this.git = mockGit({
20 | rootDir: Promise.resolve(this.gitRoot),
21 | branches: Promise.resolve(this.branches),
22 | checkoutBranch: Promise.resolve("checkoutBranch result"),
23 | delete: Promise.resolve("delete result"),
24 | deleteBranch: Promise.resolve("deleteBranch result"),
25 | abort: Promise.resolve("abort result"),
26 | });
27 | this.dialogReturn = [
28 | {name: "deleteBranch", selected: false},
29 | true,
30 | false,
31 | false,
32 | ];
33 | this.dialog = mockDialog({
34 | activate: () => Promise.resolve(this.dialogReturn),
35 | });
36 | });
37 |
38 | afterEach(async function () {
39 | await removeGitRoot(this.gitRoot);
40 | });
41 |
42 | describe("dialog", function () {
43 |
44 | it("should call dialog with correct props", async function () {
45 | spyOn(this, "dialog").and.callThrough();
46 | try {
47 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
48 | } catch (ex) {
49 | // do nothing
50 | }
51 | expect(this.dialog).toHaveBeenCalledWith({
52 | branches: this.branches,
53 | root: this.gitRoot,
54 | });
55 | });
56 |
57 | it("should call dialog.activate()", async function () {
58 | spyOn(this.dialog.prototype, "activate").and.callThrough();
59 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
60 | expect(this.dialog.prototype.activate).toHaveBeenCalled();
61 | });
62 | });
63 |
64 | describe("cancel", function () {
65 |
66 | it("should reject without an error", async function () {
67 | this.dialog = mockDialog({
68 | activate: () => Promise.reject(),
69 | });
70 | let error;
71 | try {
72 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
73 | } catch (ex) {
74 | error = !ex;
75 | }
76 | expect(error).toBeTruthy();
77 | });
78 | });
79 |
80 | describe("delete", function () {
81 | it("should return on not local or remote", async function () {
82 | spyOn(this.git, "deleteBranch").and.callThrough();
83 | this.dialogReturn[1] = false;
84 | expect(this.git.deleteBranch).not.toHaveBeenCalled();
85 | });
86 |
87 | it("should show deleting branch... in status bar", async function () {
88 | spyOn(statusBar, "show").and.callThrough();
89 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
90 | expect(statusBar.show).toHaveBeenCalledWith("Deleting Branch...");
91 | });
92 |
93 | it("should not call git.checkoutBranch if root branch is not selected", async function () {
94 | spyOn(this.git, "checkoutBranch").and.callThrough();
95 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
96 | expect(this.git.checkoutBranch).not.toHaveBeenCalled();
97 | });
98 |
99 | it("should call git.checkoutBranch if root branch is selected", async function () {
100 | this.dialogReturn[0].selected = true;
101 | spyOn(this.git, "checkoutBranch").and.callThrough();
102 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
103 | expect(this.git.checkoutBranch).toHaveBeenCalledWith(this.gitRoot, "master");
104 | });
105 |
106 | it("should call git.deleteBranch with force", async function () {
107 | this.dialogReturn[2] = true;
108 | this.dialogReturn[3] = true;
109 | spyOn(this.git, "deleteBranch").and.callThrough();
110 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
111 | expect(this.git.deleteBranch).toHaveBeenCalledWith(this.gitRoot, this.dialogReturn[0].name, false, true);
112 | expect(this.git.deleteBranch).toHaveBeenCalledWith(this.gitRoot, this.dialogReturn[0].name, true, true);
113 | });
114 |
115 | it("should call git.deleteBranch on local", async function () {
116 | this.dialogReturn[1] = true;
117 | this.dialogReturn[2] = false;
118 | spyOn(this.git, "deleteBranch").and.callThrough();
119 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
120 | expect(this.git.deleteBranch).toHaveBeenCalledWith(this.gitRoot, this.dialogReturn[0].name, false, false);
121 | expect(this.git.deleteBranch).toHaveBeenCalledTimes(1);
122 | });
123 |
124 | it("should call git.deleteBranch on remote", async function () {
125 | this.dialogReturn[1] = false;
126 | this.dialogReturn[2] = true;
127 | spyOn(this.git, "deleteBranch").and.callThrough();
128 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
129 | expect(this.git.deleteBranch).toHaveBeenCalledWith(this.gitRoot, this.dialogReturn[0].name, true, false);
130 | expect(this.git.deleteBranch).toHaveBeenCalledTimes(1);
131 | });
132 |
133 | it("should call git.deleteBranch on local and remote", async function () {
134 | this.dialogReturn[1] = true;
135 | this.dialogReturn[2] = true;
136 | spyOn(this.git, "deleteBranch").and.callThrough();
137 | await deleteBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
138 | expect(this.git.deleteBranch).toHaveBeenCalledWith(this.gitRoot, this.dialogReturn[0].name, true, false);
139 | expect(this.git.deleteBranch).toHaveBeenCalledWith(this.gitRoot, this.dialogReturn[0].name, false, false);
140 | expect(this.git.deleteBranch).toHaveBeenCalledTimes(2);
141 | });
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/spec/commands/diff-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import diff from "../../lib/commands/diff";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("diff", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | this.git = mockGit({
15 | rootDir: Promise.resolve(this.gitRoot),
16 | diff: Promise.resolve("diff result"),
17 | });
18 | });
19 |
20 | afterEach(async function () {
21 | await removeGitRoot(this.gitRoot);
22 | });
23 |
24 | it("should show diffing... in status bar", async function () {
25 | spyOn(statusBar, "show").and.callThrough();
26 | await diff.command(this.filePaths, statusBar, this.git, Notifications);
27 | expect(statusBar.show).toHaveBeenCalledWith("Diffing...");
28 | });
29 |
30 | it("should call git.diff", async function () {
31 | spyOn(this.git, "diff").and.callThrough();
32 | await diff.command(this.filePaths, statusBar, this.git, Notifications);
33 | expect(this.git.diff).toHaveBeenCalledWith(this.gitRoot, this.filePaths);
34 | });
35 |
36 | it("should open textEditor with diff results", async function () {
37 | spyOn(atom.workspace, "open").and.callThrough();
38 | await diff.command(this.filePaths, statusBar, this.git, Notifications);
39 | expect(atom.workspace.open).toHaveBeenCalled();
40 | const text = atom.workspace.getActiveTextEditor().getText();
41 | expect(text).toBe("diff result");
42 | });
43 |
44 | it("should return 'Diffed.'", async function () {
45 | const ret = await diff.command(this.filePaths, statusBar, this.git, Notifications);
46 | expect(ret.message).toBe("Diff opened.");
47 | });
48 |
49 | });
50 |
--------------------------------------------------------------------------------
/spec/commands/discard-all-changes-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import discardAllChanges from "../../lib/commands/discard-all-changes";
4 | import discardChanges from "../../lib/commands/discard-changes";
5 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("discard-all-changes", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot);
18 | });
19 |
20 | it("should call discard changes with project folders", async function () {
21 | spyOn(discardChanges, "command").and.callFake(() => Promise.resolve());
22 | await discardAllChanges.command(this.filePaths);
23 | expect(discardChanges.command.calls.mostRecent().args[0]).toEqual(atom.project.getPaths());
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/spec/commands/fetch-all-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import fetchAll from "../../lib/commands/fetch-all";
4 | import fetch from "../../lib/commands/fetch";
5 | import {removeGitRoot, createGitRoot} from "../mocks";
6 |
7 | describe("fetch-all", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot1 = await createGitRoot();
12 | this.gitRoot2 = await createGitRoot();
13 | atom.project.setPaths([this.gitRoot1, this.gitRoot2]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot1);
18 | await removeGitRoot(this.gitRoot2);
19 | });
20 |
21 | it("should call fetch with project folders", async function () {
22 | spyOn(fetch, "command").and.callFake(() => Promise.resolve());
23 | await fetchAll.command();
24 | expect(fetch.command).toHaveBeenCalledTimes(2);
25 | expect(fetch.command.calls.argsFor(0)[0]).toEqual([this.gitRoot1]);
26 | expect(fetch.command.calls.argsFor(1)[0]).toEqual([this.gitRoot2]);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/spec/commands/fetch-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import fetch from "../../lib/commands/fetch";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("fetch", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | this.git = mockGit({
15 | rootDir: Promise.resolve(this.gitRoot),
16 | fetch: Promise.resolve("fetch result"),
17 | });
18 | });
19 |
20 | afterEach(async function () {
21 | await removeGitRoot(this.gitRoot);
22 | });
23 |
24 | it("should show fetching... in status bar", async function () {
25 | spyOn(statusBar, "show").and.callThrough();
26 | await fetch.command(this.filePaths, statusBar, this.git, Notifications);
27 | expect(statusBar.show).toHaveBeenCalledWith("Fetching...");
28 | });
29 |
30 | it("should call git.fetch", async function () {
31 | spyOn(this.git, "fetch").and.callThrough();
32 | await fetch.command(this.filePaths, statusBar, this.git, Notifications);
33 | expect(this.git.fetch).toHaveBeenCalledWith(this.gitRoot);
34 | });
35 |
36 | it("should show git notification for fetch results", async function () {
37 | spyOn(Notifications, "addGit").and.callThrough();
38 | await fetch.command(this.filePaths, statusBar, this.git, Notifications);
39 | expect(Notifications.addGit).toHaveBeenCalledWith("Fetch", "fetch result");
40 | });
41 |
42 | it("should return fetched.'", async function () {
43 | const ret = await fetch.command(this.filePaths, statusBar, this.git, Notifications);
44 | expect(ret.message).toBe("Fetched.");
45 | });
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/spec/commands/ignore-changes-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import ignoreChanges from "../../lib/commands/ignore-changes";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, removeGitRoot, createGitRoot, fileStatus, files} from "../mocks";
6 |
7 | describe("ignore-changes", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.statuses = [fileStatus(" M", files.t1), fileStatus("??", files.t2)];
14 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
15 | this.git = mockGit({
16 | rootDir: Promise.resolve(this.gitRoot),
17 | status: () => Promise.resolve(this.statuses),
18 | updateIndex: Promise.resolve("updateIndex result"),
19 | });
20 | });
21 |
22 | afterEach(async function () {
23 | await removeGitRoot(this.gitRoot);
24 | });
25 |
26 | it("should call git.updateIndex", async function () {
27 | spyOn(this.git, "updateIndex").and.callThrough();
28 | await ignoreChanges.command(this.filePaths, statusBar, this.git, Notifications);
29 | expect(this.git.updateIndex).toHaveBeenCalled();
30 | });
31 |
32 | });
33 |
--------------------------------------------------------------------------------
/spec/commands/log-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import log from "../../lib/commands/log";
4 | import {getFilePath, statusBar, mockGit, mockDialog, removeGitRoot, createGitRoot, files} from "../mocks";
5 |
6 | describe("log", function () {
7 |
8 | beforeEach(async function () {
9 | await atom.packages.activatePackage("git-menu");
10 | this.gitRoot = await createGitRoot();
11 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
12 | this.git = mockGit({
13 | rootDir: () => Promise.resolve(this.gitRoot),
14 | cmd: () => Promise.resolve("cmd result"),
15 | });
16 | this.dialog = mockDialog({
17 | activate: () => Promise.resolve(),
18 | });
19 | this.format = atom.config.get("git-menu.logFormat");
20 | });
21 |
22 | afterEach(async function () {
23 | await removeGitRoot(this.gitRoot);
24 | });
25 |
26 | describe("dialog", function () {
27 |
28 | it("should call dialog with correct props", async function () {
29 | spyOn(this, "dialog").and.callThrough();
30 | try {
31 | await log.command(this.filePaths, statusBar, this.git, null, this.dialog);
32 | } catch (ex) {
33 | // do nothing
34 | }
35 | expect(this.dialog).toHaveBeenCalledWith(jasmine.objectContaining({
36 | root: this.gitRoot,
37 | gitCmd: jasmine.any(Object),
38 | format: this.format,
39 | }));
40 | });
41 |
42 | it("should call dialog.activate()", async function () {
43 | spyOn(this.dialog.prototype, "activate").and.callThrough();
44 | await log.command(this.filePaths, statusBar, this.git, null, this.dialog);
45 | expect(this.dialog.prototype.activate).toHaveBeenCalled();
46 | });
47 | });
48 |
49 | describe("cancel", function () {
50 |
51 | it("should reject without an error", async function () {
52 | this.dialog = mockDialog({
53 | activate: () => Promise.reject(),
54 | });
55 | let error;
56 | try {
57 | await log.command(this.filePaths, statusBar, this.git, null, this.dialog);
58 | } catch (ex) {
59 | error = !ex;
60 | }
61 | expect(error).toBeTruthy();
62 | });
63 | });
64 |
65 | });
66 |
--------------------------------------------------------------------------------
/spec/commands/pull-all-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import pullAll from "../../lib/commands/pull-all";
4 | import pull from "../../lib/commands/pull";
5 | import {removeGitRoot, createGitRoot} from "../mocks";
6 |
7 | describe("pull-all", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot1 = await createGitRoot();
12 | this.gitRoot2 = await createGitRoot();
13 | atom.project.setPaths([this.gitRoot1, this.gitRoot2]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot1);
18 | await removeGitRoot(this.gitRoot2);
19 | });
20 |
21 | it("should call pull with project folders", async function () {
22 | spyOn(pull, "command").and.callFake(() => Promise.resolve());
23 | await pullAll.command();
24 | expect(pull.command).toHaveBeenCalledTimes(2);
25 | expect(pull.command.calls.argsFor(0)[0]).toEqual([this.gitRoot1]);
26 | expect(pull.command.calls.argsFor(1)[0]).toEqual([this.gitRoot2]);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/spec/commands/pull-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import pull from "../../lib/commands/pull";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("pull", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | this.git = mockGit({
15 | rootDir: Promise.resolve(this.gitRoot),
16 | pull: Promise.resolve("pull result"),
17 | });
18 | });
19 |
20 | afterEach(async function () {
21 | await removeGitRoot(this.gitRoot);
22 | });
23 |
24 | it("should show pulling... in status bar", async function () {
25 | spyOn(statusBar, "show").and.callThrough();
26 | await pull.command(this.filePaths, statusBar, this.git, Notifications);
27 | expect(statusBar.show).toHaveBeenCalledWith("Pulling...");
28 | });
29 |
30 | it("should call git.pull", async function () {
31 | spyOn(this.git, "pull").and.callThrough();
32 | await pull.command(this.filePaths, statusBar, this.git, Notifications);
33 | expect(this.git.pull).toHaveBeenCalledWith(this.gitRoot, false, false);
34 | });
35 |
36 | it("should call git.pull with rebase config", async function () {
37 | spyOn(this.git, "pull").and.callThrough();
38 | atom.config.set("git-menu.rebaseOnPull", true);
39 | await pull.command(this.filePaths, statusBar, this.git, Notifications);
40 | expect(this.git.pull).toHaveBeenCalledWith(this.gitRoot, true, false);
41 | });
42 |
43 | it("should show git notification for pull results", async function () {
44 | spyOn(Notifications, "addGit").and.callThrough();
45 | await pull.command(this.filePaths, statusBar, this.git, Notifications);
46 | expect(Notifications.addGit).toHaveBeenCalledWith("Pull", "pull result");
47 | });
48 |
49 | it("should return pulled.'", async function () {
50 | const ret = await pull.command(this.filePaths, statusBar, this.git, Notifications);
51 | expect(ret.message).toBe("Pulled.");
52 | });
53 |
54 | });
55 |
--------------------------------------------------------------------------------
/spec/commands/push-all-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import pushAll from "../../lib/commands/push-all";
4 | import push from "../../lib/commands/push";
5 | import {removeGitRoot, createGitRoot} from "../mocks";
6 |
7 | describe("push-all", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot1 = await createGitRoot();
12 | this.gitRoot2 = await createGitRoot();
13 | atom.project.setPaths([this.gitRoot1, this.gitRoot2]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot1);
18 | await removeGitRoot(this.gitRoot2);
19 | });
20 |
21 | it("should call push with project folders", async function () {
22 | spyOn(push, "command").and.callFake(() => Promise.resolve());
23 | await pushAll.command();
24 | expect(push.command).toHaveBeenCalledTimes(2);
25 | expect(push.command.calls.argsFor(0)[0]).toEqual([this.gitRoot1]);
26 | expect(push.command.calls.argsFor(1)[0]).toEqual([this.gitRoot2]);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/spec/commands/push-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import push from "../../lib/commands/push";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("push", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | this.git = mockGit({
15 | rootDir: Promise.resolve(this.gitRoot),
16 | push: Promise.resolve("push result"),
17 | });
18 | });
19 |
20 | afterEach(async function () {
21 | await removeGitRoot(this.gitRoot);
22 | });
23 |
24 | it("should show pushing... in status bar", async function () {
25 | spyOn(statusBar, "show").and.callThrough();
26 | await push.command(this.filePaths, statusBar, this.git, Notifications);
27 | expect(statusBar.show).toHaveBeenCalledWith("Pushing...");
28 | });
29 |
30 | it("should call git.push", async function () {
31 | spyOn(this.git, "push").and.callThrough();
32 | await push.command(this.filePaths, statusBar, this.git, Notifications);
33 | expect(this.git.push).toHaveBeenCalledWith(this.gitRoot, false);
34 | });
35 |
36 | it("should show git notification for push results", async function () {
37 | spyOn(Notifications, "addGit").and.callThrough();
38 | await push.command(this.filePaths, statusBar, this.git, Notifications);
39 | expect(Notifications.addGit).toHaveBeenCalledWith("Push", "push result");
40 | });
41 |
42 | it("should return pushed.'", async function () {
43 | const ret = await push.command(this.filePaths, statusBar, this.git, Notifications);
44 | expect(ret.message).toBe("Pushed.");
45 | });
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/spec/commands/run-command-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import runCommand from "../../lib/commands/run-command";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, mockDialog, removeGitRoot, createGitRoot, fileStatus, files} from "../mocks";
6 |
7 | describe("run-command", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.statuses = [fileStatus("M ", files.t1)];
14 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
15 | this.git = mockGit({
16 | rootDir: Promise.resolve(this.gitRoot),
17 | status: Promise.resolve(this.statuses),
18 | cmd: Promise.resolve("cmd result"),
19 | });
20 | this.dialogReturn = [
21 | "command",
22 | [files.t1],
23 | ];
24 | this.dialog = mockDialog({
25 | activate: Promise.resolve(this.dialogReturn),
26 | });
27 | });
28 |
29 | afterEach(async function () {
30 | await removeGitRoot(this.gitRoot);
31 | });
32 |
33 | describe("dialog", function () {
34 |
35 | it("should call dialog with correct props", async function () {
36 | spyOn(this, "dialog").and.callThrough();
37 | spyOn(atom.config, "get").and.returnValue(true);
38 | try {
39 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
40 | } catch (ex) {
41 | // do nothing
42 | }
43 | expect(this.dialog).toHaveBeenCalledWith({
44 | files: this.statuses,
45 | treeView: true,
46 | });
47 | });
48 |
49 | it("should call dialog.activate()", async function () {
50 | spyOn(this.dialog.prototype, "activate").and.callThrough();
51 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
52 | expect(this.dialog.prototype.activate).toHaveBeenCalled();
53 | });
54 | });
55 |
56 | describe("cancel", function () {
57 |
58 | it("should reject without an error", async function () {
59 | this.dialog = mockDialog({
60 | activate: () => Promise.reject(),
61 | });
62 | let error;
63 | try {
64 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
65 | } catch (ex) {
66 | error = !ex;
67 | }
68 | expect(error).toBeTruthy();
69 | });
70 | });
71 |
72 | describe("run", function () {
73 |
74 | it("should reject on empty message", async function () {
75 | this.dialogReturn[0] = "";
76 | this.dialog = mockDialog({
77 | activate: Promise.resolve(this.dialogReturn),
78 | });
79 | let error;
80 | try {
81 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
82 | } catch (ex) {
83 | error = ex;
84 | }
85 | expect(error).toBe("Command cannot be blank.");
86 | });
87 |
88 | it("should show running... in status bar", async function () {
89 | spyOn(statusBar, "show").and.callThrough();
90 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
91 | expect(statusBar.show).toHaveBeenCalledWith("Running...");
92 | });
93 |
94 | it("should call git.cmd", async function () {
95 | this.dialogReturn[0] = " git command arg1 --arg2=\"test string\" ";
96 | spyOn(this.git, "cmd").and.callThrough();
97 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
98 | expect(this.git.cmd).toHaveBeenCalledWith(this.gitRoot, ["command", "arg1", "--arg2=\"test string\""]);
99 | });
100 |
101 | it("should call git.cmd", async function () {
102 | spyOn(this.git, "cmd").and.callThrough();
103 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
104 | expect(this.git.cmd).toHaveBeenCalledWith(this.gitRoot, [this.dialogReturn[0]]);
105 | });
106 |
107 | it("should call git.cmd with files", async function () {
108 | this.dialogReturn[0] = "command arg1 %files%";
109 | spyOn(this.git, "cmd").and.callThrough();
110 | await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
111 | expect(this.git.cmd).toHaveBeenCalledWith(this.gitRoot, ["command", "arg1", getFilePath(this.gitRoot, this.dialogReturn[1][0])]);
112 | });
113 |
114 | it("should return 'Ran {command}'", async function () {
115 | const ret = await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
116 | expect(ret.message).toBe("Ran 'git command'");
117 | });
118 |
119 | it("should return 'with 1 file.'", async function () {
120 | this.dialogReturn[0] = "command %files%";
121 | const ret = await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
122 | expect(ret.message).toBe("Ran 'git command %files%' with 1 file.");
123 | });
124 |
125 | it("should return 'with 2 files.'", async function () {
126 | this.dialogReturn = [
127 | "command %files%",
128 | [files.t1, files.t2],
129 | ];
130 | this.dialog = mockDialog({
131 | activate: Promise.resolve(this.dialogReturn),
132 | });
133 | const ret = await runCommand.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
134 | expect(ret.message).toBe("Ran 'git command %files%' with 2 files.");
135 | });
136 | });
137 |
138 | });
139 |
--------------------------------------------------------------------------------
/spec/commands/stage-changes-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import stageChanges from "../../lib/commands/stage-changes";
4 | import Notifications from "../../lib/Notifications";
5 | import {getFilePath, statusBar, mockGit, removeGitRoot, createGitRoot, fileStatus, files} from "../mocks";
6 |
7 | describe("stage-changes", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.statuses = [fileStatus("M ", files.t1), fileStatus("??", files.t2)];
14 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
15 | this.git = mockGit({
16 | rootDir: Promise.resolve(this.gitRoot),
17 | status: () => Promise.resolve(this.statuses),
18 | add: Promise.resolve("add result"),
19 | });
20 | });
21 |
22 | afterEach(async function () {
23 | await removeGitRoot(this.gitRoot);
24 | });
25 |
26 | it("should call git.add", async function () {
27 | spyOn(this.git, "add").and.callThrough();
28 | await stageChanges.command(this.filePaths, statusBar, this.git, Notifications);
29 | expect(this.git.add).toHaveBeenCalled();
30 | });
31 |
32 | describe("no staged changes", function () {
33 |
34 | beforeEach(function () {
35 | this.git = mockGit({
36 | status: () => {
37 | return Promise.resolve([fileStatus(" M", files.t1)]);
38 | },
39 | });
40 | });
41 |
42 | it("should throw an error", async function () {
43 | spyOn(this.git, "add").and.callThrough();
44 | let error = false;
45 | try {
46 | await stageChanges.command(this.filePaths, statusBar, this.git, Notifications);
47 | } catch (ex) {
48 | error = ex;
49 | }
50 | expect(error).toBeTruthy();
51 | expect(this.git.add).not.toHaveBeenCalled();
52 | });
53 |
54 | });
55 |
56 | });
57 |
--------------------------------------------------------------------------------
/spec/commands/switch-branch-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import {Directory} from "atom";
4 | import switchBranch from "../../lib/commands/switch-branch";
5 | import Notifications from "../../lib/Notifications";
6 | import {getFilePath, statusBar, mockGit, mockDialog, removeGitRoot, createGitRoot, fileStatus, files} from "../mocks";
7 |
8 | describe("switch-branch", function () {
9 |
10 | beforeEach(async function () {
11 | await atom.packages.activatePackage("git-menu");
12 | this.gitRoot = await createGitRoot();
13 |
14 | this.repo = await atom.project.repositoryForDirectory(new Directory(this.gitRoot));
15 |
16 | this.statuses = [fileStatus("M ", files.t1), fileStatus("M ", files.t2)];
17 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
18 | this.git = mockGit({
19 | rootDir: Promise.resolve(this.gitRoot),
20 | branches: Promise.resolve("branches result"),
21 | checkoutBranch: Promise.resolve("checkoutBranch result"),
22 | createBranch: Promise.resolve("createBranch result"),
23 | });
24 | this.dialogReturn = [
25 | "repo",
26 | "remote",
27 | ];
28 | this.dialog = mockDialog({
29 | activate: () => Promise.resolve(this.dialogReturn),
30 | });
31 | });
32 |
33 | afterEach(async function () {
34 | await removeGitRoot(this.gitRoot);
35 | });
36 |
37 | describe("dialog", function () {
38 |
39 | it("should call dialog with correct props", async function () {
40 | spyOn(this, "dialog").and.callThrough();
41 | try {
42 | await switchBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
43 | } catch (ex) {
44 | // do nothing
45 | }
46 | expect(this.dialog).toHaveBeenCalledWith({
47 | branches: "branches result",
48 | root: this.gitRoot,
49 | });
50 | });
51 |
52 | it("should call dialog.activate()", async function () {
53 | spyOn(this.dialog.prototype, "activate").and.callThrough();
54 | await switchBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
55 | expect(this.dialog.prototype.activate).toHaveBeenCalled();
56 | });
57 | });
58 |
59 | describe("cancel", function () {
60 |
61 | it("should reject without an error", async function () {
62 | this.dialog = mockDialog({
63 | activate: () => Promise.reject(),
64 | });
65 | let error;
66 | try {
67 | await switchBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
68 | } catch (ex) {
69 | error = !ex;
70 | }
71 | expect(error).toBeTruthy();
72 | });
73 | });
74 |
75 | describe("switch", function () {
76 |
77 | it("should show switching branch... in status bar", async function () {
78 | spyOn(statusBar, "show").and.callThrough();
79 | await switchBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
80 | expect(statusBar.show).toHaveBeenCalledWith("Switching Branch...");
81 | });
82 |
83 | it("should call git.createBranch if remote is not null", async function () {
84 | spyOn(this.git, "checkoutBranch").and.callThrough();
85 | spyOn(this.git, "createBranch").and.callThrough();
86 | await switchBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
87 | expect(this.git.checkoutBranch).not.toHaveBeenCalled();
88 | expect(this.git.createBranch).toHaveBeenCalled();
89 | });
90 |
91 | it("should call git.checkoutBranch if remote is null", async function () {
92 | this.dialogReturn[1] = null;
93 | spyOn(this.git, "checkoutBranch").and.callThrough();
94 | spyOn(this.git, "createBranch").and.callThrough();
95 | await switchBranch.command(this.filePaths, statusBar, this.git, Notifications, this.dialog);
96 | expect(this.git.checkoutBranch).toHaveBeenCalled();
97 | expect(this.git.createBranch).not.toHaveBeenCalled();
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/spec/commands/sync-all-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import syncAll from "../../lib/commands/sync-all";
4 | import sync from "../../lib/commands/sync";
5 | import {removeGitRoot, createGitRoot} from "../mocks";
6 |
7 | describe("sync-all", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot1 = await createGitRoot();
12 | this.gitRoot2 = await createGitRoot();
13 | atom.project.setPaths([this.gitRoot1, this.gitRoot2]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot1);
18 | await removeGitRoot(this.gitRoot2);
19 | });
20 |
21 | it("should call sync with project folders", async function () {
22 | spyOn(sync, "command").and.callFake(() => Promise.resolve());
23 | await syncAll.command();
24 | expect(sync.command).toHaveBeenCalledTimes(2);
25 | expect(sync.command.calls.argsFor(0)[0]).toEqual([this.gitRoot1]);
26 | expect(sync.command.calls.argsFor(1)[0]).toEqual([this.gitRoot2]);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/spec/commands/sync-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import sync from "../../lib/commands/sync";
4 | import pull from "../../lib/commands/pull";
5 | import push from "../../lib/commands/push";
6 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
7 |
8 | describe("sync", function () {
9 |
10 | beforeEach(async function () {
11 | await atom.packages.activatePackage("git-menu");
12 | this.gitRoot = await createGitRoot();
13 |
14 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
15 | });
16 |
17 | afterEach(async function () {
18 | await removeGitRoot(this.gitRoot);
19 | });
20 |
21 | it("should call pull and push", async function () {
22 | spyOn(pull, "command").and.callFake(() => Promise.resolve());
23 | spyOn(push, "command").and.callFake(() => Promise.resolve());
24 | await sync.command(this.filePaths);
25 | expect(pull.command.calls.mostRecent().args[0]).toEqual(this.filePaths);
26 | expect(push.command.calls.mostRecent().args[0]).toEqual(this.filePaths);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/spec/commands/unignore-changes-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import unignoreChanges from "../../lib/commands/unignore-changes";
4 | import ignoreChanges from "../../lib/commands/ignore-changes";
5 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("unignore-changes", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | this.filePaths = getFilePath(this.gitRoot, [files.t1]);
14 | });
15 |
16 | afterEach(async function () {
17 | await removeGitRoot(this.gitRoot);
18 | });
19 |
20 | it("should call ignore changes with ignore = false", async function () {
21 | spyOn(ignoreChanges, "command").and.callFake(() => Promise.resolve());
22 | await unignoreChanges.command(this.filePaths);
23 | expect(ignoreChanges.command.calls.mostRecent().args[4]).toBe(false);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/spec/dialogs/commit-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import CommitDialog from "../../lib/dialogs/CommitDialog";
4 | import {fileStatus, files} from "../mocks";
5 |
6 | describe("CommitDialog", function () {
7 |
8 | describe("selected files", function () {
9 |
10 | beforeEach(function () {
11 | this.files = [fileStatus("M ", files.t1)];
12 | });
13 |
14 | it("initial state includes files", function () {
15 | const dialog = new CommitDialog({files: this.files});
16 | expect(dialog.state.files.length).toBe(this.files.length);
17 | });
18 |
19 | it("should select all files by default", function () {
20 | const dialog = new CommitDialog({files: this.files});
21 | const selectedFiles = dialog.refs.fileTree.getSelectedFiles();
22 | expect(selectedFiles.length).toBe(this.files.length);
23 | });
24 |
25 | it("should not select files if filesSelectable is false", function () {
26 | const dialog = new CommitDialog({files: this.files, filesSelectable: false});
27 | const selectedFiles = dialog.state.files.filter(f => f.selected);
28 | expect(selectedFiles.length).toBe(0);
29 | });
30 |
31 | });
32 |
33 | describe("amend", function () {
34 |
35 | beforeEach(function () {
36 | this.message = "commit message";
37 | this.lastCommit = "last commit message";
38 | this.dialog = new CommitDialog({lastCommit: this.lastCommit});
39 | });
40 |
41 | it("should set the message as the last commit message if blank", function () {
42 | this.dialog.amendChange({target: {checked: true}});
43 |
44 | expect(this.dialog.state.message).toBe(this.lastCommit);
45 | });
46 |
47 | it("should not change if message is not blank", function () {
48 | this.dialog.state.message = this.message;
49 | this.dialog.amendChange({target: {checked: true}});
50 |
51 | expect(this.dialog.state.message).toBe(this.message);
52 | });
53 |
54 | it("should set the message empty if message is last commit", function () {
55 | this.dialog.state.message = this.lastCommit;
56 | this.dialog.amendChange({target: {checked: false}});
57 |
58 | expect(this.dialog.state.message).toBe("");
59 | });
60 |
61 | it("should not change if message is not last commit", function () {
62 | this.dialog.state.message = this.message;
63 | this.dialog.amendChange({target: {checked: false}});
64 |
65 | expect(this.dialog.state.message).toBe(this.message);
66 | });
67 |
68 | });
69 |
70 | describe("accept", function () {
71 |
72 | beforeEach(function () {
73 | this.message = "commit message";
74 | this.dialog = new CommitDialog();
75 | this.promise = this.dialog.activate();
76 | this.dialog.state.message = this.message;
77 | });
78 |
79 | it("should return the commit message", async function () {
80 | this.dialog.accept();
81 | const ret = await this.promise;
82 | expect(ret).toEqual([this.message, false, false, false, []]);
83 | });
84 |
85 | it("should return amend when checked", async function () {
86 | this.dialog.amendChange({target: {checked: true}});
87 | this.dialog.accept();
88 | const ret = await this.promise;
89 | expect(ret).toEqual([this.message, true, false, false, []]);
90 | });
91 |
92 | it("should return push when push is clicked", async function () {
93 | this.dialog.pushClick();
94 | const ret = await this.promise;
95 | expect(ret).toEqual([this.message, false, true, false, []]);
96 | });
97 |
98 | it("should return push and sync when sync is clicked", async function () {
99 | this.dialog.syncClick();
100 | const ret = await this.promise;
101 | expect(ret).toEqual([this.message, false, true, true, []]);
102 | });
103 |
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/spec/dialogs/create-branch-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import CreateBranchDialog from "../../lib/dialogs/CreateBranchDialog";
4 | import {mockGit} from "../mocks";
5 |
6 | describe("CreateBranchDialog", function () {
7 |
8 | beforeEach(function () {
9 | this.branches = [
10 | {name: "local", branch: "first", local: true},
11 | {name: "remote", branch: "second", remote: true},
12 | {name: "selected", branch: "third", selected: true, local: true, remote: true},
13 | ];
14 |
15 | this.root = "root";
16 | });
17 |
18 | describe("selected branch", function () {
19 |
20 | it("should select selected branch", function () {
21 | const dialog = new CreateBranchDialog({branches: this.branches, root: this.root});
22 | expect(dialog.state.sourceBranch).toBe("selected");
23 | });
24 |
25 | });
26 |
27 | describe("fetch", function () {
28 |
29 | it("should set state.fetching to true", async function () {
30 | const git = mockGit({
31 | branches: () => this.branches,
32 | });
33 | const dialog = new CreateBranchDialog({branches: this.branches, root: this.root, git});
34 | spyOn(dialog, "update");
35 | await dialog.fetch();
36 | expect(dialog.update).toHaveBeenCalledWith({fetching: true});
37 | });
38 |
39 | it("should change branches", async function () {
40 | const git = mockGit({
41 | branches: () => this.branches,
42 | });
43 |
44 | const branches = [
45 | {name: "name", branch: "branch", selected: true},
46 | ];
47 |
48 | const dialog = new CreateBranchDialog({branches, root: this.root, git});
49 |
50 | await dialog.fetch();
51 | expect(dialog.state.branches).toBe(this.branches);
52 | });
53 |
54 | it("should show error", async function () {
55 | const git = mockGit({
56 | fetch: () => Promise.reject("fetch error"),
57 | branches: () => this.branches,
58 | });
59 |
60 | const notifications = jasmine.createSpyObj(["addError"]);
61 |
62 | const dialog = new CreateBranchDialog({branches: this.branches, root: this.root, git, notifications});
63 |
64 | await dialog.fetch();
65 | expect(notifications.addError).toHaveBeenCalledWith("Fetch", "fetch error");
66 | });
67 |
68 | });
69 |
70 | describe("accept", function () {
71 |
72 | beforeEach(function () {
73 | this.dialog = new CreateBranchDialog({branches: this.branches, root: this.root});
74 | this.activate = this.dialog.activate();
75 | });
76 |
77 | it("should return the branch name", async function () {
78 | this.dialog.newBranchChange({target: {value: "test"}});
79 | this.dialog.accept();
80 | const ret = await this.activate;
81 | expect(ret).toEqual(["selected", "test", false]);
82 | });
83 |
84 | it("should remove illegal characters", async function () {
85 | this.dialog.newBranchChange({target: {value: "no space"}});
86 | this.dialog.accept();
87 | const ret = await this.activate;
88 | expect(ret).toEqual(["selected", "no-space", false]);
89 | });
90 |
91 | it("should return source branch", async function () {
92 | this.dialog.state.newBranch = "test";
93 | this.dialog.sourceBranchChange({target: {value: "source"}});
94 | this.dialog.accept();
95 | const ret = await this.activate;
96 | expect(ret).toEqual(["source", "test", false]);
97 | });
98 |
99 | it("should return track when checked", async function () {
100 | this.dialog.state.newBranch = "test";
101 | this.dialog.trackChange({target: {checked: true}});
102 | this.dialog.accept();
103 | const ret = await this.activate;
104 | expect(ret).toEqual(["selected", "test", true]);
105 | });
106 |
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/spec/dialogs/delete-branch-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import DeleteBranchDialog from "../../lib/dialogs/DeleteBranchDialog";
4 | import {mockGit} from "../mocks";
5 |
6 | describe("DeleteBranchDialog", function () {
7 |
8 | beforeEach(function () {
9 | this.branches = [
10 | {name: "local", branch: "first", local: true},
11 | {name: "remote", branch: "second", remote: true},
12 | {name: "selected", branch: "third", selected: true, local: true, remote: true},
13 | ];
14 |
15 | this.root = "root";
16 |
17 | spyOn(atom, "confirm").and.callFake((opts, callback) => {
18 | callback([0, false]);
19 | });
20 | });
21 |
22 | describe("selected branch", function () {
23 |
24 | it("should select selected branch", function () {
25 | const dialog = new DeleteBranchDialog({branches: this.branches, root: this.root});
26 | expect(dialog.state.branch).toBe("selected");
27 | });
28 |
29 | });
30 |
31 | describe("fetch", function () {
32 |
33 | it("should set state.fetching to true", async function () {
34 | const git = mockGit({
35 | branches: () => this.branches,
36 | });
37 | const dialog = new DeleteBranchDialog({branches: this.branches, root: this.root, git});
38 | spyOn(dialog, "update");
39 | await dialog.fetch();
40 | expect(dialog.update).toHaveBeenCalledWith({fetching: true});
41 | });
42 |
43 | it("should change branches", async function () {
44 | const git = mockGit({
45 | branches: () => this.branches,
46 | });
47 |
48 | const branches = [
49 | {name: "name", branch: "branch", selected: true},
50 | ];
51 |
52 | const dialog = new DeleteBranchDialog({branches, root: this.root, git});
53 |
54 | await dialog.fetch();
55 | expect(dialog.state.branches).toBe(this.branches);
56 | });
57 |
58 | it("should show error", async function () {
59 | const git = mockGit({
60 | fetch: () => Promise.reject("fetch error"),
61 | branches: () => this.branches,
62 | });
63 |
64 | const notifications = jasmine.createSpyObj(["addError"]);
65 |
66 | const dialog = new DeleteBranchDialog({branches: this.branches, root: this.root, git, notifications});
67 |
68 | await dialog.fetch();
69 | expect(notifications.addError).toHaveBeenCalledWith("Fetch", "fetch error");
70 | });
71 |
72 | });
73 |
74 | describe("accept", function () {
75 |
76 | beforeEach(function () {
77 | this.dialog = new DeleteBranchDialog({branches: this.branches, root: this.root});
78 | this.activate = this.dialog.activate();
79 | });
80 |
81 | it("should return the branch object", async function () {
82 | this.dialog.branchChange({target: {value: "local"}});
83 | this.dialog.accept();
84 | const ret = await this.activate;
85 | expect(ret[0]).toBe(this.branches[0]);
86 | });
87 |
88 | it("should return local", async function () {
89 | this.dialog.localChange({target: {checked: false}});
90 | this.dialog.accept();
91 | const ret = await this.activate;
92 | expect(ret[1]).toBe(false);
93 | });
94 |
95 | it("should return false if branch is not local", async function () {
96 | this.dialog.state.local = true;
97 | this.dialog.state.branch = "remote";
98 | this.dialog.accept();
99 | const ret = await this.activate;
100 | expect(ret[1]).toBe(false);
101 | });
102 |
103 | it("should return remote", async function () {
104 | this.dialog.remoteChange({target: {checked: true}});
105 | this.dialog.accept();
106 | const ret = await this.activate;
107 | expect(ret[2]).toBe(true);
108 | });
109 |
110 | it("should return false if branch is not local", async function () {
111 | this.dialog.state.remote = true;
112 | this.dialog.state.branch = "local";
113 | this.dialog.accept();
114 | const ret = await this.activate;
115 | expect(ret[2]).toBe(false);
116 | });
117 |
118 | it("should return the force", async function () {
119 | this.dialog.forceChange({target: {checked: true}});
120 | this.dialog.accept();
121 | const ret = await this.activate;
122 | expect(ret[3]).toBe(true);
123 | });
124 |
125 | describe("confirm", function () {
126 |
127 | it("should confirm on local only available", async function () {
128 | this.dialog.state.local = true;
129 | this.dialog.state.remote = false;
130 | this.dialog.state.force = true;
131 | this.dialog.state.branch = "local";
132 | this.dialog.accept();
133 | await this.activate;
134 | expect(atom.confirm).toHaveBeenCalled();
135 | });
136 |
137 | it("should confirm on remote only available", async function () {
138 | this.dialog.state.local = false;
139 | this.dialog.state.remote = true;
140 | this.dialog.state.force = true;
141 | this.dialog.state.branch = "remote";
142 | this.dialog.accept();
143 | await this.activate;
144 | expect(atom.confirm).toHaveBeenCalled();
145 | });
146 |
147 | it("should confirm on local and remote", async function () {
148 | this.dialog.state.local = true;
149 | this.dialog.state.remote = true;
150 | this.dialog.state.force = true;
151 | this.dialog.accept();
152 | await this.activate;
153 | expect(atom.confirm).toHaveBeenCalled();
154 | });
155 |
156 | it("should not confirm on local only", async function () {
157 | this.dialog.state.local = true;
158 | this.dialog.state.remote = false;
159 | this.dialog.state.force = true;
160 | this.dialog.accept();
161 | await this.activate;
162 | expect(atom.confirm).not.toHaveBeenCalled();
163 | });
164 |
165 | it("should not confirm on remote only", async function () {
166 | this.dialog.state.local = false;
167 | this.dialog.state.remote = true;
168 | this.dialog.state.force = true;
169 | this.dialog.accept();
170 | await this.activate;
171 | expect(atom.confirm).not.toHaveBeenCalled();
172 | });
173 |
174 | it("should not confirm on both false", async function () {
175 | this.dialog.state.local = false;
176 | this.dialog.state.remote = false;
177 | this.dialog.state.force = true;
178 | this.dialog.accept();
179 | await this.activate;
180 | expect(atom.confirm).not.toHaveBeenCalled();
181 | });
182 |
183 | });
184 |
185 | });
186 | });
187 |
--------------------------------------------------------------------------------
/spec/dialogs/dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /** @jsx etch.dom */
4 |
5 | import Dialog from "../../lib/dialogs/Dialog";
6 | import etch from "etch";
7 |
8 | describe("Dialog", function () {
9 | beforeEach(function () {
10 | class TestDialog extends Dialog {
11 | title() {
12 | return "test-title";
13 | }
14 |
15 | body() {
16 | return (
17 |
18 | );
19 | }
20 |
21 | buttons() {
22 | return (
23 |
24 | );
25 | }
26 |
27 | validate() {
28 | return [];
29 | }
30 | }
31 | this.TestDialog = TestDialog;
32 | });
33 |
34 | it("should call this.initialState on constructor", function () {
35 | const props = {
36 | test: 1,
37 | };
38 | spyOn(this.TestDialog.prototype, "initialState");
39 | new this.TestDialog(props);
40 | expect(this.TestDialog.prototype.initialState).toHaveBeenCalledWith(props);
41 | });
42 |
43 | it("should return a promise on activate", function () {
44 | const promise = new this.TestDialog().activate();
45 | expect(promise instanceof Promise).toBeTruthy();
46 | });
47 |
48 | it("should add a model panel on activate", function () {
49 | spyOn(atom.workspace, "addModalPanel");
50 | const dialog = new this.TestDialog();
51 | dialog.activate();
52 | expect(atom.workspace.addModalPanel).toHaveBeenCalledWith({item: dialog});
53 | });
54 |
55 | it("should call this.show on activate", function () {
56 | spyOn(this.TestDialog.prototype, "show");
57 | new this.TestDialog().activate();
58 | expect(this.TestDialog.prototype.show).toHaveBeenCalled();
59 | });
60 |
61 | it("should cancel on [esc]", function () {
62 | spyOn(this.TestDialog.prototype, "cancel");
63 | const dialog = new this.TestDialog();
64 | dialog.activate();
65 | dialog.element.dispatchEvent(new KeyboardEvent("keyup", {key: "Escape"}));
66 | expect(this.TestDialog.prototype.cancel).toHaveBeenCalled();
67 | });
68 |
69 | it("should reject on cancel", async function () {
70 | let error;
71 | const dialog = new this.TestDialog();
72 | const promise = dialog.activate();
73 | dialog.cancel();
74 | try {
75 | await promise;
76 | } catch (ex) {
77 | error = !ex;
78 | }
79 | expect(error).toBeTruthy();
80 | });
81 |
82 | it("should call this.hide on cancel", function () {
83 | spyOn(this.TestDialog.prototype, "hide");
84 | const dialog = new this.TestDialog();
85 | dialog.activate().catch(() => {});
86 | dialog.cancel();
87 | expect(this.TestDialog.prototype.hide).toHaveBeenCalled();
88 | });
89 |
90 | it("should destroy the modal panel on cancel", function () {
91 | const dialog = new this.TestDialog();
92 | dialog.activate().catch(() => {});
93 | spyOn(dialog.modalPanel, "destroy");
94 | dialog.cancel();
95 | expect(dialog.modalPanel.destroy).toHaveBeenCalled();
96 | });
97 |
98 | it("should call this.validate on accept", async function () {
99 | spyOn(this.TestDialog.prototype, "validate");
100 | const dialog = new this.TestDialog();
101 | dialog.activate();
102 | await dialog.accept();
103 | expect(this.TestDialog.prototype.validate).toHaveBeenCalled();
104 | });
105 |
106 | it("should return without resolving on this.validate returning non-array on accept", async function () {
107 | this.TestDialog.prototype.validate = (() => false);
108 | const dialog = new this.TestDialog();
109 | const promise = dialog.activate();
110 | await dialog.accept();
111 | dialog.cancel();
112 | let error;
113 | try {
114 | await promise;
115 | } catch (ex) {
116 | error = true;
117 | }
118 | expect(error).toBeTruthy();
119 | });
120 |
121 | it("should resolve to an array on accept", async function () {
122 | const dialog = new this.TestDialog();
123 | const promise = dialog.activate();
124 | await dialog.accept();
125 | dialog.cancel();
126 | let error;
127 | try {
128 | await promise;
129 | } catch (ex) {
130 | error = true;
131 | }
132 | expect(error).toBeFalsy();
133 | });
134 |
135 | it("should call this.hide on accept", async function () {
136 | spyOn(this.TestDialog.prototype, "hide");
137 | const dialog = new this.TestDialog();
138 | dialog.activate();
139 | await dialog.accept();
140 | expect(this.TestDialog.prototype.hide).toHaveBeenCalled();
141 | });
142 |
143 | it("should destroy the modal panel on accept", async function () {
144 | const dialog = new this.TestDialog();
145 | dialog.activate();
146 | spyOn(dialog.modalPanel, "destroy");
147 | await dialog.accept();
148 | expect(dialog.modalPanel.destroy).toHaveBeenCalled();
149 | });
150 |
151 | it("should set the title to this.title()", function () {
152 | const dialog = new this.TestDialog();
153 | dialog.activate();
154 | const title = dialog.element.querySelector(".title").textContent;
155 | expect(title).toBe("test-title");
156 | });
157 |
158 | it("should set the body to this.body()", function () {
159 | const dialog = new this.TestDialog();
160 | dialog.activate();
161 | const body = dialog.element.querySelector("#test-body");
162 | expect(body).not.toBeNull();
163 | });
164 |
165 | it("should set the buttons to this.buttons()", function () {
166 | const dialog = new this.TestDialog();
167 | dialog.activate();
168 | const buttons = dialog.element.querySelector("#test-buttons");
169 | expect(buttons).not.toBeNull();
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/spec/dialogs/log-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import LogDialog from "../../lib/dialogs/LogDialog";
4 | import {mockGit, createGitRoot} from "../mocks";
5 |
6 | describe("LogDialog", function () {
7 | beforeEach(async function () {
8 | await atom.packages.activatePackage("git-menu");
9 | this.gitRoot = await createGitRoot();
10 | this.git = mockGit({
11 | log: () => Promise.resolve(""),
12 | });
13 | });
14 |
15 | describe("unescaping format", function () {
16 | beforeEach(function () {
17 | spyOn(this.git, "log").and.callThrough();
18 | });
19 |
20 | it("should change \\\\ to \\", function () {
21 | const dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\\\"});
22 | dialog.getLogs();
23 |
24 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "\\");
25 | });
26 |
27 | it("should change \\\\n to %n", function () {
28 | const dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\n"});
29 | dialog.getLogs();
30 |
31 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "%n");
32 | });
33 |
34 | it("should change \\\\t to \\t", function () {
35 | const dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\t"});
36 | dialog.getLogs();
37 |
38 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "\t");
39 | });
40 |
41 | it("should not unescape a slash at the end", function () {
42 | let dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\n\\t\\"});
43 | dialog.getLogs();
44 |
45 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "%n\t\\");
46 |
47 | this.git.log.calls.reset();
48 |
49 | dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\n\\t\\\\"});
50 | dialog.getLogs();
51 |
52 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "%n\t\\");
53 | });
54 |
55 | it("should not unescape an odd number of slashes at the end", function () {
56 | let dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\n\\t\\\\\\"});
57 | dialog.getLogs();
58 |
59 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "%n\t\\\\");
60 |
61 | this.git.log.calls.reset();
62 |
63 | dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: "\\n\\t\\\\\\\\"});
64 | dialog.getLogs();
65 |
66 | expect(this.git.log).toHaveBeenCalledWith(this.gitRoot, 10, 0, "%n\t\\\\");
67 | });
68 |
69 | });
70 |
71 | it("should call getLogs when scrolled to the bottom", function () {
72 | const dialog = new LogDialog({root: this.gitRoot, gitCmd: this.git, format: ""});
73 | spyOn(dialog, "getLogs");
74 |
75 | expect(dialog.getLogs).not.toHaveBeenCalled();
76 |
77 | dialog.scroll({
78 | target: {
79 | scrollHeight: 1000,
80 | scrollTop: 0,
81 | clientHeight: 100,
82 | },
83 | });
84 |
85 | expect(dialog.getLogs).not.toHaveBeenCalled();
86 |
87 | dialog.scroll({
88 | target: {
89 | scrollHeight: 1000,
90 | scrollTop: 900,
91 | clientHeight: 100,
92 | },
93 | });
94 |
95 | expect(dialog.getLogs).toHaveBeenCalled();
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/spec/dialogs/merge-branch-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import MergeBranchDialog from "../../lib/dialogs/MergeBranchDialog";
4 | import {mockGit} from "../mocks";
5 |
6 | describe("MergeBranchDialog", function () {
7 |
8 | beforeEach(function () {
9 | this.branches = [
10 | {name: "local", branch: "first", local: true},
11 | {name: "selected", branch: "third", selected: true, local: true, remote: true},
12 | ];
13 |
14 | this.root = "root";
15 |
16 | spyOn(atom, "confirm").and.callFake((opts, callback) => {
17 | callback([0, false]);
18 | });
19 | });
20 |
21 | describe("selected branch", function () {
22 |
23 | it("should select selected branch", function () {
24 | const dialog = new MergeBranchDialog({branches: this.branches, root: this.root});
25 | expect(dialog.state.rootBranch).toBe("selected");
26 | });
27 |
28 | });
29 |
30 | describe("fetch", function () {
31 |
32 | it("should set state.fetching to true", async function () {
33 | const git = mockGit({
34 | branches: () => this.branches,
35 | });
36 | const dialog = new MergeBranchDialog({branches: this.branches, root: this.root, git});
37 | spyOn(dialog, "update");
38 | await dialog.fetch();
39 | expect(dialog.update).toHaveBeenCalledWith({fetching: true});
40 | });
41 |
42 | it("should change branches", async function () {
43 | const git = mockGit({
44 | branches: () => this.branches,
45 | });
46 |
47 | const branches = [
48 | {name: "name", branch: "branch", selected: true},
49 | ];
50 |
51 | const dialog = new MergeBranchDialog({branches, root: this.root, git});
52 |
53 | await dialog.fetch();
54 | expect(dialog.state.branches).toBe(this.branches);
55 | });
56 |
57 | it("should show error", async function () {
58 | const git = mockGit({
59 | fetch: () => Promise.reject("fetch error"),
60 | branches: () => this.branches,
61 | });
62 |
63 | const notifications = jasmine.createSpyObj(["addError"]);
64 |
65 | const dialog = new MergeBranchDialog({branches: this.branches, root: this.root, git, notifications});
66 |
67 | await dialog.fetch();
68 | expect(notifications.addError).toHaveBeenCalledWith("Fetch", "fetch error");
69 | });
70 |
71 | });
72 |
73 | describe("accept", function () {
74 |
75 | beforeEach(function () {
76 | this.dialog = new MergeBranchDialog({branches: this.branches, root: this.root});
77 | this.activate = this.dialog.activate();
78 | });
79 |
80 | it("should return the root branch object", async function () {
81 | this.dialog.rootBranchChange({target: {value: "local"}});
82 | this.dialog.mergeBranchChange({target: {value: "selected"}});
83 | this.dialog.accept();
84 | const ret = await this.activate;
85 | expect(ret[0]).toBe(this.branches[0]);
86 | });
87 |
88 | it("should return the merge branch object", async function () {
89 | this.dialog.mergeBranchChange({target: {value: "local"}});
90 | this.dialog.accept();
91 | const ret = await this.activate;
92 | expect(ret[1]).toBe(this.branches[0]);
93 | });
94 |
95 | it("should return rebase", async function () {
96 | this.dialog.mergeBranchChange({target: {value: "local"}});
97 | this.dialog.rebaseChange({target: {checked: true}});
98 | this.dialog.accept();
99 | const ret = await this.activate;
100 | expect(ret[2]).toBe(true);
101 | });
102 |
103 | it("should return delete", async function () {
104 | this.dialog.mergeBranchChange({target: {value: "local"}});
105 | this.dialog.deleteChange({target: {checked: true}});
106 | this.dialog.accept();
107 | const ret = await this.activate;
108 | expect(ret[3]).toBe(true);
109 | });
110 |
111 | it("should return abort", async function () {
112 | this.dialog.mergeBranchChange({target: {value: "local"}});
113 | this.dialog.abortChange({target: {checked: false}});
114 | this.dialog.accept();
115 | const ret = await this.activate;
116 | expect(ret[4]).toBe(false);
117 | });
118 |
119 | it("should show error on same branch", function () {
120 | this.dialog.rootBranchChange({target: {value: "selected"}});
121 | this.dialog.mergeBranchChange({target: {value: "selected"}});
122 | expect(this.dialog.refs.rootBranchInput.classList).not.toContain("error");
123 | expect(this.dialog.refs.mergeBranchInput.classList).not.toContain("error");
124 | this.dialog.accept();
125 | expect(this.dialog.refs.rootBranchInput.classList).toContain("error");
126 | expect(this.dialog.refs.mergeBranchInput.classList).toContain("error");
127 | });
128 |
129 | describe("confirm", function () {
130 |
131 | it("should confirm on delete", async function () {
132 | this.dialog.state.delete = true;
133 | this.dialog.state.rootBranch = "branch1";
134 | this.dialog.state.mergeBranch = "branch2";
135 | this.dialog.accept();
136 | await this.activate;
137 | expect(atom.confirm).toHaveBeenCalled();
138 | });
139 |
140 | it("should not confirm on not delete", async function () {
141 | this.dialog.state.delete = false;
142 | this.dialog.state.rootBranch = "branch1";
143 | this.dialog.state.mergeBranch = "branch2";
144 | this.dialog.accept();
145 | await this.activate;
146 | expect(atom.confirm).not.toHaveBeenCalled();
147 | });
148 |
149 | });
150 | });
151 | });
152 |
--------------------------------------------------------------------------------
/spec/dialogs/run-command-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import RunCommandDialog from "../../lib/dialogs/RunCommandDialog";
4 | import {fileStatus, files} from "../mocks";
5 |
6 | describe("RunCommandDialog", function () {
7 | beforeEach(function () {
8 | this.files = [fileStatus("M ", files.t1)];
9 | });
10 |
11 | it("initial state includes files", function () {
12 | const dialog = new RunCommandDialog({files: this.files});
13 | expect(dialog.state.files.length).toBe(this.files.length);
14 | });
15 |
16 | it("should return files and command on accept", async function () {
17 | const dialog = new RunCommandDialog({files: this.files});
18 | const promise = dialog.activate();
19 | dialog.state.command = "command";
20 | dialog.accept();
21 | const ret = await promise;
22 | expect(ret[0]).toBe("command");
23 | expect(ret[1][0]).toBe(files.t1);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/spec/dialogs/switch-branch-dialog-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import SwitchBranchDialog from "../../lib/dialogs/SwitchBranchDialog";
4 | import {mockGit} from "../mocks";
5 |
6 | describe("SwitchBranchDialog", function () {
7 |
8 | beforeEach(function () {
9 | this.branches = [
10 | {name: "local", branch: "first", local: true},
11 | {name: "remote", branch: "second", remote: true},
12 | {name: "selected", branch: "third", selected: true, local: true, remote: true},
13 | ];
14 |
15 | this.root = "root";
16 | });
17 |
18 | describe("selected branch", function () {
19 |
20 | it("should select selected branch", function () {
21 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root});
22 | expect(dialog.state.branch).toBe("selected");
23 | });
24 |
25 | });
26 |
27 | describe("fetch", function () {
28 |
29 | it("should set state.fetching to true", async function () {
30 | const git = mockGit({
31 | branches: () => this.branches,
32 | });
33 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root, git});
34 | spyOn(dialog, "update");
35 | await dialog.fetch();
36 | expect(dialog.update).toHaveBeenCalledWith({fetching: true});
37 | });
38 |
39 | it("should change branches", async function () {
40 | const git = mockGit({
41 | branches: () => this.branches,
42 | });
43 |
44 | const branches = [
45 | {name: "name", branch: "branch", selected: true},
46 | ];
47 |
48 | const dialog = new SwitchBranchDialog({branches, root: this.root, git});
49 |
50 | await dialog.fetch();
51 | expect(dialog.state.branches).toBe(this.branches);
52 | });
53 |
54 | it("should show error", async function () {
55 | const git = mockGit({
56 | fetch: () => Promise.reject("fetch error"),
57 | branches: () => this.branches,
58 | });
59 |
60 | const notifications = jasmine.createSpyObj(["addError"]);
61 |
62 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root, git, notifications});
63 |
64 | await dialog.fetch();
65 | expect(notifications.addError).toHaveBeenCalledWith("Fetch", "fetch error");
66 | });
67 |
68 | });
69 |
70 | describe("accept", function () {
71 |
72 | it("should return the selected branch name", async function () {
73 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root});
74 | const activate = dialog.activate();
75 | dialog.accept();
76 | const ret = await activate;
77 | expect(ret).toEqual(["selected", "origin"]);
78 | });
79 |
80 | it("should return the branch name", async function () {
81 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root});
82 | const activate = dialog.activate();
83 | dialog.branchChange({target: {value: "test"}});
84 | dialog.accept();
85 | const ret = await activate;
86 | expect(ret).toEqual(["test", null]);
87 | });
88 |
89 | it("should return the remote name", async function () {
90 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root});
91 | const activate = dialog.activate();
92 | dialog.branchChange({target: {value: "remotes/upstream/test"}});
93 | dialog.accept();
94 | const ret = await activate;
95 | expect(ret).toEqual(["test", "upstream"]);
96 | });
97 |
98 | it("should return the remote origin", async function () {
99 | const dialog = new SwitchBranchDialog({branches: this.branches, root: this.root});
100 | const activate = dialog.activate();
101 | dialog.branchChange({target: {value: "remote"}});
102 | dialog.accept();
103 | const ret = await activate;
104 | expect(ret).toEqual(["remote", "origin"]);
105 | });
106 |
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/spec/git-menu-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import commands from "../lib/commands";
4 | import config from "../lib/config";
5 | import main from "../lib/main";
6 | import {mockGit, createGitRoot, getFilePath, files} from "./mocks";
7 | import fs from "fs";
8 | import path from "path";
9 |
10 | describe("Git Menu", function () {
11 | beforeEach(async function () {
12 | atom.project.setPaths([__dirname]);
13 | await atom.packages.activatePackage("git-menu");
14 | this.configOptions = atom.config.getAll("git-menu")[0].value;
15 | this.configConfirmationDialogs = Object.keys(this.configOptions.confirmationDialogs);
16 | this.configContextMenuItems = Object.keys(this.configOptions.contextMenuItems);
17 | this.allConfig = Object.keys(this.configOptions);
18 | this.allCommands = atom.commands
19 | .findCommands({target: atom.views.getView(atom.workspace)})
20 | .map(cmd => cmd.name)
21 | .filter(cmd => cmd.startsWith("git-menu:"));
22 | this.getContextMenuItems = () => atom.contextMenu.itemSets
23 | .filter(itemSet => itemSet.selector === "atom-workspace, atom-text-editor, .tree-view, .tab-bar")
24 | .map(itemSet => itemSet.items[0].submenu[0].command);
25 | this.confirmSpy = spyOn(atom, "confirm");
26 | });
27 |
28 | describe("Config", function () {
29 | Object.keys(config).forEach(configOption => {
30 | it(`has a config option: ${configOption}`, function () {
31 | expect(this.allConfig).toContain(configOption);
32 | });
33 | });
34 | });
35 |
36 | describe("Commands", function () {
37 | Object.keys(commands).forEach(command => {
38 | const cmd = `git-menu:${command}`;
39 | const {label, confirm, description, command: func} = commands[command];
40 | const dispatch = main.dispatchCommand(command, commands[command]);
41 | describe(command, function () {
42 | beforeEach(function () {
43 | this.cmdSpy = spyOn(commands[command], "command").and.callFake(() => Promise.reject());
44 | });
45 | it("should have a command in the command pallete", function () {
46 | expect(this.allCommands).toContain(cmd);
47 | });
48 | it("should have a command", function () {
49 | expect(func).toEqual(jasmine.any(Function));
50 | });
51 | if (label) {
52 | it("should have a config option to disable it in the context menu", function () {
53 | expect(this.configContextMenuItems).toContain(command);
54 | });
55 | it("should have a description", function () {
56 | expect(description).toBeTruthy();
57 | });
58 | it("should have a context menu item", function () {
59 | expect(this.getContextMenuItems()).toContain(cmd);
60 | });
61 | it("should not have a context menu item when unchecked", function () {
62 | atom.config.set(`git-menu.contextMenuItems.${command}`, false);
63 | expect(this.getContextMenuItems()).not.toContain(cmd);
64 | });
65 | } else {
66 | it("should not have a config option to disable it in the context menu", function () {
67 | expect(this.configContextMenuItems).not.toContain(command);
68 | });
69 | it("should not have a context menu item", function () {
70 | expect(this.getContextMenuItems()).not.toContain(cmd);
71 | });
72 | }
73 |
74 | if (confirm) {
75 | it("should have a config option to disable the confirm dialog", function () {
76 | expect(this.configConfirmationDialogs).toContain(command);
77 | });
78 | it("should have a confirm message", function () {
79 | expect(confirm.message).toEqual(jasmine.any(String));
80 | });
81 | if (confirm.detail) {
82 | it("should return a string detail", async function () {
83 | const gitRoot = await createGitRoot();
84 | const filePaths = getFilePath(gitRoot, [files.t1]);
85 | const git = mockGit();
86 | let {detail} = confirm;
87 | if (typeof detail === "function") {
88 | detail = await detail(filePaths, git);
89 | }
90 | expect(detail).toEqual(jasmine.any(String));
91 | });
92 | }
93 | it("should be called if atom.confirm returns true", async function () {
94 | this.confirmSpy.and.callFake((opts, callback) => {
95 | if (callback) {
96 | callback(0, false);
97 | } else {
98 | return 0;
99 | }
100 | });
101 | await dispatch({target: atom.views.getView(atom.workspace)});
102 | expect(this.confirmSpy).toHaveBeenCalled();
103 | expect(this.cmdSpy).toHaveBeenCalled();
104 | });
105 | it("should not be called if atom.confirm is canceled", async function () {
106 | this.confirmSpy.and.callFake((opts, callback) => {
107 | if (callback) {
108 | callback(1, false);
109 | } else {
110 | return 2;
111 | }
112 | });
113 | await dispatch({target: atom.views.getView(atom.workspace)});
114 | expect(this.confirmSpy).toHaveBeenCalled();
115 | expect(this.cmdSpy).not.toHaveBeenCalled();
116 | });
117 | } else {
118 | it("should not have a config option to disable the confirm dialog", function () {
119 | expect(this.configConfirmationDialogs).not.toContain(command);
120 | });
121 | it("should not call atom.confirm but should call the command", async function () {
122 | await dispatch({target: atom.views.getView(atom.workspace)});
123 | expect(this.confirmSpy).not.toHaveBeenCalled();
124 | expect(this.cmdSpy).toHaveBeenCalled();
125 | });
126 | }
127 | });
128 | });
129 |
130 | describe("command files", function () {
131 | // eslint-disable-next-line no-sync
132 | fs.readdirSync(path.resolve(__dirname, "../lib/commands")).map(file => {
133 | describe(file, function () {
134 | it("should be in commands.js", function () {
135 | const command = file.replace(/.js$/, "");
136 | expect(command in commands).toBe(true);
137 | });
138 | });
139 | });
140 | });
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/spec/git/abort-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 | import {promisify} from "promisificator";
6 | import fs from "fs";
7 |
8 | describe("git.abort", function () {
9 |
10 | beforeEach(function () {
11 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve());
12 |
13 | this.branch = "branch";
14 | this.gitRoot = "root";
15 | });
16 |
17 | it("should send ['merge', '--abort'] to cmd", async function () {
18 | await gitCmd.abort(this.gitRoot, true);
19 |
20 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => i)).toEqual(["merge", "--abort"]);
21 | });
22 |
23 | it("should send ['rebase', '--abort'] to cmd", async function () {
24 | await gitCmd.abort(this.gitRoot, false);
25 |
26 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => i)).toEqual(["rebase", "--abort"]);
27 | });
28 |
29 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
30 |
31 | beforeEach(async function () {
32 | gitCmd.cmd.and.callThrough();
33 | await atom.packages.activatePackage("git-menu");
34 | this.gitRoot = await createGitRoot(true, true);
35 |
36 | this.gitPath = getFilePath(this.gitRoot, ".git");
37 | });
38 |
39 | afterEach(async function () {
40 | await removeGitRoot(this.gitRoot);
41 | });
42 |
43 | it("should abort a merge", async function () {
44 | const newBranch = "new-branch";
45 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
46 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test");
47 | await gitCmd.cmd(this.gitRoot, ["commit", "--all", "--message=new branch commit"]);
48 | await gitCmd.cmd(this.gitRoot, ["checkout", "master"]);
49 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test1");
50 | await gitCmd.cmd(this.gitRoot, ["commit", "--all", "--message=master branch commit"]);
51 | try {
52 | await gitCmd.cmd(this.gitRoot, ["merge", newBranch]);
53 | } catch (ex) {} // eslint-disable-line no-empty
54 |
55 | const beforeStatus = await gitCmd.cmd(this.gitRoot, ["status"]);
56 | expect(beforeStatus).toContain("You have unmerged paths.");
57 |
58 | await gitCmd.abort(this.gitRoot, true);
59 |
60 | const afterStatus = await gitCmd.cmd(this.gitRoot, ["status"]);
61 | expect(afterStatus).not.toContain("You have unmerged paths.");
62 | });
63 |
64 | it("should abort a rebase", async function () {
65 | const newBranch = "new-branch";
66 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
67 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test");
68 | await gitCmd.cmd(this.gitRoot, ["commit", "--all", "--message=new branch commit"]);
69 | await gitCmd.cmd(this.gitRoot, ["checkout", "master"]);
70 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test1");
71 | await gitCmd.cmd(this.gitRoot, ["commit", "--all", "--message=master branch commit"]);
72 | try {
73 | await gitCmd.cmd(this.gitRoot, ["rebase", newBranch]);
74 | } catch (ex) {} // eslint-disable-line no-empty
75 |
76 | const beforeStatus = await gitCmd.cmd(this.gitRoot, ["status"]);
77 | expect(beforeStatus).toContain("rebase in progress");
78 |
79 | await gitCmd.abort(this.gitRoot, false);
80 |
81 | const afterStatus = await gitCmd.cmd(this.gitRoot, ["status"]);
82 | expect(afterStatus).not.toContain("rebase in progress");
83 | });
84 |
85 | });
86 |
87 | });
88 |
--------------------------------------------------------------------------------
/spec/git/add-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 |
6 | describe("git.add", function () {
7 |
8 | beforeEach(function () {
9 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve());
10 |
11 | this.files = ["file1", "file2"];
12 | this.gitRoot = "root";
13 | });
14 |
15 | it("should send ['add', '--', ...files] to cmd", async function () {
16 | await gitCmd.add(this.gitRoot, this.files);
17 |
18 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => !!i)).toEqual(["add", "--", ...this.files]);
19 | });
20 |
21 | it("should send --verbose to cmd", async function () {
22 | await gitCmd.add(this.gitRoot, this.files, true);
23 |
24 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--verbose");
25 | });
26 |
27 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
28 |
29 | beforeEach(async function () {
30 | gitCmd.cmd.and.callThrough();
31 | await atom.packages.activatePackage("git-menu");
32 | this.gitRoot = await createGitRoot();
33 |
34 | this.gitPath = getFilePath(this.gitRoot, ".git");
35 | });
36 |
37 | afterEach(async function () {
38 | await removeGitRoot(this.gitRoot);
39 | });
40 |
41 | it("should add a file", async function () {
42 | await gitCmd.add(this.gitRoot, getFilePath(this.gitRoot, [files.t1]));
43 | const status = await gitCmd.cmd(this.gitRoot, ["status", "--short"]);
44 |
45 | expect(status).toContain(`A ${files.t1}`);
46 | });
47 |
48 | });
49 |
50 | });
51 |
--------------------------------------------------------------------------------
/spec/git/cmd-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {removeGitRoot, createGitRoot} from "../mocks";
5 |
6 | describe("git.cmd", function () {
7 |
8 | beforeEach(async function () {
9 | await atom.packages.activatePackage("git-menu");
10 | this.gitRoot = await createGitRoot();
11 | });
12 |
13 | afterEach(async function () {
14 | await removeGitRoot(this.gitRoot);
15 | });
16 |
17 | it("should call git", async function () {
18 | let error;
19 | try {
20 | await gitCmd.cmd(this.gitRoot);
21 | } catch (ex) {
22 | error = ex;
23 | }
24 | expect(error).toContain("usage: git [--version]");
25 | });
26 |
27 | it("should call git with the args", async function () {
28 | let error;
29 | try {
30 | await gitCmd.cmd(this.gitRoot, ["test"]);
31 | } catch (ex) {
32 | error = ex;
33 | }
34 | expect(error).toContain("git: 'test' is not a git command.");
35 | });
36 |
37 | it("should reject on error", async function () {
38 | let rejected;
39 | try {
40 | await gitCmd.cmd(this.gitRoot, ["test"]);
41 | } catch (ex) {
42 | rejected = true;
43 | }
44 | expect(rejected).toBeTruthy();
45 | });
46 |
47 | it("should resolve on non-error", async function () {
48 | await gitCmd.cmd(this.gitRoot, ["init"]);
49 | pass();
50 | });
51 |
52 | });
53 |
--------------------------------------------------------------------------------
/spec/git/countCommits-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot} from "../mocks";
5 |
6 | describe("git.countCommits", function () {
7 |
8 | beforeEach(async function () {
9 | await atom.packages.activatePackage("git-menu");
10 | this.gitRoot = await createGitRoot();
11 |
12 | this.gitPath = getFilePath(this.gitRoot, ".git");
13 | });
14 |
15 | afterEach(async function () {
16 | await removeGitRoot(this.gitRoot);
17 | });
18 |
19 | it("should return error if not git", async function () {
20 | let error = false;
21 | try {
22 | await gitCmd.remove(this.gitRoot);
23 | await gitCmd.countCommits(this.gitRoot);
24 | } catch (ex) {
25 | error = true;
26 | }
27 | expect(error).toBe(true);
28 | });
29 |
30 | it("should return 0 if no commits", async function () {
31 | expect(await gitCmd.countCommits(this.gitRoot)).toBe(0);
32 | });
33 |
34 | it("should be number of commits", async function () {
35 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
36 | await gitCmd.cmd(this.gitRoot, ["commit", "-m", "init"]);
37 | expect(await gitCmd.countCommits(this.gitRoot)).toBe(1);
38 | });
39 |
40 | });
41 |
--------------------------------------------------------------------------------
/spec/git/diff-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 |
5 | describe("git.diff", function () {
6 |
7 | beforeEach(function () {
8 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve());
9 |
10 | this.files = ["file1", "file2"];
11 | this.gitRoot = "root";
12 | });
13 |
14 | it("should send ['diff', '--ignore-all-space', '--', ...files] to cmd", async function () {
15 | await gitCmd.diff(this.gitRoot, this.files);
16 |
17 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => !!i)).toEqual(["diff", "--ignore-all-space", "--", ...this.files]);
18 | });
19 |
20 | });
21 |
--------------------------------------------------------------------------------
/spec/git/init-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot} from "../mocks";
5 | import fs from "fs";
6 |
7 | describe("git.init", function () {
8 |
9 | beforeEach(function () {
10 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve());
11 |
12 | this.gitRoot = "root";
13 | });
14 |
15 | it("should send init commad", async function () {
16 | await gitCmd.init(this.gitRoot, false);
17 |
18 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toEqual(["init", "--quiet"]);
19 | });
20 |
21 | it("should remove --quiet parameter", async function () {
22 | await gitCmd.init(this.gitRoot, true);
23 |
24 | expect(gitCmd.cmd.calls.mostRecent().args[1]).not.toContain("--quiet");
25 | });
26 |
27 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
28 |
29 | beforeEach(async function () {
30 | gitCmd.cmd.and.callThrough();
31 | await atom.packages.activatePackage("git-menu");
32 | this.gitRoot = await createGitRoot(false);
33 |
34 |
35 | this.gitPath = getFilePath(this.gitRoot, ".git");
36 | });
37 |
38 | afterEach(async function () {
39 | await removeGitRoot(this.gitRoot);
40 | });
41 |
42 | it("should create a .git folder", async function () {
43 | await gitCmd.init(this.gitRoot);
44 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
45 | await gitCmd.cmd(this.gitRoot, ["commit", "-m", "init"]);
46 | const commitCount = await gitCmd.cmd(this.gitRoot, ["rev-list", "--all", "--count"]);
47 |
48 | // eslint-disable-next-line no-sync
49 | expect(fs.existsSync(this.gitPath)).toBe(true);
50 | expect(commitCount.replace(/^[^\n]*\n\n/, "")).toBe("1");
51 | });
52 |
53 | it("should return nothing on --quiet", async function () {
54 | const result = await gitCmd.init(this.gitRoot);
55 |
56 | expect(result).toBe("");
57 | });
58 |
59 | it("should return something on verbose", async function () {
60 | const result = await gitCmd.init(this.gitRoot, true);
61 |
62 | expect(result).not.toBe("");
63 | });
64 |
65 | });
66 |
67 | });
68 |
--------------------------------------------------------------------------------
/spec/git/log-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot} from "../mocks";
5 |
6 | describe("git.log", function () {
7 |
8 | beforeEach(async function () {
9 | await atom.packages.activatePackage("git-menu");
10 | this.gitRoot = await createGitRoot();
11 |
12 | this.gitPath = getFilePath(this.gitRoot, ".git");
13 | });
14 |
15 | afterEach(async function () {
16 | await removeGitRoot(this.gitRoot);
17 | });
18 |
19 | it("should return error if not git", async function () {
20 | let error = false;
21 | try {
22 | await gitCmd.remove(this.gitRoot);
23 | await gitCmd.log(this.gitRoot);
24 | } catch (ex) {
25 | error = true;
26 | }
27 | expect(error).toBe(true);
28 | });
29 |
30 | it("should return an empty string if no commits", async function () {
31 | expect(await gitCmd.log(this.gitRoot)).toBe("");
32 | });
33 |
34 | it("should return the commit", async function () {
35 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
36 | await gitCmd.cmd(this.gitRoot, ["commit", "-m", "init commit"]);
37 | expect(await gitCmd.log(this.gitRoot, 1, 0, "%B")).toBe("init commit");
38 | });
39 |
40 | it("should use the format", async function () {
41 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
42 | await gitCmd.cmd(this.gitRoot, ["commit", "-m", "init commit"]);
43 | expect(await gitCmd.log(this.gitRoot, 1, 0, "oneline")).not.toBe("init commit");
44 | expect(await gitCmd.log(this.gitRoot, 1, 0, "oneline")).toContain("init commit");
45 | });
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/spec/git/merge-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 | import {promisify} from "promisificator";
6 | import fs from "fs";
7 |
8 | describe("git.merge", function () {
9 |
10 | beforeEach(function () {
11 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve());
12 |
13 | this.branch = "branch";
14 | this.gitRoot = "root";
15 | });
16 |
17 | it("should send ['merge', branch, '--quiet'] to cmd", async function () {
18 | await gitCmd.merge(this.gitRoot, this.branch);
19 |
20 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => !!i)).toEqual(["merge", this.branch, "--quiet"]);
21 | });
22 |
23 | it("should send --verbose to cmd", async function () {
24 | await gitCmd.merge(this.gitRoot, this.branch, true);
25 |
26 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--verbose");
27 | });
28 |
29 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
30 |
31 | beforeEach(async function () {
32 | gitCmd.cmd.and.callThrough();
33 | await atom.packages.activatePackage("git-menu");
34 | this.gitRoot = await createGitRoot(true, true);
35 | });
36 |
37 | afterEach(async function () {
38 | await removeGitRoot(this.gitRoot);
39 | });
40 |
41 | it("should merge a branch", async function () {
42 | const newBranch = "new-branch";
43 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
44 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test");
45 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
46 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=new branch commit"]);
47 | await gitCmd.cmd(this.gitRoot, ["checkout", "master"]);
48 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t2), "test");
49 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
50 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=master branch commit"]);
51 |
52 | await gitCmd.merge(this.gitRoot, newBranch, true);
53 |
54 | let lastCommits = await gitCmd.cmd(this.gitRoot, ["log", "--max-count=3", "--format=%B"], "", false);
55 | lastCommits = lastCommits.split("\n").filter(i => i);
56 |
57 | expect(lastCommits).toEqual([
58 | jasmine.stringMatching("Merge branch 'new-branch'"),
59 | "master branch commit",
60 | "new branch commit",
61 | ]);
62 | });
63 |
64 | });
65 |
66 | });
67 |
--------------------------------------------------------------------------------
/spec/git/pull-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 | import {promisify} from "promisificator";
6 | import fs from "fs";
7 |
8 | describe("git.pull", function () {
9 |
10 | beforeEach(function () {
11 | spyOn(gitCmd, "cmd").and.returnValues(
12 | Promise.resolve("## master"),
13 | Promise.resolve(),
14 | );
15 |
16 | this.gitRoot = "root";
17 | });
18 |
19 | it("should send ['pull', '--quiet'] to cmd", async function () {
20 | await gitCmd.pull(this.gitRoot);
21 |
22 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => !!i)).toEqual(["pull", "--quiet"]);
23 | });
24 |
25 | it("should send --rebase to cmd", async function () {
26 | await gitCmd.pull(this.gitRoot, true);
27 |
28 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--rebase");
29 | });
30 |
31 | it("should send --force to cmd", async function () {
32 | await gitCmd.pull(this.gitRoot, false, true);
33 |
34 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--force");
35 | });
36 |
37 | it("should send --verbose to cmd", async function () {
38 | await gitCmd.pull(this.gitRoot, false, false, true);
39 |
40 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--verbose");
41 | });
42 |
43 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
44 |
45 | beforeEach(async function () {
46 | gitCmd.cmd.and.callThrough();
47 | await atom.packages.activatePackage("git-menu");
48 | this.gitRoot = await createGitRoot(true, true);
49 | this.originRoot = await createGitRoot(true, true);
50 | await gitCmd.cmd(this.gitRoot, ["remote", "add", "origin", this.originRoot]);
51 | });
52 |
53 | afterEach(async function () {
54 | await removeGitRoot(this.gitRoot);
55 | });
56 |
57 | it("should merge", async function () {
58 | const newBranch = "new-branch";
59 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
60 | await gitCmd.cmd(this.gitRoot, ["push", "--set-upstream", "origin", newBranch]);
61 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test1");
62 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
63 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=test1"]);
64 |
65 | await gitCmd.cmd(this.originRoot, ["checkout", newBranch]);
66 | await promisify(fs.writeFile)(getFilePath(this.originRoot, files.t2), "test2");
67 | await gitCmd.cmd(this.originRoot, ["add", "."]);
68 | await gitCmd.cmd(this.originRoot, ["commit", "--message=test2"]);
69 |
70 | await gitCmd.pull(this.gitRoot);
71 |
72 | const content = await promisify(fs.readFile)(getFilePath(this.gitRoot, files.t2), {encoding: "utf8"});
73 | expect(content).toBe("test2");
74 |
75 | const log = await gitCmd.cmd(this.gitRoot, ["log", "--format=%s"]);
76 | expect(log.trim()).toEqual(jasmine.stringMatching(/^Merge branch/));
77 | });
78 |
79 | it("should rebase", async function () {
80 | const newBranch = "new-branch";
81 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
82 | await gitCmd.cmd(this.gitRoot, ["push", "--set-upstream", "origin", newBranch]);
83 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test1");
84 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
85 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=test1"]);
86 |
87 | await gitCmd.cmd(this.originRoot, ["checkout", newBranch]);
88 | await promisify(fs.writeFile)(getFilePath(this.originRoot, files.t2), "test2");
89 | await gitCmd.cmd(this.originRoot, ["add", "."]);
90 | await gitCmd.cmd(this.originRoot, ["commit", "--message=test2"]);
91 |
92 | await gitCmd.pull(this.gitRoot, true);
93 |
94 | const content = await promisify(fs.readFile)(getFilePath(this.gitRoot, files.t2), {encoding: "utf8"});
95 | expect(content).toBe("test2");
96 |
97 | const log = await gitCmd.cmd(this.gitRoot, ["log", "--format=%s"]);
98 | expect(log.trim()).toBe("test1\ntest2\ninit commit");
99 | });
100 |
101 | });
102 |
103 | });
104 |
--------------------------------------------------------------------------------
/spec/git/push-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 | import {promisify} from "promisificator";
6 | import fs from "fs";
7 |
8 | describe("git.push", function () {
9 |
10 | beforeEach(function () {
11 | spyOn(gitCmd, "cmd").and.returnValues(
12 | Promise.resolve("## master"),
13 | Promise.resolve(),
14 | );
15 |
16 | this.gitRoot = "root";
17 | });
18 |
19 | it("should send ['push', '--quiet', '--set-upstream', 'origin', 'master'] to cmd", async function () {
20 | await gitCmd.push(this.gitRoot);
21 |
22 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => !!i)).toEqual(["push", "--quiet", "--set-upstream", "origin", "master"]);
23 | });
24 |
25 | it("should send --force to cmd", async function () {
26 | await gitCmd.push(this.gitRoot, true);
27 |
28 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--force");
29 | });
30 |
31 | it("should send --verbose to cmd", async function () {
32 | await gitCmd.push(this.gitRoot, false, true);
33 |
34 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--verbose");
35 | });
36 |
37 | it("should track branch", async function () {
38 | await gitCmd.push(this.gitRoot);
39 |
40 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--set-upstream");
41 | });
42 |
43 | it("should not track already tracked branch", async function () {
44 | gitCmd.cmd.and.returnValues(
45 | Promise.resolve("## master...origin/master"),
46 | Promise.resolve(),
47 | );
48 | await gitCmd.push(this.gitRoot);
49 |
50 | expect(gitCmd.cmd.calls.mostRecent().args[1]).not.toContain("--set-upstream");
51 | });
52 |
53 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
54 |
55 | beforeEach(async function () {
56 | gitCmd.cmd.and.callThrough();
57 | await atom.packages.activatePackage("git-menu");
58 | this.gitRoot = await createGitRoot(true, true);
59 | this.originRoot = await createGitRoot(true, true);
60 | await gitCmd.cmd(this.gitRoot, ["remote", "add", "origin", this.originRoot]);
61 | });
62 |
63 | afterEach(async function () {
64 | await removeGitRoot(this.gitRoot);
65 | });
66 |
67 | it("should push and track a branch", async function () {
68 | const newBranch = "new-branch";
69 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
70 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test");
71 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
72 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=commit"]);
73 |
74 | await gitCmd.push(this.gitRoot);
75 |
76 | await gitCmd.cmd(this.originRoot, ["checkout", newBranch]);
77 |
78 | const content = await promisify(fs.readFile)(getFilePath(this.gitRoot, files.t1), {encoding: "utf8"});
79 | expect(content).toBe("test");
80 |
81 | const result = await gitCmd.cmd(this.gitRoot, ["status", "-b", "--porcelain"]);
82 | expect(result).toBe("## new-branch...origin/new-branch");
83 | });
84 |
85 | });
86 |
87 | });
88 |
--------------------------------------------------------------------------------
/spec/git/rebase-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 | import {promisify} from "promisificator";
6 | import fs from "fs";
7 |
8 | describe("git.rebase", function () {
9 |
10 | beforeEach(function () {
11 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve());
12 |
13 | this.branch = "branch";
14 | this.gitRoot = "root";
15 | });
16 |
17 | it("should send ['rebase', branch, '--quiet'] to cmd", async function () {
18 | await gitCmd.rebase(this.gitRoot, this.branch);
19 |
20 | expect(gitCmd.cmd.calls.mostRecent().args[1].filter(i => !!i)).toEqual(["rebase", this.branch, "--quiet"]);
21 | });
22 |
23 | it("should send --verbose to cmd", async function () {
24 | await gitCmd.rebase(this.gitRoot, this.branch, true);
25 |
26 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toContain("--verbose");
27 | });
28 |
29 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
30 |
31 | beforeEach(async function () {
32 | gitCmd.cmd.and.callThrough();
33 | await atom.packages.activatePackage("git-menu");
34 | this.gitRoot = await createGitRoot(true, true);
35 |
36 | this.gitPath = getFilePath(this.gitRoot, ".git");
37 | });
38 |
39 | afterEach(async function () {
40 | await removeGitRoot(this.gitRoot);
41 | });
42 |
43 | it("should rebase a branch", async function () {
44 | const newBranch = "new-branch";
45 | await gitCmd.cmd(this.gitRoot, ["checkout", "-b", newBranch]);
46 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t1), "test");
47 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
48 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=new branch commit"]);
49 | await gitCmd.cmd(this.gitRoot, ["checkout", "master"]);
50 | await promisify(fs.writeFile)(getFilePath(this.gitRoot, files.t2), "test");
51 | await gitCmd.cmd(this.gitRoot, ["add", "."]);
52 | await gitCmd.cmd(this.gitRoot, ["commit", "--message=master branch commit"]);
53 |
54 | await gitCmd.rebase(this.gitRoot, newBranch, true);
55 |
56 | let lastCommits = await gitCmd.cmd(this.gitRoot, ["log", "--max-count=2", "--format=%B"], "", false);
57 | lastCommits = lastCommits.split("\n").filter(i => i);
58 |
59 | expect(lastCommits).toEqual([
60 | "master branch commit",
61 | "new branch commit",
62 | ]);
63 | });
64 |
65 | });
66 |
67 | });
68 |
--------------------------------------------------------------------------------
/spec/git/remove-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot} from "../mocks";
5 | import fs from "fs";
6 |
7 | describe("git.remove", function () {
8 |
9 | beforeEach(async function () {
10 | await atom.packages.activatePackage("git-menu");
11 | this.gitRoot = await createGitRoot();
12 |
13 | await gitCmd.cmd(this.gitRoot, ["init"]);
14 | this.gitPath = getFilePath(this.gitRoot, ".git");
15 | });
16 |
17 | afterEach(async function () {
18 | await removeGitRoot(this.gitRoot);
19 | });
20 |
21 | it("should remove the .git folder", async function () {
22 | await gitCmd.remove(this.gitRoot);
23 |
24 | // eslint-disable-next-line no-sync
25 | expect(fs.existsSync(this.gitPath)).toBe(false);
26 | });
27 |
28 | it("should be idempotent", async function () {
29 | await gitCmd.remove(this.gitRoot);
30 | await gitCmd.remove(this.gitRoot);
31 |
32 | // eslint-disable-next-line no-sync
33 | expect(fs.existsSync(this.gitPath)).toBe(false);
34 | });
35 |
36 | });
37 |
--------------------------------------------------------------------------------
/spec/git/rootDir-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import path from "path";
4 | import gitCmd from "../../lib/git-cmd";
5 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
6 |
7 | describe("git.rootDir", function () {
8 |
9 | beforeEach(function () {
10 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve("root"));
11 |
12 | this.gitRoot = "root";
13 | });
14 |
15 | it("should send ['rev-parse', '--show-toplevel'] to cmd", async function () {
16 | await gitCmd.rootDir(this.gitRoot);
17 |
18 | expect(gitCmd.cmd.calls.mostRecent().args[1]).toEqual(["rev-parse", "--show-toplevel"]);
19 | });
20 |
21 | it("should return path from cmd", async function () {
22 | const root = await gitCmd.rootDir("test");
23 |
24 | expect(root).toEqual("root");
25 | });
26 |
27 | it("should return cwd for smb path", async function () {
28 | gitCmd.cmd.and.returnValue(Promise.resolve("\\\\root"));
29 | const root = await gitCmd.rootDir("test");
30 |
31 | expect(root).toEqual("test");
32 | });
33 |
34 | it("should return cwd for smb:// path", async function () {
35 | gitCmd.cmd.and.returnValue(Promise.resolve("smb://root"));
36 | const root = await gitCmd.rootDir("test");
37 |
38 | expect(root).toEqual("test");
39 | });
40 |
41 | (process.env.CI ? describe : xdescribe)("integration tests", function () {
42 |
43 | beforeEach(async function () {
44 | gitCmd.cmd.and.callThrough();
45 | await atom.packages.activatePackage("git-menu");
46 | this.gitRoot = await createGitRoot();
47 |
48 | this.gitPath = getFilePath(this.gitRoot, ".git");
49 | });
50 |
51 | afterEach(async function () {
52 | await removeGitRoot(this.gitRoot);
53 | });
54 |
55 | it("should get root dir", async function () {
56 | const root = await gitCmd.rootDir(getFilePath(this.gitRoot, path.dirname(files.tt1)));
57 |
58 | expect(root).toContain(this.gitRoot);
59 | });
60 |
61 | });
62 |
63 | });
64 |
--------------------------------------------------------------------------------
/spec/git/status-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import gitCmd from "../../lib/git-cmd";
4 | import {getFilePath, removeGitRoot, createGitRoot, files} from "../mocks";
5 |
6 | describe("git.status", function () {
7 |
8 | beforeEach(async function () {
9 | await atom.packages.activatePackage("git-menu");
10 | this.gitRoot = await createGitRoot();
11 |
12 | this.gitPath = getFilePath(this.gitRoot, ".git");
13 | });
14 |
15 | afterEach(async function () {
16 | await removeGitRoot(this.gitRoot);
17 | });
18 |
19 | it("should return status of t1", async function () {
20 | await gitCmd.cmd(this.gitRoot, ["init"]);
21 | const status = await gitCmd.status(this.gitRoot, getFilePath(this.gitRoot, [files.t1]));
22 | expect(status[0]).toEqual(jasmine.objectContaining({added: false, untracked: true, deleted: false, changed: false, file: files.t1}));
23 | });
24 |
25 | it("should return empty array on no status", async function () {
26 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve(""));
27 | const status = await gitCmd.status(this.gitRoot, getFilePath(this.gitRoot, [files.t1]));
28 | expect(status).toEqual([]);
29 | });
30 |
31 | it("should return rejected promise on unknown status code", async function () {
32 | spyOn(gitCmd, "cmd").and.returnValue(Promise.resolve(`ZZ ${files.t1}`));
33 | const error = await gitCmd.status(this.gitRoot, getFilePath(this.gitRoot, [files.t1])).then(() => false, err => err);
34 | expect(error).toBeTruthy();
35 | });
36 |
37 | });
38 |
--------------------------------------------------------------------------------
/spec/mocks.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import path from "path";
4 | import fs from "fs";
5 | import gitCmd from "../lib/git-cmd";
6 | import StatusBarManager from "../lib/widgets/StatusBarManager";
7 | import {promisify} from "promisificator";
8 | import temp from "temp";
9 | temp.track();
10 |
11 | export const statusBar = new StatusBarManager({addRightTile() {}});
12 |
13 | /**
14 | * Mock statuses for files
15 | * @param {string} code The git status code
16 | * @param {string} file The file
17 | * @return {Object} {
18 | * added: bool,
19 | * untracked: bool,
20 | * deleted: bool,
21 | * changed: bool,
22 | * file: string
23 | * }
24 | */
25 | export function fileStatus(code, file) {
26 | const status = gitCmd.statusFromCode(code);
27 | if (status === false) {
28 | throw new Error(`Invalid code '${code}'`);
29 | }
30 | return {
31 | ...status,
32 | file,
33 | };
34 | }
35 |
36 | /**
37 | * Files in spec/git-root
38 | * @type {Object}
39 | */
40 | export const files = {
41 | t1: "test1.txt",
42 | t2: "test2.txt",
43 | tt1: "test/test1.txt",
44 | tt2: "test/test2.txt",
45 | };
46 |
47 | /**
48 | * Mock a dialog
49 | * @param {Object} [methods={ methods: Promise, ... }] The methods to add to the mock
50 | * @return {class} A dialog class
51 | */
52 | export function mockDialog(methods = {activate: () => Promise.reject()}) {
53 | const dialog = Object.keys(methods).reduce((prev, method) => {
54 | prev.prototype[method] = function (...args) {
55 | if (typeof methods[method] === "function") {
56 | return methods[method].apply(dialog, args);
57 | }
58 | return methods[method];
59 | };
60 | return prev;
61 | }, function () {});
62 | return dialog;
63 | }
64 |
65 | /**
66 | * Mock git-cmd
67 | * @param {Object} [methods={ methods: Promise, ... }] The methods to add to the mock
68 | * @return {Object} An object with the methods provided
69 | */
70 | export function mockGit(methods = {}) {
71 | const mock = Object.keys(gitCmd).reduce((prev, method) => {
72 | prev[method] = () => Promise.resolve();
73 | return prev;
74 | }, {});
75 |
76 | Object.keys(methods).forEach(method => {
77 | mock[method] = function (...args) {
78 | if (typeof methods[method] === "function") {
79 | return methods[method].apply(mock, args);
80 | }
81 | return methods[method];
82 | };
83 | });
84 |
85 | return mock;
86 | }
87 |
88 | /**
89 | * Remove the test spec/git-root directory
90 | * @param {string} root The root path
91 | * @return {void}
92 | */
93 | export async function removeGitRoot(root) {
94 | try {
95 | const pathWatcher = await atom.project.getWatcherPromise(root);
96 | await pathWatcher.native.stop();
97 | pathWatcher.dispose();
98 | await temp.cleanup();
99 | } catch (ex) {
100 | // eslint-disable-next-line no-console
101 | console.error(ex);
102 | }
103 | }
104 |
105 | /**
106 | * Get path to file(s) in spec/git-root directory
107 | * @param {string} root The root path
108 | * @param {string|string[]} paths The path or paths to get
109 | * @return {string|string[]} If input is an array it will return an array
110 | */
111 | export function getFilePath(root, paths) {
112 | const isArray = Array.isArray(paths);
113 | if (!paths) {
114 | // eslint-disable-next-line no-param-reassign
115 | paths = ["/"];
116 | } else if (!isArray) {
117 | // eslint-disable-next-line no-param-reassign
118 | paths = [paths];
119 | }
120 | const fullPaths = paths.map(p => path.join(root, p));
121 |
122 | return (isArray ? fullPaths : fullPaths[0]);
123 |
124 | }
125 |
126 | /**
127 | * Create the test spec/git-root directory
128 | * @param {bool} init initialize a git repo (default: true)
129 | * @param {bool} commit commit the initial files with the message "init commit" (default: false)
130 | * @return {void}
131 | */
132 | export async function createGitRoot(init = true, commit = false) {
133 | try {
134 | let root = await temp.mkdir("git-root-");
135 | if (process.platform === "win32" && root.includes("~")) {
136 | // this should fix when root === "C:\Users\NAME~1\..."
137 | root = root.replace(/^c:\\users\\[^\\]+/i, process.env.USERPROFILE);
138 | }
139 | const dirs = getFilePath(root, ["/test"]);
140 | const filePaths = getFilePath(root, ["/test1.txt", "/test2.txt", "/test/test1.txt", "/test/test2.txt"]);
141 |
142 | for (const dir of dirs) {
143 | await promisify(fs.mkdir)(dir);
144 | }
145 | for (const file of filePaths) {
146 | await promisify(fs.open)(file, "w").then(fd => promisify(fs.close)(fd));
147 | }
148 |
149 | if (init) {
150 | await gitCmd.cmd(root, ["init"]);
151 | }
152 |
153 | if (commit) {
154 | if (!init) {
155 | throw new Error("Cannot commit without init");
156 | }
157 | await gitCmd.cmd(root, ["add", "."]);
158 | await gitCmd.cmd(root, ["commit", "--message=init commit"]);
159 | }
160 |
161 | atom.project.setPaths([root]);
162 |
163 | return root;
164 | } catch (ex) {
165 | // eslint-disable-next-line no-console
166 | console.error(ex);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/spec/runner.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import {createRunner} from "atom-jasmine3-test-runner";
4 |
5 | export default createRunner({
6 | specHelper: {
7 | jasmineFocused: true,
8 | jasminePass: true,
9 | customMatchers: true,
10 | attachToDom: true,
11 | ci: true,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/spec/widgets/autocomplete-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import etch from "etch";
4 | import Autocomplete from "../../lib/widgets/Autocomplete.js";
5 |
6 | describe("Autocomplete.js", function () {
7 | beforeEach(function () {
8 | etch.setScheduler({
9 | updateDocument(callback) {
10 | callback();
11 | },
12 | getNextUpdatePromise() {
13 | return Promise.resolve();
14 | },
15 | });
16 |
17 | this.items = [
18 | "item1",
19 | "item2",
20 | "item3",
21 | "item4",
22 | "item5",
23 | "item6",
24 | "item7",
25 | "item8",
26 | "item9",
27 | "item10",
28 | "item11",
29 | "item12",
30 | "item13",
31 | ];
32 | });
33 |
34 | it("should not display the menu on focus if value is empty", function () {
35 | const component = new Autocomplete({items: this.items, value: ""});
36 | jasmine.attachToDOM(component.element);
37 | component.refs.input.focus();
38 |
39 | expect(component.isOpen()).toBe(false);
40 | expect(component.refs.menu).toBeFalsy();
41 | });
42 |
43 | it("should display the menu on focus", function () {
44 | const component = new Autocomplete({items: this.items, value: "i"});
45 | jasmine.attachToDOM(component.element);
46 | component.refs.input.focus();
47 |
48 | expect(component.isOpen()).toBe(true);
49 | expect(component.refs.menu).toBeTruthy();
50 | for (var i = 0; i < this.items.length; i++) {
51 | expect(component.refs[`item-${i}`]).toBeTruthy();
52 | }
53 | });
54 |
55 | it("should only display max number of items", function () {
56 | const maxItems = 10;
57 | const component = new Autocomplete({items: this.items, value: "i", maxItems});
58 | jasmine.attachToDOM(component.element);
59 | component.refs.input.focus();
60 |
61 | for (var i = 0; i < this.items.length; i++) {
62 | if (i < maxItems) {
63 | expect(component.refs[`item-${i}`]).toBeTruthy();
64 | } else {
65 | expect(component.refs[`item-${i}`]).toBeFalsy();
66 | }
67 | }
68 | });
69 |
70 | it("should filter items", function () {
71 | const value = "1";
72 | const component = new Autocomplete({items: this.items, value});
73 | jasmine.attachToDOM(component.element);
74 | component.refs.input.focus();
75 |
76 | const filtered = Array.from(component.refs.menu.children).every(item => item.textContent.includes(value));
77 | expect(filtered).toBe(true);
78 | });
79 |
80 | it("should hide item when value equals item", function () {
81 | const value = "item1";
82 | const component = new Autocomplete({items: this.items, value});
83 | jasmine.attachToDOM(component.element);
84 | component.refs.input.focus();
85 |
86 | const hasItem = Array.from(component.refs.menu.children).some(item => item === value);
87 | expect(hasItem).toBe(false);
88 | });
89 |
90 | it("should call onSelect when an item is clicked", function () {
91 | const onSelect = jasmine.createSpy("onSelect");
92 | const component = new Autocomplete({items: this.items, onSelect, open: true});
93 | jasmine.attachToDOM(component.element);
94 | component.refs["item-0"].click();
95 |
96 | expect(onSelect).toHaveBeenCalled();
97 | });
98 |
99 | it("should not call onSelect when remove button is clicked", function () {
100 | const onSelect = jasmine.createSpy("onSelect");
101 | const onRemove = jasmine.createSpy("onRemove");
102 | const component = new Autocomplete({
103 | items: this.items,
104 | onSelect,
105 | onRemove,
106 | removeButton: true,
107 | open: true,
108 | });
109 | jasmine.attachToDOM(component.element);
110 | component.refs["item-0"].querySelector(".autocomplete-remove-button").click();
111 |
112 | expect(onSelect).not.toHaveBeenCalled();
113 | expect(onRemove).toHaveBeenCalled();
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/styles/Autocomplete.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 | .git-menu {
3 | .autocomplete {
4 | .autocomplete-menu {
5 | background: @overlay-background-color;
6 | border-radius: 2px;
7 | border: 1px solid @overlay-border-color;
8 |
9 | .autocomplete-item {
10 | padding: 2px 5px;
11 | color: @text-color-highlight;
12 | line-height: 2em;
13 | border-radius: 2px;
14 |
15 | &.autocomplete-highlight {
16 | box-shadow: 0 0 1px 2px @text-color-info inset;
17 |
18 | .autocomplete-remove-button {
19 | display: inline-block;
20 | }
21 | }
22 |
23 | .autocomplete-remove-button {
24 | float: right;
25 | width: 0;
26 | margin-right: 15px;
27 | cursor: pointer;
28 | display: none;
29 |
30 | &:before {
31 | font-size: 16px;
32 | line-height: 17px;
33 | }
34 |
35 | &:hover {
36 | color: @text-color-subtle;
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/styles/CommitDialog.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | .git-menu.dialog.commit {
4 | .message {
5 | height: 150px;
6 | resize: none;
7 |
8 | &.too-long {
9 | border-color: @text-color-error;
10 | box-shadow: 0 0 0 1px @text-color-error;
11 | }
12 | }
13 |
14 | .last-commit {
15 | font-weight: bold;
16 | font-style: italic;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/styles/CreateBranchDialog.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | .git-menu.dialog.create-branch {
4 | .actual-name {
5 | position: relative;
6 | top: -15px;
7 | height: 0;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/styles/FileTree.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | .git-menu {
4 | .file-tree {
5 | width: 100%;
6 | line-height: 1.4em;
7 | margin-bottom: 15px;
8 |
9 | .buttons button {
10 | border: 0;
11 | text-decoration: underline;
12 | background-color: transparent;
13 | font-size: .9em;
14 | margin-bottom: 5px;
15 | }
16 |
17 | .files {
18 | padding: 2px;
19 | max-height: 14em;
20 | overflow: auto;
21 |
22 | ul {
23 | padding-left: 0px;
24 | margin-bottom: 0px;
25 |
26 | ul {
27 | padding-left: 20px;
28 | margin-top: 10px;
29 | }
30 |
31 | li {
32 | list-style: none;
33 | margin-bottom: 10px;
34 |
35 | &:last-child {
36 | margin-bottom: 0px;
37 | }
38 |
39 | &.untracked, &.added {
40 | color: @text-color-success;
41 | }
42 |
43 | &.changed {
44 | color: @text-color-warning;
45 | }
46 |
47 | &.deleted {
48 | color: @text-color-error;
49 | }
50 |
51 | &.unchecked {
52 | color: @text-color-subtle;
53 | }
54 |
55 | &.dir {
56 | cursor: default;
57 | }
58 |
59 | &.collapsed {
60 | ul {
61 | display: none;
62 | }
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/styles/LogDialog.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | .git-menu.dialog.git-log {
4 | .logs {
5 | height: 36em;
6 | resize: none;
7 | color: @text-color-highlight;
8 | }
9 |
10 | .format-info {
11 | margin-left: 0.5em;
12 | color: @text-color-subtle;
13 | line-height: 12px;
14 |
15 | &:focus,
16 | &:hover {
17 | color: @text-color-highlight;
18 | }
19 |
20 | .icon::before {
21 | margin-right: 0;
22 | font-size: 12px;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/styles/dialog.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | .git-menu.dialog {
4 | background: @inset-panel-background-color;
5 | border: 1px solid @inset-panel-border-color;
6 | border-radius: 4px;
7 | padding: 15px;
8 | opacity: 0.95;
9 |
10 | .input-label .input-checkbox,
11 | .input-label .input-radio,
12 | .input-label .input-toggle,
13 | button {
14 | margin-right: 10px;
15 | }
16 |
17 | .body {
18 | margin-bottom: 15px;
19 | }
20 |
21 | .heading {
22 | margin-bottom: 15px;
23 |
24 | strong {
25 | color: @text-color-highlight;
26 | display: inline-block;
27 | }
28 |
29 | i.icon {
30 | float: right;
31 | width: 0;
32 | margin-right: 15px;
33 | cursor: pointer;
34 |
35 | &:before {
36 | font-size: 22px;
37 | line-height: 17px;
38 | }
39 |
40 | &:hover {
41 | color: @text-color-highlight;
42 | }
43 | }
44 | }
45 |
46 | .checkbox-label,
47 | input[type="text"],
48 | select,
49 | textarea {
50 | margin-bottom: 15px;
51 | font-size: inherit;
52 | }
53 |
54 | button,
55 | input[type="text"],
56 | select,
57 | textarea {
58 | &:focus {
59 | border-color: @text-color-info;
60 | box-shadow: 0 0 0 1px @text-color-info;
61 | }
62 |
63 | &.error {
64 | border-color: @text-color-error;
65 | box-shadow: 0 0 0 1px @text-color-error;
66 | }
67 | }
68 |
69 | input[type="checkbox"],
70 | input[type="radio"] {
71 | &:focus {
72 | box-shadow: 0 0 0 2px @text-color-info;
73 | }
74 |
75 | &.error {
76 | box-shadow: 0 0 0 2px @text-color-error;
77 | }
78 | }
79 |
80 | .input-disabled,
81 | input[readonly],
82 | select[readonly],
83 | textarea[readonly] {
84 | color: @text-color-subtle;
85 | }
86 |
87 | input[type=text],
88 | label.input-label,
89 | select {
90 | display: block;
91 | width: 100%;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/styles/status.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 |
3 | .git-menu.status {
4 | display: inline-block;
5 |
6 | &.hidden {
7 | display: none;
8 | }
9 |
10 | progress:indeterminate {
11 | max-width: 40px;
12 | }
13 |
14 | > span {
15 | margin-left: 0.5em;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------