├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── svn.js ├── package.json └── test ├── index.js ├── init.sh └── source ├── a.txt ├── a ├── a.txt ├── a │ ├── a.txt │ └── b.txt ├── b.txt └── b │ ├── a.txt │ └── b.txt ├── b.txt └── b ├── a.txt └── b.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /test/tmp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_install: 5 | - "sudo apt-get update -qq" 6 | - "sudo apt-get install -qq subversion" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 dong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svn-spawn 2 | 3 | [![npm](https://img.shields.io/npm/v/svn-spawn.svg?style=flat-square)](https://www.npmjs.com/package/svn-spawn) 4 | [![npm](https://img.shields.io/npm/dm/svn-spawn.svg?style=flat-square)](https://www.npmjs.com/package/svn-spawn) 5 | [![Travis](https://img.shields.io/travis/ddliu/node-svn-spawn.svg?style=flat-square)](https://travis-ci.org/ddliu/node-svn-spawn) 6 | ![npm](https://img.shields.io/npm/l/svn-spawn.svg?style=flat-square) 7 | 8 | Easy way to access svn repository with node.js. 9 | 10 | ## Features 11 | 12 | - Easy to use 13 | - Fast way to add local changes 14 | - Query svn infomation as array or object 15 | - Common svn commands are all supported 16 | 17 | ## Usage 18 | 19 | Create a svn client instance 20 | 21 | ```js 22 | var Client = require('svn-spawn'); 23 | var client = new Client({ 24 | cwd: '/path to your svn working directory', 25 | username: 'username', // optional if authentication not required or is already saved 26 | password: 'password', // optional if authentication not required or is already saved 27 | noAuthCache: true, // optional, if true, username does not become the logged in user on the machine 28 | }); 29 | ``` 30 | `svn update` 31 | 32 | ```js 33 | client.update(function(err, data) { 34 | console.log('updated'); 35 | }); 36 | ``` 37 | 38 | `svn info` 39 | 40 | ```js 41 | client.getInfo(function(err, data) { 42 | console.log('Repository url is %s', data.url); 43 | }); 44 | ``` 45 | 46 | Make some changes and commit all 47 | 48 | ```js 49 | client.addLocal(function(err, data) { 50 | console.log('all local changes has been added for commit'); 51 | 52 | client.commit('commit message here', function(err, data) { 53 | console.log('local changes has been committed!'); 54 | }); 55 | }); 56 | ``` 57 | 58 | Single file 59 | 60 | ```js 61 | client.add('relative/path/to/file', function(err, data) { 62 | client.commit(['commit message here', 'relative/path/to/file'], function(err, data) { 63 | console.log('committed one file!'); 64 | }); 65 | }); 66 | ``` 67 | 68 | Run any svn command 69 | 70 | ```js 71 | client.cmd(['subcommand', '--option1=xx', '--option2=xx', 'arg1', 'arg2'], function(err, data) { 72 | console.log('subcommand done'); 73 | }); 74 | ``` 75 | 76 | ## Result Object 77 | 78 | `getXXX` methods will return parsed data as object. 79 | 80 | `getInfo` result example: 81 | 82 | ```json 83 | { 84 | "$": { 85 | "path": ".", 86 | "revision": "1", 87 | "kind": "dir" 88 | }, 89 | "url": "file:///home/dong/projects/node-packages/node-svn-spawn/test/tmp/repo", 90 | "repository": { 91 | "root": "file:///home/dong/projects/node-packages/node-svn-spawn/test/tmp/repo", 92 | "uuid": "302eb8ee-a81a-4432-8477-1ad8fe3a9a1e" 93 | }, 94 | "wc-info": { 95 | "wcroot-abspath": "/home/dong/projects/node-packages/node-svn-spawn/test/tmp/copy", 96 | "schedule": "normal", 97 | "depth": "infinity" 98 | }, 99 | "commit": { 100 | "$": { 101 | "revision": "1" 102 | }, 103 | "author": "dong", 104 | "date": "2013-11-08T02:07:25.884985Z" 105 | } 106 | } 107 | ``` 108 | 109 | `getLog` result example: 110 | 111 | ```json 112 | [ 113 | { 114 | "$": { 115 | "revision": "1" 116 | }, 117 | "author": "dong", 118 | "date": "2013-11-08T02:10:37.656902Z", 119 | "msg": "init repo" 120 | }, 121 | ... 122 | ] 123 | ``` 124 | 125 | `getStatus` result example: 126 | 127 | ```json 128 | [ 129 | { 130 | "$": { 131 | "path": "a.txt" 132 | }, 133 | "wc-status": { 134 | "$": { 135 | "props": "none", 136 | "item": "modified", 137 | "revision": "1" 138 | }, 139 | "commit": { 140 | "$": { 141 | "revision": "1" 142 | }, 143 | "author": "dong", 144 | "date": "2013-11-08T02:17:20.390152Z" 145 | } 146 | } 147 | } 148 | ] 149 | ``` 150 | 151 | ## Requirements 152 | 153 | You need to have the `svn` command installed. 154 | 155 | ## Installation 156 | 157 | ```bash 158 | npm install svn-spawn 159 | ``` 160 | -------------------------------------------------------------------------------- /lib/svn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Spawn = require('easy-spawn'); 4 | var util = require('util'); 5 | var xml2js = require('xml2js'); 6 | var async = require('async'); 7 | var fs = require('fs'); 8 | 9 | 10 | var Client = function(options) { 11 | this.option({ 12 | program: 'svn' 13 | }).option(options); 14 | }; 15 | 16 | util.inherits(Client, Spawn); 17 | 18 | Client.prototype.cmd = function(params, callback) { 19 | if (!util.isArray(params)) { 20 | params = [params]; 21 | } 22 | if (this.getOption('username') !== undefined) { 23 | params.push('--username', this.getOption('username')); 24 | } 25 | if (this.getOption('password') !== undefined) { 26 | params.push('--password', this.getOption('password')); 27 | } 28 | if (this.getOption('noAuthCache') === true) { 29 | params.push('--no-auth-cache'); 30 | } 31 | return Client.super_.prototype.cmd.call(this, params, callback); 32 | }; 33 | 34 | /* 35 | SVN commands 36 | */ 37 | 38 | Client.prototype.checkout = function(params, callback) { 39 | // try making dir 40 | var cwd = this.getOption('cwd'); 41 | if (cwd !== undefined && !fs.existsSync(cwd)) { 42 | fs.mkdirSync(cwd); 43 | } 44 | 45 | if (typeof params === 'function') { 46 | callback = params; 47 | params = null; 48 | } 49 | 50 | // Default path 51 | if (typeof params === 'array' && params.length === 1) { 52 | params.splice(1, 0, '.'); 53 | } 54 | else if (typeof params === 'string') { 55 | params = [params, '.']; 56 | } 57 | 58 | params = Spawn.joinParams('checkout', params); 59 | 60 | this.cmd(params, callback); 61 | }; 62 | 63 | Client.prototype.update = function(params, callback) { 64 | if (typeof params === 'function') { 65 | callback = params; 66 | params = null; 67 | } 68 | params = Spawn.joinParams('update', params); 69 | 70 | this.cmd(params, callback); 71 | }; 72 | 73 | Client.prototype.commit = function(params, callback) { 74 | if (typeof params === 'function') { 75 | callback = params; 76 | params = null; 77 | } 78 | params = Spawn.joinParams(['commit', '-m'], params); 79 | 80 | this.cmd(params, callback); 81 | }; 82 | 83 | Client.prototype.add = function(params, callback) { 84 | if (typeof params === 'function') { 85 | callback = params; 86 | params = null; 87 | } 88 | if (typeof params === 'string' && params.indexOf('@') > -1) { 89 | params = params + '@'; 90 | } 91 | params = Spawn.joinParams('add', params); 92 | 93 | this.cmd(params, callback); 94 | }; 95 | 96 | Client.prototype.del = function(params, callback) { 97 | if (typeof params === 'function') { 98 | callback = params; 99 | params = null; 100 | } 101 | params = Spawn.joinParams('delete', params); 102 | 103 | this.cmd(params, callback); 104 | }; 105 | 106 | Client.prototype.info = function(params, callback) { 107 | if (typeof params === 'function') { 108 | callback = params; 109 | params = null; 110 | } 111 | 112 | params = Spawn.joinParams('info', params); 113 | 114 | this.cmd(params, callback); 115 | }; 116 | 117 | /** 118 | * svn info with parsed info in callback 119 | * @param {Mixed} params 120 | * @param {Function} callback 121 | */ 122 | Client.prototype.getInfo = function(params, callback) { 123 | if (typeof params === 'function') { 124 | callback = params; 125 | params = null; 126 | } 127 | var self = this; 128 | 129 | params = Spawn.joinParams(['info', '--xml'], params), 130 | 131 | async.waterfall([ 132 | function(callback) { 133 | self.session('silent', true).cmd(params, callback); 134 | }, 135 | function(data, callback) { 136 | xml2js.parseString(data, 137 | { 138 | explicitRoot: false, 139 | explicitArray: false 140 | }, 141 | callback 142 | ); 143 | }, 144 | ], function(err, data) { 145 | if (callback) { 146 | if (err) { 147 | callback(err); 148 | } 149 | else { 150 | callback(err, data.entry); 151 | } 152 | } 153 | }); 154 | }; 155 | 156 | Client.prototype.status = function(params, callback) { 157 | if (typeof params === 'function') { 158 | callback = params; 159 | params = null; 160 | } 161 | 162 | params = Spawn.joinParams('status', params); 163 | 164 | this.cmd(params, callback); 165 | }; 166 | 167 | /** 168 | * svn status with parsed info in callback 169 | * 170 | * @see status list https://github.com/apache/subversion/blob/trunk/subversion/svn/schema/status.rnc 171 | * @param {Mixed} params 172 | * @param {Function} callback 173 | */ 174 | Client.prototype.getStatus = function(params, callback) { 175 | if (typeof params === 'function') { 176 | callback = params; 177 | params = null; 178 | } 179 | var self = this; 180 | 181 | params = Spawn.joinParams(['status', '--xml'], params); 182 | 183 | async.waterfall([ 184 | function(callback) { 185 | self.session('silent', true).cmd(params, callback); 186 | }, 187 | function(data, callback) { 188 | xml2js.parseString(data, 189 | { 190 | explicitRoot: false, 191 | explicitArray: false 192 | }, 193 | callback 194 | ); 195 | } 196 | ], function(err, data) { 197 | if (callback) { 198 | if (err) { 199 | callback(err); 200 | } 201 | else { 202 | var list = []; 203 | if ('target' in data) { 204 | data = data.target; 205 | if ('entry' in data) { 206 | if (util.isArray(data.entry)) { 207 | for (var i = 0; i < data.entry.length; i++) { 208 | list.push(data.entry[i]); 209 | } 210 | } else { 211 | list.push(data.entry); 212 | } 213 | } 214 | } 215 | callback(null, list); 216 | } 217 | } 218 | }); 219 | }; 220 | 221 | Client.prototype.log = function(params, callback) { 222 | if (typeof params === 'function') { 223 | callback = params; 224 | params = null; 225 | } 226 | 227 | params = Spawn.joinParams('log', params); 228 | this.cmd(params, callback); 229 | }; 230 | 231 | /** 232 | * svn log with parsed info in callback 233 | * sample result: 234 | * 235 | * [ 236 | * { '$': { revision: '1' }, 237 | * author: 'dong', 238 | * date: '2013-05-21T10:23:35.427701Z', 239 | * msg: 'tt' 240 | * } ... 241 | * ] 242 | * 243 | * @param {Mixed} params 244 | * @param {Function} callback 245 | */ 246 | Client.prototype.getLog = function(params, callback) { 247 | if (typeof params === 'function') { 248 | callback = params; 249 | params = null; 250 | } 251 | 252 | var self = this; 253 | 254 | params = Spawn.joinParams(['log', '--xml'], params); 255 | 256 | async.waterfall([ 257 | function(callback) { 258 | self.session('silent', true).cmd(params, callback); 259 | }, 260 | function(data, callback) { 261 | xml2js.parseString(data, 262 | { 263 | explicitRoot: false, 264 | explicitArray: false 265 | }, 266 | callback 267 | ); 268 | } 269 | ], function(err, data) { 270 | if (callback) { 271 | if (err) { 272 | callback(err); 273 | } 274 | else { 275 | var list = []; 276 | if (util.isArray(data)) { 277 | for (var i = 0; i < data.length; i++) { 278 | list.push(data[i].logentry); 279 | } 280 | } 281 | else if('logentry' in data) { 282 | list.push(data.logentry); 283 | } 284 | callback(null, list); 285 | } 286 | } 287 | }); 288 | }; 289 | 290 | /** 291 | * Add all local changes to for commit 292 | * 293 | * @param {Object} options 294 | * - status 295 | * @param {Function} callback 296 | */ 297 | Client.prototype.addLocal = function(options, callback) { 298 | if (typeof options === 'function') { 299 | callback = options; 300 | options = { 301 | status: null 302 | } 303 | } 304 | 305 | if (!'status' in options) { 306 | options.status = null; 307 | } 308 | 309 | var self = this; 310 | 311 | async.waterfall([ 312 | function(callback) { 313 | self.getStatus(callback); 314 | }, 315 | function(data, callback) { 316 | // do not run in parallel, or svn might be locked 317 | async.eachSeries(data, function(info, callback) { 318 | var path = info.$.path, 319 | status = info['wc-status'].$.item; 320 | 321 | if (options.status === null || options.status.indexOf(status) !== -1) { 322 | // add 323 | if (['none', 'unversioned'].indexOf(status) !== -1) { 324 | self.add(path, function(err) { 325 | callback(err); 326 | }); 327 | } 328 | // delete 329 | else if(['missing'].indexOf(status) !== -1) { 330 | self.del(path, function(err) { 331 | callback(err); 332 | }); 333 | } 334 | else { 335 | callback(null); 336 | } 337 | } 338 | else { 339 | callback(null); 340 | } 341 | }, function(err) { 342 | callback(err); 343 | }); 344 | } 345 | ], function(err) { 346 | callback && callback(err); 347 | }); 348 | }; 349 | 350 | Client.prototype.addLocalUnversioned = function(options, callback) { 351 | if (typeof options === 'function') { 352 | callback = options; 353 | options = { 354 | }; 355 | } 356 | else if(options === undefined || options === null){ 357 | options = { 358 | 359 | } 360 | } 361 | options.status = 'unversioned'; 362 | this.addLocal(options, callback); 363 | }; 364 | 365 | 366 | 367 | module.exports = Client; 368 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svn-spawn", 3 | "version": "0.1.6", 4 | "description": "SVN access based on svn cli command", 5 | "main": "lib/svn.js", 6 | "scripts": { 7 | "test": "./test/init.sh && ./node_modules/.bin/nodeunit test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ddliu/node-svn-spawn.git" 12 | }, 13 | "keywords": [ 14 | "svn", 15 | "subversion" 16 | ], 17 | "author": "dong", 18 | "license": "MIT", 19 | "dependencies": { 20 | "easy-spawn": "0.0.2", 21 | "async": "~0.2.9", 22 | "xml2js": "~0.2.7" 23 | }, 24 | "devDependencies": { 25 | "nodeunit": "^0.9.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var Client = require('../'); 2 | var fs = require('fs'); 3 | 4 | var workingPath = __dirname + '/tmp/copy'; 5 | var repo = 'file://' + __dirname + '/tmp/repo'; 6 | 7 | var client = new Client({ 8 | cwd: workingPath 9 | }); 10 | 11 | module.exports = { 12 | 'test checkout': function(test) { 13 | // var checkoutPath = __dirname + '/tmp/checkout'; 14 | // var client = new Client({ 15 | // cwd: checkoutPath 16 | // }); 17 | 18 | client.checkout(repo, function(err, data) { 19 | test.equals(err, null); 20 | test.done(); 21 | }); 22 | }, 23 | 'test info': function(test) { 24 | client.getInfo(function(err, data) { 25 | test.equals(err, null); 26 | test.ok('url' in data); 27 | test.done(); 28 | }); 29 | }, 30 | 'test update': function(test) { 31 | client.update(function(err, data) { 32 | test.equals(err, null); 33 | test.ok(data.indexOf('At revision') !== -1); 34 | test.done(); 35 | }); 36 | }, 37 | 'test status': function(test) { 38 | client.getStatus(function(err, data) { 39 | test.equals(err, null); 40 | // test.equals(data.length, 0); 41 | test.done(); 42 | }); 43 | }, 44 | 'test log': function(test) { 45 | client.getLog(function(err, data) { 46 | test.equals(err, null); 47 | test.ok('author' in data[0]); 48 | test.done(); 49 | }); 50 | }, 51 | 'test add': function(test) { 52 | fs.writeFileSync(workingPath + '/a.txt', new Date().toString()); 53 | 54 | client.addLocal(function(err, data) { 55 | test.equals(err, null); 56 | test.done(); 57 | }); 58 | }, 59 | 'test delete': function(test) { 60 | client.del('b.txt', function(err, data) { 61 | test.equals(err, null); 62 | test.done(); 63 | }); 64 | }, 65 | 'test commit': function(test) { 66 | fs.writeFileSync(workingPath + '/a.txt', new Date().toString()); 67 | 68 | client.addLocal(function(err, data) { 69 | test.equals(err, null); 70 | client.commit('test commit', function(err, data) { 71 | test.equals(err, null); 72 | test.done(); 73 | }); 74 | }); 75 | }, 76 | 'test update': function(test) { 77 | client.update(function(err, data) { 78 | test.equals(err, null); 79 | test.done(); 80 | }); 81 | } 82 | }; -------------------------------------------------------------------------------- /test/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASEDIR=$(cd "$(dirname "$0")"; pwd) 4 | mkdir $BASEDIR/tmp 5 | rm -rf $BASEDIR/tmp/* 6 | svnadmin create $BASEDIR/tmp/repo 7 | svn checkout file://$BASEDIR/tmp/repo $BASEDIR/tmp/copy 8 | cp -r $BASEDIR/source/* $BASEDIR/tmp/copy 9 | cd $BASEDIR/tmp/copy 10 | svn st | grep "^\?" | awk "{print \$2}" | xargs svn add $1 11 | svn commit -m 'init repo' 12 | cd $BASEDIR 13 | rm -rf $BASEDIR/tmp/copy -------------------------------------------------------------------------------- /test/source/a.txt: -------------------------------------------------------------------------------- 1 | a.txt -------------------------------------------------------------------------------- /test/source/a/a.txt: -------------------------------------------------------------------------------- 1 | a.txt -------------------------------------------------------------------------------- /test/source/a/a/a.txt: -------------------------------------------------------------------------------- 1 | a.txt -------------------------------------------------------------------------------- /test/source/a/a/b.txt: -------------------------------------------------------------------------------- 1 | b.txt -------------------------------------------------------------------------------- /test/source/a/b.txt: -------------------------------------------------------------------------------- 1 | b.txt -------------------------------------------------------------------------------- /test/source/a/b/a.txt: -------------------------------------------------------------------------------- 1 | a.txt -------------------------------------------------------------------------------- /test/source/a/b/b.txt: -------------------------------------------------------------------------------- 1 | b.txt -------------------------------------------------------------------------------- /test/source/b.txt: -------------------------------------------------------------------------------- 1 | b.txt -------------------------------------------------------------------------------- /test/source/b/a.txt: -------------------------------------------------------------------------------- 1 | a.txt -------------------------------------------------------------------------------- /test/source/b/b.txt: -------------------------------------------------------------------------------- 1 | b.txt --------------------------------------------------------------------------------