├── LICENSE ├── README.md ├── bin └── gitj ├── img └── gitj.png ├── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Christopher Jeffrey (https://github.com/chjj/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitj 2 | 3 | `gitk` in your terminal. Similar to `tig`. 4 | 5 | ![](http://i.imgur.com/tEH5l0I.png) 6 | 7 | The code is slightly messy right now. 8 | 9 | ## Example Usage 10 | 11 | ``` bash 12 | cd ~/my-repo 13 | gitj 14 | ``` 15 | 16 | ## License 17 | 18 | Copyright (c) 2013, Christopher Jeffrey. (MIT License) 19 | 20 | See LICENSE for more info. 21 | -------------------------------------------------------------------------------- /bin/gitj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * gitj 5 | */ 6 | 7 | process.title = 'gitj'; 8 | 9 | /** 10 | * Modules 11 | */ 12 | 13 | var child_process = require('child_process') 14 | , spawn = child_process.spawn 15 | , exec = child_process.execFile 16 | , path = require('path') 17 | , fs = require('fs'); 18 | 19 | /** 20 | * Log Format 21 | */ 22 | 23 | var FORMAT = 'format:commit %H%nParents: %P%nAuthor: %an <%ae>%nAuthorDate: %ad%nCommit: %cn <%ce>%nCommitDate: %cd%nRefs:%d%n%n%B%n'; 24 | var SHOWFORMAT = 'format:%C(yellow)commit %H%Creset%nParents: %P%nAuthor: %an <%ae>%nAuthorDate: %ad%nCommit: %cn <%ce>%nCommitDate: %cd%nRefs:%d%n%n%B%n'; 25 | 26 | /** 27 | * Program 28 | */ 29 | 30 | var Program = require('blessed') 31 | , p; 32 | 33 | function parseLogs(text) { 34 | function grab(log, regex) { 35 | var m = regex.exec(log); 36 | if (!m) return ''; 37 | return m[1].trim(); 38 | } 39 | 40 | // text = text.replace(/\x1b\[\d*(;\d*)*m/g, ''); 41 | 42 | return text 43 | .replace(/[ \t]+$/gm, '') 44 | .split(/(?=^([|*\/\\] *)*commit [a-f0-9]{40})/m) 45 | .map(function(log) { 46 | log = log.trim(); 47 | 48 | // TODO: Parse this. 49 | // log = log.replace(/^([|*\/\\] ?)* */gm, ''); 50 | // log = log.replace(/^([|*\/\\] *)*/gm, ''); 51 | log = log.replace(/^([|*\/\\] *)* */gm, ''); 52 | 53 | if (!log) return null; 54 | 55 | log = { 56 | id: grab(log, /^commit ([a-f0-9]{40})\n/), 57 | parents: grab(log, /\Parents:[ \t]*([^\n]*)/), 58 | author: grab(log, /\nAuthor:[ \t]+([^\n]+)/), 59 | authorDate: grab(log, /\nAuthorDate:[ \t]+([^\n]+)/), 60 | committer: grab(log, /\nCommit:[ \t]+([^\n]+)/), 61 | commitDate: grab(log, /\nCommitDate:[ \t]+([^\n]+)/), 62 | refs: grab(log, /\nRefs:[ \t]*([^\n]*)/), 63 | message: log.split('\n\n').slice(1).join('\n\n').trim() 64 | }; 65 | 66 | log.short = log.id.slice(0, 7); 67 | log.committerName = log.committer.split(' <')[0]; 68 | log.committerEmail = (log.committer.split(' <')[1] || '').slice(0, -1); 69 | log.authorName = log.author.split(' <')[0]; 70 | log.authorEmail = (log.author.split(' <')[1] || '').slice(0, -1); 71 | 72 | if (log.parents.trim()) { 73 | log.parents = log.parents.trim().split(/\s+/); 74 | if (log.parents.length > 1) { 75 | log.merge = true; 76 | } 77 | } else { 78 | log.parents = []; 79 | } 80 | 81 | if (log.refs === '()') log.refs = ''; 82 | 83 | return log; 84 | }) 85 | .filter(Boolean); 86 | } 87 | 88 | function getLogs(repo, branch, callback) { 89 | if (!callback) { 90 | callback = branch; 91 | branch = null; 92 | } 93 | 94 | var args = ['log', '--graph', '--format=' + FORMAT]; 95 | 96 | if (branch) args.push(branch); 97 | 98 | return exec('git', args, { 99 | cwd: repo, 100 | stdio: ['ignore', 'pipe', 'ignore'], 101 | env: process.env, 102 | maxBuffer: Infinity 103 | }, function(err, stdout, stderr) { 104 | if (err) return callback(err); 105 | if (stderr) return callback(new Error(stderr)); 106 | return callback(null, parseLogs(stdout)); 107 | }); 108 | } 109 | 110 | function getDirty(repo, callback) { 111 | var ps = spawn('git', ['diff', '--no-ext-diff', '--quiet', '--exit-code', 'HEAD'], { 112 | cwd: repo, 113 | stdio: ['ignore', 'ignore', 'ignore'], 114 | env: process.env 115 | }); 116 | 117 | ps.on('exit', function(code) { 118 | return callback(null, code !== 0); 119 | }); 120 | 121 | return ps; 122 | } 123 | 124 | function getStash(repo, callback) { 125 | var ps = spawn('git', ['rev-parse', '--verify', 'refs/stash'], { 126 | cwd: repo, 127 | stdio: ['ignore', 'ignore', 'ignore'], 128 | env: process.env 129 | }); 130 | 131 | ps.on('exit', function(code) { 132 | return callback(null, code === 0); 133 | }); 134 | 135 | return ps; 136 | } 137 | 138 | function parseBranches(data) { 139 | var current; 140 | data = data.trim().split(/\n+/).map(function(name) { 141 | name = name.trim(); 142 | var sel = name[0] === '*'; 143 | if (sel) { 144 | name = name.replace(/^\*\s*/, ''); 145 | current = name; 146 | } 147 | return name; 148 | }); 149 | return { 150 | current: current, 151 | names: data 152 | }; 153 | } 154 | 155 | function getBranches(repo, callback) { 156 | return exec('git', ['branch'], { 157 | cwd: repo, 158 | stdio: ['ignore', 'pipe', 'ignore'], 159 | env: process.env 160 | }, function(err, stdout, stderr) { 161 | if (err) return callback(err); 162 | if (stderr) return callback(new Error(stderr)); 163 | return callback(null, parseBranches(stdout)); 164 | }); 165 | } 166 | 167 | function showCommit(repo, log, file, callback) { 168 | if (!callback) { 169 | callback = file; 170 | file = null; 171 | } 172 | 173 | if (typeof log !== 'object') { 174 | log = { id: log }; 175 | } 176 | 177 | var id = log.id; 178 | 179 | if (file) id += ':' + file; 180 | 181 | var action = id === 'HEAD' || log.merge 182 | ? 'diff' 183 | : 'show'; 184 | 185 | var cmd = 'git ' 186 | + action 187 | + ' ' 188 | + (log.merge ? log.parents[0] + ' ' + id : quote(id)) 189 | + (p.whitespace ? ' -w' : '') 190 | + (p.context ? ' --unified=' + quote(p.context) : '') 191 | + (p.patience ? ' --patience' : '') 192 | + (p.minimal ? ' --minimal' : '') 193 | + (p.word ? ' --word-diff=plain' : '') 194 | + (action === 'show' ? ' --format="' + SHOWFORMAT + '"' : '') 195 | + ' --patch-with-stat' 196 | + ' --no-ext-diff' 197 | + ' -M90' 198 | + ' --color=always | less -c -R'; 199 | 200 | var ps = spawn('sh', ['-c', cmd], { 201 | cwd: repo, 202 | customFds: [0, 1, 2], 203 | env: process.env 204 | }); 205 | 206 | ps.on('exit', function(code) { 207 | if (code !== 0) { 208 | callback(new Error('Exit code: ' + code)) 209 | callback = function() {}; 210 | } 211 | }); 212 | 213 | ps.on('close', function() { 214 | return callback(); 215 | }); 216 | 217 | return ps; 218 | } 219 | 220 | function editCommit(repo, id, callback) { 221 | if (!id) return callback(); 222 | 223 | var ps = spawn('git', ['rebase', '-i', id], { 224 | cwd: repo, 225 | customFds: [0, 1, 2], 226 | env: process.env 227 | }); 228 | 229 | ps.on('exit', function(code) { 230 | if (code !== 0) { 231 | callback(new Error('Exit code: ' + code)) 232 | callback = function() {}; 233 | } 234 | }); 235 | 236 | ps.on('close', function() { 237 | return callback(); 238 | }); 239 | 240 | return ps; 241 | } 242 | 243 | function quote(text) { 244 | text = (text + '') 245 | .replace(/([\\"$])/g, '\\$1') 246 | .replace(/\r/g, '') 247 | .replace(/\n/g, '\\n'); 248 | 249 | return '"' + text + '"'; 250 | } 251 | 252 | function clean(text) { 253 | return /^([^\r\n]*)/.exec(text)[1].trim().replace(/\x1b+/g, ' '); 254 | } 255 | 256 | function render(opt, callback) { 257 | var logs = opt.logs 258 | , branches = opt.branches; 259 | 260 | if (opt.dirty) { 261 | logs.unshift({ 262 | id: 'HEAD', 263 | short: 'HEAD ', 264 | message: 'Local uncommitted changes' 265 | }); 266 | } 267 | 268 | p = new Program; 269 | 270 | if (!p.output.isTTY) { 271 | console.log(logs); 272 | return process.exit(0); 273 | } 274 | 275 | p.setTitle('gitj'); 276 | 277 | p.offset = 0; 278 | p._index = 1; 279 | p.statusbar = opt.status ? 2 : 0; 280 | 281 | p.alternateBuffer(); 282 | p.enableMouse(); 283 | 284 | p.on('mouse', function(key) { 285 | if (key.action === 'wheelup') { 286 | return onkey(null, { name: 'u' }); 287 | } 288 | if (key.action === 'wheeldown') { 289 | return onkey(null, { name: 'd' }); 290 | } 291 | if (key.action === 'mousedown' && key.button === 'left') { 292 | if (p.statusbar && key.y === p.rows) { 293 | if (p._search) return onkey('\x1b', { name: 'escape' }); 294 | return onkey('/', { name: 'slash' }); 295 | } 296 | return p._index === key.y 297 | ? onkey(null, { name: 'enter' }) 298 | : scroll(key.y - p._index); 299 | } 300 | }); 301 | 302 | function pause() { 303 | p._paused = true; 304 | p.clear(); 305 | p.move(1, 1); 306 | p.showCursor(); 307 | p.disableMouse(); 308 | p.input.pause(); 309 | p.normalBuffer(); 310 | } 311 | 312 | function resume() { 313 | p._paused = false; 314 | p.alternateBuffer(); 315 | p.input.resume(); 316 | p.enableMouse(); 317 | p.clear(); 318 | p.hideCursor(); 319 | renderList(); 320 | } 321 | 322 | function onkey(ch, key) { 323 | if (p._needsClear) { 324 | p._needsClear = false; 325 | p.move(1, p.rows); 326 | p.eraseInLine('right'); 327 | renderList(); 328 | } 329 | 330 | if (key.name === 'mouse') { 331 | return; 332 | } 333 | 334 | if (p._search) { 335 | if (key.name === 'enter') { 336 | p.eraseInLine('left'); 337 | var search = p._search; 338 | delete p._search; 339 | p._lastSearch = search; 340 | 341 | var matcher = /^[0-9a-f]{7,}$/.test(search.data) 342 | ? function(log) { return log.id.indexOf(search.data) === 0; } 343 | : function(log) { 344 | return ~log.message.toLowerCase().indexOf(search.data.toLowerCase()); 345 | } 346 | 347 | search.logs = !search.reverse 348 | ? logs.slice(p.offset + p._index - 1 + 1) 349 | : logs.slice(0, p.offset + p._index - 1) 350 | 351 | if (search.reverse) { 352 | var i = search.logs.length; 353 | while (i--) { 354 | var log = search.logs[i]; 355 | if (matcher(log)) { 356 | p.offset = i; 357 | p._index = 1; 358 | break; 359 | } 360 | } 361 | } else { 362 | for (var i = 0; i < search.logs.length; i++) { 363 | var log = search.logs[i]; 364 | if (matcher(log)) { 365 | p.offset += p._index + i; 366 | p._index = 1; 367 | break; 368 | } 369 | } 370 | } 371 | p.hideCursor(); 372 | return renderList(); 373 | } 374 | 375 | if (key.name === 'backspace') { 376 | if (!p._search.data.length) { 377 | p.eraseInLine('left'); 378 | return onkey(null, {name:'escape'}); 379 | } 380 | p._search.data = p._search.data.slice(0, -1); 381 | p.back(); 382 | p.deleteChars(); 383 | return; 384 | } 385 | 386 | if (key.name === 'escape') { 387 | p._search.canceled = true; 388 | p._lastSearch = p._search; 389 | delete p._search; 390 | p.hideCursor(); 391 | p.eraseInLine('left'); 392 | return renderList(); 393 | } 394 | 395 | if (ch) { 396 | p._search.data += ch; 397 | return p.write(ch); 398 | } 399 | 400 | return; 401 | } 402 | 403 | if (p._enter) { 404 | if (key.name === 'enter') { 405 | p.hideCursor(); 406 | p.eraseInLine('left'); 407 | 408 | var enter = p._enter; 409 | p._lastEnter = enter; 410 | delete p._enter; 411 | 412 | var parts = enter.data.split(' ') 413 | , cmd = parts[0] 414 | , val = parts.slice(1).join(' '); 415 | 416 | if (cmd === 'w' || cmd === 'space' || cmd === 'whitespace') { 417 | p.whitespace = !p.whitespace; 418 | return p.whitespace 419 | ? echo('Ignoring whitespace.') 420 | : echo('Not ignoring whitespace.'); 421 | } 422 | 423 | if (cmd === 'c' || cmd === 'context') { 424 | p.context = val; 425 | return echo(val + ' lines of context.'); 426 | } 427 | 428 | if (cmd === 'p' || cmd === 'patience') { 429 | p.patience = !p.patience; 430 | return p.patience 431 | ? echo('Patience enabled.') 432 | : echo('Patience disabled.'); 433 | } 434 | 435 | if (cmd === 'm' || cmd === 'minimal') { 436 | p.minimal = !p.minimal; 437 | return p.minimal 438 | ? echo('Minimal enabled.') 439 | : echo('Minimal disabled.'); 440 | } 441 | 442 | if (cmd === 'word') { 443 | p.word = !p.word; 444 | return p.word 445 | ? echo('Word diff enabled.') 446 | : echo('Word diff disabled.'); 447 | } 448 | 449 | if (cmd === 'echo') { 450 | return echo(val); 451 | } 452 | 453 | if (cmd === 'status') { 454 | p.statusbar = p.statusbar ? 0 : 2; 455 | return updateStatusbar(); 456 | } 457 | 458 | if (cmd === 'branch') { 459 | return echo(branches.names.join(', ')); 460 | } 461 | 462 | if (cmd === 'q') { 463 | return exit(); 464 | } 465 | 466 | if (cmd === 'cd') { 467 | opt.repo = path.resolve(opt.repo, val); 468 | pause(); 469 | return getLogs(opt.repo, function(err, log) { 470 | if (!err) { 471 | logs = opt.logs = log; 472 | } 473 | return resume(); 474 | }); 475 | } 476 | 477 | if (cmd === 'e' || cmd === 'edit') { 478 | var parent = logs[p.offset + (p._index - 1) + 1] || 0; 479 | pause(); 480 | return editCommit(opt.repo, parent.id, function() { 481 | return resume(); 482 | }); 483 | } 484 | 485 | if (cmd === 'show' || cmd === 'file') { 486 | pause(); 487 | return showCommit(opt.repo, enter.log, val, function() { 488 | return resume(); 489 | }); 490 | } 491 | 492 | if (cmd === 'checkout' || cmd === 'view') { 493 | pause(); 494 | return getLogs(opt.repo, val, function(err, log) { 495 | if (!err) { 496 | logs = opt.logs = log; 497 | branches.current = val; 498 | p._index = 1; 499 | p.offset = 0; 500 | } 501 | return resume(); 502 | }); 503 | } 504 | 505 | return; 506 | } 507 | 508 | if (key.name === 'backspace') { 509 | if (!p._enter.data.length) { 510 | p.eraseInLine('left'); 511 | return onkey(null, {name:'escape'}); 512 | } 513 | p._enter.data = p._enter.data.slice(0, -1); 514 | p.back(); 515 | p.deleteChars(); 516 | return; 517 | } 518 | 519 | if (key.name === 'escape') { 520 | p._enter.canceled = true; 521 | p._lastEnter = p._enter; 522 | delete p._enter; 523 | p.hideCursor(); 524 | p.eraseInLine('left'); 525 | return renderList(); 526 | } 527 | 528 | if (ch) { 529 | p._enter.data += ch; 530 | p.write(ch); 531 | return; 532 | } 533 | 534 | return; 535 | } 536 | 537 | if (key.name === 'down' || key.name === 'j') { 538 | return scroll(1); 539 | } 540 | 541 | if (key.name === 'up' || key.name === 'k') { 542 | return scroll(-1); 543 | } 544 | 545 | if (key.name === 'd') { 546 | return scroll((p.rows - p.statusbar) / 2 | 0); 547 | } 548 | 549 | if (key.name === 'u') { 550 | return scroll(-((p.rows - p.statusbar) / 2 | 0)); 551 | } 552 | 553 | if (key.name === 'f') { 554 | return scroll(p.rows - p.statusbar); 555 | } 556 | 557 | if (key.name === 'b') { 558 | return scroll(-(p.rows - p.statusbar)); 559 | } 560 | 561 | if (ch === 'H') { 562 | return scroll(1 - p._index); 563 | } 564 | 565 | if (ch === 'M') { 566 | return scroll((Math.min(p.rows - p.statusbar, logs.length) / 2 | 0) - p._index); 567 | } 568 | 569 | if (ch === 'L') { 570 | return scroll(Math.min(p.rows - p.statusbar, logs.length) - p._index); 571 | } 572 | 573 | if (ch === 'g') { 574 | return scroll(-Math.max(logs.length, p.rows)); 575 | } 576 | 577 | if (ch === 'G') { 578 | return scroll(Math.max(logs.length, p.rows)); 579 | } 580 | 581 | if (ch === '/' || ch === '?') { 582 | p.move(1, p.rows); 583 | p.eraseInLine('right'); 584 | p.showCursor(); 585 | p.write('Search: '); 586 | p._search = { 587 | data: '', 588 | reverse: ch === '?' 589 | }; 590 | return; 591 | } 592 | 593 | if (ch === 'n' && p._lastSearch) { 594 | p._search = p._lastSearch; 595 | p._search.reverse = false; 596 | onkey(null, {name:'enter'}); 597 | return; 598 | } 599 | 600 | if (ch === 'N' && p._lastSearch) { 601 | p._search = p._lastSearch; 602 | p._search.reverse = true; 603 | onkey(null, {name:'enter'}); 604 | return; 605 | } 606 | 607 | if ((key.ctrl && key.name === 'c') || key.name === 'q') { 608 | return exit(); 609 | } 610 | 611 | if (key.name === 'right' || key.name === 'l' || key.name === 'enter') { 612 | pause(); 613 | return showCommit(opt.repo, logs[p.offset + p._index - 1], function() { 614 | return resume(); 615 | }); 616 | } 617 | 618 | if (key.name === 's' || ch === ':') { 619 | p.move(1, p.rows); 620 | p.eraseInLine('right'); 621 | p.showCursor(); 622 | p.write(key.name === 's' ? 'Show File: ' : ':'); 623 | p._enter = { 624 | id: logs[p.offset + p._index - 1].id, 625 | log: logs[p.offset + p._index - 1], 626 | data: key.name === 's' ? 'show ' : '' 627 | }; 628 | return; 629 | } 630 | } 631 | 632 | function echo(text) { 633 | p.move(1, p.rows); 634 | p.eraseInLine('right'); 635 | p.echo(text); 636 | p._needsClear = true; 637 | } 638 | 639 | function exit() { 640 | p.disableMouse(); 641 | p.clear(); 642 | p.showCursor(); 643 | p.normalBuffer(); 644 | return process.exit(0); 645 | } 646 | 647 | function scroll(i) { 648 | if (!i) return; 649 | 650 | if (p._search || p._enter) { 651 | onkey('\x1b', { name: 'escape' }); 652 | } 653 | 654 | if (i > 0) { 655 | if (p.offset + p._index === logs.length) return; 656 | } else if (i < 0) { 657 | if (p.offset + p._index === 1) return; 658 | } 659 | 660 | if (p._index + i > p.rows - p.statusbar) { 661 | // NOTE: Start using insertLines so 662 | // scrollRegion can be used. 663 | p._index += i; 664 | var r = p._index - (p.rows - p.statusbar); 665 | p._index = p.rows - p.statusbar; 666 | p.offset += r; 667 | if (!logs[p.offset + p._index - 1]) { 668 | p.offset = Math.max(p.rows, logs.length) - p.rows; 669 | if (p.rows <= logs.length) { 670 | p.offset += p.statusbar; 671 | } 672 | p._index = Math.min(p.rows - p.statusbar, logs.length); 673 | } 674 | return renderList(); 675 | } else if (p._index + i < 1) { 676 | p._index += i; 677 | p.offset += p._index - 1; 678 | if (p.offset < 0) p.offset = 0; 679 | p._index = 1; 680 | return renderList(); 681 | } else { 682 | if (p.offset + (p._index - 1) + i > (logs.length - 1)) { 683 | i = (logs.length - 1) - (p.offset + (p._index - 1)); 684 | } 685 | p.move(2, p._index); 686 | p.write(logs[p.offset + p._index - 1].short, 'red fg'); 687 | p._index += i; 688 | p.move(2, p._index); 689 | p.write(logs[p.offset + p._index - 1].short, 'blue bg'); 690 | p.move(1, p._index); 691 | updateStatusbar(); 692 | return; 693 | } 694 | } 695 | 696 | function updateStatusbar() { 697 | if (!p.statusbar) return; 698 | 699 | // Draw first line 700 | var out = Array(9 + 1).join('─'); 701 | out += '┴'; 702 | out += Array(p.cols - 10 + 1).join('─'); 703 | p.move(1, p.rows - 1); 704 | p.write(out); 705 | 706 | var selected = logs[p.offset + p._index - 1] 707 | , left 708 | , right; 709 | 710 | left = selected.id 711 | + (selected.authorDate ? ' - ' + selected.authorDate : '') 712 | 713 | right = 714 | (p.offset + p._index) + '/' + logs.length 715 | + ' [' 716 | + branches.current 717 | + ']' 718 | + (opt.stash ? ' $' : '') 719 | + (opt.dirty ? ' *' : ''); 720 | 721 | right = right.substring(0, p.cols); 722 | left = left.substring(0, p.cols - right.length); 723 | 724 | p.move(1, p.rows); 725 | p.bg('black'); 726 | 727 | p.write(left); 728 | 729 | // NOTE: insertChars doesn't work on screen. 730 | // p.insertChars(p.cols); 731 | p.write(Array(p.cols - left.length - right.length + 1).join(' ')); 732 | 733 | p.write(right, 'cyan fg'); 734 | 735 | p.bg('!black'); 736 | } 737 | 738 | function renderList(from) { 739 | from = from || 1; 740 | p.move(1, from); 741 | 742 | var visible = logs.slice(p.offset + (from - 1), p.offset + p.rows - p.statusbar); 743 | 744 | visible.forEach(function(log, i) { 745 | var date = new Date(log.authorDate) 746 | , msg = clean(log.message || '') 747 | , email = log.authorEmail || '' 748 | , refs = log.refs || '' 749 | , j = i + (from - 1); 750 | 751 | date = date.toString() !== 'Invalid Date' 752 | ? date.toISOString().split('T')[0] 753 | : ''; 754 | 755 | msg = msg.slice(0, p.cols - 17 - date.length - email.length - refs.length - 2); 756 | 757 | p.write(' '); 758 | 759 | // commit id 760 | p.write(log.short, j === p._index - 1 ? 'blue bg' : 'red fg'); 761 | 762 | // separator 763 | p.write(' │ '); 764 | 765 | // refs 766 | if (refs) { 767 | p.write(refs + ' ', 'yellow fg'); 768 | } 769 | 770 | // message 771 | p.write(msg); 772 | 773 | // date 774 | if (date) { 775 | p.write(' (' + date + ')', 'green fg'); 776 | } 777 | 778 | // email 779 | if (email) { 780 | p.write(' <' + email + '>', 'blue fg'); 781 | } 782 | 783 | p.eraseInLine('right'); 784 | 785 | if (i !== visible.length - 1) { 786 | p.feed(); 787 | } 788 | }); 789 | 790 | // This is needed for searching and getting 791 | // an item at index 1 even when there aren't 792 | // enough items to fill the entire screen. 793 | var i = from + visible.length - 1; 794 | while (i++ < p.rows - p.statusbar) { 795 | p.move(1, i); 796 | p.eraseInLine('right'); 797 | p.write(' │'); 798 | } 799 | 800 | updateStatusbar(); 801 | 802 | p.move(1, p._index); 803 | } 804 | 805 | p.on('keypress', onkey); 806 | 807 | p.on('resize', function() { 808 | if (p._paused) return; 809 | if (p._index > p.rows - p.statusbar) { 810 | p._index = p.rows - p.statusbar; 811 | } 812 | renderList(); 813 | }); 814 | 815 | p.clear(); 816 | p.hideCursor(); 817 | renderList(); 818 | } 819 | 820 | function parseArg(argv) { 821 | var argv = argv.slice(2) 822 | , options = {} 823 | , files = []; 824 | 825 | function getarg() { 826 | var arg = argv.shift(); 827 | 828 | if (arg.indexOf('--') === 0) { 829 | // e.g. --opt 830 | arg = arg.split('='); 831 | if (arg.length > 1) { 832 | // e.g. --opt=val 833 | argv.unshift(arg.slice(1).join('=')); 834 | } 835 | arg = arg[0]; 836 | } else if (arg[0] === '-') { 837 | if (arg.length > 2) { 838 | // e.g. -abc 839 | argv = arg.substring(1).split('').map(function(ch) { 840 | return '-' + ch; 841 | }).concat(argv); 842 | arg = argv.shift(); 843 | } else { 844 | // e.g. -a 845 | } 846 | } else { 847 | // e.g. foo 848 | } 849 | 850 | return arg; 851 | } 852 | 853 | while (argv.length) { 854 | arg = getarg(); 855 | switch (arg) { 856 | case '-r': 857 | case '--repo': 858 | options.repo = argv.shift(); 859 | break; 860 | case '-s': 861 | case '--status': 862 | options.status = true; 863 | break; 864 | case '-n': 865 | case '--no-status': 866 | options.status = false; 867 | break; 868 | case '-h': 869 | case '--help': 870 | return help(); 871 | default: 872 | if (!options.repo 873 | && fs.existsSync(arg) 874 | && fs.statSync(arg).isDirectory()) { 875 | options.repo = arg; 876 | } else { 877 | files.push(arg); 878 | } 879 | break; 880 | } 881 | } 882 | 883 | options.repo = path.resolve(process.cwd(), options.repo || '.'); 884 | 885 | if (options.status == null) { 886 | options.status = true; 887 | } 888 | 889 | return options; 890 | } 891 | 892 | var opt = parseArg(process.argv); 893 | 894 | function help() { 895 | console.log('todo'); 896 | return process.exit(0); 897 | } 898 | 899 | function poll(options, callback) { 900 | options = options || {}; 901 | options.repo = options.repo || opt.repo; 902 | callback = callback || function() {}; 903 | 904 | function getLog(callback) { 905 | return options.logs 906 | ? getLogs(options.repo, callback) 907 | : callback(); 908 | } 909 | 910 | return getLog(function(err, logs) { 911 | if (err) return callback(err); 912 | return getBranches(options.repo, function(err, branches) { 913 | if (err) return callback(err); 914 | return getDirty(options.repo, function(err, dirty) { 915 | if (err) return callback(err); 916 | return getStash(options.repo, function(err, stash) { 917 | if (err) return callback(err); 918 | 919 | if (options.update !== false) { 920 | opt.logs = logs || opt.logs; 921 | opt.branches = branches; 922 | opt.dirty = dirty; 923 | opt.stash = stash; 924 | } 925 | 926 | return callback(null, { 927 | logs: logs || opt.logs, 928 | branches: branches, 929 | dirty: dirty, 930 | stash: stash 931 | }); 932 | }); 933 | }); 934 | }); 935 | }); 936 | } 937 | 938 | function beginPoll() { 939 | var i = 0; 940 | (function self() { 941 | var options = {}; 942 | if (++i === 6) { 943 | options.logs = true; 944 | i = 0; 945 | } 946 | return poll(options, function() { 947 | return setTimeout(self, 10 * 1000); 948 | }); 949 | })(); 950 | } 951 | 952 | function main(argv, callback) { 953 | beginPoll(); 954 | return poll({ logs: true }, function(err) { 955 | if (err) return callback(err); 956 | return render(opt, function(err) { 957 | if (err) return callback(err); 958 | return callback(); 959 | }); 960 | }); 961 | } 962 | 963 | if (!module.parent) { 964 | main(process.argv.slice(), function(err) { 965 | if (err) throw err; 966 | return process.exit(0); 967 | }); 968 | } else { 969 | module.exports = main; 970 | } 971 | -------------------------------------------------------------------------------- /img/gitj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chjj/gitj/fbb9be81b97e4094bea4bab0a1bec6e33457a15a/img/gitj.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./bin/gitj'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitj", 3 | "description": "gitk in your terminal", 4 | "author": "Christopher Jeffrey", 5 | "version": "0.0.6", 6 | "main": "./bin/gitj", 7 | "bin": "./bin/gitj", 8 | "man": "./man/gitj.1", 9 | "preferGlobal": true, 10 | "repository": "git://github.com/chjj/gitj.git", 11 | "homepage": "https://github.com/chjj/gitj", 12 | "bugs": { "url": "http://github.com/chjj/gitj/issues" }, 13 | "keywords": ["git", "gitk"], 14 | "tags": ["git", "gitk"], 15 | "dependencies": { 16 | "blessed": "0.0.6" 17 | } 18 | } 19 | --------------------------------------------------------------------------------