├── .brackets.json ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── docs ├── FEATURES.md ├── GIT-FTP.md ├── images │ ├── blame.gif │ └── debug_mode.gif └── src │ └── git-ftp ├── main.js ├── nls ├── de │ └── strings.js ├── en-gb │ └── strings.js ├── fr │ └── strings.js ├── generate.js ├── it │ └── strings.js ├── pl │ └── strings.js ├── pt-br │ └── strings.js ├── root │ └── strings.js ├── strings.js └── zh-cn │ └── strings.js ├── package.json ├── requirejs-config.json ├── screenshots ├── Thumbs.db ├── commit-dialog.jpg ├── gitInstall.png ├── history-details.jpg ├── history.jpg ├── main.jpg └── settings-dialog.jpg ├── shell ├── iterm.2.9.osa ├── iterm.osa ├── terminal.osa └── terminal.sh ├── src ├── BracketsEvents.ts ├── Branch.ts ├── ChangelogDialog.ts ├── Cli.ts ├── CloseNotModified.ts ├── ErrorHandler.ts ├── EventEmitter.ts ├── Events.ts ├── ExpectedError.ts ├── ExtensionInfo.ts ├── GutterManager.ts ├── History.ts ├── HistoryViewer.ts ├── Main.ts ├── NoRepo.ts ├── Panel.ts ├── Preferences.ts ├── ProjectTreeMarks.ts ├── Remotes.ts ├── SettingsDialog.ts ├── TerminalIcon.ts ├── Utils.ts ├── brackets-modules.ts ├── declarations │ ├── brackets.ts │ ├── jquery.ts │ ├── require.ts │ ├── strings.ts │ └── window.ts ├── dialogs │ ├── Clone.ts │ ├── Progress.ts │ ├── Pull.ts │ ├── Push.ts │ ├── RemoteCommon.ts │ └── templates │ │ ├── clone-dialog.html │ │ ├── credentials-template.html │ │ ├── progress-dialog.html │ │ ├── pull-dialog.html │ │ ├── push-dialog.html │ │ └── remotes-template.html ├── ftp │ ├── Ftp.ts │ └── GitFtp.ts ├── git │ ├── Git.ts │ ├── GitCli.ts │ └── get-merge-info.ts ├── node │ ├── cli.ts │ ├── package.json │ ├── process-utils.ts │ ├── processUtils-test.ts │ └── tsconfig.json └── utils │ ├── Setup.ts │ └── Terminal.ts ├── strings.js ├── styles ├── brackets-git.less ├── brackets │ └── brackets_core_ui_variables.less ├── code-mirror.less ├── colors.less ├── commit-diff.less ├── common.less ├── dialogs │ ├── diff-dialog.less │ ├── error-dialog.less │ ├── progress.less │ └── pull-dialog.less ├── editor-holder.less ├── fonts │ ├── octicon.less │ ├── octicons-regular-webfont.eot │ ├── octicons-regular-webfont.svg │ ├── octicons-regular-webfont.ttf │ └── octicons-regular-webfont.woff ├── ftp │ └── ftp.less ├── history.less ├── icons │ ├── git-icon.svg │ ├── terminal-icon.svg │ └── warning-icon.svg └── mixins.less ├── templates ├── authors-dialog.html ├── branch-merge-dialog.html ├── branch-new-dialog.html ├── default-gitignore ├── error-report.md ├── format-diff.html ├── ftp │ └── remotes-picker.html ├── git-branches-menu.html ├── git-changelog-dialog.html ├── git-commit-dialog.html ├── git-diff-dialog.html ├── git-error-dialog.html ├── git-output.html ├── git-panel-history-commits.html ├── git-panel-history.html ├── git-panel-results.html ├── git-panel.html ├── git-question-dialog.html ├── git-remotes-picker.html ├── git-settings-dialog.html ├── git-tag-dialog.html ├── history-viewer-files.html └── history-viewer.html ├── tsconfig.json └── tslint.json /.brackets.json: -------------------------------------------------------------------------------- 1 | { 2 | "smartIndent": true, 3 | "spaceUnits": 4, 4 | "useTabChar": false, 5 | "language": { 6 | "javascript": { 7 | "linting.prefer": ["ESLint"], 8 | "linting.usePreferredOnly": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "typescript-eslint-parser", 3 | parserOptions: { sourceType: "module" }, 4 | extends: "pureprofile", 5 | env: { es6: true, node: true }, 6 | rules: { 7 | "indent": ["error", 4, { "SwitchCase": 1 }], 8 | "quotes": ["error", "double"], 9 | "no-const-assign": 0, // conflicts with ts 10 | "no-extra-parens": 0, // conflicts with ts 11 | "no-param-reassign": 0, // remove later 12 | "no-undef": 0, // conflicts with ts 13 | "no-undefined": 0, // conflicts with ts 14 | "no-unused-expressions": 0, // conflicts with ts 15 | "no-unused-vars": 0, // conflicts with ts 16 | "no-use-before-define": 0, // conflicts with ts 17 | "require-await": 0, // this doesn"t work well with our express async middleware 18 | "sort-imports": 0, // this is too much effort to fix 19 | "space-infix-ops": 0 // conflicts with ts 20 | }, 21 | globals: { 22 | ___dirname: false 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | /src/node/node_modules/ 4 | /.DS_Store 5 | /brackets-git.zip 6 | /git.log 7 | /npm-debug.log 8 | /thirdparty/*.browser.js 9 | /.vscode 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Opening issues 2 | 3 | Before opening a new issue, please be sure to check out list of already open issues [here](https://github.com/zaggino/brackets-git/issues?state=open) so we can prevent having duplicates. 4 | Always try to get as much relevant information from Brackets console (F12) as possible. To get more detailed output from console, turn on the DEBUG mode in Git settings dialog. (This mode will slow your extension down so don't forget to turn it off when you don't need it anymore) 5 | 6 | ## Pull requests 7 | 8 | When creating a new functionality which isn't noted on GitHub, please open an issue for it before working on the code as others may have hints and suggestions to consider before writing the code. Also when fixing an issue, add a comment to the issue on GitHub before working on it so it doesn't happen that two people are working on the same thing at the same time. 9 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const _ = require("lodash"); 4 | const exec = require("child_process").exec; 5 | const glob = require("glob"); 6 | const path = require("path"); 7 | 8 | module.exports = function (grunt) { 9 | 10 | grunt.initConfig({ 11 | pkg: grunt.file.readJSON("package.json"), 12 | lesslint: { 13 | src: ["styles/**/*.less"], 14 | options: { 15 | csslint: { 16 | "ids": false, 17 | "important": false, 18 | "known-properties": false 19 | } 20 | } 21 | }, 22 | zip: { 23 | main: { 24 | dest: "brackets-git.zip", 25 | src: [ 26 | "nls/**", 27 | "node_modules/**", 28 | "shell/**", 29 | "src/**", 30 | "styles/**", 31 | "templates/**", 32 | "thirdparty/**", 33 | "LICENSE", "*.js", "*.json", "*.md" 34 | ] 35 | } 36 | }, 37 | lineending: { 38 | dist: { 39 | options: { 40 | eol: "lf", 41 | overwrite: true 42 | }, 43 | files: { 44 | "": [ 45 | "main.js", 46 | "strings.js", 47 | "Gruntfile.js", 48 | "nls/**/*.js", 49 | "shell/**/*.*", 50 | "src/**/*.js", 51 | "styles/**/*.less", 52 | "templates/**/*.html", 53 | "thirdparty/**/*.js" 54 | ] 55 | } 56 | } 57 | } 58 | }); 59 | 60 | function runNpmInstall(where, callback) { 61 | grunt.log.writeln("running npm install in " + where); 62 | exec("npm install", { cwd: "./" + where }, (err, stdout, stderr) => { 63 | if (err) { 64 | grunt.log.error(stderr); 65 | } else { 66 | if (stdout) { grunt.log.writeln(stdout); } 67 | grunt.log.writeln("finished npm install in " + where); 68 | } 69 | return err ? callback(stderr) : callback(null, stdout); 70 | }); 71 | } 72 | 73 | grunt.registerTask("npm-install-subfolders", "install node_modules in src subfolders", function () { 74 | const doneWithTask = this.async(); 75 | const globs = ["src/**/package.json"]; 76 | const doneWithGlob = _.after(globs.length, doneWithTask); 77 | globs.forEach(g => { 78 | glob(g, (globErr, _files) => { 79 | if (globErr) { 80 | grunt.log.error(globErr); 81 | return doneWithTask(false); 82 | } 83 | const files = _files.filter((p) => p.indexOf("node_modules") === -1); 84 | const doneWithFile = _.after(files.length, doneWithGlob); 85 | files.forEach((file) => { 86 | runNpmInstall(path.dirname(file), (err) => { 87 | return err ? doneWithTask(false) : doneWithFile(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | }); 94 | 95 | grunt.loadNpmTasks("grunt-lesslint"); 96 | grunt.loadNpmTasks("grunt-zip"); 97 | grunt.loadNpmTasks("grunt-lineending"); 98 | 99 | grunt.registerTask("postinstall", ["npm-install-subfolders"]); 100 | grunt.registerTask("package", ["lineending", "zip"]); 101 | grunt.registerTask("less-test", ["lesslint"]); 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present Martin Zagora and other contributors 4 | https://github.com/zaggino/brackets-git/graphs/contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brackets-Git [![build status](https://travis-ci.org/zaggino/brackets-git.svg?branch=master)](https://travis-ci.org/zaggino/brackets-git) 2 | 3 | Brackets-Git is an extension for [Brackets](http://brackets.io/) editor - it provides Git integration for Brackets. 4 | It's tested and works on any platform supported by Brackets (Windows, Mac OS X, GNU/Linux). 5 | 6 | ## Installation 7 | 8 | #### Dependencies: 9 | To make **Brackets-Git** work you'll need Git installed in your system: 10 | 11 | - **Windows**: [Git for Windows](http://msysgit.github.io/) is recommended with these [settings](https://raw.github.com/zaggino/brackets-git/master/screenshots/gitInstall.png). 12 | - **Mac OS X**: [Git for Mac](http://git-scm.com/download/mac) is recommended. 13 | - **GNU/Linux**: Install the package `git`: 14 | - [Debian/Ubuntu](https://launchpad.net/~git-core/+archive/ppa) using [this guide](http://askmetutorials.blogspot.com.au/2014/03/install-git-191-on-ubuntu-linuxmint.html): 15 | 16 | ``` 17 | sudo add-apt-repository ppa:git-core/ppa 18 | sudo apt-get update 19 | sudo apt-get install git 20 | ``` 21 | 22 | - RedHat/CentOS/Fedora: `sudo yum install git` 23 | 24 | #### Extension installation: 25 | To install latest release of **Brackets-Git** use the built-in Brackets Extension Manager which downloads the extension from the [extension registry](https://brackets-registry.aboutweb.com/). 26 | 27 | #### Configuration: 28 | Extension can be configured by opening the Git Panel and clicking the ![settings...][settingsIcon] button. 29 | Alternatively you can use `File > Git Settings...` in the Brackets menu. 30 | 31 | ## Features and limitations 32 | 33 | You can find some samples of features [here](docs/FEATURES.md). 34 | 35 | Currently **Brackets-Git** supports these features (this list may be incomplete as we add new features regularly): 36 | 37 | - `init` / `clone` / `push` / `pull` 38 | - `create` / `delete` / `merge` branches 39 | - `select` / `define` / `delete` remotes 40 | - `checkout` / `reset` commits 41 | - show commit history 42 | - manage different Git settings 43 | - support for [Git-FTP](http://git-ftp.github.io/git-ftp/) ([installation instructions](docs/GIT-FTP.md)) 44 | 45 | A comprehensive list of Brackets-Git features is available reading the [`CHANGELOG.md`](CHANGELOG.md). 46 | Most of the features available are configurable and it's possible to enable and disable them selectively. 47 | If you can't find the feature you were looking for, feel free to [open an issue](https://github.com/brackets-userland/brackets-git/issues) with your idea(s). 48 | 49 | **Pull/Push to password protected repositories:** 50 | Push/Pull from and to password protected repositories is partially supported, currently it works only with `http` / `https` repositories. 51 | 52 | The [Git Credential Manager for Windows (GCM)](https://github.com/Microsoft/Git-Credential-Manager-for-Windows) is recommended to manage password protected repositories, **Brackets-Git** will eventually provide better support for them. 53 | You'll need to push manually the first time to setup your username/password into the credentials helper. 54 | 55 | **Working with SSH repositories:** 56 | SSH protocol is currently a bit more difficult, so you'll have to use command line or try to follow these [tips](https://github.com/zaggino/brackets-git/issues/524): 57 | 58 | - Linux 59 | - See [help.github.com/articles/generating-ssh-keys/](https://help.github.com/articles/generating-ssh-keys/) 60 | 61 | - Mac OSX 62 | - Create a ssh pair key with the following command on the terminal `$ ssh-keygen -t rsa -b 2048 -C "your@email.com"` 63 | - Enter a `"Secure"` passphrase or alternatively hit `Enter` twice if you don't want a passphrase `"Not Recommended"` 64 | - You should be given a string like the following `"99:ff:ff:4b:bb:2d:d3:17:h6:6d:f0:55:4d:f0:b4:db your@email.com"` 65 | - Pay attention to this line `"Your public key has been saved in /Users/USERNAME/.ssh/id_rsa.pub."` the *.pub is what you want here, take note that the names may vary. 66 | - Now start the SSH agent `"eval "$(ssh-agent -s)" "` Which should give you output like this `"Agent pid 77398"` 67 | - Next type `"ssh-add ~/.ssh/id_rsa"` 68 | - Next we want to copy the SSH key into your clipboard, there are two ways of doing this. 69 | - Method #1 type into Terminal `"pbcopy < ~/.ssh/id_rsa.pub"` taking note of before that "Your file" is named `"id_rsa.pub"`. If it is not, change it to whatever yours may be called. That's it proceed to next step. 70 | - Method #2 if for some reason you can't do method #1 this is your alternative. Find the `id_rsa.pub` key or otherwise `named.pub` key and open it with a text editor, copy the whole key including email into your clipboard (command + c). That's it. 71 | - DO NOT DELETE THE FILE! 72 | 73 | - Now we add the `SSHKEYFILE.pub` to the authorized_keys onto the git server (GitHub website). 74 | - Login to GitHub 75 | - Click your avatar -> Settings on the top right corner 76 | - Click the left hand side menu `"SSH and GPG keys"` -> `New SSH Key` 77 | - Input the name of this key, in my instance I named it `"Macbook Pro Git Key"` 78 | - Paste your key with (command + c) or right click Paste 79 | - Click `"ADD KEY"` and you're done (NOTE: You may be asked for a password) 80 | 81 | - Now to test if everything is Working In Terminal Type after the $ `"ssh -T git@github.com"` Type `"Yes"` And close. 82 | - If it does not connect, check your Internet and that you have not missed a step :) 83 | 84 | - Windows (Go to point 3 if you have already a RSA key already generated) 85 | 86 | - Create a ssh pair key with PuttyGen RSA with 2048 bytes. Don't add any password. Save the PPK and upload the public key to the git server. 87 | - Add the PPK key to the Putty agent. 88 | - ONLY IF YOU HAVE A RSA Key already from the server. You need to convert the private key to PPK. With PuttyGen load the sshkeyfilename (this file comes without extension, after loaded save it as private key. After that load this key in Putty Agent. 89 | - Insert (if not already) the pub key to the server inside the folder `/root/.ssh/authorized_keys`. (edit with `vi` the file `authorized_keys` and paste the pub key content on the file.) 90 | - Putty manage the private keys with a SSH agent always present in the task bar. 91 | 92 | ## Some screenshots: 93 | 94 | ![main](screenshots/main.jpg) 95 | *Main panel of Brackets Git* 96 | 97 | ![history](screenshots/history.jpg) 98 | *History panel of Brackets Git* 99 | 100 | ![history-details](screenshots/history-details.jpg) 101 | *Details view for a specific commit* 102 | 103 | ![commit dialog](screenshots/commit-dialog.jpg) 104 | *Commit dialog* 105 | 106 | ![settings dialog](screenshots/settings-dialog.jpg) 107 | *Settings dialog* 108 | 109 | ## Contributing 110 | 111 | Please see [`CONTRIBUTING.md`](CONTRIBUTING.md) 112 | 113 | 114 | [settingsIcon]: https://cloud.githubusercontent.com/assets/5382443/2535525/c0e254b0-b58f-11e3-9be3-9024641e5a2a.png 115 | -------------------------------------------------------------------------------- /docs/FEATURES.md: -------------------------------------------------------------------------------- 1 | # Feature list 2 | 3 | - [View authors / git blame](#blame) 4 | - [Debug mode](#debug) 5 | 6 | ## Blame 7 | 8 | Uses `git blame` to view authors of a file or a selection 9 | 10 | ![blame-image](images/blame.gif) 11 | 12 | ## Debug 13 | 14 | You can get detailed git communication and events to the console with simple setting. 15 | *Don't forget to turn this off as it really slows things down while working.* 16 | 17 | ![debug-image](images/debug_mode.gif) 18 | 19 | (animations captured with LICEcap) -------------------------------------------------------------------------------- /docs/GIT-FTP.md: -------------------------------------------------------------------------------- 1 | # How to install Git-FTP 2 | 3 | **Brackets Git** offers built-in support for **Git-FTP**, an FTP extension which allows sync repositories using FTP, to install this extension follow these steps: 4 | 5 | 1. Download the **Git-FTP** script [from this link](/docs/src/git-ftp) (N.B.: the script does not have an extension) 6 | 2. Move the downloaded script inside your Git binaries installation folder (usually `C:\Program Files\Git\bin\` on Windows, `/usr/bin` on GNU/Linux or `/usr/local/bin` on OS X) 7 | 3. Open **Brackets Git** settings and enable Git-FTP. 8 | 9 | Now you should be able to use **Git-FTP** through **Brackets Git**. 10 | -------------------------------------------------------------------------------- /docs/images/blame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/docs/images/blame.gif -------------------------------------------------------------------------------- /docs/images/debug_mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/docs/images/debug_mode.gif -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | // TODO: thirdparty module 4 | require("bluebird"); 5 | require("eventemitter2"); 6 | require("marked"); 7 | require("blueimp-md5"); 8 | require("moment"); 9 | require("URI"); 10 | 11 | // TODO: do a templates module 12 | require("text!src/dialogs/templates/clone-dialog.html"); 13 | require("text!src/dialogs/templates/credentials-template.html"); 14 | require("text!src/dialogs/templates/progress-dialog.html"); 15 | require("text!src/dialogs/templates/pull-dialog.html"); 16 | require("text!src/dialogs/templates/push-dialog.html"); 17 | require("text!src/dialogs/templates/remotes-template.html"); 18 | require("text!templates/git-branches-menu.html"); 19 | require("text!templates/branch-new-dialog.html"); 20 | require("text!templates/branch-merge-dialog.html"); 21 | require("text!templates/git-changelog-dialog.html"); 22 | require("text!templates/error-report.md"); 23 | require("text!templates/git-error-dialog.html"); 24 | require("text!templates/git-panel-history.html"); 25 | require("text!templates/git-panel-history-commits.html"); 26 | require("text!templates/history-viewer.html"); 27 | require("text!templates/history-viewer-files.html"); 28 | require("text!templates/default-gitignore"); 29 | require("text!templates/git-panel.html"); 30 | require("text!templates/git-panel-results.html"); 31 | require("text!templates/authors-dialog.html"); 32 | require("text!templates/git-commit-dialog.html"); 33 | require("text!templates/git-tag-dialog.html"); 34 | require("text!templates/git-diff-dialog.html"); 35 | require("text!templates/git-question-dialog.html"); 36 | require("text!templates/git-remotes-picker.html"); 37 | require("text!templates/git-settings-dialog.html"); 38 | require("text!templates/format-diff.html"); 39 | require("text!templates/git-question-dialog.html"); 40 | require("text!templates/git-output.html"); 41 | 42 | // Brackets modules 43 | var _ = brackets.getModule("thirdparty/lodash"), 44 | AppInit = brackets.getModule("utils/AppInit"), 45 | CommandManager = brackets.getModule("command/CommandManager"), 46 | Commands = brackets.getModule("command/Commands"), 47 | ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), 48 | Menus = brackets.getModule("command/Menus"), 49 | NodeConnection = brackets.getModule("utils/NodeConnection"); 50 | 51 | // Local modules 52 | var SettingsDialog = require("dist/SettingsDialog"), 53 | EventEmitter = require("dist/EventEmitter").default, 54 | Events = require("dist/Events"), 55 | Main = require("dist/Main"), 56 | Preferences = require("dist/Preferences"), 57 | Strings = require("strings"); 58 | 59 | window.bracketsGit = window.bracketsGit || {}; 60 | window.bracketsGit.getExtensionPath = function () { 61 | return ExtensionUtils.getModulePath(module); 62 | } 63 | window.bracketsGit.EventEmitter = EventEmitter; 64 | window.bracketsGit.Events = Events; 65 | 66 | // Load extension modules that are not included by core 67 | var modules = [ 68 | "dist/BracketsEvents", 69 | "dist/GutterManager", 70 | "dist/History", 71 | "dist/NoRepo", 72 | "dist/ProjectTreeMarks", 73 | "dist/Remotes", 74 | "dist/utils/Terminal" 75 | ]; 76 | if (Preferences.get("useGitFtp")) { modules.push("dist/ftp/Ftp"); } 77 | if (Preferences.get("showTerminalIcon")) { modules.push("dist/TerminalIcon"); } 78 | require(modules); 79 | 80 | // Load CSS 81 | ExtensionUtils.loadStyleSheet(module, "styles/brackets-git.less"); 82 | ExtensionUtils.loadStyleSheet(module, "styles/fonts/octicon.less"); 83 | if (Preferences.get("useGitFtp")) { ExtensionUtils.loadStyleSheet(module, "styles/ftp/ftp.less"); } 84 | 85 | // Register command and add it to the menu. 86 | var SETTINGS_COMMAND_ID = "brackets-git.settings"; 87 | CommandManager.register(Strings.GIT_SETTINGS, SETTINGS_COMMAND_ID, SettingsDialog.show); 88 | Menus.getMenu(Menus.AppMenuBar.FILE_MENU).addMenuItem(SETTINGS_COMMAND_ID, "", Menus.AFTER, Commands.FILE_PROJECT_SETTINGS); 89 | 90 | AppInit.appReady(function () { 91 | Main.init(); 92 | }); 93 | 94 | var nodeDomains = {}; 95 | 96 | // keeps a track of who is accessing node domains 97 | NodeConnection.prototype.loadDomains = _.wrap(NodeConnection.prototype.loadDomains, function (loadDomains) { 98 | 99 | var paths = arguments[1]; 100 | if (!Array.isArray(paths)) { paths = [paths]; } 101 | 102 | var extId = "unknown"; 103 | var stack = new Error().stack.split("\n").slice(2).join("\n"); 104 | var m = stack.match(/extensions\/user\/([^\/]+)/); 105 | if (m) { 106 | extId = m[1]; 107 | } 108 | 109 | if (!nodeDomains[extId]) { nodeDomains[extId] = []; } 110 | nodeDomains[extId] = _.uniq(nodeDomains[extId].concat(paths)); 111 | 112 | // call the original method 113 | return loadDomains.apply(this, _.toArray(arguments).slice(1)); 114 | }); 115 | 116 | }); 117 | -------------------------------------------------------------------------------- /nls/en-gb/strings.js: -------------------------------------------------------------------------------- 1 | define({ 2 | DATE_FORMAT: "DD/MM/YYYY HH:mm:ss" 3 | }); 4 | -------------------------------------------------------------------------------- /nls/generate.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | var dirContents = fs.readdirSync(__dirname); 4 | 5 | var langs = dirContents.reduce(function (arr, entry) { 6 | if (entry.indexOf(".js") !== -1) { return arr; } 7 | if (entry.indexOf("root") !== -1) { return arr; } 8 | arr.push(entry); 9 | return arr; 10 | }, []); 11 | 12 | var rootFile = fs.readFileSync("./root/strings.js").toString("utf8"); 13 | 14 | langs.forEach(function (lang) { 15 | 16 | var fileName = "./" + lang + "/strings.js"; 17 | 18 | var langFile = fs.readFileSync(fileName).toString("utf8"); 19 | 20 | var langLines = langFile.split("\n").reduce(function (arr, line) { 21 | if (line.match(/^\s*[a-zA-Z0-9_]+:/)) { 22 | arr.push(line); 23 | } else { 24 | console.log("ignored line (" + lang + "): " + line); 25 | } 26 | return arr; 27 | }, []); 28 | 29 | var langObj = langLines.reduce(function (obj, line) { 30 | var m = line.match(/\s*(\S+):\s*(\S.*)/); 31 | obj[m[1]] = m[2]; 32 | return obj; 33 | }, {}); 34 | 35 | var newLines = rootFile.split("\n").reduce(function (arr, line) { 36 | 37 | var m = line.match(/^(\s*)(\S+):(\s*)(\S.*)/); 38 | if (m) { 39 | var white = m[1]; 40 | var key = m[2]; 41 | var white2 = m[3]; 42 | var val = m[4]; 43 | arr.push(white + "// " + key + ":" + white2 + val); 44 | var newVal = langObj[key]; 45 | if (newVal) { 46 | arr.push(white + key + ":" + white2 + " " + langObj[key]); 47 | } else { 48 | arr.push(white + "// TODO: localize " + key + " to " + lang); 49 | } 50 | } else { 51 | arr.push(line); 52 | } 53 | 54 | return arr; 55 | 56 | }, []); 57 | 58 | fs.writeFile(fileName, newLines.join("\n")); 59 | console.log(lang + " done!"); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /nls/strings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @see: https://github.com/adobe/brackets/tree/master/src/extensions/samples/LocalizationExample 3 | */ 4 | define(function (require, exports, module) { 5 | // Code that needs to display user strings should call require("strings") to load 6 | // strings.js. This file will dynamically load strings.js for the specified by bracketes.locale. 7 | // 8 | // Translations for other locales should be placed in nls/>/strings.js 9 | // Localization is provided via the i18n plugin. 10 | // All other bundles for languages need to add a prefix to the exports below so i18n can find them. 11 | module.exports = { 12 | root: true, 13 | "en-gb": true, 14 | "de": true, 15 | "pl": true, 16 | "pt-br": true, 17 | "zh-cn": true, 18 | "it": true, 19 | "fr": true 20 | }; 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brackets-git", 3 | "title": "Brackets Git", 4 | "version": "1.0.0", 5 | "engines": { 6 | "brackets": ">=1.8.0" 7 | }, 8 | "description": "Integration of Git into Brackets", 9 | "keywords": [ 10 | "brackets-extension", 11 | "git", 12 | "version-control", 13 | "source-control" 14 | ], 15 | "homepage": "https://github.com/zaggino/brackets-git", 16 | "bugs": "https://github.com/zaggino/brackets-git/issues", 17 | "license": "MIT", 18 | "author": { 19 | "name": "Martin Zagora", 20 | "email": "zaggino@gmail.com", 21 | "url": "https://github.com/zaggino" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/zaggino/brackets-git.git" 26 | }, 27 | "scripts": { 28 | "postinstall": "npm run webpack-all && grunt postinstall && npm run build", 29 | "build": "rimraf ./dist && npm run tsc-projects", 30 | "tsc-projects": "concurrently \"tsc\" \"tsc -p src/node\"", 31 | "dev": "concurrently \"tsc --watch\" \"tsc -p src/node --watch\"", 32 | "test": "npm run build && npm run eslint && npm run tslint && npm run tslint-node", 33 | "eslint": "eslint --ext .ts ./src", 34 | "tslint": "tslint -c tslint.json --project tsconfig.json", 35 | "tslint-node": "tslint -c tslint.json --project src/node/tsconfig.json", 36 | "webpack-all": "npm run webpack-bluebird && npm run webpack-blueimp-md5 && npm run webpack-eventemitter2 && npm run webpack-marked && npm run webpack-moment && npm run webpack-urijs", 37 | "webpack-bluebird": "webpack ./node_modules/bluebird/js/main/bluebird.js ./thirdparty/bluebird.browser.js -p --output-library-target=amd", 38 | "webpack-blueimp-md5": "webpack ./node_modules/blueimp-md5/js/md5.js ./thirdparty/blueimp-md5.browser.js -p --output-library-target=amd", 39 | "webpack-eventemitter2": "webpack ./node_modules/eventemitter2/lib/eventemitter2.js ./thirdparty/eventemitter2.browser.js -p --output-library-target=amd", 40 | "webpack-marked": "webpack ./node_modules/marked/lib/marked.js ./thirdparty/marked.browser.js -p --output-library-target=amd", 41 | "webpack-moment": "webpack ./node_modules/moment/moment.js ./thirdparty/moment.browser.js -p --output-library-target=amd", 42 | "webpack-urijs": "webpack ./node_modules/urijs/src/URI.js ./thirdparty/urijs.browser.js -p --output-library-target=amd" 43 | }, 44 | "dependencies": { 45 | "bluebird": "2.11.0", 46 | "blueimp-md5": "2.6.0", 47 | "eventemitter2": "2.2.1", 48 | "lodash": "4.17.4", 49 | "marked": "0.3.9", 50 | "moment": "2.21.0", 51 | "urijs": "1.18.4", 52 | "webpack": "1.14.0", 53 | "which": "1.2.12" 54 | }, 55 | "devDependencies": { 56 | "@types/bluebird": "2.0.30", 57 | "@types/blueimp-md5": "1.1.30", 58 | "@types/eventemitter2": "2.2.1", 59 | "@types/jquery": "2.0.39", 60 | "@types/marked": "0.0.28", 61 | "@types/urijs": "1.15.31", 62 | "@types/which": "1.0.28", 63 | "concurrently": "3.1.0", 64 | "eslint": "3.13.1", 65 | "eslint-config-pureprofile": "2.2.0", 66 | "glob": "7.1.1", 67 | "grunt": "latest", 68 | "grunt-lesslint": "latest", 69 | "grunt-lineending": "latest", 70 | "grunt-zip": "latest", 71 | "rimraf": "2.5.4", 72 | "tslint": "4.3.1", 73 | "typescript": "2.1.5", 74 | "typescript-eslint-parser": "1.0.2" 75 | }, 76 | "i18n": [ 77 | "en", 78 | "en-gb", 79 | "de", 80 | "fr", 81 | "it", 82 | "pl", 83 | "pt-br", 84 | "zh-cn" 85 | ], 86 | "package-i18n": { 87 | "de": { 88 | "description": "Git-Integration für Brackets", 89 | "keywords": [ 90 | "git", 91 | "version-control", 92 | "source-control", 93 | "Versionsverwaltung", 94 | "Versionskontrollsystem" 95 | ] 96 | }, 97 | "pl": { 98 | "description": "Integracja Gita w Brackets", 99 | "keywords": [ 100 | "git", 101 | "version-control", 102 | "source-control", 103 | "kontrola-wersji", 104 | "system-kontroli-wersji" 105 | ] 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /requirejs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "bluebird": "./thirdparty/bluebird.browser", 4 | "eventemitter2": "./thirdparty/eventemitter2.browser", 5 | "marked": "./thirdparty/marked.browser", 6 | "blueimp-md5": "./thirdparty/blueimp-md5.browser", 7 | "moment": "./thirdparty/moment.browser", 8 | "URI": "./thirdparty/urijs.browser" 9 | }, 10 | "config": { 11 | "moment": { 12 | "noGlobal": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /screenshots/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/Thumbs.db -------------------------------------------------------------------------------- /screenshots/commit-dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/commit-dialog.jpg -------------------------------------------------------------------------------- /screenshots/gitInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/gitInstall.png -------------------------------------------------------------------------------- /screenshots/history-details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/history-details.jpg -------------------------------------------------------------------------------- /screenshots/history.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/history.jpg -------------------------------------------------------------------------------- /screenshots/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/main.jpg -------------------------------------------------------------------------------- /screenshots/settings-dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/screenshots/settings-dialog.jpg -------------------------------------------------------------------------------- /shell/iterm.2.9.osa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript 2 | on run argv 3 | 4 | set dir to quoted form of (first item of argv) 5 | 6 | -- set list of shells and other misc variables 7 | set shells to {"bash", "zsh", "ksh", "tcsh", "sh"} 8 | set run_cmd to "cd " & dir & " && git status" 9 | set repo_name to basename(dir) 10 | 11 | set git_window to null 12 | set git_tab to null 13 | set git_session to null 14 | 15 | tell application "iTerm" 16 | if ((count of windows) > 0) then 17 | repeat with this_window in windows 18 | tell this_window 19 | repeat with this_tab in tabs of this_window 20 | repeat with this_session in sessions of this_tab 21 | if name of this_session contains repo_name then 22 | repeat with i from 1 to (length of shells) -- check the session title for the default OS X shells 23 | if name of current session of this_tab contains item i of shells then -- the session title includes the currently-running command (uses the name of the shell if no other command is running) 24 | set git_session to this_session 25 | set git_tab to this_tab 26 | set git_window to (number of this_window) 27 | end if 28 | end repeat 29 | end if 30 | end repeat 31 | end repeat 32 | end tell 33 | end repeat 34 | end if 35 | 36 | if (count of windows) is 0 then -- if no windows exist, create a window 37 | create window with default profile -- create a new window if necessary 38 | set git_window to 1 39 | end if 40 | if git_tab is null then -- if windows exist but no git sessions exist, create a new tab 41 | tell current window 42 | set git_tab to create tab with default profile -- create a new tab in existing window 43 | set name of current session to repo_name 44 | tell current session to write text run_cmd 45 | end tell 46 | set git_window to 1 47 | else 48 | select window git_window 49 | select git_tab 50 | tell git_session 51 | write text run_cmd 52 | end tell 53 | end if 54 | 55 | activate 56 | 57 | end tell 58 | 59 | end run 60 | 61 | -- adapted from http://macscripter.net/viewtopic.php?id=13286 62 | on basename(the_path) -- Requires POSIX path 63 | set ASTID to AppleScript's text item delimiters 64 | set AppleScript's text item delimiters to "/" 65 | set the_path to text item -2 of the_path 66 | set AppleScript's text item delimiters to ASTID 67 | return the_path 68 | end basename 69 | -------------------------------------------------------------------------------- /shell/iterm.osa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript 2 | on run argv 3 | 4 | set dir to quoted form of (first item of argv) 5 | 6 | -- set list of shells and other misc variables 7 | set shells to {"bash", "zsh", "ksh", "tcsh", "sh"} 8 | set run_cmd to "cd " & dir & " && git status" 9 | set repo_name to basename(dir) 10 | 11 | -- set tab/session title 12 | set tab_title to "git: " & repo_name 13 | set git_terminal to null 14 | set git_session to null 15 | 16 | tell application "iTerm" 17 | activate 18 | 19 | if (count of terminals) > 0 then -- loop through all terminals 20 | repeat with ter in terminals 21 | set counter to 0 22 | repeat with ses in sessions of ter 23 | set counter to counter + 1 24 | set this_session_name to name of ses 25 | if this_session_name contains tab_title then -- check for terminal with this project’s name 26 | repeat with i from 1 to (length of shells) -- check the session title for the default OS X shells 27 | if the this_session_name contains item i of shells then -- the session title includes the currently-running command (uses the name of the shell if no other command is running) 28 | set git_terminal to ter 29 | set git_session to counter 30 | exit repeat 31 | end if 32 | end repeat 33 | exit repeat 34 | end if 35 | end repeat 36 | end repeat 37 | end if 38 | 39 | if git_session is null then -- if no sessions exist, create a session 40 | if (count of terminals) is 0 then 41 | set git_terminal to (make new terminal) -- create a new terminal if necessary 42 | else 43 | set git_terminal to front terminal 44 | end if 45 | tell git_terminal 46 | set git_session to launch session tab_title 47 | set the name of git_session to tab_title 48 | tell git_session to write text run_cmd 49 | end tell 50 | else 51 | -- select a currently-running session 52 | select git_terminal 53 | select session git_session of front terminal 54 | end if 55 | end tell 56 | end run 57 | 58 | -- adapted from http://macscripter.net/viewtopic.php?id=13286 59 | on basename(the_path) -- Requires POSIX path 60 | set ASTID to AppleScript's text item delimiters 61 | set AppleScript's text item delimiters to "/" 62 | set the_path to text item -2 of the_path 63 | set AppleScript's text item delimiters to ASTID 64 | return the_path 65 | end basename 66 | -------------------------------------------------------------------------------- /shell/terminal.osa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript 2 | on run argv 3 | 4 | set dir to quoted form of (first item of argv) 5 | set new_cmd to 0 6 | 7 | set doesExist to false 8 | try 9 | tell application "Finder" to get application file id "com.googlecode.iterm2" 10 | set doesExist to true 11 | on error 12 | set doesExist to false 13 | end try 14 | 15 | if doesExist then 16 | tell application "Finder" 17 | set myPath to container of (path to me) as text 18 | set app_version to (get version of application "iTerm") 19 | end tell 20 | if app_version >= 2.9 then 21 | run script (alias (myPath & "iterm.2.9.osa")) with parameters (argv) 22 | else 23 | run script (alias (myPath & "iterm.osa")) with parameters (argv) 24 | end 25 | 26 | else 27 | 28 | tell application "System Events" 29 | if "Terminal" is not in name of processes then 30 | launch application "Terminal" 31 | 32 | tell application "Terminal" 33 | activate 34 | do script "" 35 | end tell 36 | 37 | set new_cmd to 1 38 | end if 39 | end tell 40 | 41 | tell application "Terminal" 42 | if (new_cmd) is 1 then 43 | -- Terminal was just launched 44 | do script "cd " & dir & "; git status" in window 1 45 | else 46 | -- Terminal was already open 47 | activate 48 | do script "cd " & dir & "; git status" 49 | end if 50 | end tell 51 | 52 | end if 53 | 54 | do shell script "echo ok" 55 | 56 | end run 57 | -------------------------------------------------------------------------------- /shell/terminal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if command -v gnome-terminal >/dev/null 2>&1 3 | then 4 | gnome-terminal --window --working-directory="$1" 5 | elif command -v konsole >/dev/null 2>&1 6 | then 7 | konsole --workdir "$1" 8 | elif command -v xfce4-terminal >/dev/null 2>&1 9 | then 10 | xfce4-terminal --working-directory="$1" 11 | elif command -v xterm >/dev/null 2>&1 12 | then 13 | xterm -e 'cd $1 && bash' 14 | fi 15 | -------------------------------------------------------------------------------- /src/BracketsEvents.ts: -------------------------------------------------------------------------------- 1 | import { _, DocumentManager, FileSystem, ProjectManager, MainViewManager } from "./brackets-modules"; 2 | import * as Events from "./Events"; 3 | import EventEmitter from "./EventEmitter"; 4 | import * as HistoryViewer from "./HistoryViewer"; 5 | import * as Preferences from "./Preferences"; 6 | import * as Utils from "./Utils"; 7 | 8 | // White-list for .git file watching 9 | const watchedInsideGit = ["HEAD"]; 10 | 11 | FileSystem.on("change", (evt, file) => { 12 | // we care only for files in current project 13 | const currentGitRoot = Preferences.get("currentGitRoot"); 14 | if (file && file.fullPath.indexOf(currentGitRoot) === 0) { 15 | 16 | if (file.fullPath.indexOf(currentGitRoot + ".git/") === 0) { 17 | 18 | const whitelisted = _.any(watchedInsideGit, (entry) => { 19 | return file.fullPath === currentGitRoot + ".git/" + entry; 20 | }); 21 | if (!whitelisted) { 22 | Utils.consoleDebug("Ignored FileSystem.change event: " + file.fullPath); 23 | return; 24 | } 25 | 26 | } 27 | 28 | EventEmitter.emit(Events.BRACKETS_FILE_CHANGED, evt, file); 29 | } 30 | }); 31 | 32 | DocumentManager.on("documentSaved", (evt, doc) => { 33 | // we care only for files in current project 34 | if (doc.file.fullPath.indexOf(Preferences.get("currentGitRoot")) === 0) { 35 | EventEmitter.emit(Events.BRACKETS_DOCUMENT_SAVED, evt, doc); 36 | } 37 | }); 38 | 39 | MainViewManager.on("currentFileChange", (evt, currentDocument, previousDocument) => { 40 | if (!HistoryViewer.isVisible()) { 41 | const _currentDocument = currentDocument || DocumentManager.getCurrentDocument(); 42 | EventEmitter.emit(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, evt, _currentDocument, previousDocument); 43 | } else { 44 | HistoryViewer.hide(); 45 | } 46 | }); 47 | 48 | ProjectManager.on("projectOpen", () => EventEmitter.emit(Events.BRACKETS_PROJECT_CHANGE)); 49 | 50 | ProjectManager.on("projectRefresh", () => EventEmitter.emit(Events.BRACKETS_PROJECT_REFRESH)); 51 | 52 | // Disable Git when closing a project so listeners won't fire before new is opened 53 | ProjectManager.on("beforeProjectClose", () => EventEmitter.emit(Events.GIT_DISABLED)); 54 | -------------------------------------------------------------------------------- /src/ChangelogDialog.ts: -------------------------------------------------------------------------------- 1 | import { Dialogs, FileSystem, FileUtils, Mustache, StringUtils } from "./brackets-modules"; 2 | import * as Utils from "./Utils"; 3 | import * as Preferences from "./Preferences"; 4 | import * as Strings from "strings"; 5 | import * as marked from "marked"; 6 | 7 | const changelogDialogTemplate = require("text!templates/git-changelog-dialog.html"); 8 | 9 | let dialog; 10 | 11 | export function show() { 12 | Strings.EXTENSION_VERSION = Preferences.get("lastVersion"); 13 | const title = StringUtils.format(Strings.EXTENSION_WAS_UPDATED_TITLE, Strings.EXTENSION_VERSION); 14 | const compiledTemplate = Mustache.render(changelogDialogTemplate, { Strings, TITLE: title }); 15 | dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 16 | 17 | FileUtils.readAsText(FileSystem.getFileForPath(Utils.getExtensionDirectory() + "CHANGELOG.md")) 18 | .done((content) => { 19 | $("#git-changelog", dialog.getElement()).html(marked(content, { 20 | gfm: true, 21 | breaks: true 22 | })); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/CloseNotModified.ts: -------------------------------------------------------------------------------- 1 | import { DocumentManager, FileSystem, MainViewManager } from "./brackets-modules"; 2 | import * as Events from "./Events"; 3 | import EventEmitter from "./EventEmitter"; 4 | import * as Git from "./git/GitCli"; 5 | import * as Preferences from "./Preferences"; 6 | import * as Strings from "strings"; 7 | 8 | let $icon = $(null); 9 | 10 | function handleCloseNotModified(event) { 11 | let reopenModified = false; 12 | if (event.shiftKey) { 13 | reopenModified = true; 14 | } 15 | 16 | Git.status().then((modifiedFiles) => { 17 | const openFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES); 18 | const currentGitRoot = Preferences.get("currentGitRoot"); 19 | 20 | openFiles.forEach((openFile) => { 21 | let removeOpenFile = true; 22 | modifiedFiles.forEach((modifiedFile) => { 23 | if (currentGitRoot + modifiedFile.file === openFile.fullPath) { 24 | removeOpenFile = false; 25 | modifiedFile.isOpen = true; 26 | } 27 | }); 28 | 29 | if (removeOpenFile) { 30 | // check if file doesn't have any unsaved changes 31 | const doc = DocumentManager.getOpenDocumentForPath(openFile.fullPath); 32 | if (doc && doc.isDirty) { 33 | removeOpenFile = false; 34 | } 35 | } 36 | 37 | if (removeOpenFile && !reopenModified) { 38 | MainViewManager._close(MainViewManager.ALL_PANES, openFile); 39 | } 40 | }); 41 | 42 | if (reopenModified) { 43 | const filesToReopen = modifiedFiles.filter((modifiedFile) => !modifiedFile.isOpen); 44 | filesToReopen.forEach((fileObj) => { 45 | const fileEntry = FileSystem.getFileForPath(currentGitRoot + fileObj.file); 46 | MainViewManager.addToWorkingSet(MainViewManager.ACTIVE_PANE, fileEntry); 47 | }); 48 | } 49 | 50 | MainViewManager.focusActivePane(); 51 | }); 52 | } 53 | 54 | function updateIconState() { 55 | if (MainViewManager.getPaneCount() === 1 && 56 | MainViewManager.getWorkingSetSize(MainViewManager.ACTIVE_PANE) === 0) { 57 | $icon.toggleClass("working-set-not-available", true); 58 | $icon.toggleClass("working-set-available", false); 59 | } else { 60 | $icon.toggleClass("working-set-not-available", false); 61 | $icon.toggleClass("working-set-available", true); 62 | } 63 | } 64 | 65 | export function init() { 66 | // Add close not modified button near working files list 67 | $icon = $("
") 68 | .addClass("git-close-not-modified btn-alt-quiet") 69 | .attr("title", Strings.TOOLTIP_CLOSE_NOT_MODIFIED) 70 | .html("") 71 | .on("click", handleCloseNotModified) 72 | .appendTo("#sidebar"); 73 | updateIconState(); 74 | } 75 | 76 | EventEmitter.on(Events.GIT_ENABLED, () => $icon.show()); 77 | 78 | EventEmitter.on(Events.GIT_DISABLED, () => $icon.hide()); 79 | 80 | MainViewManager.on([ 81 | "workingSetAdd", 82 | "workingSetAddList", 83 | "workingSetRemove", 84 | "workingSetRemoveList", 85 | "workingSetUpdate", 86 | "paneCreate", 87 | "paneDestroy" 88 | ].join(" "), () => updateIconState()); 89 | -------------------------------------------------------------------------------- /src/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { _, Dialogs, Mustache, NativeApp } from "./brackets-modules"; 2 | import ExpectedError from "./ExpectedError"; 3 | import * as ExtensionInfo from "./ExtensionInfo"; 4 | import * as Strings from "strings"; 5 | import * as Utils from "./Utils"; 6 | 7 | const markdownReportTemplate = require("text!templates/error-report.md"); 8 | const errorDialogTemplate = require("text!templates/git-error-dialog.html"); 9 | const errorQueue = []; 10 | 11 | export interface ExtendedError extends Error { 12 | __shown?: boolean; 13 | detailsUrl?: string; 14 | match?: () => any; 15 | } 16 | 17 | function getMdReport(params) { 18 | return Mustache.render(markdownReportTemplate, _.defaults(params || {}, { 19 | brackets: [brackets.metadata.name, brackets.metadata.version, "(" + brackets.platform + ")"].join(" "), 20 | bracketsGit: "Brackets-Git " + ExtensionInfo.getSync().version, 21 | git: Strings.GIT_VERSION 22 | })).trim(); 23 | } 24 | 25 | function errorToString(err) { 26 | return Utils.encodeSensitiveInformation(err.toString()); 27 | } 28 | 29 | export function rewrapError(err, errNew) { 30 | const oldText = "Original " + err.toString(); 31 | let oldStack; 32 | if (err.stack) { 33 | if (err.stack.indexOf(err.toString()) === 0) { 34 | oldStack = "Original " + err.stack; 35 | } else { 36 | oldStack = oldText + "\n" + err.stack; 37 | } 38 | } 39 | if (typeof errNew === "string") { 40 | errNew = new Error(errNew); 41 | } 42 | errNew.toString = function () { 43 | return Error.prototype.toString.call(this) + "\n" + oldText; 44 | }; 45 | errNew.stack += "\n\n" + oldStack; 46 | return errNew; 47 | } 48 | 49 | function _reportBug(params) { 50 | ExtensionInfo.hasLatestVersion((hasLatestVersion, currentVersion, latestVersion) => { 51 | if (hasLatestVersion) { 52 | NativeApp.openURLInDefaultBrowser(params); 53 | } else { 54 | const err = new ExpectedError( 55 | "Latest version of extension is " + latestVersion + ", yours is " + currentVersion 56 | ); 57 | showError(err, "Outdated extension version!"); 58 | } 59 | }); 60 | } 61 | 62 | export function reportBug() { 63 | const mdReport = getMdReport({ 64 | errorStack: errorQueue.map((err, index) => "#" + (index + 1) + ". " + errorToString(err)).join("\n") 65 | }); 66 | _reportBug(ExtensionInfo.getSync().homepage + "/issues/new?body=" + encodeURIComponent(mdReport)); 67 | } 68 | 69 | export function isTimeout(err) { 70 | return err instanceof Error && ( 71 | err.message.indexOf("cmd-execute-timeout") === 0 || 72 | err.message.indexOf("cmd-spawn-timeout") === 0 73 | ); 74 | } 75 | 76 | export function equals(err, what) { 77 | return err.toString().toLowerCase() === what.toLowerCase(); 78 | } 79 | 80 | export function contains(err, what) { 81 | return err.toString().toLowerCase().indexOf(what.toLowerCase()) !== -1; 82 | } 83 | 84 | export function matches(err, regExp) { 85 | return err.toString().match(regExp); 86 | } 87 | 88 | export function logError(err: Error | string) { 89 | let msg = err; 90 | if (typeof err !== "string") { 91 | msg = err && err.stack ? err.stack : err; 92 | } 93 | Utils.consoleLog("[brackets-git] " + msg, "error"); 94 | errorQueue.push(err); 95 | return err; 96 | } 97 | 98 | export function showError(err: ExtendedError, title?: string) { 99 | if (err.__shown) { return err; } 100 | 101 | logError(err); 102 | 103 | let errorBody; 104 | let errorStack; 105 | 106 | let showDetailsButton = false; 107 | if (err.detailsUrl) { 108 | showDetailsButton = true; 109 | } 110 | 111 | if (typeof err === "string") { 112 | errorBody = err; 113 | } else if (err instanceof Error) { 114 | errorBody = errorToString(err); 115 | errorStack = err.stack || ""; 116 | } 117 | 118 | if (!errorBody || errorBody === "[object Object]") { 119 | try { 120 | errorBody = JSON.stringify(err, null, 4); 121 | } catch (e) { 122 | errorBody = "Error can't be stringified by JSON.stringify"; 123 | } 124 | } 125 | 126 | const compiledTemplate = Mustache.render(errorDialogTemplate, { 127 | title, 128 | body: errorBody, 129 | showDetailsButton, 130 | Strings 131 | }); 132 | 133 | const dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 134 | 135 | dialog.done((buttonId) => { 136 | if (buttonId === "report") { 137 | const mdReport = getMdReport({ 138 | title, 139 | errorBody, 140 | errorStack 141 | }); 142 | _reportBug(ExtensionInfo.getSync().homepage + "/issues/new?title=" + 143 | encodeURIComponent(title) + 144 | "&body=" + 145 | encodeURIComponent(mdReport)); 146 | } 147 | if (buttonId === "details") { 148 | NativeApp.openURLInDefaultBrowser(err.detailsUrl); 149 | } 150 | }); 151 | 152 | if (typeof err === "string") { err = new Error(err); } 153 | err.__shown = true; 154 | return err; 155 | } 156 | 157 | export function toError(arg) { 158 | // FUTURE: use this everywhere and have a custom error class for this extension 159 | if (arg instanceof Error) { return arg; } 160 | const err: ExtendedError = new Error(arg); 161 | // TODO: new class for this? 162 | err.match = function (...args) { 163 | return arg.match(...args); 164 | }; 165 | return err; 166 | } 167 | -------------------------------------------------------------------------------- /src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { _ } from "./brackets-modules"; 2 | import { EventEmitter2 } from "eventemitter2"; 3 | import * as Preferences from "./Preferences"; 4 | import * as Utils from "./Utils"; 5 | 6 | const debugOn = Preferences.get("debugMode"); 7 | 8 | export interface MyEventEmitter2 extends EventEmitter2 { 9 | _emit: Function; 10 | emitFactory: (eventName: string, ...args: any[]) => () => void; 11 | } 12 | 13 | const emInstance = new EventEmitter2({ 14 | wildcard: false 15 | }) as MyEventEmitter2; 16 | 17 | export default emInstance; 18 | 19 | if (debugOn) { 20 | emInstance._emit = emInstance.emit; 21 | emInstance.emit = function (...args) { 22 | 23 | const eventName = args.shift(); 24 | if (!eventName) { 25 | throw new Error("no event has been thrown!"); 26 | } 27 | 28 | const listenersCount = this.listeners(eventName).length; 29 | 30 | let argsString = args.map((arg) => { 31 | if (arg === null) { return "null"; } 32 | if (typeof arg === "undefined") { return "undefined"; } 33 | if (typeof arg === "function") { return "function(){...}"; } 34 | if (!arg.toString) { return Object.prototype.toString.call(arg); } 35 | return arg.toString(); 36 | }).join(", "); 37 | 38 | if (argsString) { argsString = " - " + argsString; } 39 | argsString = argsString + " (" + listenersCount + " listeners)"; 40 | 41 | if (listenersCount > 0) { 42 | Utils.consoleLog("[brackets-git] Event invoked: " + eventName + argsString); 43 | } 44 | 45 | return this._emit(eventName, ...args); 46 | }; 47 | } 48 | 49 | emInstance.emitFactory = function (eventName: string, ...args: any[]): () => void { 50 | if (!eventName) { 51 | throw new Error("no event has been passed to the factory!"); 52 | } 53 | 54 | let lastClick = 0; 55 | 56 | return (...args2) => { 57 | 58 | // prevent doubleclicks with 500ms timeout 59 | const now = new Date().valueOf(); 60 | if (now - lastClick < 500) { 61 | return; 62 | } 63 | lastClick = now; 64 | 65 | this.emit(eventName, ...args, ...args2); 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/Events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * List of Events to be used in the extension. 3 | * Events should be structured by file who emits them. 4 | */ 5 | 6 | // Brackets events 7 | export const BRACKETS_CURRENT_DOCUMENT_CHANGE = "brackets.current.document.change"; 8 | export const BRACKETS_PROJECT_CHANGE = "brackets.project.change"; 9 | export const BRACKETS_PROJECT_REFRESH = "brackets.project.refresh"; 10 | export const BRACKETS_DOCUMENT_SAVED = "brackets.document.saved"; 11 | export const BRACKETS_FILE_CHANGED = "brackets.file.changed"; 12 | 13 | // Git events 14 | export const GIT_USERNAME_CHANGED = "git.username.changed"; 15 | export const GIT_EMAIL_CHANGED = "git.email.changed"; 16 | export const GIT_COMMITED = "git.commited"; 17 | export const GIT_NO_BRANCH_EXISTS = "git.no.branch.exists"; 18 | export const GIT_CHANGE_USERNAME = "git.change.username"; 19 | export const GIT_CHANGE_EMAIL = "git.change.email"; 20 | 21 | // Gerrit events 22 | export const GERRIT_TOGGLE_PUSH_REF = "gerrit.toggle.push.ref"; 23 | export const GERRIT_PUSH_REF_TOGGLED = "gerrit.push.ref.toggled"; 24 | 25 | // Startup events 26 | export const REFRESH_ALL = "git.refresh.all"; 27 | export const GIT_ENABLED = "git.enabled"; 28 | export const GIT_DISABLED = "git.disabled"; 29 | export const REBASE_MERGE_MODE = "rebase.merge.mode"; 30 | 31 | // Panel.js 32 | export const HANDLE_GIT_INIT = "handle.git.init"; 33 | export const HANDLE_GIT_CLONE = "handle.git.clone"; 34 | export const HANDLE_GIT_COMMIT = "handle.git.commit"; 35 | export const HANDLE_FETCH = "handle.fetch"; 36 | export const HANDLE_PUSH = "handle.push"; 37 | export const HANDLE_PULL = "handle.pull"; 38 | export const HANDLE_REMOTE_PICK = "handle.remote.pick"; 39 | export const HANDLE_REMOTE_DELETE = "handle.remote.delete"; 40 | export const HANDLE_REMOTE_CREATE = "handle.remote.create"; 41 | export const HANDLE_FTP_PUSH = "handle.ftp.push"; 42 | export const HISTORY_SHOW = "history.show"; 43 | export const REFRESH_COUNTERS = "refresh.counters"; 44 | 45 | // Git results 46 | export const GIT_STATUS_RESULTS = "git.status.results"; 47 | 48 | // Remotes.js 49 | export const GIT_REMOTE_AVAILABLE = "git.remote.available"; 50 | export const GIT_REMOTE_NOT_AVAILABLE = "git.remote.not.available"; 51 | export const REMOTES_REFRESH_PICKER = "remotes.refresh.picker"; 52 | export const FETCH_STARTED = "remotes.fetch.started"; 53 | export const FETCH_COMPLETE = "remotes.fetch.complete"; 54 | 55 | // utils/Terminal.js 56 | export const TERMINAL_OPEN = "terminal.open"; 57 | export const TERMINAL_DISABLE = "terminal.disable"; 58 | -------------------------------------------------------------------------------- /src/ExpectedError.ts: -------------------------------------------------------------------------------- 1 | class ExpectedError extends Error { 2 | 3 | public detailsUrl: string | undefined; 4 | 5 | constructor(message: string) { 6 | super(message); 7 | this.name = "ExpectedError"; 8 | this.message = message; 9 | } 10 | 11 | public toString() { 12 | return this.name + ": " + this.message; 13 | } 14 | 15 | } 16 | 17 | export default ExpectedError; 18 | -------------------------------------------------------------------------------- /src/ExtensionInfo.ts: -------------------------------------------------------------------------------- 1 | import { _, ExtensionManager, ExtensionUtils, FileSystem, FileUtils } from "./brackets-modules"; 2 | import * as Promise from "bluebird"; 3 | 4 | let packageJson; 5 | 6 | function getPackageJsonPath() { 7 | const extensionPath = window.bracketsGit.getExtensionPath(); 8 | return extensionPath + "package.json"; 9 | } 10 | 11 | // immediately read the package json info 12 | let jsonPromise; 13 | 14 | // gets the promise for extension info which will be resolved once the package.json is read 15 | export function get() { 16 | if (jsonPromise) { 17 | return jsonPromise; 18 | } 19 | const readPromise = FileUtils.readAsText(FileSystem.getFileForPath(getPackageJsonPath())); 20 | jsonPromise = Promise.cast(readPromise) 21 | .then((content) => { 22 | packageJson = JSON.parse(content); 23 | return packageJson; 24 | }); 25 | return jsonPromise; 26 | } 27 | 28 | // gets the extension info from package.json, should be safe to call once extension is loaded 29 | export function getSync() { 30 | if (packageJson) { 31 | return packageJson; 32 | } 33 | throw new Error("[brackets-git] package.json is not loaded yet!"); 34 | } 35 | 36 | // triggers the registry download if registry hasn't been downloaded yet 37 | function loadRegistryInfo() { 38 | const registryInfo = ExtensionManager.extensions[packageJson.name].registryInfo; 39 | if (!registryInfo) { 40 | return Promise.cast(ExtensionManager.downloadRegistry()); 41 | } 42 | return Promise.resolve(); 43 | } 44 | 45 | // gets the latest version that is available in the extension registry or null if something fails 46 | function getLatestRegistryVersion() { 47 | return loadRegistryInfo().then(() => { 48 | const registryInfo = ExtensionManager.extensions[packageJson.name].registryInfo; 49 | return registryInfo.metadata.version; 50 | }).catch(() => { 51 | return null; 52 | }); 53 | } 54 | 55 | // responds to callback with: hasLatestVersion, currentVersion, latestVersion 56 | export function hasLatestVersion(callback) { 57 | getLatestRegistryVersion().then((registryVersion) => { 58 | if (registryVersion === null) { 59 | return callback(true, packageJson.version, "unknown"); 60 | } 61 | const has = packageJson.version >= registryVersion; 62 | callback(has, packageJson.version, registryVersion); 63 | }); 64 | } 65 | 66 | export function getInstalledExtensions() { 67 | const rv = {}; 68 | _.each(ExtensionManager.extensions, (obj, name) => { 69 | if (obj.installInfo && obj.installInfo.locationType !== "default") { 70 | rv[name] = { 71 | name: obj.installInfo.metadata.title, 72 | version: obj.installInfo.metadata.version 73 | }; 74 | } 75 | }); 76 | return rv; 77 | } 78 | -------------------------------------------------------------------------------- /src/Main.ts: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | import { _, AppInit, CommandManager, Menus, FileSystem, ProjectManager } from "./brackets-modules"; 4 | import ExpectedError from "./ExpectedError"; 5 | import * as Events from "./Events"; 6 | import EventEmitter from "./EventEmitter"; 7 | import * as Strings from "strings"; 8 | import * as ErrorHandler from "./ErrorHandler"; 9 | import * as Panel from "./Panel"; 10 | import * as Branch from "./Branch"; 11 | import * as ChangelogDialog from "./ChangelogDialog"; 12 | import * as SettingsDialog from "./SettingsDialog"; 13 | import * as CloseNotModified from "./CloseNotModified"; 14 | import * as ExtensionInfo from "./ExtensionInfo"; 15 | import * as Setup from "./utils/Setup"; 16 | import * as Preferences from "./Preferences"; 17 | import * as Utils from "./Utils"; 18 | import * as Promise from "bluebird"; 19 | 20 | const CMD_ADD_TO_IGNORE = "git.addToIgnore"; 21 | const CMD_REMOVE_FROM_IGNORE = "git.removeFromIgnore"; 22 | 23 | export const $icon = $("") 24 | .attr("title", Strings.LOADING) 25 | .addClass("loading") 26 | .appendTo($("#main-toolbar .buttons")); 27 | 28 | EventEmitter.on(Events.GIT_DISABLED, () => $icon.removeClass("dirty")); 29 | EventEmitter.on(Events.GIT_STATUS_RESULTS, (results) => $icon.toggleClass("dirty", results.length !== 0)); 30 | 31 | // This only launches when Git is available 32 | function initUi() { 33 | // FUTURE: do we really need to launch init from here? 34 | Panel.init(); 35 | Branch.init(); 36 | CloseNotModified.init(); 37 | // Attach events 38 | $icon.on("click", Panel.toggle); 39 | } 40 | 41 | function _addRemoveItemInGitignore(selectedEntry, method) { 42 | const gitRoot = Preferences.get("currentGitRoot"); 43 | const entryPath = "/" + selectedEntry.fullPath.substring(gitRoot.length); 44 | const gitignoreEntry = FileSystem.getFileForPath(gitRoot + ".gitignore"); 45 | 46 | gitignoreEntry.read((err, _content) => { 47 | let content = _content; 48 | if (err) { 49 | Utils.consoleLog(err, "warn"); 50 | content = ""; 51 | } 52 | 53 | // use trimmed lines only 54 | let lines = content.split("\n").map((l) => l.trim()); 55 | // clean start and end empty lines 56 | while (lines.length > 0 && !lines[0]) { lines.shift(); } 57 | while (lines.length > 0 && !lines[lines.length - 1]) { lines.pop(); } 58 | 59 | if (method === "add") { 60 | // add only when not already present 61 | if (lines.indexOf(entryPath) === -1) { lines.push(entryPath); } 62 | } else if (method === "remove") { 63 | lines = _.without(lines, entryPath); 64 | } 65 | 66 | // always have an empty line at the end of the file 67 | if (lines[lines.length - 1]) { lines.push(""); } 68 | 69 | gitignoreEntry.write(lines.join("\n"), (err2) => { 70 | if (err2) { 71 | return ErrorHandler.showError(err2, "Failed modifying .gitignore"); 72 | } 73 | return Panel.refresh(); 74 | }); 75 | }); 76 | } 77 | 78 | function addItemToGitingore() { 79 | return _addRemoveItemInGitignore(ProjectManager.getSelectedItem(), "add"); 80 | } 81 | 82 | function removeItemFromGitingore() { 83 | return _addRemoveItemInGitignore(ProjectManager.getSelectedItem(), "remove"); 84 | } 85 | 86 | function addItemToGitingoreFromPanel() { 87 | const filePath = Panel.getPanel().find("tr.selected").attr("x-file"); 88 | const fileEntry = FileSystem.getFileForPath(Preferences.get("currentGitRoot") + filePath); 89 | return _addRemoveItemInGitignore(fileEntry, "add"); 90 | } 91 | 92 | function removeItemFromGitingoreFromPanel() { 93 | const filePath = Panel.getPanel().find("tr.selected").attr("x-file"); 94 | const fileEntry = FileSystem.getFileForPath(Preferences.get("currentGitRoot") + filePath); 95 | return _addRemoveItemInGitignore(fileEntry, "remove"); 96 | } 97 | 98 | function _displayExtensionInfoIfNeeded() { 99 | return new Promise((resolve) => { 100 | // Display settings panel on first start / changelog dialog on version change 101 | ExtensionInfo.get().then((packageJson) => { 102 | // do not display dialogs when running tests 103 | if (window.isBracketsTestWindow) { 104 | return; 105 | } 106 | 107 | const lastVersion = Preferences.get("lastVersion"); 108 | const currentVersion = packageJson.version; 109 | 110 | if (!lastVersion) { 111 | Preferences.persist("lastVersion", "firstStart"); 112 | SettingsDialog.show(); 113 | } else if (lastVersion !== currentVersion) { 114 | Preferences.persist("lastVersion", currentVersion); 115 | ChangelogDialog.show(); 116 | } 117 | 118 | resolve(); 119 | }); 120 | }); 121 | } 122 | 123 | export function init() { 124 | // Initialize items dependent on HTML DOM 125 | AppInit.htmlReady(() => { 126 | $icon.removeClass("loading").removeAttr("title"); 127 | 128 | // Try to get Git version, if succeeds then Git works 129 | Setup.findGit().then((version) => { 130 | 131 | Strings.GIT_VERSION = version; 132 | 133 | _displayExtensionInfoIfNeeded(); 134 | 135 | initUi(); 136 | 137 | }).catch((err) => { 138 | $icon.addClass("error").attr("title", Strings.CHECK_GIT_SETTINGS + " - " + err.toString()); 139 | 140 | _displayExtensionInfoIfNeeded().then(() => { 141 | const expected = new ExpectedError(err); 142 | expected.detailsUrl = "https://github.com/zaggino/brackets-git#dependencies"; 143 | ErrorHandler.showError(expected, Strings.CHECK_GIT_SETTINGS); 144 | }); 145 | 146 | }); 147 | 148 | // register commands for project tree / working files 149 | CommandManager.register(Strings.ADD_TO_GITIGNORE, CMD_ADD_TO_IGNORE, addItemToGitingore); 150 | CommandManager.register(Strings.REMOVE_FROM_GITIGNORE, CMD_REMOVE_FROM_IGNORE, removeItemFromGitingore); 151 | 152 | // create context menu for git panel 153 | const panelCmenu = Menus.registerContextMenu("git-panel-context-menu"); 154 | CommandManager.register(Strings.ADD_TO_GITIGNORE, CMD_ADD_TO_IGNORE + "2", addItemToGitingoreFromPanel); 155 | CommandManager.register( 156 | Strings.REMOVE_FROM_GITIGNORE, CMD_REMOVE_FROM_IGNORE + "2", removeItemFromGitingoreFromPanel 157 | ); 158 | panelCmenu.addMenuItem(CMD_ADD_TO_IGNORE + "2"); 159 | panelCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE + "2"); 160 | }); 161 | } 162 | 163 | let _toggleMenuEntriesState = false; 164 | let _divider1 = null; 165 | let _divider2 = null; 166 | 167 | function toggleMenuEntries(bool) { 168 | if (bool === _toggleMenuEntriesState) { 169 | return; 170 | } 171 | const projectCmenu = Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU); 172 | const workingCmenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU); 173 | if (bool) { 174 | _divider1 = projectCmenu.addMenuDivider(); 175 | _divider2 = workingCmenu.addMenuDivider(); 176 | projectCmenu.addMenuItem(CMD_ADD_TO_IGNORE); 177 | workingCmenu.addMenuItem(CMD_ADD_TO_IGNORE); 178 | projectCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE); 179 | workingCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE); 180 | } else { 181 | projectCmenu.removeMenuDivider(_divider1.id); 182 | workingCmenu.removeMenuDivider(_divider2.id); 183 | projectCmenu.removeMenuItem(CMD_ADD_TO_IGNORE); 184 | workingCmenu.removeMenuItem(CMD_ADD_TO_IGNORE); 185 | projectCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE); 186 | workingCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE); 187 | } 188 | _toggleMenuEntriesState = bool; 189 | } 190 | 191 | // Event handlers 192 | EventEmitter.on(Events.GIT_ENABLED, () => toggleMenuEntries(true)); 193 | EventEmitter.on(Events.GIT_DISABLED, () => toggleMenuEntries(false)); 194 | -------------------------------------------------------------------------------- /src/NoRepo.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem, FileUtils, ProjectManager } from "./brackets-modules"; 2 | import * as Promise from "bluebird"; 3 | import * as ErrorHandler from "./ErrorHandler"; 4 | import * as Events from "./Events"; 5 | import EventEmitter from "./EventEmitter"; 6 | import ExpectedError from "./ExpectedError"; 7 | import * as ProgressDialog from "./dialogs/Progress"; 8 | import * as CloneDialog from "./dialogs/Clone"; 9 | import * as Git from "./git/GitCli"; 10 | import * as Git2 from "./git/Git"; 11 | import * as Preferences from "./Preferences"; 12 | import * as Utils from "./Utils"; 13 | 14 | const gitignoreTemplate = require("text!templates/default-gitignore"); 15 | 16 | function createGitIgnore() { 17 | const gitIgnorePath = Preferences.get("currentGitRoot") + ".gitignore"; 18 | return Utils.pathExists(gitIgnorePath).then((exists) => { 19 | if (!exists) { 20 | return Promise.cast(FileUtils.writeText(FileSystem.getFileForPath(gitIgnorePath), gitignoreTemplate)); 21 | } 22 | return null; 23 | }); 24 | } 25 | 26 | function stageGitIgnore() { 27 | return createGitIgnore().then(() => Git.stage(".gitignore")); 28 | } 29 | 30 | function handleGitInit() { 31 | Utils.isProjectRootWritable().then((writable) => { 32 | if (!writable) { 33 | throw new ExpectedError("Folder " + Utils.getProjectRoot() + " is not writable!"); 34 | } 35 | return Git.init().catch((err) => { 36 | 37 | if (ErrorHandler.contains(err, "Please tell me who you are")) { 38 | const defer = Promise.defer(); 39 | EventEmitter.emit(Events.GIT_CHANGE_USERNAME, null, () => { 40 | EventEmitter.emit(Events.GIT_CHANGE_EMAIL, null, () => { 41 | Git.init() 42 | .then((result) => defer.resolve(result)) 43 | .catch((initErr) => defer.reject(initErr)); 44 | }); 45 | }); 46 | return defer.promise; 47 | } 48 | 49 | throw err; 50 | 51 | }); 52 | }) 53 | .then(() => stageGitIgnore()) 54 | .catch((err) => ErrorHandler.showError(err, "Initializing new repository failed")) 55 | .then(() => EventEmitter.emit(Events.REFRESH_ALL)); 56 | } 57 | 58 | // This checks if the project root is empty (to let Git clone repositories) 59 | function isProjectRootEmpty() { 60 | return new Promise((resolve, reject) => { 61 | ProjectManager.getProjectRoot().getContents((err, entries) => { 62 | if (err) { 63 | return reject(err); 64 | } 65 | resolve(entries.length === 0); 66 | }); 67 | }); 68 | } 69 | 70 | function handleGitClone() { 71 | const $gitPanel = $("#git-panel"); 72 | const $cloneButton = $gitPanel.find(".git-clone"); 73 | $cloneButton.prop("disabled", true); 74 | isProjectRootEmpty().then((isEmpty) => { 75 | if (isEmpty) { 76 | CloneDialog.show().then((cloneConfig) => { 77 | let q: Promise = Promise.resolve(); 78 | // put username and password into remote url 79 | let remoteUrl = cloneConfig.remoteUrl; 80 | if (cloneConfig.remoteUrlNew) { 81 | remoteUrl = cloneConfig.remoteUrlNew; 82 | } 83 | 84 | // do the clone 85 | q = q.then(() => ProgressDialog.show(Git.clone(remoteUrl, "."))) 86 | .catch((err) => ErrorHandler.showError(err, "Cloning remote repository failed!")); 87 | 88 | // restore original url if desired 89 | if (cloneConfig.remoteUrlRestore) { 90 | q = q.then(() => Git2.setRemoteUrl(cloneConfig.remote, cloneConfig.remoteUrlRestore)); 91 | } 92 | 93 | return q.finally(() => EventEmitter.emit(Events.REFRESH_ALL)); 94 | }).catch((err) => { 95 | // when dialog is cancelled, there's no error 96 | if (err) { ErrorHandler.showError(err, "Cloning remote repository failed!"); } 97 | }); 98 | 99 | } else { 100 | const err = new ExpectedError("Project root is not empty, be sure you have deleted hidden files"); 101 | ErrorHandler.showError(err, "Cloning remote repository failed!"); 102 | } 103 | }) 104 | .catch((err) => ErrorHandler.showError(err)) 105 | .finally(() => $cloneButton.prop("disabled", false)); 106 | } 107 | 108 | // Event subscriptions 109 | EventEmitter.on(Events.HANDLE_GIT_INIT, () => handleGitInit()); 110 | EventEmitter.on(Events.HANDLE_GIT_CLONE, () => handleGitClone()); 111 | EventEmitter.on(Events.GIT_NO_BRANCH_EXISTS, () => stageGitIgnore()); 112 | -------------------------------------------------------------------------------- /src/Preferences.ts: -------------------------------------------------------------------------------- 1 | import { _, PreferencesManager } from "./brackets-modules"; 2 | 3 | const StateManager = PreferencesManager.stateManager; 4 | const prefix = "brackets-git"; 5 | 6 | const defaultPreferences = { 7 | // features 8 | stripWhitespaceFromCommits: { type: "boolean", value: true }, 9 | addEndlineToTheEndOfFile: { type: "boolean", value: true }, 10 | removeByteOrderMark: { type: "boolean", value: false }, 11 | normalizeLineEndings: { type: "boolean", value: false }, 12 | useGitGutter: { type: "boolean", value: true }, 13 | markModifiedInTree: { type: "boolean", value: true }, 14 | useCodeInspection: { type: "boolean", value: true }, 15 | useGitFtp: { type: "boolean", value: false }, 16 | avatarType: { type: "string", value: "AVATAR_COLOR" }, 17 | showBashButton: { type: "boolean", value: true }, 18 | dateMode: { type: "number", value: 1 }, 19 | dateFormat: { type: "string", value: null }, 20 | enableAdvancedFeatures: { type: "boolean", value: false }, 21 | useVerboseDiff: { type: "boolean", value: false }, 22 | useDifftool: { type: "boolean", value: false }, 23 | clearWhitespaceOnSave: { type: "boolean", value: false }, 24 | gerritPushref: { type: "boolean", value: false }, 25 | // shortcuts 26 | panelShortcut: { type: "string", value: "Ctrl-Alt-G" }, 27 | commitCurrentShortcut: { type: "string", value: null }, 28 | commitAllShortcut: { type: "string", value: null }, 29 | bashShortcut: { type: "string", value: null }, 30 | pushShortcut: { type: "string", value: null }, 31 | pullShortcut: { type: "string", value: null }, 32 | gotoPrevChangeShortcut: { type: "string", value: null }, 33 | gotoNextChangeShortcut: { type: "string", value: null }, 34 | refreshShortcut: { type: "string", value: null }, 35 | showTerminalIcon: { type: "boolean", value: false }, 36 | // system 37 | debugMode: { type: "boolean", value: false }, 38 | gitTimeout: { type: "number", value: 30 }, 39 | gitPath: { type: "string", value: "" }, 40 | terminalCommand: { type: "string", value: "" }, 41 | terminalCommandArgs: { type: "string", value: "" } 42 | }; 43 | 44 | const prefs = PreferencesManager.getExtensionPrefs(prefix); 45 | _.each(defaultPreferences, (definition, key) => { 46 | if (definition.os && definition.os[brackets.platform]) { 47 | prefs.definePreference(key, definition.type, definition.os[brackets.platform].value); 48 | } else { 49 | prefs.definePreference(key, definition.type, definition.value); 50 | } 51 | }); 52 | prefs.save(); 53 | 54 | export function get(key, ...rest) { 55 | const location = defaultPreferences[key] ? PreferencesManager : StateManager; 56 | return location.get(prefix + "." + key, ...rest); 57 | } 58 | 59 | export function set(key, ...rest) { 60 | const location = defaultPreferences[key] ? PreferencesManager : StateManager; 61 | return location.set(prefix + "." + key, ...rest); 62 | } 63 | 64 | export function getAll() { 65 | const obj = {}; 66 | _.each(defaultPreferences, (definition, key) => { 67 | obj[key] = get(key); 68 | }); 69 | return obj; 70 | } 71 | 72 | export function getDefaults() { 73 | const obj = {}; 74 | _.each(defaultPreferences, (definition, key) => { 75 | let defaultValue; 76 | if (definition.os && definition.os[brackets.platform]) { 77 | defaultValue = definition.os[brackets.platform].value; 78 | } else { 79 | defaultValue = definition.value; 80 | } 81 | obj[key] = defaultValue; 82 | }); 83 | return obj; 84 | } 85 | 86 | export function getType(key) { 87 | return defaultPreferences[key].type; 88 | } 89 | 90 | export function getGlobal(key) { 91 | return PreferencesManager.get(key); 92 | } 93 | 94 | export function persist(key, value) { 95 | // FUTURE: remote this method 96 | set(key, value); 97 | save(); 98 | } 99 | 100 | export function save() { 101 | PreferencesManager.save(); 102 | StateManager.save(); 103 | } 104 | -------------------------------------------------------------------------------- /src/ProjectTreeMarks.ts: -------------------------------------------------------------------------------- 1 | import { _, FileSystem, ProjectManager } from "./brackets-modules"; 2 | import EventEmitter from "./EventEmitter"; 3 | import * as Events from "./Events"; 4 | import * as Git from "./git/GitCli"; 5 | import * as Preferences from "./Preferences"; 6 | import * as Promise from "bluebird"; 7 | 8 | let ignoreEntries = []; 9 | let newPaths = []; 10 | let modifiedPaths = []; 11 | 12 | function loadIgnoreContents() { 13 | const defer = Promise.defer(); 14 | const gitRoot = Preferences.get("currentGitRoot"); 15 | let excludeContents; 16 | let gitignoreContents; 17 | 18 | const finish = _.after(2, () => defer.resolve(excludeContents + "\n" + gitignoreContents)); 19 | 20 | FileSystem.getFileForPath(gitRoot + ".git/info/exclude").read((err, content) => { 21 | excludeContents = err ? "" : content; 22 | finish(); 23 | }); 24 | 25 | FileSystem.getFileForPath(gitRoot + ".gitignore").read((err, content) => { 26 | gitignoreContents = err ? "" : content; 27 | finish(); 28 | }); 29 | 30 | return defer.promise; 31 | } 32 | 33 | function refreshIgnoreEntries() { 34 | function regexEscape(str) { 35 | // NOTE: We cannot use StringUtils.regexEscape() here because we don't wanna replace * 36 | return str.replace(/([.?+^$\\(){}|])/g, "\\$1"); 37 | } 38 | 39 | return loadIgnoreContents().then((content: string) => { 40 | const gitRoot = Preferences.get("currentGitRoot"); 41 | 42 | ignoreEntries = _.compact(_.map(content.split("\n"), (_line) => { 43 | // Rules: http://git-scm.com/docs/gitignore 44 | let line = _line; 45 | let type = "deny"; 46 | let leadingSlash; 47 | let trailingSlash; 48 | let regex; 49 | 50 | line = line.trim(); 51 | if (!line || line.indexOf("#") === 0) { 52 | return null; 53 | } 54 | 55 | // handle explicitly allowed files/folders with a leading ! 56 | if (line.indexOf("!") === 0) { 57 | line = line.slice(1); 58 | type = "accept"; 59 | } 60 | // handle lines beginning with a backslash, which is used for escaping ! or # 61 | if (line.indexOf("\\") === 0) { 62 | line = line.slice(1); 63 | } 64 | // handle lines beginning with a slash, which only matches files/folders in the root dir 65 | if (line.indexOf("/") === 0) { 66 | line = line.slice(1); 67 | leadingSlash = true; 68 | } 69 | // handle lines ending with a slash, which only exludes dirs 70 | if (line.lastIndexOf("/") === line.length) { 71 | // a line ending with a slash ends with ** 72 | line += "**"; 73 | trailingSlash = true; 74 | } 75 | 76 | // NOTE: /(.{0,})/ is basically the same as /(.*)/, but we can't use it because the asterisk 77 | // would be replaced later on 78 | 79 | // create the intial regexp here. We need the absolute path 'cause it could be that there 80 | // are external files with the same name as a project file 81 | regex = regexEscape(gitRoot) + 82 | (leadingSlash ? "" : "((.+)/)?") + regexEscape(line) + (trailingSlash ? "" : "(/.{0,})?"); 83 | // replace all the possible asterisks 84 | regex = regex.replace(/\*\*$/g, "(.{0,})").replace(/(\*\*|\*$)/g, "(.+)").replace(/\*/g, "([^/]*)"); 85 | regex = "^" + regex + "$"; 86 | 87 | return { regexp: new RegExp(regex), type }; 88 | })); 89 | }); 90 | } 91 | 92 | function isIgnored(path) { 93 | let ignored = false; 94 | _.forEach(ignoreEntries, (entry) => { 95 | if (entry.regexp.test(path)) { 96 | ignored = (entry.type === "deny"); 97 | } 98 | }); 99 | return ignored; 100 | } 101 | 102 | function isNew(fullPath) { 103 | return newPaths.indexOf(fullPath) !== -1; 104 | } 105 | 106 | function isModified(fullPath) { 107 | return modifiedPaths.indexOf(fullPath) !== -1; 108 | } 109 | 110 | function _refreshOpenFiles() { 111 | $("#working-set-list-container").find("li").each(function () { 112 | const $li = $(this); 113 | const data = $li.data("file"); 114 | if (data) { 115 | const fullPath = data.fullPath; 116 | $li.toggleClass("git-ignored", isIgnored(fullPath)) 117 | .toggleClass("git-new", isNew(fullPath)) 118 | .toggleClass("git-modified", isModified(fullPath)); 119 | } 120 | }); 121 | } 122 | 123 | const refreshOpenFiles = _.debounce(() => _refreshOpenFiles(), 100); 124 | 125 | function attachEvents() { 126 | $("#working-set-list-container").on("contentChanged", refreshOpenFiles).triggerHandler("contentChanged"); 127 | } 128 | 129 | function detachEvents() { 130 | $("#working-set-list-container").off("contentChanged", refreshOpenFiles); 131 | } 132 | 133 | if (Preferences.get("markModifiedInTree")) { 134 | 135 | // init here 136 | ProjectManager.addClassesProvider((data) => { 137 | const fullPath = data.fullPath; 138 | if (isIgnored(fullPath)) { 139 | return "git-ignored"; 140 | } else if (isNew(fullPath)) { 141 | return "git-new"; 142 | } else if (isModified(fullPath)) { 143 | return "git-modified"; 144 | } 145 | return null; 146 | }); 147 | 148 | // this will refresh ignore entries when .gitignore is modified 149 | EventEmitter.on(Events.BRACKETS_FILE_CHANGED, (evt, file) => { 150 | if (file.fullPath === Preferences.get("currentGitRoot") + ".gitignore") { 151 | refreshIgnoreEntries().finally(() => { 152 | refreshOpenFiles(); 153 | }); 154 | } 155 | }); 156 | 157 | // this will refresh new/modified paths on every status results 158 | EventEmitter.on(Events.GIT_STATUS_RESULTS, (files) => { 159 | const gitRoot = Preferences.get("currentGitRoot"); 160 | 161 | newPaths = []; 162 | modifiedPaths = []; 163 | 164 | files.forEach((entry) => { 165 | const _isNew = entry.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1 || 166 | entry.status.indexOf(Git.FILE_STATUS.ADDED) !== -1; 167 | const fullPath = gitRoot + entry.file; 168 | if (_isNew) { 169 | newPaths.push(fullPath); 170 | } else { 171 | modifiedPaths.push(fullPath); 172 | } 173 | }); 174 | 175 | ProjectManager.rerenderTree(); 176 | refreshOpenFiles(); 177 | }); 178 | 179 | // this will refresh ignore entries when git project is opened 180 | EventEmitter.on(Events.GIT_ENABLED, () => { 181 | refreshIgnoreEntries(); 182 | attachEvents(); 183 | }); 184 | 185 | // this will clear entries when non-git project is opened 186 | EventEmitter.on(Events.GIT_DISABLED, () => { 187 | ignoreEntries = []; 188 | newPaths = []; 189 | modifiedPaths = []; 190 | detachEvents(); 191 | }); 192 | } 193 | -------------------------------------------------------------------------------- /src/SettingsDialog.ts: -------------------------------------------------------------------------------- 1 | import { _, CommandManager, Dialogs, Mustache } from "./brackets-modules"; 2 | import * as Preferences from "./Preferences"; 3 | import * as ChangelogDialog from "./ChangelogDialog"; 4 | import * as Strings from "strings"; 5 | import * as Git from "./git/GitCli"; 6 | 7 | const settingsDialogTemplate = require("text!templates/git-settings-dialog.html"); 8 | const questionDialogTemplate = require("text!templates/git-question-dialog.html"); 9 | 10 | let dialog; 11 | let $dialog; 12 | 13 | function setValues(values) { 14 | $("*[settingsProperty]", $dialog).each(function () { 15 | const $this = $(this); 16 | const type = $this.attr("type"); 17 | const tag = $this.prop("tagName").toLowerCase(); 18 | const property = $this.attr("settingsProperty"); 19 | if (type === "checkbox") { 20 | $this.prop("checked", values[property]); 21 | } else if (tag === "select") { 22 | $("option[value=" + values[property] + "]", $this).prop("selected", true); 23 | } else { 24 | $this.val(values[property]); 25 | } 26 | }); 27 | $("#git-settings-dateFormat-container", $dialog).toggle(values.dateMode === 3); 28 | } 29 | 30 | function collectValues() { 31 | $("*[settingsProperty]", $dialog).each(function () { 32 | const $this = $(this); 33 | const type = $this.attr("type"); 34 | const property = $this.attr("settingsProperty"); 35 | const prefType = Preferences.getType(property); 36 | if (type === "checkbox") { 37 | Preferences.set(property, $this.prop("checked")); 38 | } else if (prefType === "number") { 39 | let newValue = parseInt($this.val().trim(), 10); 40 | if (isNaN(newValue)) { newValue = Preferences.getDefaults()[property]; } 41 | Preferences.set(property, newValue); 42 | } else { 43 | Preferences.set(property, $this.val().trim() || null); 44 | } 45 | }); 46 | Preferences.save(); 47 | } 48 | 49 | function assignActions() { 50 | const $useDifftoolCheckbox = $("#git-settings-useDifftool", $dialog); 51 | 52 | Git.getConfig("diff.tool").then((diffToolConfiguration) => { 53 | 54 | if (!diffToolConfiguration) { 55 | $useDifftoolCheckbox.prop({ 56 | checked: false, 57 | disabled: true 58 | }); 59 | } else { 60 | $useDifftoolCheckbox.prop({ 61 | disabled: false 62 | }); 63 | } 64 | 65 | }).catch(() => { 66 | 67 | // an error with git 68 | // we were not able to check whether diff tool is configured or not 69 | // so we disable it just to be sure 70 | $useDifftoolCheckbox.prop({ 71 | checked: false, 72 | disabled: true 73 | }); 74 | 75 | }); 76 | 77 | $("#git-settings-stripWhitespaceFromCommits", $dialog).on("change", function () { 78 | const on = $(this).is(":checked"); 79 | $( 80 | "#git-settings-addEndlineToTheEndOfFile," + 81 | "#git-settings-removeByteOrderMark," + 82 | "#git-settings-normalizeLineEndings", 83 | $dialog 84 | ).prop("checked", on).prop("disabled", !on); 85 | }); 86 | 87 | $("#git-settings-dateMode", $dialog).on("change", function () { 88 | $("#git-settings-dateFormat-container", $dialog).toggle($("option:selected", this).prop("value") === "3"); 89 | }); 90 | 91 | $("button[data-button-id='defaults']", $dialog).on("click", (e) => { 92 | e.stopPropagation(); 93 | setValues(Preferences.getDefaults()); 94 | }); 95 | 96 | $("button[data-button-id='changelog']", $dialog).on("click", (e) => { 97 | e.stopPropagation(); 98 | ChangelogDialog.show(); 99 | }); 100 | } 101 | 102 | function init() { 103 | setValues(Preferences.getAll()); 104 | assignActions(); 105 | $("#git-settings-tabs a", $dialog).click(function (e) { 106 | e.preventDefault(); 107 | $(this).tab("show"); 108 | }); 109 | } 110 | 111 | function showRestartDialog() { 112 | const compiledTemplate = Mustache.render(questionDialogTemplate, { 113 | title: Strings.RESTART, 114 | question: _.escape(Strings.Q_RESTART_BRACKETS), 115 | Strings 116 | }); 117 | Dialogs.showModalDialogUsingTemplate(compiledTemplate).done((buttonId) => { 118 | if (buttonId === "ok") { 119 | CommandManager.execute("debug.refreshWindow"); 120 | } 121 | }); 122 | } 123 | 124 | export function show() { 125 | const compiledTemplate = Mustache.render(settingsDialogTemplate, Strings); 126 | 127 | dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 128 | $dialog = dialog.getElement(); 129 | 130 | init(); 131 | 132 | dialog.done((buttonId) => { 133 | if (buttonId === "ok") { 134 | // Save everything to preferences 135 | collectValues(); 136 | // Restart brackets to reload changes. 137 | showRestartDialog(); 138 | } 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /src/TerminalIcon.ts: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | import * as Events from "./Events"; 4 | import EventEmitter from "./EventEmitter"; 5 | 6 | $("") 7 | .appendTo("#main-toolbar .buttons") 8 | .on("click", EventEmitter.emitFactory(Events.TERMINAL_OPEN)); 9 | -------------------------------------------------------------------------------- /src/brackets-modules.ts: -------------------------------------------------------------------------------- 1 | /* global brackets */ 2 | 3 | export const platform = brackets.platform; 4 | export const _ = brackets.getModule("thirdparty/lodash"); 5 | export const AppInit = brackets.getModule("utils/AppInit"); 6 | export const CodeInspection = brackets.getModule("language/CodeInspection"); 7 | export const CommandManager = brackets.getModule("command/CommandManager"); 8 | export const Commands = brackets.getModule("command/Commands"); 9 | export const DefaultDialogs = brackets.getModule("widgets/DefaultDialogs"); 10 | export const Dialogs = brackets.getModule("widgets/Dialogs"); 11 | export const DocumentManager = brackets.getModule("document/DocumentManager"); 12 | export const EditorManager = brackets.getModule("editor/EditorManager"); 13 | export const ExtensionManager = brackets.getModule("extensibility/ExtensionManager"); 14 | export const ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); 15 | export const FileSyncManager = brackets.getModule("project/FileSyncManager"); 16 | export const FileSystem = brackets.getModule("filesystem/FileSystem"); 17 | export const FileUtils = brackets.getModule("file/FileUtils"); 18 | export const FileViewController = brackets.getModule("project/FileViewController"); 19 | export const FindInFiles = brackets.getModule("search/FindInFiles"); 20 | export const KeyBindingManager = brackets.getModule("command/KeyBindingManager"); 21 | export const LanguageManager = brackets.getModule("language/LanguageManager"); 22 | export const MainViewManager = brackets.getModule("view/MainViewManager"); 23 | export const Menus = brackets.getModule("command/Menus"); 24 | export const Mustache = brackets.getModule("thirdparty/mustache/mustache"); 25 | export const NativeApp = brackets.getModule("utils/NativeApp"); 26 | export const NodeConnection = brackets.getModule("utils/NodeConnection"); 27 | export const PopUpManager = brackets.getModule("widgets/PopUpManager"); 28 | export const PreferencesManager = brackets.getModule("preferences/PreferencesManager"); 29 | export const ProjectManager = brackets.getModule("project/ProjectManager"); 30 | export const StringUtils = brackets.getModule("utils/StringUtils"); 31 | export const WorkspaceManager = brackets.getModule("view/WorkspaceManager"); 32 | -------------------------------------------------------------------------------- /src/declarations/brackets.ts: -------------------------------------------------------------------------------- 1 | declare const brackets: { 2 | 3 | fs: any; 4 | 5 | getLocale: () => string; 6 | 7 | getModule: (path: string) => any; 8 | 9 | metadata: { 10 | name: string; 11 | version: string; 12 | } 13 | 14 | platform: string; 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /src/declarations/jquery.ts: -------------------------------------------------------------------------------- 1 | interface JQuery { 2 | andSelf: () => JQuery; 3 | tab: (cmd: string) => JQuery; 4 | } 5 | -------------------------------------------------------------------------------- /src/declarations/require.ts: -------------------------------------------------------------------------------- 1 | declare const require: Function; 2 | -------------------------------------------------------------------------------- /src/declarations/strings.ts: -------------------------------------------------------------------------------- 1 | declare module "strings"; 2 | -------------------------------------------------------------------------------- /src/declarations/window.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | 3 | bracketsGit: { 4 | getExtensionPath: () => string; 5 | }; 6 | 7 | isBracketsTestWindow: boolean; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/dialogs/Clone.ts: -------------------------------------------------------------------------------- 1 | import * as Promise from "bluebird"; 2 | import * as RemoteCommon from "./RemoteCommon"; 3 | import * as Strings from "strings"; 4 | import { Dialogs, Mustache } from "../brackets-modules"; 5 | 6 | const template = require("text!src/dialogs/templates/clone-dialog.html"); 7 | const credentialsTemplate = require("text!src/dialogs/templates/credentials-template.html"); 8 | 9 | let defer; 10 | let $cloneInput; 11 | 12 | function _attachEvents($dialog) { 13 | // Detect changes to URL, disable auth if not http 14 | $cloneInput.on("keyup change", () => { 15 | const $authInputs = $dialog.find("input[name='username'],input[name='password'],input[name='saveToUrl']"); 16 | if ($cloneInput.val().length > 0) { 17 | if (/^https?:/.test($cloneInput.val())) { 18 | $authInputs.prop("disabled", false); 19 | 20 | // Update the auth fields if the URL contains auth 21 | const auth = /:\/\/([^:]+):?([^@]*)@/.exec($cloneInput.val()); 22 | if (auth) { 23 | $("input[name=username]", $dialog).val(auth[1]); 24 | $("input[name=password]", $dialog).val(auth[2]); 25 | } 26 | } else { 27 | $authInputs.prop("disabled", true); 28 | } 29 | } else { 30 | $authInputs.prop("disabled", false); 31 | } 32 | }); 33 | $cloneInput.focus(); 34 | } 35 | 36 | export function show() { 37 | defer = Promise.defer(); 38 | 39 | const templateArgs = { 40 | modeLabel: Strings.CLONE_REPOSITORY, 41 | Strings 42 | }; 43 | 44 | const compiledTemplate = Mustache.render(template, templateArgs, { 45 | credentials: credentialsTemplate 46 | }); 47 | const dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 48 | const $dialog = dialog.getElement(); 49 | 50 | $cloneInput = $dialog.find("#git-clone-url"); 51 | 52 | _attachEvents($dialog); 53 | 54 | dialog.done((buttonId) => { 55 | if (buttonId === "ok") { 56 | const cloneConfig = { 57 | remote: "origin", 58 | remoteUrl: $cloneInput.val() 59 | }; 60 | RemoteCommon.collectValues(cloneConfig, $dialog); 61 | defer.resolve(cloneConfig); 62 | } else { 63 | defer.reject(); 64 | } 65 | }); 66 | 67 | return defer.promise; 68 | } 69 | -------------------------------------------------------------------------------- /src/dialogs/Progress.ts: -------------------------------------------------------------------------------- 1 | import * as Promise from "bluebird"; 2 | import * as Strings from "strings"; 3 | import { _, Dialogs, Mustache } from "../brackets-modules"; 4 | 5 | const template = require("text!src/dialogs/templates/progress-dialog.html"); 6 | let lines; 7 | let $textarea; 8 | 9 | function addLine(str) { 10 | lines.push(str); 11 | } 12 | 13 | function onProgress(str?) { 14 | if (typeof str !== "undefined") { 15 | addLine(str); 16 | } 17 | if ($textarea) { 18 | $textarea.val(lines.join("\n")); 19 | $textarea.scrollTop($textarea[0].scrollHeight - $textarea.height()); 20 | } 21 | } 22 | 23 | export interface ShowOptions { 24 | preDelay?: number; 25 | postDelay?: number; 26 | } 27 | 28 | export function show(promise, title = null, options: ShowOptions = {}) { 29 | if (!promise || !promise.finally || !promise.progressed) { 30 | throw new Error("Invalid argument for progress dialog!"); 31 | } 32 | 33 | if (typeof title === "object") { 34 | options = title; // eslint-disable-line 35 | title = false; // eslint-disable-line 36 | } 37 | options = options || {}; // eslint-disable-line 38 | 39 | return new Promise((resolve, reject) => { 40 | 41 | lines = []; 42 | $textarea = null; 43 | 44 | let dialog; 45 | let finished = false; 46 | 47 | function showDialog() { 48 | if (finished) { 49 | return; 50 | } 51 | 52 | const templateArgs = { title: title || Strings.OPERATION_IN_PROGRESS_TITLE, Strings }; 53 | 54 | const compiledTemplate = Mustache.render(template, templateArgs); 55 | dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 56 | 57 | $textarea = dialog.getElement().find("textarea"); 58 | onProgress(); 59 | } 60 | 61 | function finish() { 62 | finished = true; 63 | if (dialog) { 64 | dialog.close(); 65 | } 66 | promise 67 | .then((val) => resolve(val)) 68 | .catch((err) => reject(err)); 69 | } 70 | 71 | if (!options.preDelay) { 72 | showDialog(); 73 | } else { 74 | setTimeout(() => showDialog(), options.preDelay * 1000); 75 | } 76 | 77 | promise 78 | .progressed((string) => onProgress(string)) 79 | .finally(() => { 80 | onProgress("Finished!"); 81 | if (!options.postDelay || !dialog) { 82 | finish(); 83 | } else { 84 | setTimeout(() => finish(), options.postDelay * 1000); 85 | } 86 | }); 87 | 88 | }); 89 | } 90 | 91 | export function waitForClose() { 92 | return new Promise((resolve) => { 93 | function check() { 94 | const visible = $("#git-progress-dialog").is(":visible"); 95 | if (!visible) { 96 | resolve(); 97 | } else { 98 | _.defer(check); 99 | } 100 | } 101 | _.defer(check); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /src/dialogs/Pull.ts: -------------------------------------------------------------------------------- 1 | import * as Preferences from "../Preferences"; 2 | import * as Promise from "bluebird"; 3 | import * as RemoteCommon from "./RemoteCommon"; 4 | import * as Strings from "strings"; 5 | import { Dialogs, Mustache } from "../brackets-modules"; 6 | 7 | const template = require("text!src/dialogs/templates/pull-dialog.html"); 8 | const remotesTemplate = require("text!src/dialogs/templates/remotes-template.html"); 9 | const credentialsTemplate = require("text!src/dialogs/templates/credentials-template.html"); 10 | 11 | let defer; 12 | let pullConfig; 13 | 14 | function _attachEvents($dialog) { 15 | RemoteCommon.attachCommonEvents(pullConfig, $dialog); 16 | 17 | // load last used 18 | $dialog 19 | .find("input[name='strategy']") 20 | .filter("[value='" + (Preferences.get("pull.strategy") || "DEFAULT") + "']") 21 | .prop("checked", true); 22 | } 23 | 24 | function _show() { 25 | const templateArgs = { 26 | config: pullConfig, 27 | mode: "PULL_FROM", 28 | modeLabel: Strings.PULL_FROM, 29 | Strings 30 | }; 31 | const compiledTemplate = Mustache.render(template, templateArgs, { 32 | credentials: credentialsTemplate, 33 | remotes: remotesTemplate 34 | }); 35 | const dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 36 | const $dialog = dialog.getElement(); 37 | 38 | _attachEvents($dialog); 39 | 40 | dialog.done((buttonId) => { 41 | if (buttonId === "ok") { 42 | RemoteCommon.collectValues(pullConfig, $dialog); 43 | Preferences.set("pull.strategy", pullConfig.strategy); 44 | defer.resolve(pullConfig); 45 | } else { 46 | defer.reject(); 47 | } 48 | }); 49 | } 50 | 51 | export function show(_pullConfig) { 52 | defer = Promise.defer(); 53 | pullConfig = _pullConfig; 54 | pullConfig.pull = true; 55 | RemoteCommon.collectInfo(pullConfig).then(_show); 56 | return defer.promise; 57 | } 58 | -------------------------------------------------------------------------------- /src/dialogs/Push.ts: -------------------------------------------------------------------------------- 1 | import { Dialogs, Mustache } from "../brackets-modules"; 2 | import * as RemoteCommon from "./RemoteCommon"; 3 | import * as Promise from "bluebird"; 4 | import * as Strings from "strings"; 5 | 6 | const template = require("text!src/dialogs/templates/push-dialog.html"); 7 | const remotesTemplate = require("text!src/dialogs/templates/remotes-template.html"); 8 | const credentialsTemplate = require("text!src/dialogs/templates/credentials-template.html"); 9 | 10 | let defer; 11 | let pushConfig; 12 | 13 | function _attachEvents($dialog) { 14 | RemoteCommon.attachCommonEvents(pushConfig, $dialog); 15 | 16 | // select default - we don't want to remember forced or delete branch as default 17 | $dialog 18 | .find("input[name='strategy']") 19 | .filter("[value='DEFAULT']") 20 | .prop("checked", true); 21 | } 22 | 23 | function _show() { 24 | const templateArgs = { 25 | config: pushConfig, 26 | mode: "PUSH_TO", 27 | modeLabel: Strings.PUSH_TO, 28 | Strings 29 | }; 30 | 31 | const compiledTemplate = Mustache.render(template, templateArgs, { 32 | credentials: credentialsTemplate, 33 | remotes: remotesTemplate 34 | }); 35 | const dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); 36 | const $dialog = dialog.getElement(); 37 | 38 | _attachEvents($dialog); 39 | 40 | dialog.done((buttonId) => { 41 | if (buttonId === "ok") { 42 | RemoteCommon.collectValues(pushConfig, $dialog); 43 | defer.resolve(pushConfig); 44 | } else { 45 | defer.reject(); 46 | } 47 | }); 48 | } 49 | 50 | export function show(_pushConfig) { 51 | defer = Promise.defer(); 52 | pushConfig = _pushConfig; 53 | pushConfig.push = true; 54 | RemoteCommon.collectInfo(pushConfig).then(_show); 55 | return defer.promise; 56 | } 57 | -------------------------------------------------------------------------------- /src/dialogs/RemoteCommon.ts: -------------------------------------------------------------------------------- 1 | import { _, Mustache } from "../brackets-modules"; 2 | import * as ErrorHandler from "../ErrorHandler"; 3 | import * as Git from "../git/GitCli"; 4 | import * as Git2 from "../git/Git"; 5 | import * as ProgressDialog from "./Progress"; 6 | import * as URI from "URI"; 7 | 8 | function fillBranches(config, $dialog) { 9 | Git.getAllBranches().then((branches) => { 10 | // filter only branches for this remote 11 | branches = _.filter(branches, (branch) => branch.remote === config.remote); 12 | const template = "{{#branches}}{{/branches}}"; 14 | const html = Mustache.render(template, { branches }); 15 | $dialog.find(".branchSelect").html(html); 16 | }).catch((err) => { 17 | ErrorHandler.showError(err, "Getting branch list failed"); 18 | }); 19 | } 20 | 21 | export function collectInfo(config) { 22 | return Git.getCurrentUpstreamBranch().then((upstreamBranch) => { 23 | config.currentTrackingBranch = upstreamBranch; 24 | 25 | return Git2.getRemoteUrl(config.remote).then((remoteUrl) => { 26 | config.remoteUrl = remoteUrl; 27 | 28 | if (remoteUrl.match(/^https?:/)) { 29 | const uri = new URI(remoteUrl); 30 | config.remoteUsername = uri.username(); 31 | config.remotePassword = uri.password(); 32 | } else { 33 | // disable the inputs 34 | config._usernamePasswordDisabled = true; 35 | } 36 | 37 | if (!upstreamBranch) { 38 | return Git.getCurrentBranchName().then((currentBranchName) => { 39 | config.currentBranchName = currentBranchName; 40 | }); 41 | } 42 | return null; 43 | }); 44 | }).catch((err) => { 45 | ErrorHandler.showError(err, "Getting remote information failed"); 46 | }); 47 | } 48 | 49 | export function attachCommonEvents(config, $dialog) { 50 | const handleRadioChange = function () { 51 | const val = $dialog.find("input[name='action']:checked").val(); 52 | $dialog.find(".only-from-selected").toggle(val === "PULL_FROM_SELECTED" || val === "PUSH_TO_SELECTED"); 53 | }; 54 | $dialog.on("change", "input[name='action']", handleRadioChange); 55 | handleRadioChange(); 56 | 57 | let trackingBranchRemote = null; 58 | if (config.currentTrackingBranch) { 59 | trackingBranchRemote = config.currentTrackingBranch.substring(0, config.currentTrackingBranch.indexOf("/")); 60 | } 61 | 62 | // if we're pulling from another remote than current tracking remote 63 | if (config.currentTrackingBranch && trackingBranchRemote !== config.remote) { 64 | if (config.pull) { 65 | $dialog.find("input[value='PULL_FROM_CURRENT']").prop("disabled", true); 66 | $dialog.find("input[value='PULL_FROM_SELECTED']").prop("checked", true).trigger("change"); 67 | } else { 68 | $dialog.find("input[value='PUSH_TO_CURRENT']").prop("disabled", true); 69 | $dialog.find("input[value='PUSH_TO_SELECTED']").prop("checked", true).trigger("change"); 70 | } 71 | } 72 | 73 | $dialog.on("click", ".fetchBranches", () => { 74 | ProgressDialog.show(Git.fetchRemote(config.remote)) 75 | .then(() => { 76 | fillBranches(config, $dialog); 77 | }).catch((err) => { 78 | throw ErrorHandler.showError(err, "Fetching remote information failed"); 79 | }); 80 | }); 81 | fillBranches(config, $dialog); 82 | 83 | if (config._usernamePasswordDisabled) { 84 | $dialog.find("input[name='username'],input[name='password'],input[name='saveToUrl']").prop("disabled", true); 85 | } 86 | } 87 | 88 | export function collectValues(config, $dialog) { 89 | const action = $dialog.find("input[name='action']:checked").val(); 90 | if (action === "PULL_FROM_CURRENT" || action === "PUSH_TO_CURRENT") { 91 | 92 | if (config.currentTrackingBranch) { 93 | config.branch = config.currentTrackingBranch.substring(config.remote.length + 1); 94 | } else { 95 | config.branch = config.currentBranchName; 96 | config.pushToNew = true; 97 | } 98 | 99 | } else if (action === "PULL_FROM_SELECTED" || action === "PUSH_TO_SELECTED") { 100 | config.branch = $dialog.find(".branchSelect").val().substring(config.remote.length + 1); 101 | config.setBranchAsTracking = $dialog.find("input[name='setBranchAsTracking']").is(":checked"); 102 | } 103 | 104 | config.strategy = $dialog.find("input[name='strategy']:checked").val(); 105 | config.tags = $dialog.find("input[name='send_tags']:checked").val(); 106 | 107 | config.remoteUsername = $dialog.find("input[name='username']").val(); 108 | config.remotePassword = $dialog.find("input[name='password']").val(); 109 | 110 | // new url that has to be set for merging 111 | let remoteUrlNew; 112 | if (config.remoteUrl.match(/^https?:/)) { 113 | const uri = new URI(config.remoteUrl); 114 | uri.username(config.remoteUsername); 115 | uri.password(config.remotePassword); 116 | remoteUrlNew = uri.toString(); 117 | } 118 | 119 | // assign remoteUrlNew only if it's different from the original url 120 | if (remoteUrlNew && config.remoteUrl !== remoteUrlNew) { 121 | config.remoteUrlNew = remoteUrlNew; 122 | } 123 | 124 | // old url that has to be put back after merging 125 | const saveToUrl = $dialog.find("input[name='saveToUrl']").is(":checked"); 126 | // assign restore branch only if remoteUrlNew has some value 127 | if (config.remoteUrlNew && !saveToUrl) { 128 | config.remoteUrlRestore = config.remoteUrl; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/dialogs/templates/clone-dialog.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/dialogs/templates/credentials-template.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 |
8 | 11 | 14 |
15 | 16 |
17 | 20 | 23 |
24 | 25 |
26 | 29 |
30 | -------------------------------------------------------------------------------- /src/dialogs/templates/progress-dialog.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/dialogs/templates/pull-dialog.html: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /src/dialogs/templates/push-dialog.html: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /src/dialogs/templates/remotes-template.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 11 |
12 | 13 |
14 | 17 | 20 |
21 | 22 |
23 | 26 |
27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 | 39 |
40 | -------------------------------------------------------------------------------- /src/ftp/Ftp.ts: -------------------------------------------------------------------------------- 1 | import { _, DefaultDialogs, Dialogs, Mustache, StringUtils } from "../brackets-modules"; 2 | import * as ErrorHandler from "../ErrorHandler"; 3 | import * as Events from "../Events"; 4 | import EventEmitter from "../EventEmitter"; 5 | import * as Strings from "strings"; 6 | import * as Utils from "../Utils"; 7 | import * as GitFtp from "./GitFtp"; 8 | 9 | const ftpScopesTemplate = require("text!templates/ftp/remotes-picker.html"); 10 | let $gitPanel = null; 11 | let $remotesDropdown = null; 12 | 13 | const attachEvents = _.once(() => { 14 | $gitPanel 15 | .on("click", ".gitftp-remote-new", () => handleGitFtpScopeCreation()) 16 | .on("click", ".gitftp-remove-remote", function () { handleGitFtpScopeRemove($(this)); }) 17 | .on("click", ".gitftp-init-remote", function () { handleGitFtpInitScope($(this)); }) 18 | .on("click", ".git-push", function () { 19 | const typeOfRemote = $(this).attr("x-selected-remote-type"); 20 | if (typeOfRemote === "ftp") { 21 | handleGitFtpPush(); 22 | } 23 | }); 24 | }); 25 | 26 | function initVariables() { 27 | $gitPanel = $("#git-panel"); 28 | $remotesDropdown = $gitPanel.find(".git-remotes-dropdown"); 29 | attachEvents(); 30 | } 31 | 32 | function handleGitFtpPush() { 33 | const gitFtpScope = $gitPanel.find(".git-selected-remote").text().trim(); 34 | 35 | $gitPanel.find(".git-push") 36 | .addClass("btn-loading") 37 | .prop("disabled", true); 38 | 39 | return GitFtp.push(gitFtpScope).then((result) => { 40 | 41 | Dialogs.showModalDialog( 42 | DefaultDialogs.DIALOG_ID_INFO, 43 | Strings.GITFTP_PUSH_RESPONSE, // title 44 | result // message 45 | ); 46 | 47 | }).catch((err) => { 48 | 49 | ErrorHandler.showError(err, "Failed push to Git-FTP remote."); 50 | 51 | }).finally(() => { 52 | 53 | $gitPanel.find(".git-push") 54 | .removeClass("btn-loading") 55 | .prop("disabled", false); 56 | 57 | }); 58 | } 59 | 60 | function handleGitFtpScopeCreation() { 61 | $gitPanel.find(".git-remotes") 62 | .addClass("btn-loading") 63 | .prop("disabled", true); 64 | 65 | return Utils.askQuestion(Strings.CREATE_GITFTP_NEW_SCOPE, Strings.ENTER_GITFTP_SCOPE_NAME) 66 | .then((name) => { 67 | return Utils.askQuestion( 68 | Strings.CREATE_GITFTP_NEW_SCOPE, 69 | Strings.ENTER_GITFTP_SCOPE_URL, 70 | { defaultValue: "ftp://user:passwd@example.org/folder" } 71 | ) 72 | .then((url) => { 73 | return GitFtp.addScope(name, url).then(() => { 74 | 75 | // Render the list element of the new remote 76 | // FUTURE: replace this part with a way to call `Remotes.refreshRemotesPicker()` 77 | const $newScope = $("
  • ") 78 | .addClass("gitftp-remote") 79 | .append("") 80 | .find("a") 81 | .attr({ 82 | "href": "#", 83 | "data-remote-name": name, 84 | "data-type": "ftp" 85 | }) 86 | .addClass("remote-name") 87 | .append("") 88 | .find("span") 89 | .addClass("trash-icon gitftp-remove-remote") 90 | .html("×") 91 | .end() 92 | .append("") 93 | .find("span:nth-child(2)") 94 | .addClass("change-remote") 95 | .text(name) 96 | .end() 97 | .end(); 98 | 99 | $gitPanel.find(".git-remotes-dropdown .ftp-remotes-header").after($newScope); 100 | 101 | }).catch((err) => { 102 | ErrorHandler.showError(err, "Git-FTP remote creation failed"); 103 | }); 104 | }); 105 | }) 106 | .finally(() => { 107 | $gitPanel.find(".git-remotes") 108 | .removeClass("btn-loading") 109 | .prop("disabled", false); 110 | }); 111 | } 112 | 113 | function handleGitFtpScopeRemove($this) { 114 | $gitPanel.find(".git-remotes") 115 | .addClass("btn-loading") 116 | .prop("disabled", true); 117 | 118 | const $selectedElement = $this.closest(".remote-name"); 119 | const $currentScope = $gitPanel.find(".git-selected-remote"); 120 | const scopeName = $selectedElement.data("remote-name"); 121 | 122 | return Utils.askQuestion( 123 | Strings.DELETE_SCOPE, 124 | StringUtils.format(Strings.DELETE_SCOPE_NAME, scopeName), 125 | { booleanResponse: true } 126 | ).then((response) => { 127 | if (response) { 128 | return GitFtp.removeScope(scopeName).then(() => { 129 | $selectedElement.parent().remove(); 130 | const newScope = $gitPanel.find(".git-remotes-dropdown .remote").first().find("a").data("remote-name"); 131 | $currentScope.data("remote-name", newScope).html(newScope); 132 | }).catch((err) => { 133 | ErrorHandler.showError(err, "Remove scope failed"); 134 | }); 135 | } 136 | return null; 137 | }).finally(() => { 138 | $gitPanel.find(".git-remotes") 139 | .removeClass("btn-loading") 140 | .prop("disabled", false); 141 | }); 142 | } 143 | 144 | function handleGitFtpInitScope($this) { 145 | $gitPanel.find(".git-remotes") 146 | .addClass("btn-loading") 147 | .prop("disabled", true); 148 | 149 | const $selectedElement = $this.closest(".remote-name"); 150 | const scopeName = $selectedElement.data("remote-name"); 151 | 152 | return Utils.askQuestion( 153 | Strings.INIT_GITFTP_SCOPE, 154 | StringUtils.format(Strings.INIT_GITFTP_SCOPE_NAME, scopeName), 155 | { booleanResponse: true } 156 | ).then((response) => { 157 | if (response) { 158 | return GitFtp.init(scopeName).catch((err) => { 159 | ErrorHandler.showError(err, "Init scope failed"); 160 | }); 161 | } 162 | return null; 163 | }).finally(() => { 164 | $gitPanel.find(".git-remotes") 165 | .removeClass("btn-loading") 166 | .prop("disabled", false); 167 | }); 168 | } 169 | 170 | function addFtpScopesToPicker() { 171 | const otherRemotes = $remotesDropdown.find("li.remote"); 172 | 173 | GitFtp.getScopes().then((ftpScopes) => { 174 | if ($gitPanel.find(".ftp-remotes-header").length === 0) { 175 | // Pass to Mustache the needed data 176 | const compiledTemplate = Mustache.render(ftpScopesTemplate, { 177 | Strings, 178 | ftpScopes, 179 | hasFtpScopes: ftpScopes.length > 0 180 | }); 181 | $remotesDropdown.prepend(compiledTemplate); 182 | 183 | // if there are only ftp remotes, enable the push button and make first ftp remote selected 184 | if (otherRemotes.length === 0 && ftpScopes.length > 0) { 185 | $gitPanel 186 | .find(".git-push") 187 | .prop("disabled", false) 188 | .attr("x-selected-remote-type", "ftp"); 189 | $gitPanel 190 | .find(".git-selected-remote") 191 | .text(ftpScopes[0].name); 192 | } 193 | } 194 | }).catch((err) => { 195 | ErrorHandler.showError(err, "Getting FTP remotes failed!"); 196 | }); 197 | } 198 | 199 | GitFtp.isAvailable().then(() => { 200 | // Event subscriptions 201 | EventEmitter.on(Events.GIT_ENABLED, () => { 202 | initVariables(); 203 | }); 204 | 205 | EventEmitter.on(Events.REMOTES_REFRESH_PICKER, () => { 206 | addFtpScopesToPicker(); 207 | }); 208 | }).catch((err) => { 209 | ErrorHandler.showError(err, "Git-FTP seems not installed in your system, please install it and restart Brackets."); 210 | }); 211 | -------------------------------------------------------------------------------- /src/ftp/GitFtp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | To make these features work you need Git-FTP (https://github.com/git-ftp/git-ftp) 3 | */ 4 | 5 | import { git } from "../git/GitCli"; 6 | import * as Promise from "bluebird"; 7 | import * as URI from "URI"; 8 | 9 | export function isAvailable() { 10 | return git(["ftp"]) 11 | .then(() => true) 12 | .catch((err) => err); 13 | } 14 | 15 | export function init(scope) { 16 | return git(["ftp", "init", "--scope", scope]); 17 | } 18 | 19 | export function push(scope) { 20 | return git(["ftp", "push", "--scope", scope]); 21 | } 22 | 23 | export function getScopes() { 24 | return git(["config", "--list"]).then((stdout) => { 25 | return stdout.split("\n").reduce((result, row) => { 26 | const io = row.indexOf(".url"); 27 | if (row.substring(0, 8) === "git-ftp." && row.substring(io, io + 4) === ".url") { 28 | result.push({ 29 | name: row.split(".")[1], 30 | url: row.split("=")[1] 31 | }); 32 | } 33 | return result; 34 | }, []); 35 | }); 36 | } 37 | 38 | export function addScope(scope, url) { 39 | const uri = new URI(url); 40 | const username = uri.username(); 41 | const password = uri.password(); 42 | 43 | uri.username(""); 44 | uri.password(""); 45 | url = uri.toString(); // eslint-disable-line 46 | 47 | const scopeArgs = ["config", "--add", "git-ftp." + scope + ".url", url]; 48 | const usernameArgs = ["config", "--add", "git-ftp." + scope + ".user", username]; 49 | const passwordArgs = ["config", "--add", "git-ftp." + scope + ".password", password]; 50 | 51 | return Promise.all([ 52 | git(scopeArgs), 53 | git(usernameArgs), 54 | git(passwordArgs) 55 | ]); 56 | } 57 | 58 | export function removeScope(scope) { 59 | return git(["ftp", "remove-scope", scope]); 60 | } 61 | -------------------------------------------------------------------------------- /src/git/Git.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file acts as an entry point to GitCli.js and other possible 3 | implementations of Git communication besides Cli. Application 4 | should not access GitCli directly. 5 | */ 6 | 7 | import * as Preferences from "../Preferences"; 8 | import * as Promise from "bluebird"; 9 | import * as GitCli from "../git/GitCli"; 10 | import * as Utils from "../Utils"; 11 | 12 | export function pushToNewUpstream(remoteName, remoteBranch) { 13 | return GitCli.push(remoteName, remoteBranch, ["--set-upstream"]); 14 | } 15 | 16 | export function getRemoteUrl(remote) { 17 | return GitCli.getConfig("remote." + remote + ".url"); 18 | } 19 | 20 | export function setRemoteUrl(remote, url) { 21 | return GitCli.setConfig("remote." + remote + ".url", url); 22 | } 23 | 24 | function sortBranches(branches) { 25 | return branches.sort((a, b) => { 26 | const ar = a.remote || ""; 27 | const br = b.remote || ""; 28 | // origin remote first 29 | if (br && ar === "origin" && br !== "origin") { 30 | return -1; 31 | } else if (ar && ar !== "origin" && br === "origin") { 32 | return 1; 33 | } 34 | // sort by remotes 35 | if (ar < br) { 36 | return -1; 37 | } else if (ar > br) { 38 | return 1; 39 | } 40 | // sort by sortPrefix (# character) 41 | if (a.sortPrefix < b.sortPrefix) { 42 | return -1; 43 | } else if (a.sortPrefix > b.sortPrefix) { 44 | return 1; 45 | } 46 | // master branch first 47 | if (a.sortName === "master" && b.sortName !== "master") { 48 | return -1; 49 | } else if (a.sortName !== "master" && b.sortName === "master") { 50 | return 1; 51 | } 52 | // sort by sortName (lowercased branch name) 53 | if (a.sortName < b.sortName) { 54 | return -1; 55 | } 56 | if (a.sortName > b.sortName) { 57 | return 1; 58 | } 59 | return 0; 60 | }); 61 | } 62 | 63 | export function getBranches() { 64 | return GitCli.getBranches().then((branches) => sortBranches(branches)); 65 | } 66 | 67 | export function getAllBranches() { 68 | return GitCli.getAllBranches().then((branches) => sortBranches(branches)); 69 | } 70 | 71 | export function getHistory(branch, skip: number = 0) { 72 | return GitCli.getHistory(branch, skip); 73 | } 74 | 75 | export function getFileHistory(file, branch, skip: number = 0) { 76 | return GitCli.getHistory(branch, skip, file); 77 | } 78 | 79 | export function resetIndex() { 80 | return GitCli.reset(); 81 | } 82 | 83 | export function discardAllChanges() { 84 | return GitCli.reset("--hard").then(() => GitCli.clean()); 85 | } 86 | 87 | export function discardFileChanges(file) { 88 | return GitCli.unstage(file).then(() => GitCli.checkout(file)); 89 | } 90 | 91 | export function pushForced(remote, branch) { 92 | return GitCli.push(remote, branch, ["--force"]); 93 | } 94 | 95 | export function deleteRemoteBranch(remote, branch) { 96 | return GitCli.push(remote, branch, ["--delete"]); 97 | } 98 | 99 | export function undoLastLocalCommit() { 100 | return GitCli.reset("--soft", "HEAD~1"); 101 | } 102 | -------------------------------------------------------------------------------- /src/git/get-merge-info.ts: -------------------------------------------------------------------------------- 1 | import * as Preferences from "../Preferences"; 2 | import * as Utils from "../Utils"; 3 | 4 | export default function getMergeInfo() { 5 | const baseCheck = ["MERGE_MODE", "rebase-apply"]; 6 | const mergeCheck = ["MERGE_HEAD", "MERGE_MSG"]; 7 | const rebaseCheck = ["rebase-apply/next", "rebase-apply/last", "rebase-apply/head-name"]; 8 | const gitFolder = Preferences.get("currentGitRoot") + ".git/"; 9 | return Promise.all(baseCheck.map((fileName) => Utils.loadPathContent(gitFolder + fileName))) 10 | .then(([ mergeMode, rebaseMode ]) => { 11 | const obj = { 12 | mergeMode: mergeMode !== null, 13 | mergeHead: null, 14 | mergeMessage: null, 15 | mergeConflicts: null, 16 | rebaseMode: rebaseMode !== null, 17 | rebaseNext: null, 18 | rebaseLast: null, 19 | rebaseHead: null 20 | }; 21 | if (obj.mergeMode) { 22 | 23 | return Promise.all(mergeCheck.map((fileName) => Utils.loadPathContent(gitFolder + fileName))) 24 | .then(([ head, msg ]) => { 25 | if (head) { 26 | obj.mergeHead = head.trim(); 27 | } 28 | const msgSplit = msg ? msg.trim().split(/conflicts:/i) : []; 29 | if (msgSplit[0]) { 30 | obj.mergeMessage = msgSplit[0].trim(); 31 | } 32 | if (msgSplit[1]) { 33 | obj.mergeConflicts = msgSplit[1].trim().split("\n").map((line) => line.trim()); 34 | } 35 | return obj; 36 | }); 37 | 38 | } 39 | if (obj.rebaseMode) { 40 | 41 | return Promise.all(rebaseCheck.map((fileName) => Utils.loadPathContent(gitFolder + fileName))) 42 | .then(([ next, last, head ]) => { 43 | if (next) { obj.rebaseNext = next.trim(); } 44 | if (last) { obj.rebaseLast = last.trim(); } 45 | if (head) { obj.rebaseHead = head.trim().substring("refs/heads/".length); } 46 | return obj; 47 | }); 48 | 49 | } 50 | return obj; 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/node": "^6.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/node/process-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import { exec } from "child_process"; 4 | import * as fs from "fs"; 5 | import * as Path from "path"; 6 | import * as which from "which"; 7 | 8 | const isWin = /^win/.test(process.platform); 9 | 10 | function fixEOL(str: string) { 11 | return str[str.length - 1] === "\n" ? str.slice(0, -1) : str; 12 | } 13 | 14 | function findChildren(arr: Array<{ processid: number, parentprocessid: number }>, pid: number): number[] { 15 | let result: number[] = []; 16 | arr.forEach((obj) => { 17 | if (obj.parentprocessid === pid) { 18 | // add children pid first 19 | result = result.concat(findChildren(arr, obj.processid)); 20 | result.push(obj.processid); 21 | } 22 | }); 23 | return result; 24 | } 25 | 26 | export function killSingleProcess( 27 | pid: number, 28 | callback: (stderr: string | null, stdout: string | null) => void 29 | ) { 30 | if (isWin) { 31 | // "taskkill /F /PID 827" 32 | exec("taskkill /F /PID " + pid, (err, stdout, stderr) => { 33 | callback(err ? fixEOL(stderr) : null, err ? null : fixEOL(stdout)); 34 | }); 35 | } else { 36 | // "kill -9 2563" 37 | exec("kill -9 " + pid, (err, stdout, stderr) => { 38 | callback(err ? fixEOL(stderr) : null, err ? null : fixEOL(stdout)); 39 | }); 40 | } 41 | } 42 | 43 | export function getChildrenOfPid( 44 | pid: number, 45 | callback: (stderr: string | null, pids: number[]) => void 46 | ) { 47 | if (isWin) { 48 | exec("wmic process get parentprocessid,processid", (err, stdout, stderr) => { 49 | if (err) { 50 | return callback(fixEOL(stderr), []); 51 | } 52 | 53 | const map = fixEOL(stdout).split("\n").map((line) => { 54 | const parts = line.trim().split(/\s+/); 55 | const processid = parseInt(parts.pop() as string, 10); 56 | const parentprocessid = parseInt(parts.pop() as string, 10); 57 | return { processid, parentprocessid }; 58 | }); 59 | 60 | callback(null, findChildren(map, pid)); 61 | }); 62 | } else { 63 | exec("ps -A -o ppid,pid", (err, stdout, stderr) => { 64 | if (err) { 65 | return callback(fixEOL(stderr), []); 66 | } 67 | 68 | const map = fixEOL(stdout).split("\n").map((line) => { 69 | const parts = line.trim().split(/\s+/); 70 | const processid = parseInt(parts.pop() as string, 10); 71 | const parentprocessid = parseInt(parts.pop() as string, 10); 72 | return { processid, parentprocessid }; 73 | }); 74 | 75 | callback(null, findChildren(map, pid)); 76 | }); 77 | } 78 | } 79 | 80 | export function executableExists( 81 | command: string, 82 | callback: (err: NodeJS.ErrnoException | null, exists: boolean, resolvedPath: string | null) => void 83 | ) { 84 | which(command, (whichErr, _path) => { 85 | if (whichErr) { 86 | return callback(whichErr, false, null); 87 | } 88 | const path = Path.normalize(_path); 89 | fs.stat(path, (statErr, stats) => { 90 | if (statErr) { 91 | return callback(statErr, false, null); 92 | } 93 | const exists = stats.isFile(); 94 | return callback(null, exists, exists ? path : null); 95 | }); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /src/node/processUtils-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | /* eslint-env node */ 3 | 4 | import * as ProcessUtils from "./process-utils"; 5 | 6 | /* 7 | var pid = 5064; 8 | ProcessUtils.getChildrenOfPid(pid, function (err, children) { 9 | console.log(children); 10 | children.push(pid); 11 | children.forEach(function (pid) { 12 | ProcessUtils.killSingleProcess(pid); 13 | }); 14 | }); 15 | */ 16 | 17 | [ 18 | "git", 19 | "C:\\Program Files (x86)\\Git\\cmd\\git.exe", 20 | "C:/Program Files (x86)/Git/cmd/git.exe", 21 | "C:/Program Files (x86)/Git/cmd/git2.exe", 22 | "C:/Program Files (x86)/Git/cmd/", 23 | "C:/Program Files (x86)/Git/cmd", 24 | "C:\\Program Files (x86)\\Git\\Git Bash.vbs" 25 | ].forEach((path) => { 26 | 27 | ProcessUtils.executableExists(path, (err, exists, resolvedPath) => { 28 | if (err) { 29 | console.error("executableExists error: " + err); 30 | } 31 | console.log("ProcessUtils.executableExists for: " + path); 32 | console.log(" - exists: " + exists); 33 | console.log(" - resolvedPath: " + resolvedPath); 34 | }); 35 | 36 | }); 37 | 38 | /* 39 | ProcessUtils.executableExists("git", function (err, result, resolvedPath) { 40 | console.log("git"); 41 | console.log(result); 42 | }); 43 | ProcessUtils.executableExists("C:\\Program Files (x86)\\Git\\cmd\\git.exe", function (err, result) { 44 | console.log("result for C:\\Program Files (x86)\\Git\\cmd\\git.exe"); 45 | console.log(result); 46 | }); 47 | ProcessUtils.executableExists("C:/Program Files (x86)/Git/cmd/git.exe", function (err, result) { 48 | console.log("result for C:/Program Files (x86)/Git/cmd/git.exe"); 49 | console.log(result); 50 | }); 51 | 52 | ProcessUtils.executableExists("C:/Program Files (x86)/Git/cmd/git2.exe", function (err, result) { 53 | console.log("result for C:/Program Files (x86)/Git/cmd/git2.exe"); 54 | console.log(result); 55 | }); 56 | ProcessUtils.executableExists("C:/Program Files (x86)/Git/cmd/", function (err, result) { 57 | console.log("result for C:/Program Files (x86)/Git/cmd/"); 58 | console.log(result); 59 | }); 60 | ProcessUtils.executableExists("C:/Program Files (x86)/Git/cmd", function (err, result) { 61 | console.log("result for C:/Program Files (x86)/Git/cmd"); 62 | console.log(result); 63 | }); 64 | */ 65 | -------------------------------------------------------------------------------- /src/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../../dist/node", 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "forceConsistentCasingInFileNames": true, 10 | "newLine": "lf", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "removeComments": true, 17 | "strictNullChecks": true 18 | }, 19 | "include": [ 20 | "./**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/Setup.ts: -------------------------------------------------------------------------------- 1 | import * as Cli from "../Cli"; 2 | import * as Git from "../git/GitCli"; 3 | import * as Preferences from "../Preferences"; 4 | import * as Promise from "bluebird"; 5 | import * as Utils from "../Utils"; 6 | import { _ } from "../brackets-modules"; 7 | 8 | const standardGitPathsWin = [ 9 | "C:\\Program Files (x86)\\Git\\cmd\\git.exe", 10 | "C:\\Program Files\\Git\\cmd\\git.exe" 11 | ]; 12 | 13 | const standardGitPathsNonWin = [ 14 | "/usr/local/git/bin/git", 15 | "/usr/local/bin/git", 16 | "/usr/bin/git" 17 | ]; 18 | 19 | export function findGit() { 20 | return new Promise((resolve, reject) => { 21 | 22 | // TODO: do this in two steps - first check user config and then check all 23 | let pathsToLook = [Preferences.get("gitPath"), "git"] 24 | .concat(brackets.platform === "win" ? standardGitPathsWin : standardGitPathsNonWin); 25 | pathsToLook = _.unique(_.compact(pathsToLook)); 26 | 27 | const results = []; 28 | const errors = []; 29 | const finish = _.after(pathsToLook.length, () => { 30 | 31 | const searchedPaths = "\n\nSearched paths:\n" + pathsToLook.join("\n"); 32 | 33 | if (results.length === 0) { 34 | // no git found 35 | reject("No Git has been found on this computer" + searchedPaths); 36 | } else { 37 | // at least one git is found 38 | const gits = _.sortBy(results, "version").reverse(); 39 | let latestGit = gits[0]; 40 | const m = latestGit.version.match(/([0-9]+)\.([0-9]+)/); 41 | const major = parseInt(m[1], 10); 42 | const minor = parseInt(m[2], 10); 43 | 44 | if (major === 1 && minor < 8) { 45 | return reject( 46 | "Brackets Git requires Git 1.8 or later - latest version found was " + 47 | latestGit.version + searchedPaths 48 | ); 49 | } 50 | 51 | // prefer the first defined so it doesn't change all the time and confuse people 52 | latestGit = _.sortBy(_.filter(gits, (git) => git.version === latestGit.version), "index")[0]; 53 | 54 | // this will save the settings also 55 | Git.setGitPath(latestGit.path); 56 | resolve(latestGit.version); 57 | } 58 | 59 | }); 60 | 61 | pathsToLook.forEach((path, index) => { 62 | Cli.spawnCommand(path, ["--version"], { 63 | cwd: Utils.getExtensionDirectory() 64 | }).then((stdout) => { 65 | const m = stdout.match(/^git version\s+(.*)$/); 66 | if (m) { 67 | results.push({ 68 | path, 69 | version: m[1], 70 | index 71 | }); 72 | } 73 | }).catch((err) => { 74 | errors.push({ 75 | path, 76 | err 77 | }); 78 | }).finally(() => finish()); 79 | }); 80 | 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/Terminal.ts: -------------------------------------------------------------------------------- 1 | import { _, platform } from "../brackets-modules"; 2 | import * as Cli from "../Cli"; 3 | import * as ErrorHandler from "../ErrorHandler"; 4 | import * as Events from "../Events"; 5 | import EventEmitter from "../EventEmitter"; 6 | import ExpectedError from "../ExpectedError"; 7 | import * as Preferences from "../Preferences"; 8 | import * as Promise from "bluebird"; 9 | import * as Utils from "../Utils"; 10 | 11 | // Converts UNC (Samba) urls from `//server/` to `\\server\` 12 | function normalizeUncUrls(url) { 13 | return url.substring(0, 1) === "//" && platform === "win" ? url.replace("/", "\\") : url; 14 | } 15 | 16 | function chmodTerminalScript(allowExec) { 17 | const files = platform === "mac" ? [ 18 | // mac 19 | "terminal.osa", 20 | "iterm.osa" 21 | ] : [ 22 | // linux 23 | "terminal.sh" 24 | ]; 25 | 26 | const args = [allowExec ? "+x" : "-x"].concat(files.map((file) => { 27 | return Cli.escapeShellArg(Utils.getExtensionDirectory() + "shell/" + file); 28 | })); 29 | 30 | return Cli.executeCommand("chmod", args); 31 | } 32 | 33 | function open(event?: string) { 34 | const folder = Utils.getProjectRoot(); 35 | const customCmd = Preferences.get("terminalCommand"); 36 | const customArgs = Preferences.get("terminalCommandArgs"); 37 | 38 | let cmd = customCmd; 39 | const args = customArgs ? customArgs.split(" ").map((arg) => { 40 | return arg.replace("$1", Cli.escapeShellArg(normalizeUncUrls(folder))); 41 | }) : []; 42 | const opts = { timeout: false }; 43 | 44 | if (platform === "mac" && cmd.match(/\.osa$/)) { 45 | args.unshift(Cli.escapeShellArg(cmd)); 46 | cmd = "osascript"; 47 | } 48 | 49 | return Cli.executeCommand(cmd, args, opts).catch((err) => { 50 | if (ErrorHandler.isTimeout(err)) { 51 | // process is running after 1 second timeout so terminal is opened 52 | return; 53 | } 54 | const pathExecuted = [cmd].concat(args).join(" "); 55 | throw new Error(err + ": " + pathExecuted); 56 | }).catch((err) => { 57 | if (event !== "retry" && ErrorHandler.contains(err, "Permission denied")) { 58 | chmodTerminalScript(true).catch((chmodErr) => { 59 | throw ErrorHandler.showError(chmodErr, "Failed to open terminal"); 60 | }).then(() => { 61 | open("retry"); 62 | }); 63 | return; 64 | } 65 | ErrorHandler.showError(err, "Failed to open terminal"); 66 | }); 67 | } 68 | 69 | const setup = _.once(() => { 70 | return new Promise((resolve) => { 71 | 72 | let paths = [Preferences.get("terminalCommand")]; 73 | 74 | if (platform === "win") { 75 | paths.push("C:\\Program Files (x86)\\Git\\Git Bash.vbs"); 76 | paths.push("C:\\Program Files\\Git\\Git Bash.vbs"); 77 | paths.push("C:\\Program Files (x86)\\Git\\git-bash.exe"); 78 | paths.push("C:\\Program Files\\Git\\git-bash.exe"); 79 | } else if (platform === "mac") { 80 | paths.push(Utils.getExtensionDirectory() + "shell/terminal.osa"); 81 | } else { 82 | paths.push(Utils.getExtensionDirectory() + "shell/terminal.sh"); 83 | } 84 | 85 | paths = _.unique(paths); 86 | 87 | const results = []; 88 | const finish = _.after(paths.length, () => { 89 | 90 | if (!results[0]) { 91 | // configuration is not set to something meaningful 92 | const validPaths = _.compact(results); 93 | if (validPaths.length === 0) { 94 | // nothing meaningful found, so restore default configuration 95 | Preferences.set("terminalCommand", paths[1]); 96 | Preferences.set("terminalCommandArgs", "$1"); 97 | resolve(false); 98 | return; 99 | } 100 | Preferences.set("terminalCommand", validPaths[0]); 101 | if (/git-bash\.exe$/.test(validPaths[0])) { 102 | Preferences.set("terminalCommandArgs", "--cd=$1"); 103 | } else { 104 | Preferences.set("terminalCommandArgs", "$1"); 105 | } 106 | } else { 107 | Preferences.set("terminalCommand", results[0]); 108 | } 109 | resolve(true); 110 | 111 | }); 112 | 113 | // verify if these paths exist 114 | paths.forEach((path, index) => { 115 | if (!path) { 116 | results[index] = null; 117 | finish(); 118 | return; 119 | } 120 | Cli.which(path).then((_path) => { 121 | results[index] = _path; 122 | }).catch(() => { 123 | results[index] = null; 124 | }).finally(() => { 125 | finish(); 126 | }); 127 | }); 128 | 129 | }).then((result) => { 130 | // my mac yosemite will actually fail if the scripts are executable! 131 | // so do -x here and then do +x when permission denied is encountered 132 | // TODO: explore linux/ubuntu behaviour 133 | if (platform === "mac") { 134 | return chmodTerminalScript(false).then(() => result); 135 | } 136 | return result; 137 | }); 138 | }); 139 | 140 | // Event subscriptions 141 | EventEmitter.on(Events.TERMINAL_OPEN, () => { 142 | setup() 143 | .then((configuredOk) => { 144 | if (configuredOk) { 145 | open(); 146 | } else { 147 | throw new ExpectedError( 148 | "Terminal configuration invalid, restoring defaults. Restart Brackets to apply." 149 | ); 150 | } 151 | }) 152 | .catch((err) => { 153 | // disable the button for this session 154 | EventEmitter.emit(Events.TERMINAL_DISABLE); 155 | ErrorHandler.showError(err, "Error setting up the terminal"); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /strings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provides the interface to user visible strings in Brackets. Code that needs 3 | * to display strings should should load this module by calling var Strings = require("strings"). 4 | * The i18n plugin will dynamically load the strings for the right locale and populate 5 | * the exports variable. See nls/root/strings.js for the master file of English strings. 6 | */ 7 | define(function (require, exports, module) { 8 | module.exports = require("i18n!nls/strings"); 9 | }); 10 | -------------------------------------------------------------------------------- /styles/brackets/brackets_core_ui_variables.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | /* 22 | * Brackets Colors 23 | * 24 | * These are general purpose colors that can be used in defining 25 | * themes or UI elements. 26 | 27 | * IMPORTANT: IF we want a UI element to be themeable, these variable names or 28 | * color literals (#aaa) should not be used in its definition. 29 | * 30 | * Instead, a new semantically-meaningful variables/mixins should be added 31 | * to the "brackets_theme_default.less" file, and then these variables/mixins 32 | * should be used in the definition of the UI element 33 | * 34 | * For UI elements we do NOT want to theme, we should use these color names 35 | * 36 | * All brackets color variable names (that refer to an actual color) 37 | * are prefixed with "bc-" for "brackets". This is to avoid confusion 38 | * with system and css color names. (We define our own colors because system 39 | * colors are ugly.) 40 | */ 41 | 42 | // General 43 | @bc-bg-highlight: #e0f0fa; 44 | @bc-bg-inline-widget: #e6e9e9; 45 | @bc-bg-tool-bar: #5D5F60; 46 | @bc-bg-status-bar: #fff; 47 | @bc-disabled-opacity: 0.3; 48 | @bc-error: #f74687; 49 | @bc-modal-backdrop-opacity: 0.4; 50 | 51 | // Highlights and Shadows 52 | @bc-highlight: rgba(255, 255, 255, 0.12); 53 | @bc-highlight-hard: rgba(255, 255, 255, 0.5); 54 | @bc-shadow: rgba(0, 0, 0, 0.24); 55 | @bc-shadow-large: rgba(0, 0, 0, 0.5); 56 | @bc-shadow-small: rgba(0, 0, 0, 0.06); 57 | 58 | // Border Radius 59 | @bc-border-radius: 3px; 60 | @bc-border-radius-large: 5px; 61 | @bc-border-radius-small: 2px; 62 | 63 | // Menu 64 | @bc-menu-bg: #fff; 65 | @bc-menu-text: #000; 66 | @bc-menu-separator: #eaeaea; 67 | 68 | // Warning 69 | @bc-warning-bg: #fdf5cc; 70 | @bc-warning-text: #635301; 71 | 72 | // Text 73 | @bc-text: #333; 74 | @bc-text-alt: #fff; 75 | @bc-text-emphasized: #111; 76 | @bc-text-link: #0083e8; 77 | @bc-text-medium: #606060; 78 | @bc-text-quiet: #aaa; 79 | @bc-text-thin: #000; 80 | @bc-text-thin-quiet: #777; 81 | 82 | // Panel 83 | @bc-panel-bg: #dfe2e2; 84 | @bc-panel-bg-alt: #e6e9e9; 85 | @bc-panel-bg-promoted: #d4d7d7; 86 | @bc-panel-bg-hover: rgba(255, 255, 255, 0.6); 87 | @bc-panel-bg-hover-alt: rgba(0, 0, 0, 0.04); 88 | @bc-panel-bg-selected: #d0d5d5; 89 | @bc-panel-bg-text-highlight: #fff; 90 | @bc-panel-border: rgba(0, 0, 0, 0.09); 91 | @bc-panel-separator: #c3c6c5; 92 | 93 | // Default Button 94 | @bc-btn-bg: #e5e9e9; 95 | @bc-btn-bg-down: #d3d7d7; 96 | @bc-btn-bg-down-alt: #404141; 97 | @bc-btn-border: #b2b5b5; 98 | @bc-btn-border-error: #fa689d; 99 | @bc-btn-border-error-glow: #ffb0cd; 100 | @bc-btn-border-focused: #2893ef; 101 | @bc-btn-border-focused-glow: #94ceff; 102 | @bc-btn-triangle: #878787; 103 | @bc-input-bg: #fff; 104 | 105 | // Primary Button 106 | @bc-primary-btn-bg: #288edf; 107 | @bc-primary-btn-bg-down: #0380e8; 108 | @bc-primary-btn-border: #1474bf; 109 | 110 | // Secondary Button 111 | @bc-secondary-btn-bg: #91cc41; 112 | @bc-secondary-btn-bg-down: #82b839; 113 | @bc-secondary-btn-border: #74B120; 114 | 115 | // Sidebar 116 | @bc-sidebar-bg: #3C3F41; 117 | @bc-sidebar-selection: #2D2E30; 118 | 119 | // images 120 | @button-icon: "images/find-replace-sprites.svg"; 121 | @jstree-sprite: url("images/jsTreeSprites.svg") !important; 122 | 123 | 124 | 125 | /* Dark Core UI variables -----------------------------------------------------------------------------*/ 126 | 127 | // General 128 | @dark-bc-bg-highlight: #005ecc; 129 | @dark-bc-bg-inline-widget: #1b1b1b; 130 | @dark-bc-bg-tool-bar: #5D5F60; 131 | @dark-bc-bg-status-bar: #1c1c1e; 132 | @dark-bc-disabled-opacity: 0.3; 133 | @dark-bc-error: #f74687; 134 | @dark-bc-modal-backdrop-opacity: 0.7; 135 | 136 | // Highlights and Shadows 137 | @dark-bc-highlight: rgba(255, 255, 255, 0.06); 138 | @dark-bc-highlight-hard: rgba(255, 255, 255, 0.2); 139 | @dark-bc-shadow: rgba(0, 0, 0, 0.24); 140 | @dark-bc-shadow-medium: rgba(0, 0, 0, 0.12); 141 | @dark-bc-shadow-large: rgba(0, 0, 0, 0.5); 142 | @dark-bc-shadow-small: rgba(0, 0, 0, 0.06); 143 | 144 | // Border Radius 145 | @dark-bc-border-radius: 3px; 146 | @dark-bc-border-radius-large: 5px; 147 | @dark-bc-border-radius-small: 2px; 148 | 149 | // Menu 150 | @dark-bc-menu-bg: #000; 151 | @dark-bc-menu-text: #fff; 152 | @dark-bc-menu-separator: #343434; 153 | 154 | // Warning 155 | @dark-bc-warning-bg: #c95800; 156 | @dark-bc-warning-text: #fff; 157 | 158 | // Text 159 | @dark-bc-text: #ccc; 160 | @dark-bc-text-alt: #fff; 161 | @dark-bc-text-emphasized: #fff; 162 | @dark-bc-text-link: #6bbeff; 163 | @dark-bc-text-medium: #ccc; 164 | @dark-bc-text-quiet: #aaa; 165 | @dark-bc-text-thin: #fff; 166 | @dark-bc-text-thin-quiet: #bbb; 167 | 168 | // Panel 169 | @dark-bc-panel-bg: #2c2c2c; 170 | @dark-bc-panel-bg-alt: #313131; 171 | @dark-bc-panel-bg-promoted: #222; 172 | @dark-bc-panel-bg-hover: rgba(255, 255, 255, 0.12); 173 | @dark-bc-panel-bg-hover-alt: rgba(0, 0, 0, 0.04); 174 | @dark-bc-panel-bg-selected: #3d3e40; 175 | @dark-bc-panel-bg-text-highlight: #000; 176 | @dark-bc-panel-border: #000; 177 | @dark-bc-panel-separator: #343434; 178 | 179 | // Default Button 180 | @dark-bc-btn-bg: #3f3f3f; 181 | @dark-bc-btn-bg-down: #222; 182 | @dark-bc-btn-bg-down-alt: #404141; 183 | @dark-bc-btn-border: #202020; 184 | @dark-bc-btn-border-error: #fa689d; 185 | @dark-bc-btn-border-error-glow: transparent; 186 | @dark-bc-btn-border-focused: #2893ef; 187 | @dark-bc-btn-border-focused-glow: transparent; 188 | @dark-bc-btn-triangle: #aaa; 189 | @dark-bc-input-bg: #555; 190 | 191 | // Primary Button 192 | @dark-bc-primary-btn-bg: #016dc4; 193 | @dark-bc-primary-btn-bg-down: #00569b; 194 | @dark-bc-primary-btn-border: #202020; 195 | 196 | // Secondary Button 197 | @dark-bc-secondary-btn-bg: #5b9e00; 198 | @dark-bc-secondary-btn-bg-down: #437900; 199 | @dark-bc-secondary-btn-border: #202020; 200 | 201 | // Sidebar 202 | @dark-bc-sidebar-bg: #3C3F41; 203 | @dark-bc-sidebar-selection: #2D2E30; 204 | 205 | // images 206 | @dark-button-icon: "images/find-replace-sprites-dark.svg"; 207 | @dark-jstree-sprite: url("images/jsTreeSprites-dark.svg") !important; 208 | -------------------------------------------------------------------------------- /styles/code-mirror.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | 3 | .CodeMirror { 4 | .CodeMirror-gutter-elt { 5 | // do not enable pointer events 6 | // pointer-events: all; 7 | } 8 | .brackets-git-gutter { 9 | width: @gutterWidth; 10 | margin-left: 1px; 11 | } 12 | .brackets-git-gutter-added, 13 | .brackets-git-gutter-modified, 14 | .brackets-git-gutter-removed { 15 | background-size: @gutterWidth @gutterWidth; 16 | background-repeat: no-repeat; 17 | font-size: 1em; 18 | font-weight: bold; 19 | color: @bc-menu-bg; 20 | 21 | .dark & { 22 | color: @dark-bc-menu-bg; 23 | } 24 | } 25 | .brackets-git-gutter-added { 26 | background-color: @green; 27 | } 28 | .brackets-git-gutter-added.brackets-git-gutter-hover:before { 29 | content: "+"; 30 | } 31 | .brackets-git-gutter-modified { 32 | background-color: @orange; 33 | } 34 | .brackets-git-gutter-modified.brackets-git-gutter-hover:before { 35 | content: "*"; 36 | } 37 | .brackets-git-gutter-removed { 38 | background-color: @red; 39 | } 40 | .brackets-git-gutter-removed.brackets-git-gutter-hover:before { 41 | content: "-"; 42 | } 43 | .brackets-git-gutter-deleted-lines { 44 | color: @bc-text; 45 | background-color: lighten(@red, 25%); 46 | .selectable-text(); 47 | 48 | .dark & { 49 | background-color: darken(@red, 25%); 50 | color: @dark-bc-text; 51 | } 52 | 53 | position: relative; 54 | .brackets-git-gutter-copy-button { 55 | position: absolute; 56 | left: 0; 57 | top: 0; 58 | padding: 1px; 59 | height: 1.2em; 60 | line-height: 0.5em; 61 | } 62 | } 63 | .CodeMirror-linenumber.brackets-git-gutter-hover { 64 | background-color: @bc-panel-bg-alt; 65 | color: @bc-text-thin; 66 | 67 | .dark & { 68 | background-color: @dark-bc-panel-bg-alt; 69 | color: @dark-bc-text-alt !important; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /styles/colors.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | 3 | // TODO: try to reuse colors from brackets_colors.less instead of these 4 | 5 | @moreDarkGrey: #868888; 6 | @green: #91CC41; 7 | @red: #F74687; 8 | @red-text: #F74687; 9 | @red-background: #FF7CAD; 10 | @blue-text: #1976DD; 11 | @dark-blue-text: #51c0ff; 12 | @orange: #E3B551; 13 | @orange-text: #e28200; 14 | 15 | // Diff colors ('d' for 'dark', `l` for "light") 16 | @diff-gray-text: #333333; 17 | @diff-dgray-bg: #444444; 18 | @diff-lgray-bg: #F0F0F7; 19 | @diff-gray-border: #CBCBCB; 20 | @diff-lgreen-bg: #DBFFDB; 21 | @diff-green-bg: #CEFFCE; 22 | @diff-green-border: #A1CFA1; 23 | @diff-lred-bg: #FFDBDB; 24 | @diff-red-bg: #F7C8C8; 25 | @diff-red-border: #E9AEAE; 26 | 27 | @dark-diff-gray-text: #eeeeee; 28 | @dark-diff-dgray-bg: #3f3f3f; 29 | @dark-diff-lgray-bg: #555555; 30 | @dark-diff-gray-border: #3f3f3f; 31 | @dark-diff-lgreen-bg: #197e19; 32 | @dark-diff-green-bg: #137413; 33 | @dark-diff-green-border: #005c00; 34 | @dark-diff-lred-bg: #af4462; 35 | @dark-diff-red-bg: #a33a57; 36 | @dark-diff-red-border: #831919; 37 | -------------------------------------------------------------------------------- /styles/commit-diff.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | 3 | .commit-diff { 4 | @lineHeight: 15px; 5 | color: @diff-gray-text; 6 | .selectable-text(); 7 | 8 | .dark & { 9 | background-color: @dark-diff-lgray-bg; 10 | border: 1px solid @dark-bc-btn-border; 11 | color: @dark-diff-gray-text; 12 | } 13 | 14 | code, pre { 15 | background-color: @diff-lgray-bg; 16 | color: @diff-gray-text; 17 | border: none; 18 | padding: 0 3px; 19 | margin: 0; 20 | 21 | .dark & { 22 | background-color: @dark-diff-lgray-bg; 23 | color: @dark-diff-gray-text; 24 | } 25 | } 26 | background-color: @diff-lgray-bg; 27 | border: 1px solid @bc-btn-border; 28 | border-radius: 3px; 29 | margin-bottom: 1em; 30 | overflow: auto; 31 | padding: 0; 32 | 33 | // FIXME: this part will be removed after github.com/adobe/brackets/issues/7673 34 | table:not(.table-striped) { 35 | > tbody { 36 | > tr:nth-child(even), tr:nth-child(odd) { 37 | > td, 38 | > th { 39 | background-color: transparent; 40 | } 41 | } 42 | } 43 | } 44 | table { 45 | width: 100%; 46 | cursor: text; 47 | tbody { 48 | tr.meta-file { 49 | th { 50 | border-top: 0; 51 | color: @blue-text; 52 | padding: @lineHeight / 2; 53 | text-align: left; 54 | 55 | .dark & { 56 | color: @dark-blue-text; 57 | } 58 | } 59 | &:not(:first-child) { 60 | border-top: 1px dashed @bc-btn-border; 61 | 62 | .dark & { 63 | border-top: 1px dashed @dark-bc-btn-border; 64 | } 65 | } 66 | } 67 | tr.separator { 68 | height: @lineHeight; 69 | &:first-child, &:last-child { 70 | display: none; 71 | } 72 | } 73 | tr { 74 | td { 75 | border-width: 0px; 76 | padding: 0 8px; 77 | &.row-num { 78 | width: 1px; 79 | border-right: 1px solid; 80 | border-color: @diff-gray-border; 81 | text-align: right; 82 | color: @bc-text; 83 | .user-select(none); 84 | 85 | .dark & { 86 | border-color: @dark-diff-gray-border; 87 | color: @dark-bc-text; 88 | } 89 | } 90 | pre { 91 | white-space: nowrap; 92 | .trailingWhitespace { 93 | background-color: @diff-red-bg; 94 | 95 | .dark & { 96 | background-color: @dark-diff-red-bg; 97 | } 98 | } 99 | } 100 | } 101 | &.added { 102 | &, & pre { 103 | background-color: @diff-lgreen-bg; 104 | 105 | .dark & { 106 | background-color: @dark-diff-lgreen-bg; 107 | } 108 | } 109 | pre:before { 110 | content: "+"; 111 | } 112 | .row-num { 113 | background-color: @diff-green-bg !important; //FIXME: `!important` will be removed after github.com/adobe/brackets/issues/7673 114 | border-color: @diff-green-border; 115 | 116 | .dark & { 117 | background-color: @dark-diff-green-bg !important; //FIXME: `!important` will be removed after github.com/adobe/brackets/issues/7673 118 | border-color: @dark-diff-green-border; 119 | } 120 | } 121 | } 122 | &.removed { 123 | &, & pre { 124 | background-color: @diff-lred-bg; 125 | 126 | .dark & { 127 | background-color: @dark-diff-lred-bg; 128 | } 129 | } 130 | pre:before { 131 | content: "-"; 132 | } 133 | .row-num { 134 | background-color: @diff-red-bg !important; //FIXME: `!important` will be removed after github.com/adobe/brackets/issues/7673 135 | border-color: @diff-red-border; 136 | 137 | .dark & { 138 | background-color: @dark-diff-red-bg !important; 139 | border-color: @dark-diff-red-border; 140 | } 141 | } 142 | } 143 | &.unchanged { 144 | pre:before { 145 | content: "\0A0"; //   146 | } 147 | } 148 | &.diffCmd { 149 | color: @blue-text; 150 | } 151 | &.position { 152 | td { 153 | padding: 8px; 154 | } 155 | &, & pre { 156 | color: @diff-gray-text; 157 | background-color: @diff-lgray-bg; 158 | 159 | .dark & { 160 | color: @dark-diff-gray-text; 161 | background-color: @dark-diff-lgray-bg; 162 | } 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /styles/common.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | .git { 3 | hr { 4 | margin: 12px auto; 5 | width: 95%; 6 | height: 1px; 7 | border: none; 8 | background-color: @bc-panel-separator; 9 | 10 | .dark & { 11 | background-color: @dark-bc-panel-separator; 12 | } 13 | } 14 | 15 | /* radio buttons until they are styled in brackets */ 16 | input[type="radio"] { 17 | margin: 0; 18 | } 19 | input[type="radio"] { 20 | height: 13px; 21 | width: 13px; 22 | vertical-align: middle; 23 | border: 1px solid @bc-btn-border; 24 | border-radius: 13px; 25 | background-color: @bc-btn-bg; 26 | -webkit-appearance: none; 27 | box-shadow: inset 0 1px 0 @bc-highlight; 28 | 29 | .dark & { 30 | border: 1px solid @dark-bc-btn-border; 31 | background-color: @dark-bc-btn-bg; 32 | box-shadow: inset 0 1px 0 @dark-bc-highlight; 33 | } 34 | } 35 | input[type="radio"]:active:not(:disabled) { 36 | background-color: @bc-btn-bg-down; 37 | box-shadow: inset 0 1px 0 @bc-shadow-small; 38 | 39 | .dark & { 40 | background-color: @dark-bc-btn-bg-down; 41 | box-shadow: inset 0 1px 0 @dark-bc-shadow-small; 42 | } 43 | } 44 | input[type="radio"]:focus { 45 | outline:none; 46 | border: 1px solid @bc-btn-border-focused; 47 | box-shadow: 0 0 0 1px @bc-btn-border-focused-glow; 48 | 49 | .dark & { 50 | border: 1px solid @dark-bc-btn-border-focused; 51 | box-shadow: 0 0 0 1px @dark-bc-btn-border-focused-glow; 52 | } 53 | } 54 | input[type="radio"]:checked:before { 55 | font-weight: bold; 56 | color: @bc-text; 57 | content: '\25cf'; 58 | -webkit-margin-start: 0; 59 | position: relative; 60 | left: 2px; 61 | top: -4px; 62 | font-size: 12px; 63 | 64 | .dark & { 65 | color: @dark-bc-text; 66 | } 67 | } 68 | /* /radio buttons */ 69 | 70 | .text-bold { 71 | font-weight: 500; 72 | } 73 | .text-quiet { 74 | color: @bc-text-quiet; 75 | 76 | .dark & { 77 | color: @dark-bc-text-quiet; 78 | } 79 | } 80 | 81 | @small: 5px; 82 | 83 | .padding-right-small { 84 | padding-right: @small; 85 | } 86 | } 87 | 88 | // Additional icons for GitHub Octicon iconic font 89 | .octicon { 90 | &.octicon-expand, &.octicon-collapse { 91 | position: relative; 92 | width: 12px; 93 | height: 12px; 94 | zoom: 1.3; 95 | &:before, &:after { 96 | position: absolute; 97 | } 98 | &:before { 99 | -webkit-transform: rotate(135deg); 100 | } 101 | &:after { 102 | -webkit-transform: rotate(315deg); 103 | } 104 | } 105 | &.octicon-expand { 106 | &:before, &:after { 107 | content: "\F071"; 108 | } 109 | &:before { 110 | top: -5px; 111 | left: 5px; 112 | } 113 | } 114 | &.octicon-collapse { 115 | -webkit-transform: translateY(1px); 116 | &:before, &:after { 117 | content: "\F0A1"; 118 | } 119 | &:before { 120 | top: -6px; 121 | left: 6px; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /styles/dialogs/diff-dialog.less: -------------------------------------------------------------------------------- 1 | // main: ../brackets-git.less 2 | 3 | #git-diff-dialog { 4 | .commit-diff { 5 | .meta-file { 6 | display: none; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /styles/dialogs/error-dialog.less: -------------------------------------------------------------------------------- 1 | // main: ../brackets-git.less 2 | 3 | #git-error-dialog { 4 | .modal-header { 5 | background-color: @bc-error; 6 | .dialog-title { 7 | color: @bc-text-alt; 8 | } 9 | } 10 | pre { 11 | white-space: pre; 12 | word-wrap: normal; 13 | overflow: scroll; 14 | .selectable-text(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /styles/dialogs/progress.less: -------------------------------------------------------------------------------- 1 | #git-progress-dialog { 2 | textarea { 3 | height: 300px; 4 | &[readonly="readonly"] { 5 | cursor: default; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /styles/dialogs/pull-dialog.less: -------------------------------------------------------------------------------- 1 | #git-pull-dialog { 2 | .modal-body { 3 | max-height: 500px; 4 | } 5 | .row-fluid { 6 | label { 7 | line-height: 28px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /styles/editor-holder.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | 3 | #editor-holder { 4 | .git.spinner { 5 | display: none; 6 | z-index: 1000; 7 | position: absolute; 8 | top: 50%; 9 | left: 50%; 10 | &.spin { 11 | display: block; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /styles/fonts/octicons-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/styles/fonts/octicons-regular-webfont.eot -------------------------------------------------------------------------------- /styles/fonts/octicons-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/styles/fonts/octicons-regular-webfont.ttf -------------------------------------------------------------------------------- /styles/fonts/octicons-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brackets-userland/brackets-git/334b702d2a35f62a2a863b9aaa25d1bec95c7ed7/styles/fonts/octicons-regular-webfont.woff -------------------------------------------------------------------------------- /styles/ftp/ftp.less: -------------------------------------------------------------------------------- 1 | #git-panel { 2 | 3 | .git-remotes-dropdown { 4 | .gitftp-init-remote { 5 | position: relative; 6 | top: 2px; 7 | right: 4px; 8 | } 9 | } 10 | 11 | a[data-type=ftp] .change-remote:before, span[data-type=ftp]:before { 12 | content: "FTP:"; 13 | padding-right: 5px; 14 | opacity: 0.5; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /styles/history.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | 3 | .commit-author-avatar-mixin(@size) { 4 | position: relative; 5 | width: @size; 6 | height: @size; 7 | text-align: center; 8 | span, img { 9 | position: absolute; 10 | left: 0; 11 | right: 0; 12 | top: 0; 13 | bottom: 0; 14 | height: @size; 15 | width: @size; 16 | border-radius: 2px; 17 | } 18 | span { 19 | color: @bc-text; 20 | font-weight: 500; 21 | font-size: @size; 22 | line-height: @size; 23 | text-transform: uppercase; 24 | &.avatar-bw { 25 | -webkit-filter: grayscale(100%); 26 | } 27 | 28 | .dark & { 29 | color: @dark-bc-text; 30 | } 31 | } 32 | } 33 | 34 | #git-history-list { 35 | table-layout: fixed; 36 | tbody tr td { 37 | vertical-align: middle; 38 | position: relative; 39 | overflow: hidden; 40 | text-overflow: ellipsis; 41 | white-space: nowrap; 42 | &:nth-child(1) { // author avatar 43 | width: 30px; 44 | .commit-author-avatar { 45 | .commit-author-avatar-mixin(18px); 46 | } 47 | } 48 | &:nth-child(2) { // commit date/author 49 | width: 250px; 50 | .commit-author { 51 | font-weight: 500; 52 | } 53 | } 54 | &:nth-child(3) { // commit title 55 | .commit-tags { 56 | float: right; 57 | max-width: 150px; 58 | overflow: hidden; 59 | text-overflow: ellipsis; 60 | } 61 | } 62 | &:last-child { // commit hash 63 | width: 50px; 64 | } 65 | } 66 | } 67 | 68 | #history-viewer { 69 | position: absolute; 70 | z-index: @baseZindex; 71 | top: 0px; 72 | right: 0px; 73 | bottom: 0px; 74 | left: 0px; 75 | overflow: hidden; 76 | background: @bc-panel-bg; 77 | .flex-box(column); 78 | 79 | .dark & { 80 | background: @dark-bc-panel-bg; 81 | } 82 | 83 | > .header { 84 | .flex-item(0, 0); 85 | .author-line { 86 | margin-bottom: 1em; 87 | } 88 | .commit-author { 89 | @lineHeight: 36px; 90 | height: @lineHeight; 91 | line-height: @lineHeight; 92 | .commit-author-avatar { 93 | .commit-author-avatar-mixin(@lineHeight); 94 | display: inline-block; 95 | top: @lineHeight / 3; 96 | } 97 | .commit-author-name, .commit-author-email { 98 | margin-left: 1em; 99 | .selectable-text(); 100 | } 101 | } 102 | padding: 25px 30px 5px; 103 | &.shadow { 104 | box-shadow: 0 -1px 3px 2px rgba(0, 0, 0, 0.15); 105 | z-index: 1; 106 | } 107 | .commit-title { 108 | font-size: 20px; 109 | line-height: 24px; 110 | font-weight: 500; 111 | margin: 0 0 5px; 112 | width: 90% 113 | } 114 | .close { 115 | margin: -5px -3px 0 0; 116 | padding-left: 50px; 117 | } 118 | .commit-hash, .commit-author, .commit-time { 119 | margin-right: 10px; 120 | display: inline-block; 121 | color: @bc-text-thin-quiet; 122 | 123 | .dark & { 124 | color: @dark-bc-text-thin-quiet; 125 | } 126 | i { 127 | color: @bc-text-thin-quiet; 128 | margin-right: 2px; 129 | 130 | .dark & { 131 | color: @dark-bc-text-thin-quiet; 132 | } 133 | } 134 | } 135 | .actions { 136 | float: right; 137 | margin: -10px; 138 | >* { 139 | margin-right: 10px; 140 | display: inline-block; 141 | } 142 | .git-reset-label + .caret { 143 | margin-left: 5px; 144 | } 145 | .dropdown-menu { 146 | left: auto; 147 | right: 0; 148 | line-height: 28px; 149 | } 150 | } 151 | 152 | } 153 | > .body { 154 | .flex-item(1, 1); 155 | position: relative; 156 | overflow-y: scroll; 157 | li > a { 158 | background: @bc-btn-bg; 159 | border: 1px solid transparent; 160 | border-right: 0; 161 | border-left: 0; 162 | color: @bc-text; 163 | margin: 10px; 164 | margin-bottom: 0; 165 | 166 | .dark & { 167 | background: @dark-bc-btn-bg; 168 | color: @dark-bc-text; 169 | } 170 | } 171 | .commit-diff { 172 | > pre { 173 | white-space: pre-line 174 | } 175 | max-height: 0px; 176 | margin: 10px; 177 | opacity: 0; 178 | transition: all ease 0.3s; 179 | transition-property: padding, height, opacity; 180 | padding-top: 0; 181 | padding-bottom: 0; 182 | border-top: 0; 183 | margin-top: 0px; 184 | border-radius: 0 0 3px 3px; 185 | border-color: @bc-btn-border; 186 | margin-bottom: 0; 187 | 188 | .dark & { 189 | border-color: @dark-bc-btn-border; 190 | } 191 | .separator, .meta-file { 192 | display: none; 193 | } 194 | } 195 | .active+.commit-diff { 196 | max-height: 99999px; 197 | opacity: 1; 198 | border-color: @bc-btn-border; 199 | margin-bottom: 10px; 200 | padding: 10px 0; 201 | 202 | .dark & { 203 | border-color: @dark-bc-btn-border; 204 | } 205 | } 206 | .opened { 207 | display: none; 208 | margin-top: 7px; 209 | } 210 | .closed { 211 | display: inline-block; 212 | margin-top: 5px; 213 | } 214 | .active { 215 | background: @bc-menu-bg; 216 | border: 1px solid @bc-btn-border; 217 | margin-bottom: 0; 218 | border-bottom: 0; 219 | border-radius: 4px 4px 0 0; 220 | 221 | .dark & { 222 | background: @dark-bc-menu-bg; 223 | border: 1px solid @dark-bc-btn-border; 224 | } 225 | 226 | a { 227 | border: none; 228 | background-color: transparent; 229 | } 230 | .opened { 231 | display: inline-block; 232 | } 233 | .closed { 234 | display: none; 235 | } 236 | } 237 | .caret { 238 | border-left: 4px solid transparent; 239 | border-right: 4px solid transparent; 240 | border-top: 5px solid @bc-text; 241 | margin-right: 2px; 242 | 243 | .dark & { 244 | border-top: 5px solid @dark-bc-text; 245 | } 246 | } 247 | .caret.caret-right { 248 | border-bottom: 4px solid transparent; 249 | border-top: 4px solid transparent; 250 | border-left: 5px solid @bc-text; 251 | margin-right: -1px; 252 | margin-left: 3px; 253 | 254 | .dark & { 255 | border-left: 5px solid @dark-bc-text; 256 | } 257 | } 258 | .commitBody { 259 | padding: 0 30px; 260 | } 261 | .commit-files { 262 | padding: 0 20px; 263 | .openFile, .difftool { 264 | vertical-align: -1px; 265 | margin-left: 10px; 266 | cursor: pointer; 267 | opacity: 0.7; 268 | } 269 | } 270 | .filesContainer { 271 | margin-bottom: 10px; 272 | } 273 | .loadMore { 274 | margin-bottom: 10px; 275 | margin-left: 10px; 276 | } 277 | } 278 | .message { 279 | display: block; 280 | padding: 10px; 281 | } 282 | 283 | .dropdown-menu(); 284 | 285 | .toggle-diffs { 286 | cursor: pointer; 287 | margin-right: -10px; 288 | margin-top: 14px; 289 | .collapse { 290 | display: none; 291 | } 292 | span.collapse { 293 | height: auto; 294 | } 295 | .expand { 296 | vertical-align: middle; 297 | } 298 | span.expand { 299 | margin-left: 2px; 300 | } 301 | &.opened { 302 | .expand { 303 | display: none; 304 | } 305 | .collapse { 306 | display: inline-block; 307 | vertical-align: middle; 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /styles/icons/terminal-icon.svg: -------------------------------------------------------------------------------- 1 | 5 | Octicons Terminal Icon 6 | Octicons Terminal Icon by GitHub is licensed under the SIL OFL 1.1 license. 7 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /styles/icons/warning-icon.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /styles/mixins.less: -------------------------------------------------------------------------------- 1 | // main: brackets-git.less 2 | 3 | .selectable-text { 4 | .user-select(text); 5 | } 6 | 7 | .dropdown-menu() { 8 | .dropdown-menu { 9 | // don't mess with this, the dropdown menu is at the top so it should grow from bottom left to top right. 10 | -webkit-transform-origin: 0 100%; 11 | a { 12 | padding: 5px 26px 5px 26px; 13 | color: @bc-menu-text; 14 | 15 | .dark & { 16 | color: @dark-bc-menu-text; 17 | } 18 | 19 | &:hover { 20 | background: @bc-bg-highlight; 21 | color: @bc-menu-text; 22 | 23 | .dark & { 24 | background: @dark-bc-bg-highlight; 25 | color: @dark-bc-menu-text; 26 | } 27 | } 28 | } 29 | border: none; 30 | border-radius: @bc-border-radius; 31 | box-shadow: 0 3px 9px @bc-shadow; 32 | 33 | // mixin 34 | .both() { 35 | background: @bc-highlight; 36 | color: @bc-menu-text; 37 | 38 | .dark & { 39 | background: @dark-bc-bg-highlight; 40 | color: @dark-bc-menu-text; 41 | } 42 | } 43 | 44 | > li { 45 | > a { 46 | cursor: default; 47 | &:hover, > &:focus { 48 | .both(); 49 | } 50 | } 51 | } 52 | 53 | .dropdown-submenu:hover, .dropdown-submenu:focus { 54 | > a { 55 | .both(); 56 | } 57 | } 58 | 59 | .dropdown-header { 60 | display: block; 61 | padding: 3px 20px; 62 | font-size: 12px; 63 | line-height: 1.42857143; 64 | color: @bc-menu-text; 65 | 66 | .dark & { 67 | color: @dark-bc-menu-text; 68 | } 69 | } 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /templates/authors-dialog.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /templates/branch-merge-dialog.html: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /templates/branch-new-dialog.html: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /templates/default-gitignore: -------------------------------------------------------------------------------- 1 | # https://git-scm.com/docs/gitignore 2 | # https://help.github.com/articles/ignoring-files 3 | # Example .gitignore files: https://github.com/github/gitignore 4 | /bower_components/ 5 | /node_modules/ -------------------------------------------------------------------------------- /templates/error-report.md: -------------------------------------------------------------------------------- 1 | [I'm supposed to describe steps to replicate my issue here and if I don't do that, I understand that my issue will be probably ignored.] 2 | 3 | -------------------------------- 4 | 5 | *{{brackets}}{{#git}}, Git {{git}}{{/git}} & {{bracketsGit}}* 6 | 7 | {{#title}}**{{title}}**{{/title}} 8 | 9 | {{#errorBody}}``` 10 | {{{errorBody}}} 11 | ```{{/errorBody}} 12 | 13 | {{#errorStack}}``` 14 | {{{errorStack}}} 15 | ```{{/errorStack}} 16 | -------------------------------------------------------------------------------- /templates/format-diff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#files}} 4 | 5 | 6 | 7 | {{#lines}} 8 | 9 | 10 | 11 | 12 | 13 | {{/lines}} 14 | 15 | {{/files}} 16 | 17 |
    {{name}}
    {{numLineOld}}{{numLineNew}}
    {{{line}}}
    18 | -------------------------------------------------------------------------------- /templates/ftp/remotes-picker.html: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 | {{#hasFtpScopes}} 4 | {{#ftpScopes}} 5 |
  • 6 | 7 | × 8 | {{name}} 9 | 10 | 11 |
  • 12 | {{/ftpScopes}} 13 | {{/hasFtpScopes}} 14 |
  • {{Strings.CREATE_NEW_GITFTP_SCOPE}}
  • 15 |
  • 16 | 17 | -------------------------------------------------------------------------------- /templates/git-branches-menu.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /templates/git-changelog-dialog.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /templates/git-commit-dialog.html: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /templates/git-diff-dialog.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /templates/git-error-dialog.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /templates/git-output.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /templates/git-panel-history-commits.html: -------------------------------------------------------------------------------- 1 | {{#commits}} 2 | 3 | 4 |
    5 | {{#useBwAvatar}}{{avatarLetter}}{{/useBwAvatar}} 6 | {{#useColoredAvatar}}{{avatarLetter}}{{/useColoredAvatar}} 7 | {{#usePicture}}{{/usePicture}} 8 | {{#useIdenticon}}{{/useIdenticon}} 9 |
    10 | 11 | {{date.shown}} {{Strings.HISTORY_COMMIT_BY}} {{author}} 12 | {{subject}} {{#hasTag}}{{tags}}{{/hasTag}} 13 | {{hashShort}} 14 | 15 | {{/commits}} 16 | -------------------------------------------------------------------------------- /templates/git-panel-history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{> commits}} 4 | 5 |
    6 | -------------------------------------------------------------------------------- /templates/git-panel-results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#files}} 4 | 5 | 6 | 9 | 10 | 11 | 17 | 18 | {{/files}} 19 | 20 |
    7 | {{#allowDiff}}{{/allowDiff}} 8 | {{statusText}}{{display}} 12 |
    13 | {{#allowUndo}} {{/allowUndo}} 14 | {{#allowDelete}} {{/allowDelete}} 15 |
    16 |
    21 | -------------------------------------------------------------------------------- /templates/git-panel.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 |
    7 | 8 |
    9 |
    10 |
    11 | 14 | 17 | 20 | 25 |
    26 |
    27 |
    28 |
    29 | 32 | 35 | 40 |
    41 |
    42 |
    43 | 44 | 45 |
    46 |
    47 | 48 | 49 |
    50 |
    51 | 52 | 53 |
    54 | 55 |
    56 | 57 | 77 |
    78 | 79 |
    80 |
    81 | 82 |
    83 |
    84 | 85 | 89 | 90 | 91 | 92 |
    93 |
    94 | {{#showBashButton}}{{/showBashButton}} 95 | 96 |
    97 |
    98 | × 99 | 100 |
    101 |
    102 |
    103 | -------------------------------------------------------------------------------- /templates/git-question-dialog.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /templates/git-remotes-picker.html: -------------------------------------------------------------------------------- 1 | 2 | {{#remotes}} 3 |
  • 4 | 5 | {{#deletable}}×{{/deletable}} 6 | {{name}} 7 | 8 |
  • 9 | {{/remotes}} 10 |
  • {{Strings.CREATE_NEW_REMOTE}}
  • 11 | -------------------------------------------------------------------------------- /templates/git-tag-dialog.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /templates/history-viewer-files.html: -------------------------------------------------------------------------------- 1 | {{#files}} 2 |
  • 3 | 4 | 5 | 6 | {{name}}{{extension}} 7 | 8 | {{#useDifftool}}{{/useDifftool}} 9 | 10 |
    11 |
  • 12 | {{/files}} 13 | -------------------------------------------------------------------------------- /templates/history-viewer.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | × 5 |
    6 |
    7 | 8 |
    9 | {{#useBwAvatar}}{{commit.avatarLetter}}{{/useBwAvatar}} 10 | {{#useColoredAvatar}}{{commit.avatarLetter}}{{/useColoredAvatar}} 11 | {{#usePicture}}{{/usePicture}} 12 | {{#useIdenticon}}{{/useIdenticon}} 13 |
    14 | {{commit.author}} 15 | <{{commit.email}}> 16 |
    17 | 18 |   19 | {{commit.date.shown}} 20 | 21 | 22 |  {{commit.hashShort}} 23 | 24 | 25 |
    26 | {{#enableAdvancedFeatures}} 27 | 28 | 39 | {{/enableAdvancedFeatures}} 40 | 41 | 42 | 43 | {{Strings.EXPAND_ALL}} 44 | {{Strings.COLLAPSE_ALL}} 45 | 46 |
    47 |
    48 |
    49 |

    {{commit.subject}}

    50 |
    51 |
    52 |
    53 |
    {{{bodyMarkdown}}}
    54 |
    55 |
    56 | 57 | 60 |
    61 |
    62 |
    63 |
    64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "src", 5 | "sourceMap": true, 6 | "target": "es6", 7 | "module": "amd", 8 | "moduleResolution": "node", 9 | "forceConsistentCasingInFileNames": true, 10 | "newLine": "lf", 11 | "noFallthroughCasesInSwitch": false, 12 | "noImplicitAny": false, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": false, 15 | "noUnusedLocals": false, 16 | "removeComments": false, 17 | "strictNullChecks": false 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": [ 23 | "src/node/**/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "align": false, 5 | "arrow-parens": false, 6 | "forin": false, 7 | "interface-name": false, 8 | "no-console": false, 9 | "no-empty-interface": false, 10 | "no-var-requires": false, 11 | "object-literal-sort-keys": false, 12 | "only-arrow-functions": false, 13 | "ordered-imports": false, 14 | "promise-function-async": false, 15 | "quotemark": false, 16 | "space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}], 17 | "trailing-comma": false, 18 | "variable-name": false 19 | } 20 | } 21 | --------------------------------------------------------------------------------