├── .gitignore ├── README.md ├── backend.js ├── components ├── gerrit │ ├── gerrit.html │ └── gerrit.js └── repository │ └── repository.js ├── gerrit.js ├── package.json └── ungit-plugin.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gerrit plugin for Ungit 2 | ======================= 3 | Adds a [gerrit](https://code.google.com/p/gerrit/) integration panel to Ungit. 4 | 5 | Installation 6 | ------------ 7 | 1. Go to your ungit plugin dir (defaults to ~/.ungit/plugins, create it if it doesn't exsist). 8 | 2. `git clone https://github.com/FredrikNoren/ungit-gerrit.git` 9 | 3. `npm install` in the ungit-gerrit folder 10 | 4. Restart Ungit 11 | 12 | Configuration 13 | ------------- 14 | In your `.ungitrc`: 15 | 16 | { 17 | "pluginConfigs": { 18 | "gerrit": { 19 | 20 | // Ssh username. Defaults to what the repository is configured with, or the currently logged in user. 21 | "sshUsername": undefined, 22 | 23 | // Ssh agent. Defaults to pageant on Windows and SSH_AUTH_SOCK on Unix. 24 | sshAgent: undefined, 25 | 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var gerrit = require('./gerrit'); 4 | var os = require('os'); 5 | var child_process = require('child_process'); 6 | var path = require('path'); 7 | 8 | 9 | function ensurePathExists(req, res, next) { 10 | var path = req.param('path'); 11 | if (!fs.existsSync(path)) { 12 | res.json(400, { error: 'No such path: ' + path, errorCode: 'no-such-path' }); 13 | } else { 14 | next(); 15 | } 16 | } 17 | 18 | exports.install = function(env) { 19 | var app = env.app; 20 | var ensureAuthenticated = env.ensureAuthenticated; 21 | var git = env.git; 22 | 23 | app.get(env.httpPath + '/commithook', ensureAuthenticated, ensurePathExists, function(req, res) { 24 | var repoPath = req.param('path'); 25 | var hookPath = path.join(repoPath, '.git', 'hooks', 'commit-msg'); 26 | if (fs.existsSync(hookPath)) res.json({ exists: true }); 27 | else res.json({ exists: false }); 28 | }); 29 | 30 | app.post(env.httpPath + '/commithook', ensureAuthenticated, ensurePathExists, function(req, res) { 31 | var repoPath = req.param('path'); 32 | git.getRemoteAddress(repoPath, 'origin') 33 | .fail(function(err) { 34 | res.json(400, err); 35 | }) 36 | .done(function(remote) { 37 | if (!remote.host) throw new Error("Failed to parse host from: " + remote.address); 38 | var command = 'scp -p '; 39 | if (remote.port) command += ' -P ' + remote.port + ' '; 40 | command += remote.host + ':hooks/commit-msg .git/hooks/'; 41 | var hooksPath = path.join(repoPath, '.git', 'hooks'); 42 | if (!fs.existsSync(hooksPath)) fs.mkdirSync(hooksPath); 43 | child_process.exec(command, { cwd: repoPath }, 44 | function (err, stdout, stderr) { 45 | if (err) return res.json(400, { error: err, stdout: stdout, stderr: stderr }); 46 | res.json({}); 47 | }); 48 | }); 49 | }); 50 | 51 | app.get(env.httpPath + '/changes', ensureAuthenticated, ensurePathExists, function(req, res) { 52 | var repoPath = req.param('path'); 53 | git.getRemoteAddress(repoPath, 'origin') 54 | .fail(function(err) { 55 | res.json(400, err); 56 | }) 57 | .done(function(remote) { 58 | if (!remote.host) throw new Error("Failed to parse host from: " + remote.address); 59 | var command = 'query --format=JSON --current-patch-set status:open project:' + remote.project + ''; 60 | var sshConfig = { 61 | host: remote.host, 62 | port: remote.port, 63 | username: remote.username || env.pluginConfig.sshUsername, 64 | agent: env.pluginConfig.sshAgent, 65 | } 66 | if (!sshConfig.agent) { 67 | if (os.type() == 'Windows_NT') sshConfig.agent = 'pageant'; 68 | else sshConfig.agent = '' + process.env.SSH_AUTH_SOCK; 69 | } 70 | gerrit(sshConfig, command, res, function(err, result) { 71 | if (err || result.indexOf('Invalid command') != -1) 72 | return res.json(400, err || { error: result }); 73 | result = result.split('\n').filter(function(r) { return r.trim(); }); 74 | result = result.map(function(r) { return JSON.parse(r); }); 75 | res.json(result); 76 | }); 77 | }); 78 | }); 79 | 80 | } -------------------------------------------------------------------------------- /components/gerrit/gerrit.html: -------------------------------------------------------------------------------- 1 | 2 |
46 | -------------------------------------------------------------------------------- /components/gerrit/gerrit.js: -------------------------------------------------------------------------------- 1 | 2 | var ko = require('knockout'); 3 | var _ = require('lodash'); 4 | 5 | 6 | var components = require('ungit-components'); 7 | 8 | components.register('gerrit', function(args) { 9 | return new GerritIntegrationViewModel(args.repositoryViewModel); 10 | }); 11 | 12 | var GerritIntegrationViewModel = function(repository) { 13 | this.repository = repository; 14 | this.server = repository.server; 15 | this.showInitCommmitHook = ko.observable(false); 16 | this.status = ko.observable('loading'); 17 | this.initGerritHookProgressBar = components.create('progressBar', { 18 | predictionMemoryKey: 'gerrit-init-hook-' + repository.repoPath, 19 | fallbackPredictedTimeMs: 4000, 20 | temporary: true 21 | }); 22 | this.changesLoader = components.create('progressBar', { 23 | predictionMemoryKey: 'gerrit-changes-' + repository.repoPath, 24 | fallbackPredictedTimeMs: 4000 25 | }); 26 | this.pushingProgressBar = components.create('progressBar', { 27 | predictionMemoryKey: 'gerrit-push-' + repository.repoPath, 28 | fallbackPredictedTimeMs: 4000, 29 | temporary: true 30 | }); 31 | this.changes = ko.observable(); 32 | this.updateCommitHook(); 33 | this.updateChanges(); 34 | } 35 | 36 | GerritIntegrationViewModel.prototype.updateNode = function(parentElement) { 37 | ko.renderTemplate('gerrit', this, {}, parentElement); 38 | } 39 | 40 | GerritIntegrationViewModel.prototype.updateCommitHook = function() { 41 | var self = this; 42 | this.server.get('/plugins/gerrit/commithook', { path: this.repository.repoPath }, function(err, hook) { 43 | self.showInitCommmitHook(!hook.exists); 44 | }); 45 | } 46 | GerritIntegrationViewModel.prototype.updateChanges = function() { 47 | var self = this; 48 | self.status('loading'); 49 | this.changesLoader.start(); 50 | this.server.get('/plugins/gerrit/changes', { path: this.repository.repoPath }, function(err, changes) { 51 | self.changesLoader.stop(); 52 | if (err) { 53 | self.status('failed'); 54 | return true; 55 | } 56 | self.changes(changes.slice(0, changes.length - 1).map(function(c) { return new GerritChangeViewModel(self, c); })); 57 | self.status('loaded'); 58 | }); 59 | } 60 | GerritIntegrationViewModel.prototype.initCommitHook = function() { 61 | var self = this; 62 | this.initGerritHookProgressBar.start(); 63 | this.server.post('/plugins/gerrit/commithook', { path: this.repository.repoPath }, function(err) { 64 | self.updateCommitHook(); 65 | self.initGerritHookProgressBar.stop(); 66 | }); 67 | } 68 | GerritIntegrationViewModel.prototype.getChange = function(changeId) { 69 | return _.find(this.changes(), { data: { id: changeId } }); 70 | } 71 | GerritIntegrationViewModel.prototype.getChangeIdFromMessage = function(message) { 72 | var changeId = message.split('\n').pop().trim(); 73 | if (changeId && changeId.indexOf('Change-Id: ') == 0) { 74 | return changeId.slice('Change-Id: '.length).trim(); 75 | } 76 | } 77 | GerritIntegrationViewModel.prototype.getChangeFromNode = function(node) { 78 | var changeId = this.getChangeIdFromMessage(node.message()); 79 | if (!changeId) return; 80 | return this.getChange(changeId); 81 | } 82 | GerritIntegrationViewModel.prototype.pushForReview = function() { 83 | var self = this; 84 | this.pushingProgressBar.start(); 85 | var branch = this.repository.graph.checkedOutBranch(); 86 | var change = this.getChangeFromNode(this.repository.graph.HEAD()); 87 | if (change) branch = change.data.branch; 88 | 89 | this.server.post('/push', { path: this.repository.graph.repoPath, remote: this.repository.remotes.currentRemote(), remoteBranch: 'refs/for/' + branch }, function(err, res) { 90 | self.updateChanges(); 91 | self.pushingProgressBar.stop(); 92 | }); 93 | } 94 | 95 | var GerritChangeViewModel = function(gerritIntegration, args) { 96 | this.gerritIntegration = gerritIntegration; 97 | this.repository = gerritIntegration.repository; 98 | this.server = gerritIntegration.server; 99 | this.subject = args.subject; 100 | this.ownerName = args.owner.name; 101 | this.sha1 = args.sha1; 102 | this.data = args; 103 | this.gerritUrl = this.data.url; 104 | this.checkingOutProgressBar = components.create('progressBar', { 105 | predictionMemoryKey: 'gerrit-checkout-' + repository.repoPath, 106 | fallbackPredictedTimeMs: 4000, 107 | temporary: true 108 | }); 109 | this.cherryPickingProgressBar = components.create('progressBar', { 110 | predictionMemoryKey: 'gerrit-cherry-pick-' + repository.repoPath, 111 | fallbackPredictedTimeMs: 4000, 112 | temporary: true 113 | }); 114 | }; 115 | GerritChangeViewModel.prototype.checkout = function() { 116 | var self = this; 117 | this.checkingOutProgressBar.start(); 118 | this.server.post('/fetch', { path: this.gerritIntegration.repository.repoPath, remote: self.gerritIntegration.repository.remotes.currentRemote(), ref: this.data.currentPatchSet.ref }, function(err) { 119 | self.server.post('/checkout', { path: self.gerritIntegration.repository.repoPath, name: 'FETCH_HEAD' }, function(err) { 120 | self.checkingOutProgressBar.stop(); 121 | }); 122 | }); 123 | } 124 | GerritChangeViewModel.prototype.cherryPick = function() { 125 | var self = this; 126 | this.cherryPickingProgressBar.start(); 127 | this.server.post('/fetch', { path: this.gerritIntegration.repository.repoPath, remote: self.gerritIntegration.repository.remotes.currentRemote(), ref: this.data.currentPatchSet.ref }, function(err) { 128 | self.server.post('/cherrypick', { path: self.gerritIntegration.repository.repoPath, name: 'FETCH_HEAD' }, function(err) { 129 | self.cherryPickingProgressBar.stop(); 130 | }); 131 | }); 132 | } 133 | 134 | 135 | -------------------------------------------------------------------------------- /components/repository/repository.js: -------------------------------------------------------------------------------- 1 | 2 | var components = require('ungit-components'); 3 | 4 | var repositoryConstructor = components.registered['repository']; 5 | components.register('repository', function(args) { 6 | var repository = repositoryConstructor(args); 7 | var repositoryUpdateNode = repository.updateNode.bind(repository); 8 | var gerrit = components.create('gerrit', { repositoryViewModel: repository }); 9 | repository.updateNode = function(parentElement) { 10 | var node = document.createElement('div'); 11 | node.className = 'row'; 12 | var gerritNode = document.createElement('div'); 13 | gerritNode.className = 'col-lg-3'; 14 | gerrit.updateNode(gerritNode); 15 | var repoNode = document.createElement('div'); 16 | repoNode.className = 'col-lg-9'; 17 | repositoryUpdateNode(repoNode); 18 | node.appendChild(gerritNode); 19 | node.appendChild(repoNode); 20 | return node; 21 | }; 22 | return repository; 23 | }); 24 | -------------------------------------------------------------------------------- /gerrit.js: -------------------------------------------------------------------------------- 1 | 2 | var child_process = require('child_process'); 3 | var Ssh2Connection; 4 | 5 | var getProcessUsername = function(callback) { 6 | child_process.exec('whoami', function(err, res) { 7 | if (err) callback(err); 8 | else { 9 | res = res.split('\n')[0]; 10 | if (res.indexOf('/') != -1) res = res.split('/').pop(); 11 | res = res.trim(); 12 | callback(null, res); 13 | } 14 | }); 15 | }; 16 | 17 | var ssh2 = function(sshConfig, command, callback) { 18 | if (!Ssh2Connection) Ssh2Connection = require('ssh2'); 19 | 20 | var connection = new Ssh2Connection(); 21 | connection.on('connect', function() { 22 | }); 23 | connection.on('ready', function() { 24 | connection.exec(command, function(err, stream) { 25 | if (err) return callback(err); 26 | var result = ''; 27 | stream.on('data', function(data, extended) { 28 | result += data.toString(); 29 | }); 30 | stream.on('end', function() { 31 | callback(null, result); 32 | }); 33 | }); 34 | }); 35 | connection.on('error', function(err) { 36 | callback(err); 37 | }); 38 | var doConnect = function() { connection.connect(sshConfig); }; 39 | if (sshConfig.username) doConnect(); 40 | else getProcessUsername(function(err, username) { 41 | if (err) callback(err); 42 | else { 43 | sshConfig.username = username; 44 | doConnect(); 45 | } 46 | }); 47 | }; 48 | 49 | var gerrit = function(sshConfig, command, res, callback) { 50 | command = 'gerrit ' + command; 51 | ssh2(sshConfig, command, function(error, result) { 52 | var errorCode = 'unknown' 53 | if (result && result.indexOf('gerrit: command not found') != -1) { 54 | errorCode = error = 'not-a-gerrit-remote'; 55 | } 56 | if (error) { 57 | var err = { errorCode: errorCode, command: command, error: error.toString() }; 58 | if (!callback || !callback(err, result)) { 59 | res.json(400, err); 60 | } 61 | } else { 62 | callback(null, result); 63 | } 64 | }); 65 | }; 66 | 67 | module.exports = gerrit; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ungit-gerrit", 3 | "version": "0.1.0", 4 | "description": "Ungit gerrit plugin", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ssh2": "~0.2.17" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ungit-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gerrit", 3 | "server": "backend.js", 4 | "disabled": false, 5 | "exports": { 6 | "javascript": [ 7 | "components/gerrit/gerrit.js", 8 | "components/repository/repository.js" 9 | ], 10 | "knockoutTemplates": { 11 | "gerrit": "components/gerrit/gerrit.html" 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------