├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CNAME ├── LICENSE ├── README.md ├── banner.png ├── banner.svg ├── css ├── sprite.png └── style.css ├── favicon.svg ├── favicon ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── safari-pinned-tab.svg └── site.webmanifest ├── git-webite-brand.svg ├── index.html ├── js ├── contributors.js ├── git-worker.js └── main.js ├── sw.js └── viewer-icon.svg /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | If this is your first PR please add yourself to js/contributors.js file. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | *~ 3 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | git-terminal.js.org 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jakub Jankiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GIT Web Terminal 2 | 3 | This project was created using [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) and other 4 | libraries, you can check which ones was used when you 5 | follow this link 6 | that will execute `credits` command in terminal. 7 | 8 | First version of the app was created on [codepen](https://codepen.io/jcubic/pen/Gddxpg). 9 | 10 | ### Usage 11 | 12 | Steps to make changes to remote git repo (tested with github): 13 | 14 | 1. clone repo using `git clone` (using https url), 15 | 2. cd to directory, 16 | 3. edit file using `vi` or `emacs`, 17 | 4. use `git login` and put your credentials (user/pass is for push and the rest is for commit), 18 | 5. use `git commit -am "message"` or `git add -A` then `git commit -m ""`, 19 | 6. use `git push` to push your changes to remote. 20 | 21 | ### Web app viewer 22 | 23 | If you're working on web app you can open it from browser just prefix the path with `__browserfs__` the app 24 | is using service worker to serve files from browser fs. So if you clone this repo you will be able to view the local file 25 | using https://jcubic.github.io/git/__browserfs__/git/. If you open directory that don't have index.html it will 26 | display page with directory listing like normal web server. 27 | 28 | ### Contributors 29 | 30 | To see list of contributors you can check 31 | credits command. 32 | 33 | ### Proxy 34 | 35 | Proxy used to fetch files from remote repositories is located on my hosting 36 | [https://jcubic.pl/proxy.php](https://jcubic.pl/proxy.php). You can read the source code of that file 37 | [here](https://github.com/jcubic/git/blob/357848672683d1959dbd1fa3d5023302a4151474/proxy.php) (it's in commit because 38 | I've needed to delete it because of GPL license). 39 | 40 | ### License 41 | 42 | Licensed under [MIT](http://opensource.org/licenses/MIT) license 43 | 44 | Copyright (c) 2018 [Jakub Jankiewicz](http://jcubic.pl/jakub-jankiewicz) 45 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/banner.png -------------------------------------------------------------------------------- /banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | image/svg+xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ____ ___ _____ / ___|_ _|_ _| __ __ _ _____ _ _| | _ | | | | \ \ / /__| |__ |_ _|__ _ _ _ __ (_)_ _ __ _| || |_| || | | | \ \/\/ / -_) '_ \ | |/ -_) '_| ' \| | ' \/ _` | | \____|___| |_| \_/\_/\___|_.__/ |_|\___|_| |_|_|_|_|_||_\__,_|_| 19 | 20 | -------------------------------------------------------------------------------- /css/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/css/sprite.png -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /**@license 2 | * ___ ___ _____ __ __ _ _____ _ _ 3 | * / __|_ _|_ _| \ \ / /__| |__ |_ _|__ _ _ _ __ (_)_ _ __ _| | 4 | * | (_ || | | | \ \/\/ / -_) '_ \ | |/ -_) '_| ' \| | ' \/ _` | | 5 | * \___|___| |_| \_/\_/\___|_.__/ |_|\___|_| |_|_|_|_|_||_\__,_|_| 6 | * 7 | * Copyright (c) 2018 Jakub Jankiewicz 8 | * Released under the MIT license 9 | * 10 | */ 11 | .DlLayout { 12 | position: absolute; 13 | } 14 | .terminal { 15 | --size: 1.3; 16 | min-height: 100vh; 17 | } 18 | .Ymacs-frame-content { 19 | display: inline-block; 20 | min-width: 100%; 21 | } 22 | .Ymacs-Theme-dark.Ymacs-Theme-y .Ymacs_Frame .builtin { 23 | color: rgb(176, 196, 222); 24 | } 25 | /* bar cursor for ymacs */ 26 | body .Ymacs-Theme-dark .Ymacs_Frame-focus .Ymacs-caret { 27 | background: transparent !important; 28 | color: inherit !important; 29 | } 30 | .Ymacs-caret { 31 | position: relative; 32 | } 33 | .Ymacs-caret:before { 34 | content: " "; 35 | display: block; 36 | position: absolute; 37 | top: 0; 38 | left: 0; 39 | border-left: 1px solid white; 40 | bottom: 0; 41 | } 42 | .terminal-view > .DlContainer { 43 | position: absolute; 44 | display: none; 45 | top: 0; 46 | right: 0; 47 | bottom: 0; 48 | left: 0; 49 | width: 100% !important; 50 | } 51 | .Ymacs_Minibuffer { 52 | min-height: 14px; 53 | } 54 | .terminal-view { 55 | position: relative; 56 | height: 100%; 57 | } 58 | .viewer { 59 | display: none; 60 | } 61 | .split { 62 | height: 100vh !important; 63 | z-index: 100; 64 | } 65 | .splitter_panel .viewer { 66 | overflow: hidden; 67 | display: flex; 68 | flex-direction: column; 69 | } 70 | .viewer header { 71 | height: 32px; 72 | padding: 4px; 73 | display: flex; 74 | width: 100%; 75 | background: #cecece; 76 | border-bottom: 1px solid #818181; 77 | } 78 | .viewer header, .viewer input, .viewer .close { 79 | box-sizing: border-box; 80 | } 81 | .viewer input { 82 | flex-grow: 1; 83 | border-radius: 2px; 84 | border: 1px solid #818181; 85 | padding-left: 5px; 86 | } 87 | .viewer .close { 88 | width: 24px; 89 | height: 24px; 90 | text-align: center; 91 | float: right; 92 | z-index: 1000; 93 | position: relative; 94 | cursor: pointer; 95 | padding-top: 2px; 96 | } 97 | .viewer iframe { 98 | border: none; 99 | width: 100%; 100 | flex-grow: 1; 101 | } 102 | .viewer .controls ul { 103 | list-style: none; 104 | padding: 0; 105 | margin: 0; 106 | display: flex; 107 | } 108 | .viewer .controls { 109 | width: calc(3 * 24px); 110 | padding-right: 5px; 111 | } 112 | .viewer .controls li a { 113 | width: 24px; 114 | height: 24px; 115 | text-indent: -9999em; 116 | background: url(sprite.png) no-repeat; 117 | display: block; 118 | } 119 | .viewer .controls li a:hover:not(.disabled) { 120 | background-color: #bbbbbb; 121 | } 122 | .viewer .controls li a.disabled { 123 | opacity: 0.3; 124 | } 125 | .viewer .controls li a.next { 126 | background-position: -24px 0; 127 | } 128 | .viewer .controls li a.refresh { 129 | background-position: -48px 0; 130 | } 131 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | image/svg+xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/favicon.ico -------------------------------------------------------------------------------- /favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcubic/git/a9f5f6e729f24944db1a4af49f827710818dc614/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /git-webite-brand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | image/svg+xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GIT Web Terminal 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 |
103 |
104 | 105 |
106 |
107 |
108 |
109 | 114 |
115 | 116 | 117 |
118 | 119 |
120 |
121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /js/contributors.js: -------------------------------------------------------------------------------- 1 | const contributors = [ 2 | { 3 | name: 'jcubic', 4 | fullname: 'Jakub Jankiewicz', 5 | url: 'https://jcubic.pl/me' 6 | }, 7 | { 8 | fullname: 'Brett Zamir', 9 | name: 'brettz9', 10 | url: 'http://brett-zamir.me/' 11 | }, 12 | { 13 | fullname: 'William Hilton', 14 | name: 'wmhilton', 15 | url: 'https://onename.com/wmhilton' 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /js/git-worker.js: -------------------------------------------------------------------------------- 1 | /* global importScripts, self, BrowserFS, git, EventEmitter */ 2 | window = self; 3 | importScripts( 4 | "https://unpkg.com/isomorphic-git@0.x.x", 5 | // '../../../iso-git-latest/dist/bundle.umd.min.js', 6 | "https://unpkg.com/browserfs@1.x.x", 7 | "https://rawgit.com/Olical/EventEmitter/master/EventEmitter.js" 8 | ); 9 | 10 | const localStorage = self.localStorage = wrap('localStorage', [ 11 | 'getItem', 12 | 'setItem', 13 | 'removeItem' 14 | ]); 15 | function wrap(name, names) { 16 | const obj = {}; 17 | var id = 0; 18 | names.forEach(method => { 19 | obj[method] = (...args) => { 20 | return new Promise((resolve, reject) => { 21 | self.addEventListener('message', ({ data }) => { 22 | if (data.type === 'RPC' && data.id === id && data.method === method && 23 | data.object === name) { 24 | if (data.error) { 25 | reject(data.error); 26 | } else { 27 | resolve(data.result); 28 | } 29 | } 30 | }); 31 | self.postMessage({ type: 'RPC', id: ++id, object: name, method, args}); 32 | }); 33 | }; 34 | }); 35 | return obj; 36 | } 37 | 38 | const term = wrap('terminal', ['read', 'set_mask', 'confirm', 'resume', 'pause', 'paused']); 39 | 40 | const CredentialManager = { 41 | async fill ({ url }) { 42 | let paused = await term.paused(); 43 | if (paused) await term.resume(); 44 | let auth = await localStorage.getItem(url); 45 | if (auth) return JSON.parse(auth); 46 | const username = await term.read(`Enter username for ${new URL(url).host}: `); 47 | await term.set_mask(true); 48 | const password = await term.read('Enter password: '); 49 | await term.set_mask(false); 50 | if (paused) await term.pause(); 51 | return { 52 | username, 53 | password 54 | }; 55 | }, 56 | async approved({ url, auth }) { 57 | let savedAuth = await localStorage.getItem(url); 58 | if (savedAuth) { 59 | savedAuth = JSON.parse(savedAuth); 60 | if (savedAuth.username === auth.username && savedAuth.password === auth.password) { 61 | // It's already saved. 62 | return 63 | } 64 | } 65 | let paused = await term.paused(); 66 | if (paused) await term.resume(); 67 | const response = await term.confirm('Do you want to save this password? (Y/N) '); 68 | console.log('confirm', response) 69 | if (response) { 70 | await localStorage.setItem(url, JSON.stringify(auth)); 71 | } 72 | if (paused) await term.pause(); 73 | }, 74 | async rejected({ url, auth }) { 75 | if (await localStorage.getItem(url)) { 76 | let paused = await term.paused(); 77 | if (paused) await term.resume(); 78 | if (await term.confirm(`Authentication to ${new URL(url).host} was unsuccessful. Delete saved password? (Y/N) `)) { 79 | await localStorage.removeItem(url); 80 | } 81 | if (paused) await term.pause(); 82 | } 83 | } 84 | }; 85 | 86 | 87 | 88 | self.addEventListener('message', ({ data }) => { 89 | BrowserFS.configure({ 90 | fs: 'IndexedDB', options: {} 91 | }, function (err) { 92 | self.fs = BrowserFS.BFSRequire('fs'); 93 | git.plugins.set('fs', self.fs); 94 | git.plugins.set('credentialManager', CredentialManager); 95 | let id = data.id; 96 | if (data.type !== 'RPC' || id === null) { 97 | return; 98 | } 99 | if (data.method && typeof git[data.method] === 'function') { 100 | if (data.params.emitter) { 101 | data.params.emitter = new EventEmitter(); 102 | data.params.emitter.on('message', (message) => { 103 | self.postMessage({ type: 'EMITTER', id, message }); 104 | }); 105 | } 106 | git[data.method].call(git, data.params).then(result => { 107 | self.postMessage({ type: 'RPC', id, result}); 108 | }).catch((err) => { 109 | self.postMessage({ type: 'RPC', id, error: ''+err}); 110 | }); 111 | } 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | /**@license 2 | * ___ ___ _____ __ __ _ _____ _ _ 3 | * / __|_ _|_ _| \ \ / /__| |__ |_ _|__ _ _ _ __ (_)_ _ __ _| | 4 | * | (_ || | | | \ \/\/ / -_) '_ \ | |/ -_) '_| ' \| | ' \/ _` | | 5 | * \___|___| |_| \_/\_/\___|_.__/ |_|\___|_| |_|_|_|_|_||_\__,_|_| 6 | * 7 | * Copyright (c) 2018 Jakub Jankiewicz 8 | * Released under the MIT license 9 | * 10 | */ 11 | /* global fs, git, BrowserFS, zip, location, $, Worker, localStorage, EventEmitter, path, vi */ 12 | var term = { 13 | completion: {} 14 | }; 15 | 16 | // restore the Array::find overwriten by dynarch from Ymacs - make it double purpose 17 | Array.prototype.find = function(value) { 18 | if (typeof value === 'function') { 19 | // original 20 | let i = 0; 21 | for (let item of this) { 22 | if (value(item, i++)) { 23 | return item; 24 | } 25 | } 26 | } else { 27 | var t = value; 28 | for(var e=this.length;--e>=0;)if(this[e]===t)return e;return-1; 29 | } 30 | }; 31 | term.banner = [ 32 | ' ____ ___ _____ ', 33 | ' / ___|_ _|_ _| __ __ _ _____ _ _', 34 | '| | _ | | | | \\ \\ / /__| |__ |_ _|__ _ _ _ __ (_)_ _ __ _| |', 35 | '| |_| || | | | \\ \\/\\/ / -_) \'_ \\ | |/ -_) \'_| \' \\| | \' \\/ _` | |', 36 | ' \\____|___| |_| \\_/\\_/\\___|_.__/ |_|\\___|_| |_|_|_|_|_||_\\__,_|_|' 37 | ]; 38 | function greetings() { 39 | var title; 40 | if (this.cols() > term.banner[1].length) { 41 | title = term.banner.join('\n'); 42 | } else { 43 | title = 'GIT Web Terminal'; 44 | } 45 | return title + '\n\n' + 'use [[;#fff;]help] to see the available commands' + 46 | ' or [[;#fff;]credits] to list the projects used\n'; 47 | } 48 | function BrowserFSConfigure() { 49 | return new Promise(function(resolve, reject) { 50 | BrowserFS.configure({ 51 | fs: 'IndexedDB', 52 | options: {} 53 | }, function (err) { 54 | if (err) { 55 | var term = $.terminal.active(); 56 | if (!term) { 57 | term = $('.term').terminal(function(command, term) { 58 | term.error('BrowserFS was not initialized'); 59 | }, {greetings: false, name: 'git'}).echo(greetings); 60 | } 61 | term.error(err.message || err); 62 | reject(err.message || err); 63 | } else { 64 | resolve(); 65 | } 66 | }); 67 | }); 68 | } 69 | 70 | BrowserFSConfigure().then(() => { 71 | var name = 'git'; // terminal name for history 72 | window.fs = BrowserFS.BFSRequire('fs'); 73 | window.path = BrowserFS.BFSRequire('path'); 74 | window.buffer = BrowserFS.BFSRequire('buffer'); 75 | git.plugins.set('fs', window.fs); 76 | if (typeof zip !== 'undefined') { 77 | zip.workerScriptsPath = location.pathname.replace(/\/[^\/]+$/, '/') + 'js/zip/'; 78 | } 79 | var scope = location.pathname.replace(/\/[^\/]+$/, '/'); 80 | 81 | $.fn.confirm = async function(message) { 82 | var term = $(this).terminal(); 83 | const response = await new Promise(function(resolve) { 84 | term.push(function(command) { 85 | if (command.match(/Y(es)?/i)) { 86 | resolve(true); 87 | } else if (command.match(/N(o)?/i)) { 88 | resolve(false); 89 | } 90 | }, { 91 | prompt: message 92 | }); 93 | }); 94 | term.pop(); 95 | return response; 96 | }; 97 | if ('serviceWorker' in navigator) { 98 | // loading this repo from browerFS will not work because serviceWorker can't be loaded from 99 | // service worker. 100 | if (!scope.match(/__browserfs__/)) { 101 | navigator.serviceWorker.register('sw.js', {scope}) 102 | .then(function(reg) { 103 | reg.addEventListener('updatefound', function() { 104 | var installingWorker = reg.installing; 105 | console.log('A new service worker is being installed:', 106 | installingWorker); 107 | }); 108 | // registration worked 109 | console.log('Registration succeeded. Scope is ' + reg.scope); 110 | }).catch(function(error) { 111 | // registration failed 112 | console.log('Registration failed with ' + error); 113 | }); 114 | } 115 | } 116 | var git_wrapper = term.git = {}; 117 | if (typeof Worker !== 'undefined') { 118 | var worker = new Worker(scope + 'js/git-worker.js'); 119 | let count = 0; 120 | worker.addEventListener("message", function handler({ data }) { 121 | function response(id, method, result) { 122 | worker.postMessage({ type: "RPC", method, object: data.object, id, result}); 123 | } 124 | if (data.type === 'RPC') { 125 | var object; 126 | if (data.object === 'terminal') { 127 | object = instance; 128 | } else if (data.object === 'localStorage') { 129 | object = localStorage; 130 | } 131 | if (object) { 132 | if (typeof object[data.method] === 'function') { 133 | var result = object[data.method].apply(object, data.args || []); 134 | if (result !== object) { 135 | if (result && typeof result.then === 'function') { 136 | result.then(function(result) { 137 | response(data.id, data.method, result); 138 | }); 139 | } else { 140 | response(data.id, data.method, result); 141 | } 142 | } else { 143 | response(data.id, data.method); 144 | } 145 | } 146 | } 147 | } 148 | }); 149 | Object.getOwnPropertyNames(git).forEach(function(name) { 150 | var emitter_handler = null; 151 | if (typeof git[name] === 'function') { 152 | git_wrapper[name] = function({emitter, fs, ...args}) { 153 | return new Promise(function(resolve, reject) { 154 | var id = `rpc${++count}`; 155 | if (emitter instanceof EventEmitter) { 156 | emitter_handler = function handler({data}) { 157 | if (data.type === 'EMITTER' && id === data.id) { 158 | emitter.trigger('message', [data.message]); 159 | } 160 | }; 161 | args.emitter = true; 162 | worker.addEventListener("message", emitter_handler); 163 | } 164 | worker.addEventListener("message", function handler({ data }) { 165 | if (data.type === 'RPC' && id === data.id) { 166 | if (emitter_handler) { 167 | worker.removeEventListener("message", emitter_handler); 168 | } 169 | // Sync BrowserFS 170 | BrowserFSConfigure().then(() => { 171 | window.fs = BrowserFS.BFSRequire('fs'); 172 | if (data.error) { 173 | reject(data.error); 174 | } else { 175 | resolve(data.result); 176 | } 177 | }); 178 | worker.removeEventListener("message", handler); 179 | } 180 | }); 181 | worker.postMessage({ type: "RPC", method: name, id, params: args}); 182 | }); 183 | }; 184 | } 185 | }); 186 | } else { 187 | Object.getOwnPropertyNames(git).forEach(function(name) { 188 | git_wrapper[name] = git[name].bind(git); 189 | }); 190 | } 191 | var dir = '/'; 192 | var cwd = '/'; 193 | var credentials = {}; 194 | ['username', 'email', 'fullname'].forEach((name) => { 195 | const value = localStorage.getItem('git_' + name); 196 | if (value) { 197 | credentials[name] = value; 198 | } 199 | }); 200 | var branch; 201 | // ----------------------------------------------------------------------------------------------------- 202 | // :: resolve path 203 | // ----------------------------------------------------------------------------------------------------- 204 | function resolve(p) { 205 | return path.resolve(p[0] == '/' ? p : path.join(cwd, p)); 206 | } 207 | // ----------------------------------------------------------------------------------------------------- 208 | function color(name, string) { 209 | var colors = { 210 | blue: '#55f', 211 | green: '#4d4', 212 | grey: '#999', 213 | red: '#A00', 214 | yellow: '#FF5', 215 | violet: '#a320ce', 216 | white: '#fff', 217 | 'persian-green': '#0aa' 218 | }; 219 | if (colors[name]) { 220 | return '[[;' + colors[name] + ';]' + string + ']'; 221 | } else { 222 | return string; 223 | } 224 | } 225 | // ----------------------------------------------------------------------------------------------------- 226 | function messageEmitter() { 227 | var emitter = new EventEmitter(); 228 | var first = {}; 229 | var re = /^([^:]+):\s+[0-9]+%?/; 230 | emitter.on('message', (message) => { 231 | var m = message.match(re); 232 | if (m) { 233 | if (typeof first[m[1]] == 'undefined') { 234 | term.echo(message); 235 | first[m[1]] = term.last_index(); 236 | } else { 237 | term.update(first[m[1]], message); 238 | } 239 | } else { 240 | term.echo(message); 241 | } 242 | }); 243 | return emitter; 244 | } 245 | // ----------------------------------------------------------------------------------------------------- 246 | function list(path) { 247 | term.pause(); 248 | return listDir(path).then((list) => (term.resume(), list)); 249 | } 250 | 251 | // ----------------------------------------------------------------------------------------------------- 252 | // return path for cd 253 | function get_path(string) { 254 | var path = cwd.replace(/^\//, '').split('/'); 255 | if (path[0] === '') { 256 | path = path.slice(1); 257 | } 258 | var parts = string === '/' 259 | ? string.split('/') 260 | : string.replace(/\/?[^\/]*$/, '').split('/'); 261 | if (parts[0] === '') { 262 | parts = parts.slice(1); 263 | } 264 | if (string === '/') { 265 | return []; 266 | } else if (string.startsWith('/')) { 267 | return parts; 268 | } else if (path.length) { 269 | return path.concat(parts); 270 | } else { 271 | return parts; 272 | } 273 | } 274 | // ----------------------------------------------------------------------------------------------------- 275 | function read(cmd, cb, raw) { 276 | var filename = typeof cmd === 'string' ? cmd : cmd.args.length == 1 ? cwd + '/' + cmd.args[0] : null; 277 | if (filename) { 278 | fs.readFile(filename, function(err, data) { 279 | if (err) { 280 | term.error(err.message); 281 | } else { 282 | var text = data.toString('utf8').replace(/\n$/, ''); 283 | var m = filename.match(/\.([^.]+)$/); 284 | if (m) { 285 | var language = m[1]; 286 | } 287 | if (!raw && language && Prism.languages[language]) { 288 | text = $.terminal.prism(language, text); 289 | } 290 | cb(text); 291 | } 292 | }); 293 | } 294 | } 295 | // ----------------------------------------------------------------------------------------------------- 296 | function split_args(args) { 297 | return { 298 | options: args.filter(arg => arg.match(/^-/)).join('').replace(/-/g, ''), 299 | args: args.filter(arg => !arg.match(/^-/)) 300 | }; 301 | } 302 | // ----------------------------------------------------------------------------------------------------- 303 | function processGitFiles(files) { 304 | return gitroot(cwd).then((dir) => { 305 | var re = new RegExp('^' + dir + '/'); 306 | return { 307 | files: files.map(filepath => path.resolve(cwd + '/' + filepath).replace(re, '')), 308 | dir 309 | }; 310 | }); 311 | } 312 | // ----------------------------------------------------------------------------------------------------- 313 | function getOption(option, args) { 314 | option = args.reduce((acc, arg) => { 315 | if (typeof acc == 'string') { 316 | return acc; 317 | } else if (acc === true) { 318 | return arg; 319 | } else if (option instanceof RegExp ? arg.match(option) : arg === option) { 320 | return true; 321 | } 322 | return false; 323 | }, false); 324 | return option === true ? false : option; 325 | } 326 | // ----------------------------------------------------------------------------------------------------- 327 | function getAllStats({cwd, branch}) { 328 | function notGitDir(name) { 329 | return !name.match(/^\.git\/?/); 330 | } 331 | return gitroot(cwd).then((dir) => { 332 | var all = listAllFiles({dir, pattern: /^(?!.git\/)[^\/]+\//}); 333 | return Promise.all([listBranchFiles({dir, branch}), all]).then(([tracked, files]) => { 334 | var re = new RegExp('^' + dir); 335 | return Promise.all(union(tracked, files).map(filepath => { 336 | return git.status({dir, filepath}).then(status => { 337 | return [filepath, status]; 338 | }); 339 | })); 340 | }); 341 | }); 342 | } 343 | // ----------------------------------------------------------------------------------------------------- 344 | function gitAddAll({dir, branch, all}) { 345 | return getAllStats({cwd: dir, branch}).then((files) => { 346 | var skip_status = ['unmodified', 'ignored', 'modified', 'deleted', 'added', 'absent']; 347 | if (!all) { 348 | skip_status.push('*added'); 349 | } 350 | return files.filter(([_, status]) => !skip_status.includes(status)); 351 | }).then((files) => { 352 | return Promise.all(files.map(([filepath, status]) => git.add({dir, filepath}))); 353 | }); 354 | } 355 | const error = (e) => term.error(e.message || e).resume(); 356 | term.commands = { 357 | mkdir: function(cmd) { 358 | if (cmd.args.length > 0) { 359 | var options = []; 360 | var args = []; 361 | cmd.args.forEach((arg) => { 362 | var m = arg.match(/^-([^\-].*)/); 363 | if (m) { 364 | options = options.concat(m[1].split('')); 365 | } else { 366 | args.push(arg); 367 | } 368 | }); 369 | if (args.length) { 370 | term.pause(); 371 | Promise.all(args.map(dir => { 372 | dir = dir[0] === '/' ? dir : path.join(cwd, dir); 373 | return mkdir(dir, options.includes('p')) 374 | })).then(term.resume).catch(error); 375 | } 376 | } 377 | }, 378 | cd: function(cmd) { 379 | if (cmd.args.length === 1) { 380 | var dirname = path.resolve(cwd + '/' + cmd.args[0]); 381 | term.pause(); 382 | fs.stat(dirname, (err, stat) => { 383 | if (err) { 384 | term.error("Directory doesn't exist").resume(); 385 | } else if (stat.isFile()) { 386 | term.error(`"${dirname}" is not a directory`).resume(); 387 | } else { 388 | cwd = dirname == '/' ? dirname : dirname.replace(/\/$/, ''); 389 | gitBranch({cwd}).then(b => { 390 | branch = b; 391 | term.resume(); 392 | }); 393 | } 394 | }); 395 | } 396 | }, 397 | emacs: function(cmd) { 398 | term.resume(); 399 | var fname = cmd.args[0]; 400 | if (typeof fname !== 'string') { 401 | fname = String(fname); 402 | } 403 | function init() { 404 | setTimeout(function() { 405 | term.focus(false); 406 | $('.Ymacs, .DlLayout').show(); 407 | ymacs.focus(); 408 | ymacs.disabled(false); 409 | ymacs.callHooks('onResize'); 410 | }, 0); 411 | } 412 | if (fname) { 413 | var path; 414 | if (fname.match(/^\//)) { 415 | path = fname; 416 | } else { 417 | path = (cwd === '/' ? '' : cwd) + '/' + fname; 418 | } 419 | init(); 420 | ymacs.getActiveBuffer().cmd('find_file', path); 421 | } else { 422 | init(); 423 | } 424 | }, 425 | vi: function(cmd) { 426 | var textarea = $('.vi'); 427 | var editor; 428 | var fname = cmd.args[0]; 429 | if (typeof fname !== 'string') { 430 | fname = String(fname); 431 | } 432 | term.focus(false); 433 | if (fname) { 434 | var path; 435 | if (fname.match(/^\//)) { 436 | path = fname; 437 | } else { 438 | path = (cwd === '/' ? '' : cwd) + '/' + fname; 439 | } 440 | function open(file) { 441 | textarea.val(file); 442 | editor = window.editor = vi(textarea[0], { 443 | color: '#ccc', 444 | backgroundColor: '#000', 445 | onSave: function() { 446 | // we need to replace < and & because jsvi is converting them to entities 447 | var file = textarea.val().replace(/&/g, '&').replace(/</g, '<'); 448 | fs.writeFile(path, file, function(err, wr) { 449 | if (err) { 450 | term.error(err.message); 451 | } 452 | }); 453 | }, 454 | onExit: term.focus 455 | }); 456 | } 457 | fs.stat(path, (err, stat) => { 458 | if (stat && stat.isFile()) { 459 | read(cmd, open, true); 460 | } else { 461 | var dir = path.replace(/[^\/]+$/, ''); 462 | fs.stat(dir, (err, stat) => { 463 | if (stat && stat.isDirectory()) { 464 | open('') 465 | } else if (err) { 466 | term.error(err.message); 467 | } else { 468 | term.error(`${dir} directory don't exists`); 469 | } 470 | }); 471 | } 472 | }); 473 | } 474 | }, 475 | cat: function(cmd) { 476 | read(cmd, term.echo); 477 | }, 478 | less: function(cmd) { 479 | read(cmd, term.less.bind(term)); 480 | }, 481 | ls: function(cmd) { 482 | var {options, args} = split_args(cmd.args); 483 | function filter(list) { 484 | if (options.match(/a/)) { 485 | return list; 486 | } else if (options.match(/A/)) { 487 | return list.filter(name => !name.match(/^\.{1,2}$/)); 488 | } else { 489 | return list.filter(name => !name.match(/^\./)); 490 | } 491 | } 492 | list(cwd + '/' + (args[0] || '')).then((content) => { 493 | var dirs = filter(['.', '..'].concat(content.dirs)).map((dir) => color('blue', dir)); 494 | var result = dirs.concat(filter(content.files)); 495 | if (result.length) { 496 | term.echo(result); 497 | } 498 | }); 499 | }, 500 | clean: function() { 501 | term.push(function(yesno) { 502 | if (yesno.match(/^y(es)?$/i)) { 503 | fs.getRootFS().empty(); 504 | } 505 | if (yesno.match(/^(y(es)?|n(o)?)$/i)) { 506 | term.pop(); 507 | } 508 | }, { 509 | prompt: 'are you sure you want clean File System [Y/N]? ' 510 | }); 511 | }, 512 | rm: function(cmd) { 513 | var {options, args} = split_args(cmd.args); 514 | 515 | var len = args.length; 516 | if (len) { 517 | term.pause(); 518 | } 519 | args.forEach(arg => { 520 | var path_name = path.resolve(cwd + '/' + arg); 521 | fs.stat(path_name, async (err, stat) => { 522 | if (err) { 523 | error(err); 524 | } else if (stat) { 525 | try { 526 | if (stat.isDirectory()) { 527 | if (options.match(/r/)) { 528 | await rmdir(path_name); 529 | } else { 530 | term.error(`${path_name} is directory`); 531 | } 532 | } else if (stat.isFile()) { 533 | await new Promise((resolve) => fs.unlink(path_name, resolve)); 534 | } else { 535 | term.error(`${path_name} is invalid`); 536 | } 537 | if (!--len) { 538 | term.resume(); 539 | } 540 | } catch(e) { 541 | error(e); 542 | } 543 | } 544 | }); 545 | }); 546 | }, 547 | zip: function(cmd) { 548 | term.pause(); 549 | if (cmd.args.length === 2) { 550 | makeZip.apply(null, cmd.args.map(resolve)).then(() => { 551 | term.echo(`click to download [[!;;;;__browserfs__/${cmd.args[1]}]${cmd.args[1]}]`).resume(); 552 | }).catch(error); 553 | } else { 554 | term.echo('compress directory\nzip '); 555 | } 556 | }, 557 | view: function(cmd) { 558 | if (cmd.args.length === 1) { 559 | view(cmd.args[0]); 560 | } 561 | }, 562 | record: function(cmd) { 563 | if (cmd.args[0] == 'start') { 564 | term.history_state(true); 565 | } else if (cmd.args[0] == 'stop') { 566 | term.history_state(false); 567 | } else { 568 | term.echo('usage: record [stop|start]'); 569 | } 570 | }, 571 | git: { 572 | reset: async function(cmd) { 573 | cmd.args.shift(); 574 | term.pause(); 575 | const hard = cmd.args.includes('--hard'); 576 | try { 577 | var dir = await gitroot(cwd); 578 | const ref = cmd.args.filter(arg => arg.match(/^HEAD/))[0]; 579 | if (ref) { 580 | await gitReset({dir, git: git_wrapper, hard, ref, branch}); 581 | const commits = await git.log({dir, depth: 1}); 582 | const commit = commits.pop(); 583 | const head = await git.resolveRef({dir, ref: 'HEAD'}); 584 | term.echo(`HEAD is now at ${commit.oid.substring(0, 7)} ${commit.message.trim()}`); 585 | } else { 586 | const matrix = await git.statusMatrix({fs, dir}); 587 | const files = matrix.filter(file => [0,3].includes(file[3])); 588 | files.forEach(([filepath]) => { 589 | git.resetIndex({fs, dir: '/gaiman', filepath}); 590 | }); 591 | } 592 | } catch(e) { 593 | term.exception(e); 594 | } finally { 595 | term.resume(); 596 | } 597 | }, 598 | branch: function(cmd) { 599 | term.echo('to be implemented'); 600 | }, 601 | test: async function(cmd) { 602 | try { 603 | 604 | } catch (e) { 605 | term.exception(e); 606 | } finally { 607 | term.resume(); 608 | } 609 | }, 610 | pull: async function(cmd) { 611 | try { 612 | term.pause(); 613 | var dir = await gitroot(cwd); 614 | var remote = 'origin'; 615 | var HEAD_before = await git.resolveRef({dir, ref: 'HEAD'}); 616 | var url = await repoURL({dir}); 617 | var output = []; 618 | var auth = {}; 619 | if (credentials.password && credentials.username) { 620 | auth.authUsername = credentials.username; 621 | auth.authPassword = credentials.password; 622 | } 623 | var ref = await git.resolveRef({ 624 | dir, 625 | ref: 'HEAD', 626 | depth: 1 627 | }); 628 | await git_wrapper.pull({ 629 | dir, 630 | fastForwardOnly: true, 631 | ...auth, 632 | emitter: messageEmitter() 633 | }); 634 | // isomorphic git patch 635 | const head = await git.resolveRef({ 636 | dir, 637 | ref: 'HEAD', 638 | depth: 1 639 | }); 640 | if (head != ref) { 641 | await new Promise((resolve) => fs.writeFile(`${dir}/.git/HEAD`, ref, resolve)); 642 | } 643 | var HEAD_after = await git.resolveRef({dir, ref: 'HEAD'}); 644 | if (HEAD_after === HEAD_before) { 645 | term.echo('Already up-to-date.'); 646 | } else { 647 | output.push(`From ${url}`); 648 | output.push([ 649 | ' ', 650 | HEAD_before.substring(0, 7), 651 | '..', 652 | HEAD_after.substring(0, 7), 653 | ' ', 654 | branch, 655 | ' -> ', 656 | remote, 657 | '/', 658 | branch 659 | ].join('')); 660 | output.push('Fast-froward'); 661 | const diffs = await gitCommitDiff({dir, oldSha: HEAD_before, newSha: HEAD_after}); 662 | output.push(diffStat(Object.values(diffs).map(val => val.diff))); 663 | term.echo(output.join('\n')); 664 | } 665 | } catch (e) { 666 | term.exception(e); 667 | } finally { 668 | term.resume(); 669 | } 670 | /* TODO: 671 | * 672 | * remote: Counting objects: 3, done. 673 | * remote: Compressing objects: 100% (2/2), done. 674 | * remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 675 | * Unpacking objects: 100% (3/3), done. 676 | * From github.com:jcubic/test 677 | * b132560..fd4588d master -> origin/master 678 | * Updating b132560..fd4588d 679 | * Fast-forward 680 | * bar | 1 + 681 | * 1 file changed, 1 insertion(+) 682 | */ 683 | }, 684 | fetch: async function(cmd) { 685 | cmd.args.shift(); 686 | try { 687 | if (cmd.args.length) { 688 | term.pause(); 689 | var dir = await gitroot(cwd); 690 | var emitter = messageEmitter(); 691 | await git_wrapper.fetch({ 692 | dir, 693 | ref: cmd.args[0], 694 | emitter 695 | }); 696 | } 697 | } catch(e) { 698 | term.error(e.message || e); 699 | } finally { 700 | term.resume(); 701 | } 702 | }, 703 | checkout: async function(cmd) { 704 | cmd.args.shift(); 705 | try { 706 | if (cmd.args.length) { 707 | term.pause(); 708 | var dir = await gitroot(cwd); 709 | var branches = await git.listBranches({dir}); 710 | if (branches.includes(cmd.args[0])) { 711 | await git_wrapper.checkout({dir, ref: cmd.args[0]}); 712 | branch = await gitBranch({cwd}); 713 | } else { 714 | var filepath = cmd.args[0].replace(dir, ''); 715 | await gitCheckoutFile({dir, filepath, branch}); 716 | } 717 | } else { 718 | term.echo('to be implemented'); 719 | /* 720 | * M js/main.js 721 | * Your branch is up-to-date with 'origin/gh-pages'. 722 | */ 723 | } 724 | } catch (e) { 725 | term.error(e.message || e); 726 | } finally { 727 | term.resume(); 728 | } 729 | /* TODO: 730 | * 731 | * Switched to branch 'gh-pages' 732 | * Your branch is up-to-date with 'origin/gh-pages'. 733 | * 734 | * Switched to a new branch 'test' 735 | * 736 | * Switched to branch 'master' 737 | * Your branch is ahead of 'origin/master' by 2 commits. 738 | * (use "git push" to publish your local commits) 739 | */ 740 | }, 741 | push: async function(cmd) { 742 | if (true || credentials.username && credentials.password) { 743 | term.pause(); 744 | var emitter = messageEmitter(); 745 | try { 746 | var dir = await gitroot(cwd); 747 | var url = await repoURL({dir}); 748 | var branches = await git.listBranches({dir, remote: 'origin'}); 749 | var output = [`To ${url}`]; 750 | if (branches.includes(branch)) { 751 | var ref = await git.resolveRef({dir, ref: `refs/remotes/origin/${branch}`}); 752 | var HEAD = await git.resolveRef({dir, ref: 'HEAD'}); 753 | output.push(` ${ref.substring(0, 7)}..${HEAD.substring(0, 7)} ${branch} -> ${branch}`); 754 | } else { 755 | output.push(` * [new branch] ${branch} -> ${branch}`); 756 | } 757 | try { 758 | await git_wrapper.push({ 759 | dir, 760 | corsProxy: 'https://jcubic.pl/proxy.php?', 761 | ref: branch, 762 | //authUsername: credentials.username, 763 | //authPassword: credentials.password, 764 | emitter 765 | }); 766 | } catch(e) { 767 | console.log(e); 768 | } 769 | term.echo(output.join('\n')); 770 | } catch (e) { 771 | term.error(e.message || e); 772 | } finally { 773 | term.resume(); 774 | } 775 | } else { 776 | term.error('You need to call `git login` to set username and password before push'); 777 | } 778 | }, 779 | commit: async function(cmd) { 780 | cmd.args.shift(); 781 | term.pause(); 782 | try { 783 | async function commit() { 784 | var head = await git.resolveRef({dir, ref: 'HEAD'}); 785 | var commit = await git.commit({ 786 | dir, 787 | author: { 788 | email: credentials.email, 789 | name 790 | }, 791 | message 792 | }); 793 | function mod(field, label) { 794 | return Object.keys(diffs).filter(fielpath => diffs[fielpath][field]).map(fielpath => { 795 | return ` ${label} mode 100644 ${fielpath}`; 796 | }).join('\n'); 797 | } 798 | var diffs = await gitCommitDiff({dir, newSha: commit, oldSha: head}); 799 | var stat = diffStat(Object.values(diffs).map(value => value.diff)); 800 | term.echo(`[master ${commit.substring(0, 7)}] ${message}\n${stat}`); 801 | term.echo(mod('deleted', 'delete')); 802 | term.echo(mod('added', 'create')); 803 | } 804 | var dir = await gitroot(cwd); 805 | var all = !!cmd.args.filter(arg => arg.match(/^-.*a/)).length; 806 | if (all) { 807 | await gitAddAll({dir, branch}); 808 | } 809 | var message = getOption(/-.*m$/, cmd.args); 810 | var name = credentials.fullname || credentials.username; 811 | if (!name) { 812 | term.error('You need to use git login first'); 813 | } else if (!message) { 814 | var textarea = $('.vi'); 815 | var file = [ 816 | '', 817 | '# Please enter the commit message for your changes. Lines starting', 818 | '# with \'#\' will be ignored, and an empty message aborts the commit.', 819 | '#' 820 | ]; 821 | var head = await getHEAD({dir}); 822 | var remote = await getHEAD({dir, remote: true}); 823 | if (head === remote) { 824 | file = file.concat([ 825 | `# On branch ${branch}`, 826 | `# Your branch is up-to-date with 'origin/${branch}'.`, 827 | '#' 828 | ]); 829 | } 830 | var files = await getAllStats({cwd, branch}); 831 | var staged = files.filter(([_, stat]) => ['modified', 'deleted', 'added'].includes(stat)); 832 | var not_staged = files.filter(([_, stat]) => ['*modified', '*deleted'].includes(stat)); 833 | var new_files = files.filter(([_, stat]) => stat === '*added'); 834 | var mapping = { 835 | 'added': 'new file' 836 | }; 837 | const format = ([filepath, status]) => { 838 | status = status.replace(/^\*/, ''); 839 | return '# ' + padright(`${mapping[status] || status}:`, 12) + filepath; 840 | }; 841 | const append = (array, message) => file = file.concat([message], array.map(format), ['#']); 842 | if (staged.length) { 843 | append(staged, '# Changes to be committed:'); 844 | } 845 | if (not_staged.length) { 846 | append(not_staged, '# Changes not staged for commit:'); 847 | } 848 | if (new_files.length) { 849 | file = file.concat([ 850 | '# Untracked files:' 851 | ], new_files.map(([file]) => `# ${file}`), ['#']); 852 | } 853 | textarea.val(file.join('\n').replace(/ !line.startsWith('#')).join('\n').trim(); 863 | if (!message) { 864 | term.echo('Aborting commit due to empty commit message.'); 865 | } else { 866 | await commit(); 867 | } 868 | term.focus(); 869 | } 870 | }); 871 | } else { 872 | await commit(); 873 | } 874 | } catch(e) { 875 | term.exception(e); 876 | throw e; 877 | } finally { 878 | term.resume(); 879 | } 880 | }, 881 | add: function(cmd) { 882 | term.pause(); 883 | cmd.args.shift(); 884 | var all_git = cmd.args.filter(arg => arg.match(/^(-A|-all)$/)).length; 885 | var all = !!cmd.args.filter(arg => arg === '.').length; 886 | if (all || all_git) { 887 | gitroot(cwd).then(dir => { 888 | return gitAddAll({dir, branch, all}); 889 | }).then(term.resume).catch(error); 890 | } else if (cmd.args.length > 0) { 891 | processGitFiles(cmd.args).then(({files, dir}) => { 892 | return Promise.all(files.map(async filepath => { 893 | const status = await git.status({dir, filepath}); 894 | if (status.match(/^\*/)) { 895 | if (status === '*deleted') { 896 | return git.remove({dir, filepath}); 897 | } 898 | await git.add({dir, filepath}); 899 | } 900 | })); 901 | }).then(term.resume).catch(error); 902 | } else { 903 | term.resume(); 904 | } 905 | }, 906 | rm: function(cmd) { 907 | cmd.args.shift(); 908 | var long_options = cmd.args.filter(name => name.match(/^--/)); 909 | var {args, options} = split_args(cmd.args.filter(name => !name.match(/^--/))); 910 | var len = args.length; 911 | if (!len) { 912 | term.error('Nothing to remove'); 913 | } else { 914 | term.pause(); 915 | gitroot(cwd).then((dir) => { 916 | var re = new RegExp('^' + dir + '/'); 917 | args.forEach(arg => { 918 | var path_name = path.resolve(cwd + '/' + arg); 919 | fs.stat(path_name, (err, stat) => { 920 | if (err) { 921 | term.error(err); 922 | } else if (stat) { 923 | var filepath = path_name.replace(re, ''); 924 | if (stat.isDirectory()) { 925 | var promise = git.listDir({dir}).then((list) => { 926 | var files = list.filter(name => name.startsWith(filepath)); 927 | return Promise.all(files.map(file => git.remove({dir, filepath}))); 928 | }).catch(err => term.error(err)); 929 | if (options.match(/r/)) { 930 | if (!long_options.includes(/--cached/)) { 931 | promise.then(() => rmdir(path_name)); 932 | } 933 | } else { 934 | term.error(`${path_name} is directory`); 935 | } 936 | } else if (stat.isFile()) { 937 | if (!long_options.includes(/--cached/)) { 938 | git.remove({dir, filepath}).then(() => fs.unlink(path_name)); 939 | } else { 940 | git.remove({dir, filepath}); 941 | } 942 | } 943 | } else { 944 | term.error('uknown error'); 945 | } 946 | if (!--len) { 947 | term.resume(); 948 | } 949 | }); 950 | }); 951 | }); 952 | } 953 | }, 954 | login: function() { 955 | var questions = [ 956 | {name: 'username'}, 957 | {name: 'password', mask: true}, 958 | {name: 'fullname'}, 959 | {name: 'email'} 960 | ]; 961 | term.echo('This will not authenticate to test if login/password are correct,\n'+ 962 | 'only save credentials in localStorage (expect password) and use them when push/clone/commit'); 963 | var history = term.history(); 964 | history.disable(); 965 | (function loop() { 966 | var question = questions.shift(); 967 | if (!question) { 968 | history.enable(); 969 | } else { 970 | var name = question.name; 971 | term.push(answer => { 972 | credentials[name] = answer; 973 | if (!question.mask) { 974 | localStorage.setItem('git_' + name, answer); 975 | } 976 | term.pop(); 977 | loop(); 978 | }, { 979 | prompt: name + ': ', 980 | name: 'read' 981 | }).set_mask(!!question.mask); 982 | if (!question.mask) { 983 | term.insert(localStorage.getItem('git_' + name) || ''); 984 | } 985 | } 986 | })(); 987 | }, 988 | status: function(cmd) { 989 | var dir = cwd.split('/')[1]; 990 | term.pause(); 991 | /* TODO: 992 | * On branch master 993 | * Your branch is ahead of 'origin/master' by 1 commit. 994 | * (use "git push" to publish your local commits) 995 | * 996 | * nothing to commit, working tree clean 997 | */ 998 | getAllStats({cwd, branch}).then((files) => { 999 | function filter(files, name) { 1000 | if (name instanceof Array) { 1001 | return files.filter(([_, status]) => name.includes(status)); 1002 | } 1003 | return files.filter(([_, status]) => status === name); 1004 | } 1005 | function not(files, name) { 1006 | if (name instanceof Array) { 1007 | return files.filter(([_, status]) => !name.includes(status)); 1008 | } 1009 | return files.filter(([_, status]) => status !== name); 1010 | } 1011 | var changes = not(files, ['unmodified', 'ignored']); 1012 | if (!changes.length) { 1013 | git.log({dir, depth: 2, ref: branch}).then((commits) => { 1014 | term.echo(`On branch ${branch}`); 1015 | if (commits.length == 2) { 1016 | term.echo('nothing to commit, working directory clean\n'); 1017 | } else { 1018 | // new repo 1019 | term.echo('nothing to commit (create/copy files and use "git add" to track)\n'); 1020 | } 1021 | term.resume(); 1022 | }); 1023 | } else { 1024 | var label = { 1025 | 'deleted': 'deleted: ', 1026 | 'added': 'new file: ', 1027 | 'modified': 'modified: ', 1028 | 'absent': 'deleted: ' 1029 | }; 1030 | var padding = ' '; 1031 | var output = [`On branch ${branch}`]; 1032 | function listFiles(files, colorname) { 1033 | return files.map(([name, status]) => { 1034 | return padding + color(colorname, label[status.replace(/^\*/, '')] + name); 1035 | }); 1036 | } 1037 | var lines; 1038 | var to_be_added = filter(changes, ['added', 'modified', 'deleted']); 1039 | if (to_be_added.length) { 1040 | lines = [ 1041 | 'Changes to be committed:', 1042 | ' (use "git rm --cached ..." to unstage)', 1043 | '' 1044 | ]; 1045 | lines = lines.concat(listFiles(to_be_added, 'green')); 1046 | output.push(lines.join('\n')); 1047 | } 1048 | var not_added = filter(changes, ['*modified', '*deleted', '*absent']); 1049 | if (not_added.length) { 1050 | lines = [ 1051 | 'Changes not staged for commit:', 1052 | ' (use "git add ..." to update what will be committed)', 1053 | ' (use "git checkout -- ..." to discard changes in the working directory)', 1054 | '' 1055 | ]; 1056 | lines = lines.concat(listFiles(not_added, 'red')); 1057 | output.push(lines.join('\n')); 1058 | } 1059 | var untracked = filter(changes, '*added'); 1060 | if (untracked.length) { 1061 | lines = [ 1062 | 'Untracked files:', 1063 | ' (use "git add ..." to include in what will be committed)', 1064 | '' 1065 | ]; 1066 | lines = lines.concat(untracked.map(([name, status]) => padding + color('red', name))); 1067 | output.push(lines.join('\n')); 1068 | } 1069 | if (output.length) { 1070 | term.echo(output.join('\n\n') + '\n'); 1071 | } 1072 | term.resume(); 1073 | } 1074 | }).catch(error); 1075 | }, 1076 | diff: function(cmd) { 1077 | cmd.args.shift(); 1078 | term.pause(); 1079 | function diff({dir, filepath}) { 1080 | return gitDiff({dir, filepath, branch}).then(diff => { 1081 | const text = diff.hunks.map(hunk => { 1082 | let output = []; 1083 | output.push(color( 1084 | 'persian-green', 1085 | [ 1086 | '@@ -', 1087 | hunk.oldStart, 1088 | ',', 1089 | hunk.oldLines, 1090 | ' +', 1091 | hunk.newStart, 1092 | ',', 1093 | hunk.newLines, 1094 | ' @@' 1095 | ].join('') 1096 | )); 1097 | output = output.concat(hunk.lines.map(line => { 1098 | let color_name; 1099 | if (line[0].match(/[+-]/)) { 1100 | color_name = line[0] == '-' ? 'red' : 'green'; 1101 | } 1102 | if (color_name) { 1103 | return color(color_name, $.terminal.escape_brackets(line)); 1104 | } else { 1105 | return line; 1106 | } 1107 | })); 1108 | return output.join('\n'); 1109 | }).join('\n'); 1110 | return { 1111 | text, 1112 | filepath 1113 | }; 1114 | }); 1115 | } 1116 | function format(diff) { 1117 | const header = ['diff --git a/' + diff.filepath + ' b/' + diff.filepath]; 1118 | header.push('--- ' + diff.filepath); 1119 | header.push('+++ ' + diff.filepath); 1120 | return [color('white', header.join('\n')), diff.text].join('\n'); 1121 | } 1122 | gitroot(cwd).then(dir => { 1123 | if (!cmd.args.length) { 1124 | return git.listFiles({dir}).then(files => { 1125 | return Promise.all(files.map((filepath) => { 1126 | try { 1127 | return git.status({dir, filepath}).then(status => { 1128 | if (['unmodified', 'ignored'].includes(status)) { 1129 | return null; 1130 | } else { 1131 | return diff({dir, filepath}); 1132 | } 1133 | }); 1134 | } catch(e) { 1135 | debugger; 1136 | throw e; 1137 | } 1138 | })); 1139 | }).then((diffs) => { 1140 | return diffs.filter(Boolean).reduce((acc, diff) => { 1141 | acc.push(format(diff)); 1142 | return acc; 1143 | }, []).join('\n'); 1144 | }); 1145 | } else { 1146 | var re = new RegExp('^' + dir + '/?'); 1147 | var filepath = fname.replace(re, ''); 1148 | return diff({dir, filepath}).then(({diff}) => diff).then(format); 1149 | } 1150 | }).then(text => { 1151 | if (text.length - 1 > term.rows()) { 1152 | term.less(text); 1153 | } else { 1154 | term.echo(text); 1155 | } 1156 | term.resume(); 1157 | }).catch(err => term.error(err.message).resume()); 1158 | }, 1159 | log: function(cmd) { 1160 | term.pause(); 1161 | var depth = getOption('-n', cmd.args); 1162 | depth = depth ? +depth : undefined; 1163 | gitroot(cwd).then(dir => { 1164 | return Promise.all([getHEAD({dir}), getHEAD({dir, remote: true})]) 1165 | .then(([head, remote_head]) => ({ dir, head, remote_head })); 1166 | }).then(({dir, head, remote_head}) => { 1167 | function format(commit) { 1168 | var output = []; 1169 | var suffix = ''; 1170 | if (head === remote_head && head === commit.oid) { 1171 | suffix = [ 1172 | ' (' + color('persian-green', 'HEAD -> '), 1173 | color('green', branch) + ',', 1174 | color('red', 'origin/' + branch) + ')' 1175 | ].join(' '); 1176 | } else if (remote_head === commit.oid) { 1177 | suffix = [ 1178 | ' (' + color('red', `origin/${branch}`) + ',', 1179 | color('red', 'origin/HEAD') + ')' 1180 | ].join(' '); 1181 | } else if (head === commit.oid) { 1182 | suffix = [ 1183 | ' (' + color('persian-green', 'HEAD -> '), 1184 | color('green', branch) + ')' 1185 | ].join(' '); 1186 | } 1187 | output.push(color('yellow', `commit ${commit.oid}` + suffix)); 1188 | var committer = commit.committer; 1189 | if (committer) { 1190 | output.push(`Author: ${committer.name} <${committer.email}>`); 1191 | output.push(`Date: ${date(committer.timestamp, committer.timezoneOffset)}`); 1192 | } 1193 | output.push(''); 1194 | output.push(` ${commit.message}`); 1195 | return output.join('\n'); 1196 | } 1197 | return git.log({dir, depth, ref: branch}).then(commits => { 1198 | var text = commits.filter(commit => !commit.error).map(format).join('\n\n'); 1199 | if (text.length - 1 > term.rows()) { 1200 | term.less(text); 1201 | } else { 1202 | term.echo(text); 1203 | } 1204 | term.resume(); 1205 | }); 1206 | }).catch(error); 1207 | }, 1208 | clone: function(cmd) { 1209 | term.pause(); 1210 | cmd.args.shift(); 1211 | var args = []; 1212 | var options = {}; 1213 | var long; 1214 | var re = /^--(.*)/; 1215 | cmd.args.forEach(function(arg) { 1216 | if (long) { 1217 | options[long[1]] = arg; 1218 | } else if (!String(arg).match(re)) { 1219 | args.push(arg); 1220 | } 1221 | long = String(arg).match(re); 1222 | }); 1223 | var depth = getOption(/^--depth/, cmd.args); 1224 | var url = args[0]; 1225 | re = /\/([^\/]+?)(\.git)?$/; 1226 | var repo_dir = path.join(cwd, (args.length === 2 ? args[1] : args[0].match(re)[1])); 1227 | fs.stat(repo_dir, function(err, stat) { 1228 | if (err) { 1229 | mkdir(repo_dir, true).then(clone).catch(error); 1230 | } else if (stat) { 1231 | if (stat.isFile()) { 1232 | term.error(`"${repo_dir}" is a file`).resume(); 1233 | } else { 1234 | fs.readdir(repo_dir, function(err, list) { 1235 | if (list.length) { 1236 | term.error(`"${repo_dir}" exists and is not empty`).resume(); 1237 | } else { 1238 | clone(); 1239 | } 1240 | }); 1241 | } 1242 | } 1243 | }); 1244 | function clone() { 1245 | term.echo(`Cloning into '${repo_dir}'...`); 1246 | var auth = {}; 1247 | if (credentials.username && credentials.password) { 1248 | auth = { 1249 | authUsername: credentials.username, 1250 | authPassword: credentials.password 1251 | }; 1252 | } 1253 | git_wrapper.clone({ 1254 | dir: repo_dir, 1255 | corsProxy: 'https://jcubic.pl/proxy.php?', 1256 | url: url, 1257 | ...auth, 1258 | depth: depth ? +depth : undefined, 1259 | emitter: new messageEmitter() 1260 | }).then(term.resume).catch(error); 1261 | } 1262 | } 1263 | }, 1264 | credits: function() { 1265 | var lines = [ 1266 | '', 1267 | 'Projects used with GIT Web Terminal:', 1268 | '\t[[!;;;;https://isomorphic-git.github.io]isomorphic-git] v. ' + git.version() + ' by William Hilton', 1269 | '\t[[!;;;;https://github.com/jvilk/BrowserFS]BrowserFS] by John Vilk', 1270 | '\t[[!;;;;https://terminal.jcubic.pl]jQuery Terminal] v.' + $.terminal.version + ' by Jakub Jankiewicz', 1271 | '\t[[!;;;;https://github.com/timoxley/wcwidth]wcwidth] by Jun Woong', 1272 | '\t[[!;;;;https://github.com/inexorabletash/polyfill]keyboard key polyfill] by Joshua Bell', 1273 | '\t[[!;;;;https://github.com/jcubic/jsvi]jsvi] originaly by Internet Connection, Inc. with changes from Jakub Jankiewicz', 1274 | '\t[[!;;;;https://github.com/Olical/EventEmitter/]EventEmitter] by Oliver Caldwell', 1275 | '\t[[!;;;;https://github.com/PrismJS/prism]PrismJS] by Lea Verou', 1276 | '\t[[!;;;;https://github.com/kpdecker/jsdiff]jsdiff] by Kevin Decker', 1277 | '\t[[!;;;;https://github.com/softius/php-cross-domain-proxy]AJAX Cross Domain (PHP) Proxy] by Iacovos Constantinou', 1278 | '\t[[!;;;;https://github.com/jcubic/Clarity]Clarity icons] by Jakub Jankiewicz', 1279 | '\t[[!;;;;https://github.com/jcubic/jquery.splitter]jQuery Splitter] by Jakub Jankiewicz', 1280 | '\t[[!;;;;http://www.ymacs.org/]Ymacs] by Mihai Bazon & Dynarch.com', 1281 | '\t[[!;;;;http://stuk.github.io/jszip/]JSZip] by Stuart Knightley', 1282 | '', 1283 | 'Contributors:' 1284 | ].concat(contributors.map(user => '\t[[!;;;;' + user.url + ']' + (user.fullname || user.name) + ']')); 1285 | term.echo(lines.join('\n') + '\n'); 1286 | }, 1287 | help: function() { 1288 | term.echo('\nList of commands: ' + Object.keys(term.commands).join(', '), {keepWords: true}); 1289 | term.echo('List of Git commands: ' + Object.keys(term.commands.git).join(', '), {keepWords: true}); 1290 | term.echo([ 1291 | '', 1292 | 'to use git you first need to clone the repo (use --depth to speed up cloning of big repos),', 1293 | 'then you can made changes using [[;#fff;]vi] or [[;#fff;]emacs], use [[;#fff;]git add] and then ' + 1294 | '[[;#fff;]git commit].', 1295 | '', 1296 | 'Before you commit you need to use the command [[b;#fff;]git login] which will ask for credentials.', 1297 | 'It will also ask for full name and email to be used in [[b;#fff;]git commit]. If you set the correct', 1298 | 'username and password you can push to remote; if you\'ll type wrong credentials you can call login again.', 1299 | '', 1300 | 'To view the files you can use [[;#fff;]view ] commands that will split terminal and open browser.', 1301 | 'You can also open directories (it\'s just iframe with files served from browserfs using service worker).', 1302 | 'If you have web app you can open it using file browser and [[;#fff;]view] command.', 1303 | '', 1304 | 'You can use [[;#fff;]record start] to record commands in url hash so you can share commands you\'ll type.', 1305 | '' 1306 | ].join('\n'), {keepWords: true}); 1307 | } 1308 | }; 1309 | var ymacs = init_ymacs(); 1310 | var scrollTop; 1311 | var view = (function() { 1312 | var base = location.pathname.replace(/\/[^\/]+$/, '/').replace(/__browserfs__.*/, ''); 1313 | var viewer = $('.viewer'); 1314 | var iframe = viewer.find('iframe'); 1315 | var splitter; 1316 | viewer.on('click', '.close', function() { 1317 | if (splitter) { 1318 | splitter.destroy(); 1319 | splitter = null; 1320 | viewer.hide(); 1321 | $('.terminal-view').css('width', ''); 1322 | } 1323 | }); 1324 | var adress = viewer.find('input').on('keydown', function(e) { 1325 | if (e.key.toLowerCase() == 'enter') { 1326 | view(adress.val()); 1327 | } 1328 | }); 1329 | viewer.on('click', '.refresh', function() { 1330 | view(adress.val()); 1331 | }); 1332 | var paths = []; 1333 | var index = 0; 1334 | var next = viewer.find('.next').on('click', function() { 1335 | if (!next.is('.disabled')) { 1336 | view(paths[++index], true); 1337 | } 1338 | }); 1339 | var prev = viewer.find('.prev').on('click', function() { 1340 | if (!prev.is('.disabled')) { 1341 | view(paths[--index], true); 1342 | } 1343 | }); 1344 | 1345 | function view(path, soft) { 1346 | if (!splitter) { 1347 | splitter = $('.split').split({ 1348 | orientation: 'vertical', 1349 | limit: 400 1350 | }); 1351 | } 1352 | iframe.off('load').on('load', function() { 1353 | var path = iframe[0].contentWindow.location.href.replace(/.*__browserfs__/, ''); 1354 | adress.val(path); 1355 | // this need to be added each time because we need to access soft prop 1356 | if (!soft) { 1357 | paths = paths.slice(0, index + 1); 1358 | paths.push(path); 1359 | index = paths.length - 1; 1360 | } 1361 | soft = false; 1362 | next.toggleClass('disabled', index === paths.length - 1); 1363 | prev.toggleClass('disabled', index === 0); 1364 | }).attr('src', base + '__browserfs__' + resolve(path)); 1365 | } 1366 | 1367 | return view; 1368 | })(); 1369 | var instance = $('.term').terminal(function(command, term) { 1370 | var cmd = $.terminal.split_command(command); 1371 | if (term.commands[cmd.name]) { 1372 | var action = term.commands[cmd.name]; 1373 | var args = cmd.args.slice(); 1374 | while (true) { 1375 | if (typeof action == 'object' && args.length) { 1376 | action = action[args.shift()]; 1377 | } else { 1378 | break; 1379 | } 1380 | } 1381 | if (action) { 1382 | action.call(term, cmd); 1383 | } else { 1384 | term.error('Unknown command'); 1385 | } 1386 | } else if (command) { 1387 | term.error('Unknown command'); 1388 | } 1389 | }, { 1390 | execHash: true, 1391 | onResize: function() { 1392 | this.innerHeight(this.parent().height()); 1393 | }, 1394 | completion: function(string, cb) { 1395 | var cmd = $.terminal.parse_command(this.before_cursor()); 1396 | function processAssets(callback) { 1397 | var dir = get_path(string); 1398 | return list('/' + dir.join('/')).then(callback); 1399 | } 1400 | function prepend(list) { 1401 | if (string.match(/\//)) { 1402 | var path = string.replace(/\/[^\/]+$/, '').replace(/\/+$/, ''); 1403 | return list.map((dir) => path + '/' + dir); 1404 | } else { 1405 | return list; 1406 | } 1407 | } 1408 | function trailing(list) { 1409 | return list.map((dir) => dir + '/'); 1410 | } 1411 | if (cmd.name !== string) { 1412 | switch (cmd.name) { 1413 | // complete file and directories 1414 | case 'rm': 1415 | case 'cat': 1416 | case 'vi': 1417 | case 'less': 1418 | case 'emacs': 1419 | case 'view': 1420 | processAssets(content => cb(prepend(trailing(content.dirs).concat(content.files)))); 1421 | break; 1422 | // complete directories 1423 | case 'ls': 1424 | case 'cd': 1425 | processAssets(content => cb(prepend(trailing(content.dirs)))); 1426 | break; 1427 | default: 1428 | if (term.completion[cmd.name]) { 1429 | processAssets(content => { 1430 | term.completion[cmd.name].call( 1431 | term, 1432 | string, 1433 | cb, 1434 | content.files, 1435 | prepend(trailing(content.dirs)) 1436 | ); 1437 | }); 1438 | } 1439 | } 1440 | } 1441 | if (cmd.args.length) { 1442 | var command = term.commands[cmd.name]; 1443 | if (command) { 1444 | var args = cmd.args.slice(); 1445 | while (true) { 1446 | if (typeof command == 'object' && args.length > 1) { 1447 | command = command[args.shift()]; 1448 | } else { 1449 | break; 1450 | } 1451 | } 1452 | if ($.isPlainObject(command)) { 1453 | cb(Object.keys(command)); 1454 | } 1455 | } 1456 | } else { 1457 | cb(Object.keys(term.commands)); 1458 | } 1459 | }, 1460 | greetings: false, 1461 | name, 1462 | prompt: function(cb) { 1463 | var path = color('blue', cwd); 1464 | var b = branch ? ' [' + color('violet', branch) + ']' : ''; 1465 | cb([ 1466 | color('green', (credentials.username || 'anonymous') + '@gitwebterm'), 1467 | ':', 1468 | path, 1469 | b, 1470 | '$ ' 1471 | ].join('')); 1472 | } 1473 | }).echo(greetings); 1474 | term = $.extend(instance, term); 1475 | }); 1476 | 1477 | // --------------------------------------------------------------------------------------------------------- 1478 | function init_ymacs() { 1479 | var ymacs = new Ymacs({ 1480 | buffers: [ ], 1481 | className: "Ymacs-blinking-caret" 1482 | }); 1483 | ymacs.setColorTheme([ "dark", "y" ]); 1484 | ymacs.disabled(true); 1485 | var wrapper = $('.terminal-view').resizer(function() { 1486 | ymacs.callHooks('onResize'); 1487 | }); 1488 | $(ymacs.getElement()).appendTo(wrapper); 1489 | try { 1490 | ymacs.getActiveBuffer().cmd("eval_file", ".ymacs"); 1491 | } catch(ex) {} 1492 | // ------------------------------------------------------------------------------------------------- 1493 | Ymacs.prototype.fs_setFileContents = function(name, content, stamp, cont) { 1494 | var self = this; 1495 | if (stamp) { 1496 | fs.readFile(name, function(err, file) { 1497 | if (file != stamp) { 1498 | cont(null); 1499 | } else { 1500 | self.fs_setFileContents(name, content, false, cont); 1501 | } 1502 | }); 1503 | } else { 1504 | fs.writeFile(name, content, function(err, written) { 1505 | if (!err) { 1506 | cont(content); 1507 | } else { 1508 | self.getActiveBuffer().signalInfo("Can't save file"); 1509 | } 1510 | }); 1511 | } 1512 | }; 1513 | // ------------------------------------------------------------------------------------------------- 1514 | Ymacs.prototype.fs_getFileContents = function(name, nothrow, cont) { 1515 | var self = this; 1516 | fs.stat(name, function(err, stat) { 1517 | if (err || !stat.isFile()) { 1518 | cont(null, null); 1519 | } else { 1520 | fs.readFile(name, function(err, file) { 1521 | if (err) { 1522 | if (!nothrow) { 1523 | throw new Ymacs_Exception("File not found"); 1524 | } else { 1525 | self.getActiveBuffer().signalInfo("Can't open file"); 1526 | } 1527 | } else { 1528 | cont(file.toString(), file.toString()); 1529 | } 1530 | }); 1531 | } 1532 | }); 1533 | }; 1534 | // ------------------------------------------------------------------------------------------------- 1535 | Ymacs.prototype.fs_fileType = function(name, cont) { 1536 | fs.stat(name, function(err, stat) { 1537 | cont(err || stat.isFile() ? true : null); 1538 | }); 1539 | }; 1540 | // ------------------------------------------------------------------------------------------------- 1541 | Ymacs.prototype.fs_normalizePath = function(path) { 1542 | return path; 1543 | }; 1544 | // ------------------------------------------------------------------------------------------------- 1545 | Ymacs.prototype.fs_getDirectory = function(dir, cont) { 1546 | fs.readdir(dir, function(err, list) { 1547 | var result = {}; 1548 | (function loop() { 1549 | var obj = list.shift(); 1550 | if (!obj) { 1551 | cont(result); 1552 | } else { 1553 | fs.stat(dir + '/' + obj, function(err, stat) { 1554 | if (!err) { 1555 | result[obj] = {type: stat.isFile() ? 'file' : 'directory'} 1556 | loop(); 1557 | } 1558 | }); 1559 | } 1560 | })(); 1561 | }); 1562 | }; 1563 | // ------------------------------------------------------------------------------------------------- 1564 | Ymacs_Buffer.newCommands({ 1565 | exit: Ymacs_Interactive(function() { 1566 | var buffs = ymacs.buffers.slice(); 1567 | (function loop() { 1568 | var buff = buffs.shift(); 1569 | if (buff.length > 1) { 1570 | function next() { 1571 | ymacs.killBuffer(buff); 1572 | loop(); 1573 | } 1574 | if (buff.name != '*scratch*') { 1575 | if (buff.dirty()) { 1576 | var msg = 'Save file ' + buff.name + ' yes or no?'; 1577 | buff.cmd('minibuffer_yn', msg, function(yes) { 1578 | if (yes) { 1579 | buff.cmd('save_buffer_with_continuation', 1580 | false, 1581 | next); 1582 | } else { 1583 | next(); 1584 | } 1585 | }); 1586 | } else { 1587 | next(); 1588 | } 1589 | } 1590 | } else { 1591 | $('.DlDesktop, .DlLayout, .Ymacs').hide(); 1592 | ymacs.disabled(true); 1593 | $.terminal.active().focus(); 1594 | } 1595 | })(); 1596 | }), 1597 | next_buffer: Ymacs_Interactive(function() { 1598 | var buffs = ymacs.buffers.slice(); 1599 | var buff = ymacs.getActiveBuffer(); 1600 | while(buffs.shift() != buff) {} 1601 | if (buffs.length) { 1602 | ymacs.switchToBuffer(buffs[0]); 1603 | } else { 1604 | ymacs.switchToBuffer(ymacs.buffers[0]); 1605 | } 1606 | }) 1607 | }); 1608 | // ------------------------------------------------------------------------------------------------- 1609 | DEFINE_SINGLETON("Ymacs_Keymap_Leash", Ymacs_Keymap_Emacs, function(D, P) { 1610 | D.KEYS = { 1611 | "C-x C-c": "exit" 1612 | }; 1613 | }); 1614 | return ymacs; 1615 | } 1616 | 1617 | // --------------------------------------------------------------------------------------------------------- 1618 | function time() { 1619 | var d = new Date(); 1620 | return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => ('0' + n).slice(-2)).join(':'); 1621 | } 1622 | 1623 | // --------------------------------------------------------------------------------------------------------- 1624 | async function listAllFiles({dir, pathname = '', pattern = null}) { 1625 | let files = []; 1626 | var root = pathname ? path.join(dir, pathname) : dir; 1627 | let content = await listDir(root); 1628 | function skip(name) { 1629 | return pattern !== null && !name.match(pattern); 1630 | } 1631 | for (let filename of content.files) { 1632 | if (pathname) { 1633 | filename = path.join(pathname, filename); 1634 | } 1635 | if (skip(filename)) { 1636 | continue; 1637 | } 1638 | files.push(filename); 1639 | } 1640 | for (let dirname of content.dirs) { 1641 | if (pathname) { 1642 | dirname = path.join(pathname, dirname); 1643 | } 1644 | if (skip(dirname + '/')) { 1645 | continue; 1646 | } 1647 | files = files.concat(await listAllFiles({dir, pathname: dirname, pattern})); 1648 | } 1649 | return files; 1650 | } 1651 | // --------------------------------------------------------------------------------------------------------- 1652 | function listDir(path) { 1653 | return new Promise(function(resolve, reject) { 1654 | fs.readdir(path, function(err, dirList) { 1655 | if (err) { 1656 | return reject(err); 1657 | } 1658 | var result = { 1659 | files: [], 1660 | dirs: [] 1661 | }; 1662 | var len = dirList.length; 1663 | if (!len) { 1664 | resolve(result); 1665 | } 1666 | dirList.forEach(function(filename) { 1667 | var file = (path === '/' ? '' : path) + '/' + filename; 1668 | fs.stat(file, function(err, stat) { 1669 | if (err) { 1670 | throw new Error(err); 1671 | } 1672 | if (stat) { 1673 | result[stat.isFile() ? 'files' : 'dirs'].push(filename); 1674 | } 1675 | if (!--len) { 1676 | resolve(result); 1677 | } 1678 | }); 1679 | }); 1680 | 1681 | }); 1682 | }); 1683 | } 1684 | 1685 | // --------------------------------------------------------------------------------------------------------- 1686 | // source: https://stackoverflow.com/a/3629861/387194 1687 | function union(x, y) { 1688 | var obj = {}; 1689 | for (var i = x.length-1; i >= 0; -- i) 1690 | obj[x[i]] = x[i]; 1691 | for (var i = y.length-1; i >= 0; -- i) 1692 | obj[y[i]] = y[i]; 1693 | var res = []; 1694 | for (var k in obj) { 1695 | if (obj.hasOwnProperty(k)) // <-- optional 1696 | res.push(obj[k]); 1697 | } 1698 | return res; 1699 | } 1700 | 1701 | // --------------------------------------------------------------------------------------------------------- 1702 | function intersection(a, b) { 1703 | return a.filter(function(n) { 1704 | return b.includes(n); 1705 | }); 1706 | } 1707 | 1708 | // --------------------------------------------------------------------------------------------------------- 1709 | async function rmdir(dir) { 1710 | return new Promise(function(resolve, reject) { 1711 | fs.readdir(dir, async function(err, list) { 1712 | if (err) { 1713 | return reject(err); 1714 | } 1715 | for(var i = 0; i < list.length; i++) { 1716 | var filename = path.join(dir, list[i]); 1717 | var stat = await new Promise(function(resolve, reject) { 1718 | fs.stat(filename, function(err, stat) { 1719 | if (err) { 1720 | return reject(err); 1721 | } 1722 | resolve(stat); 1723 | }); 1724 | }); 1725 | if (!filename.match(/^\.{1,2}$/)) { 1726 | if(stat.isDirectory()) { 1727 | await rmdir(filename); 1728 | } else { 1729 | await new Promise(function(resolve, reject) { 1730 | fs.unlink(filename, function(err) { 1731 | if (err) { 1732 | return reject(err); 1733 | } 1734 | resolve(); 1735 | }); 1736 | }); 1737 | } 1738 | } 1739 | } 1740 | fs.rmdir(dir, resolve); 1741 | }); 1742 | }); 1743 | } 1744 | 1745 | 1746 | 1747 | // --------------------------------------------------------------------------------------------------------- 1748 | async function listBranchFiles({dir, branch}) { 1749 | const sha = await git.resolveRef({ dir, ref: `refs/remotes/origin/${branch}` }); 1750 | const { object: { tree } } = await git.readObject({ dir, oid: sha }); 1751 | var list = []; 1752 | return traverseCommit({dir, sha, callback: ({filepath}) => list.push(filepath)}).then(() => list); 1753 | } 1754 | // --------------------------------------------------------------------------------------------------------- 1755 | async function traverseCommit({dir, sha, callback = $.noop}) { 1756 | const { object: { tree } } = await git.readObject({ dir, oid: sha }); 1757 | return await (async function readFiles(oid, path) { 1758 | const { object: { entries } } = await git.readObject({ dir, oid}); 1759 | var i = 0; 1760 | return (async function loop() { 1761 | var entry = entries[i++]; 1762 | if (entry) { 1763 | if (entry.type == 'blob') { 1764 | var filepath = path.concat(entry.path).join('/'); 1765 | await callback({entry, filepath, oid: entry.oid}); 1766 | } else if (entry.type == 'tree' && entry.path !== '.git') { 1767 | await readFiles(entry.oid, path.concat(entry.path)); 1768 | } 1769 | return loop(); 1770 | } 1771 | })(); 1772 | })(tree, []); 1773 | } 1774 | 1775 | // --------------------------------------------------------------------------------------------------------- 1776 | async function gitCommitDiff({dir, newSha, oldSha}) { 1777 | var result = {}; 1778 | function reader(name) { 1779 | return async ({filepath, oid}) => { 1780 | try { 1781 | const { object: pkg } = await git.readObject({ dir, oid }); 1782 | result[filepath] = result[filepath] || {}; 1783 | result[filepath][name] = pkg.toString('utf8'); 1784 | } catch(e) { 1785 | // ignore missing file/object 1786 | } 1787 | }; 1788 | } 1789 | await traverseCommit({dir, sha: oldSha, callback: reader('oldFile')}); 1790 | await traverseCommit({dir, sha: newSha, callback: reader('newFile')}); 1791 | Object.keys(result).forEach(key => { 1792 | var diff = JsDiff.structuredPatch(key, key, result[key].oldFile || '', result[key].newFile || ''); 1793 | if (typeof result[key].oldFile === 'undefined') { 1794 | result[key].added = true; 1795 | } else if (typeof result[key].newFile === 'undefined') { 1796 | result[key].deleted = true; 1797 | } else if (!diff.hunks.length) { 1798 | delete result[key]; 1799 | } 1800 | if (diff.hunks.length) { 1801 | result[key].diff = diff; 1802 | } 1803 | }); 1804 | return result; 1805 | } 1806 | 1807 | // --------------------------------------------------------------------------------------------------------- 1808 | function diffStat(diffs) { 1809 | var modifications = diffs.reduce((acc, {hunks}) => { 1810 | hunks.forEach(function(hunk) { 1811 | hunk.lines.forEach((line) => { 1812 | if (line[0] === '-') { 1813 | acc.minus++; 1814 | } else if (line[0] === '+') { 1815 | acc.plus++; 1816 | } 1817 | }); 1818 | }); 1819 | return acc; 1820 | }, {plus: 0, minus: 0}); 1821 | const plural = n => n == 1 ? '' : 's'; 1822 | var stat = [' ' + diffs.length + ' file' + plural(diffs.length)]; 1823 | if (modifications.plus) { 1824 | stat.push(`${modifications.plus} insertion${plural(modifications.plus)}(+)`); 1825 | } 1826 | if (modifications.minus) { 1827 | stat.push(`${modifications.minus} deletion${plural(modifications.minus)}(-)`); 1828 | } 1829 | return stat.join(', '); 1830 | } 1831 | 1832 | // --------------------------------------------------------------------------------------------------------- 1833 | async function readBranchFile({ dir, filepath, branch }) { 1834 | const ref = 'refs/remotes/origin/' + branch; 1835 | const sha = await git.resolveRef({ dir, ref }); 1836 | const { object: { tree } } = await git.readObject({ dir, oid: sha }); 1837 | return (async function loop(tree, path) { 1838 | if (!path.length) { 1839 | throw new Error(`File ${filepath} not found`); 1840 | } 1841 | var name = path.shift(); 1842 | const { object: { entries } } = await git.readObject({ dir, oid: tree }); 1843 | const packageEntry = entries.find((entry) => { 1844 | return entry.path === name; 1845 | }); 1846 | if (!packageEntry) { 1847 | throw new Error(`File ${filepath} not found`); 1848 | } else { 1849 | if (packageEntry.type == 'blob') { 1850 | const { object: pkg } = await git.readObject({ dir, oid: packageEntry.oid }); 1851 | return pkg.toString('utf8'); 1852 | } else if (packageEntry.type == 'tree') { 1853 | return loop(packageEntry.oid, path); 1854 | } 1855 | } 1856 | })(tree, filepath.split('/')); 1857 | } 1858 | const padleft = (input, n, str) => (new Array(n + 1).join(str || ' ') + input).slice(-n); 1859 | const padright = (input, n, str) => (input + new Array(n + 1).join(str || ' ')).substring(0, n); 1860 | // --------------------------------------------------------------------------------------------------------- 1861 | function date(timestamp, timezoneOffset) { 1862 | timezoneOffset *= -1; 1863 | var d = new Date(timestamp * 1000); 1864 | var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 1865 | var months = [ 1866 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 1867 | 'September', 'October', 'November', 'December' 1868 | ]; 1869 | var day = days[d.getDay()].substring(0, 3); 1870 | var month = months[d.getMonth()].substring(0, 3); 1871 | const pad = (input) => padleft(input, 2, '0'); 1872 | function offset(offset) { 1873 | var timezone = ('0' + (offset / 60).toString().replace('.', '') + '00').slice(-4); 1874 | return (offset > 1 ? '+' : '-') + timezone; 1875 | } 1876 | return [day, month, pad(d.getDate()), 1877 | [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':'), 1878 | d.getFullYear(), 1879 | offset(timezoneOffset) 1880 | ].join(' '); 1881 | } 1882 | 1883 | // --------------------------------------------------------------------------------------------------------- 1884 | function gitroot(cwd) { 1885 | return git.findRoot({filepath: cwd}); 1886 | } 1887 | // --------------------------------------------------------------------------------------------------------- 1888 | async function gitBranch({cwd}) { 1889 | try { 1890 | var dir = await gitroot(cwd); 1891 | return git.currentBranch({dir}); 1892 | } catch(e) { 1893 | } 1894 | } 1895 | // --------------------------------------------------------------------------------------------------------- 1896 | function gitCheckoutFile({dir, filepath, branch}) { 1897 | var fname = dir + '/' + filepath; 1898 | return new Promise(function(resolve, reject) { 1899 | readBranchFile({dir, branch, filepath}).then(oldFile => { 1900 | if (!oldFile) { 1901 | return; 1902 | } 1903 | fs.writeFile(fname, oldFile, err => { 1904 | if (err) { 1905 | reject(err); 1906 | } else { 1907 | resolve(); 1908 | } 1909 | }); 1910 | }); 1911 | }); 1912 | } 1913 | // --------------------------------------------------------------------------------------------------------- 1914 | function gitDiff({dir, filepath, branch}) { 1915 | var fname = dir + '/' + filepath; 1916 | return new Promise(function(resolve, reject) { 1917 | fs.readFile(fname, async function(err, newFile) { 1918 | if (!err) { 1919 | newFile = newFile.toString(); 1920 | } 1921 | readBranchFile({dir, branch, filepath}).then(oldFile => { 1922 | const diff = JsDiff.structuredPatch(filepath, filepath, oldFile || '', newFile || ''); 1923 | resolve(diff); 1924 | }).catch(err => reject(err)); 1925 | }); 1926 | }); 1927 | } 1928 | // --------------------------------------------------------------------------------------------------------- 1929 | async function gitReset({git, dir, ref, branch, hard = false}) { 1930 | var re = /^HEAD~([0-9]+)$/; 1931 | var m = ref.match(re); 1932 | if (m) { 1933 | var count = +m[1]; 1934 | var commits = await git.log({dir, depth: count + 1}); 1935 | return new Promise((resolve, reject) => { 1936 | if (commits.length < count + 1) { 1937 | return reject('Not enough commits'); 1938 | } 1939 | var commit = commits.pop().oid; 1940 | fs.writeFile(`${dir}/.git/refs/heads/${branch}`, commit + '\n', (err) => { 1941 | if (err) { 1942 | return reject(err); 1943 | } 1944 | if (!hard) { 1945 | resolve(); 1946 | } else { 1947 | // clear the index (if any) 1948 | fs.unlink(`${dir}/.git/index`, (err) => { 1949 | if (err) { 1950 | return reject(err); 1951 | } 1952 | // checkout the branch into the working tree 1953 | git.checkout({ dir, ref: branch }).then(resolve); 1954 | }); 1955 | } 1956 | }); 1957 | }); 1958 | } 1959 | return Promise.reject(`Wrong ref ${ref}`); 1960 | } 1961 | 1962 | // --------------------------------------------------------------------------------------------------------- 1963 | function gitURL({dir, gitdir = path.join(dir, '.git'), remote = 'origin'}) { 1964 | return git.config({gitdir, path: `remote.${remote}.url`}); 1965 | } 1966 | 1967 | // --------------------------------------------------------------------------------------------------------- 1968 | async function repoURL({dir, gitdir = path.join(dir, '.git'), remote = 'origin'}) { 1969 | var url = await gitURL({dir, gitdir, remote}); 1970 | return url.replace(/^https:\/\/jcubic.pl\/proxy.php\?/, ''); 1971 | } 1972 | 1973 | // --------------------------------------------------------------------------------------------------------- 1974 | function getHEAD({dir, gitdir, remote = false}) { 1975 | if (!remote) { 1976 | return git.resolveRef({dir, ref: 'HEAD'}); 1977 | } else { 1978 | //return git.resolveRef({dir: 'test', ref: 'refs/remotes/origin/HEAD'}); 1979 | } 1980 | return new Promise((resolve, reject) => { 1981 | var base = `${dir}/${gitdir || '.git'}/`; 1982 | var ref_file = remote ? `${base}refs/remotes/origin/HEAD` : `${base}HEAD`; 1983 | fs.readFile(ref_file, function(err, data) { 1984 | var ref; 1985 | if (err) { 1986 | //return reject(`can't read ${ref_file}: ${err}` ); 1987 | ref = 'refs/remotes/origin/master'; 1988 | } else { 1989 | ref = data.toString().match(/ref: (.*)/)[1]; 1990 | } 1991 | fs.readFile(base + ref, function(err, data) { 1992 | if (err) { 1993 | return reject(`can't read ${base + ref}: ${err}`); 1994 | } 1995 | resolve(data.toString().trim()); 1996 | }); 1997 | }); 1998 | }); 1999 | } 2000 | 2001 | // --------------------------------------------------------------------------------------------------------- 2002 | function mkdir(dir, parent = false) { 2003 | return new Promise(function(resolve, reject) { 2004 | if (parent) { 2005 | dir = dir.split('/'); 2006 | if (!dir.length) { 2007 | return reject('Invalid argument'); 2008 | } 2009 | var full_path = '/'; 2010 | (function loop() { 2011 | if (!dir.length) { 2012 | return resolve(); 2013 | } 2014 | full_path = path.join(full_path, dir.shift()); 2015 | fs.stat(full_path, function(err, stat) { 2016 | if (err) { 2017 | fs.mkdir(full_path, function(err) { 2018 | if (err) { 2019 | reject(err); 2020 | } else { 2021 | loop(); 2022 | } 2023 | }); 2024 | } else if (stat) { 2025 | if (stat.isDirectory()) { 2026 | loop(); 2027 | } else if (stat.isFile()) { 2028 | reject(`${full_path} is a file`); 2029 | } 2030 | } 2031 | }); 2032 | })(); 2033 | } else { 2034 | fs.stat(dir, function(err, stat) { 2035 | if (err) { 2036 | fs.mkdir(dir, function(err) { 2037 | if (err) { 2038 | reject(err); 2039 | } else { 2040 | resolve(); 2041 | } 2042 | }); 2043 | } else if (stat) { 2044 | if (stat.isDirectory()) { 2045 | reject('Directory already exists'); 2046 | } else if (stat.isFile()) { 2047 | reject(`${dir} is a File`); 2048 | } 2049 | } 2050 | }); 2051 | } 2052 | }); 2053 | } 2054 | 2055 | // --------------------------------------------------------------------------------------------------------- 2056 | async function makeZip(dir, filepath) { 2057 | dir = dir.replace(/\/?$/, '/'); 2058 | var zip = new JSZip(); 2059 | var directories = {}; 2060 | var re = new RegExp('^' + dir); 2061 | await traverseDirectory(dir, async function(stat, filepath) { 2062 | var relPath = filepath.replace(re, '').replace(/^\//, ''); 2063 | if (stat == 'directory') { 2064 | if (filepath !== dir) { 2065 | directories[relPath] = zip.folder(relPath); 2066 | } 2067 | } else if (stat == 'file') { 2068 | var parts = relPath.split('/'); 2069 | var filename = parts.pop(); 2070 | var dir = parts.join('/'); 2071 | await new Promise(function(resolve, reject) { 2072 | fs.readFile(filepath, function(err, data) { 2073 | if (err) { 2074 | return reject(err); 2075 | } 2076 | var blob = new Blob([data], { 2077 | type : "text/plain" 2078 | }); 2079 | (directories[dir] || zip).file(filename, data); 2080 | resolve(); 2081 | }); 2082 | }); 2083 | } 2084 | }, {parentFirst: true}); 2085 | return writeZip(zip, filepath); 2086 | } 2087 | 2088 | // --------------------------------------------------------------------------------------------------------- 2089 | function writeZip(zip, filepath) { 2090 | return new Promise(function(resolve, reject) { 2091 | zip.generateAsync({type:"blob"}).then(function(blob) { 2092 | var fileReader = new FileReader(); 2093 | fileReader.onload = function() { 2094 | var arrayBuffer = this.result; 2095 | fs.writeFile(filepath, new buffer.Buffer(arrayBuffer), function(err) { 2096 | if (err) { 2097 | reject(err); 2098 | } else { 2099 | resolve(); 2100 | } 2101 | }); 2102 | }; 2103 | fileReader.readAsArrayBuffer(blob); 2104 | }); 2105 | }); 2106 | } 2107 | 2108 | // --------------------------------------------------------------------------------------------------------- 2109 | function traverseDirectory(dir, callback, options) { 2110 | var settings = Object.assign({}, { 2111 | parentFirst: false 2112 | }, options); 2113 | return new Promise(function(resolve, reject) { 2114 | fs.readdir(dir, async function(err, list) { 2115 | if (err) { 2116 | return reject(err); 2117 | } 2118 | if (settings.parentFirst) { 2119 | await callback('directory', dir); 2120 | } 2121 | for (var i = 0; i < list.length; i++) { 2122 | var filename = path.join(dir, list[i]); 2123 | var stat = await new Promise(function(resolve, reject) { 2124 | fs.stat(filename, function(err, stat) { 2125 | if (err) { 2126 | return reject(err); 2127 | } 2128 | resolve(stat); 2129 | }); 2130 | }); 2131 | if (!filename.match(/^\.{1,2}$/)) { 2132 | if (stat.isDirectory()) { 2133 | await traverseDirectory(filename, callback, options); 2134 | } else { 2135 | await callback('file', filename); 2136 | } 2137 | } 2138 | } 2139 | if (!settings.parentFirst) { 2140 | await callback('directory', dir); 2141 | } 2142 | resolve(); 2143 | }); 2144 | }); 2145 | } 2146 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | /**@license 2 | * ___ ___ _____ __ __ _ _____ _ _ 3 | * / __|_ _|_ _| \ \ / /__| |__ |_ _|__ _ _ _ __ (_)_ _ __ _| | 4 | * | (_ || | | | \ \/\/ / -_) '_ \ | |/ -_) '_| ' \| | ' \/ _` | | 5 | * \___|___| |_| \_/\_/\___|_.__/ |_|\___|_| |_|_|_|_|_||_\__,_|_| 6 | * 7 | * this is service worker and it's part of GIT Web terminal 8 | * 9 | * Copyright (c) 2018 Jakub Jankiewicz 10 | * Released under the MIT license 11 | * 12 | */ 13 | /* global BrowserFS, Response, setTimeout, fetch, Blob, Headers */ 14 | self.importScripts('https://cdn.jsdelivr.net/npm/browserfs'); 15 | 16 | self.addEventListener('install', self.skipWaiting); 17 | 18 | self.addEventListener('activate', self.skipWaiting); 19 | 20 | self.addEventListener('fetch', function (event) { 21 | let path = BrowserFS.BFSRequire('path'); 22 | let fs = new Promise(function(resolve, reject) { 23 | BrowserFS.configure({ fs: 'IndexedDB', options: {} }, function (err) { 24 | if (err) { 25 | reject(err); 26 | } else { 27 | resolve(BrowserFS.BFSRequire('fs')); 28 | } 29 | }); 30 | }); 31 | event.respondWith(fs.then(function(fs) { 32 | return new Promise(function(resolve, reject) { 33 | function sendFile(path) { 34 | fs.readFile(path, function(err, buffer) { 35 | if (err) { 36 | err.fn = 'readFile(' + path + ')'; 37 | return reject(err); 38 | } 39 | var ext = path.replace(/.*\./, ''); 40 | var mime = { 41 | 'html': 'text/html', 42 | 'json': 'application/json', 43 | 'js': 'application/javascript', 44 | 'css': 'text/css' 45 | }; 46 | var headers = new Headers({ 47 | 'Content-Type': mime[ext] 48 | }); 49 | resolve(new Response(buffer, {headers})); 50 | }); 51 | } 52 | var url = event.request.url; 53 | function redirect_dir() { 54 | return resolve(Response.redirect(url + '/', 301)); 55 | } 56 | function serve(path) { 57 | fs.stat(path, function(err, stat) { 58 | if (err) { 59 | return resolve(textResponse(error404Page(path))); 60 | } 61 | if (stat.isFile()) { 62 | sendFile(path); 63 | } else if (stat.isDirectory()) { 64 | if (path.substr(-1, 1) !== '/') { 65 | return redirect_dir(); 66 | } 67 | fs.readdir(path, function(err, list) { 68 | if (err) { 69 | err.fn = 'readdir(' + path + ')'; 70 | return reject(err); 71 | } 72 | var len = list.length; 73 | if (list.includes('index.html')) { 74 | sendFile(path + '/index.html'); 75 | } else { 76 | listDirectory({fs, path, list}).then(function(list) { 77 | resolve(textResponse(fileListingPage(path, list))); 78 | }).catch(reject); 79 | } 80 | }); 81 | } 82 | }); 83 | } 84 | var m = url.match(/__browserfs__(.*)/); 85 | if (m) { 86 | var path = m[1]; 87 | if (path === '') { 88 | return redirect_dir(); 89 | } 90 | console.log('serving ' + path + ' from browserfs'); 91 | serve(path.replace(/\?.*$/, '')); 92 | } else { 93 | if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { 94 | return; 95 | } 96 | //request = credentials: 'include' 97 | fetch(event.request).then(resolve).catch(reject); 98 | } 99 | }); 100 | })); 101 | }); 102 | // ----------------------------------------------------------------------------- 103 | function listDirectory({fs, path, list}) { 104 | return new Promise(function(resolve, reject) { 105 | var items = []; 106 | (function loop() { 107 | var item = list.shift(); 108 | if (!item) { 109 | return resolve(items); 110 | } 111 | fs.stat(path + '/' + item, function(err, stat) { 112 | if (err) { 113 | err.fn = 'stat(' + path + '/' + item + ')'; 114 | return reject(err); 115 | } 116 | items.push(stat.isDirectory() ? item + '/' : item); 117 | loop(); 118 | }); 119 | })(); 120 | }); 121 | } 122 | 123 | // ----------------------------------------------------------------------------- 124 | function textResponse(string, filename) { 125 | var blob = new Blob([string], { 126 | type: 'text/html' 127 | }); 128 | return new Response(blob); 129 | } 130 | 131 | // ----------------------------------------------------------------------------- 132 | function fileListingPage(path, list) { 133 | var output = [ 134 | '', 135 | '', 136 | '', 137 | `

BrowserFS ${path}

`, 138 | '
    ' 139 | ]; 140 | if (path.match(/^\/(.*\/)/)) { 141 | output.push('
  • ..
  • '); 142 | } 143 | list.forEach(function(name) { 144 | output.push('
  • ' + name + '
  • '); 145 | }); 146 | output = output.concat(['
', '', '']); 147 | return output.join('\n'); 148 | } 149 | 150 | // ----------------------------------------------------------------------------- 151 | function error404Page(path) { 152 | var output = [ 153 | '', 154 | '', 155 | '', 156 | '

404 File Not Found

', 157 | `

File ${path} not found in browserfs`, 158 | '', 159 | '' 160 | ]; 161 | return output.join('\n'); 162 | } 163 | -------------------------------------------------------------------------------- /viewer-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | image/svg+xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------