svn-dav-fs
Version:
handler for 'svn+https' url scheme (plain js svn dav fs)
650 lines (572 loc) • 21.5 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var urlResolverFs = require('url-resolver-fs');
/**
* Encodes objects into strings as used by svn
* @example
* (create-txn-with-props
* (svn:txn-user-agent 48 SVN/1.9.4 (x86_64-apple-darwin15.0.0) serf/1.3.8
* svn:log 19 this is the message
* svn:txn-client-compat-version 5 1.9.4))
* @param {object} object to be encoded
* @return {string} encoded object value
*/
function encodeProperties(object) {
return '(' + Object.keys(object).map(k => {
const v = object[k];
if (typeof v === 'string' || v instanceof String) {
return `${k} ${v.length} ${v}`;
}
return `${k} ${encodeProperties(v)}`;
}).join(' ') + ')';
}
function headerIntoSet(header, target) {
if (header) {
header.split(/\s*,\s*/).forEach(e => target.add(e));
}
}
var _asyncGenerator = function () { function AwaitValue(value) { this.value = value; } function AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; if (value instanceof AwaitValue) { Promise.resolve(value.value).then(function (arg) { resume("next", arg); }, function (arg) { resume("throw", arg); }); } else { settle(result.done ? "return" : "normal", result.value); } } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; return { wrap: function (fn) { return function () { return new AsyncGenerator(fn.apply(this, arguments)); }; }, await: function (value) { return new AwaitValue(value); } }; }();
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
const sax = require('sax');
const { URL } = require('url');
/**
* @module svn-dav-fs
*/
const XML_HEADER = '<?xml version="1.0" encoding="utf-8"?>';
const XML_CONTENT_TYPE = 'text/xml';
const SVN_SKEL_CONTENT_TYPE = 'application/vnd.svn-skel';
const SVN_SVNDIFF_CONTENT_TYPE = 'application/vnd.svn-svndiff';
const NS_SVN_DAV = 'http://subversion.tigris.org/xmlns/dav/';
const NS_SVN_DAV_DEPTH = NS_SVN_DAV + 'svn/depth';
const NS_SVN_DAV_MERGINFO = NS_SVN_DAV + 'svn/mergeinfo';
const NS_SVN_DAV_LOG_REVPROPS = NS_SVN_DAV + 'svn/log-revprops';
/*
const NS_SVN_DAV_ATOMIC_REVPROPS = NS_SVN_DAV + 'svn/atomic-revprops';
const NS_SVN_DAV_PARTIAL_REPLAY = NS_SVN_DAV + 'svn/partial-replay';
const DAVFeatures = {
'1': {},
'2': {},
'version-control': {},
'checkout': {},
'working-resource': {},
'merge': {},
'baseline': {},
'activity': {},
'version-controlled-collection': {},
'http://subversion.tigris.org/xmlns/dav/svn/inherited-props': {},
'http://subversion.tigris.org/xmlns/dav/svn/inline-props': {},
'http://subversion.tigris.org/xmlns/dav/svn/reverse-file-revs': {},
'http://subversion.tigris.org/xmlns/dav/svn/ephemeral-txnprops': {},
'http://subversion.tigris.org/xmlns/dav/svn/replay-rev-resource': {},
[NS_SVN_DAV_MERGINFO]: {},
[NS_SVN_DAV_PARTIAL_REPLAY]: {},
[NS_SVN_DAV_DEPTH]: {},
[NS_SVN_DAV_LOG_REVPROPS]: {},
[NS_SVN_DAV_ATOMIC_REVPROPS]: {}
};
*/
const SVNHeaders = ['SVN-Repository-UUID', 'SVN-Repository-Root', 'SVN-Rev-Root-Stub', // /svn/delivery_notes/!svn/rvr
'SVN-Rev-Stub', // /svn/delivery_notes/!svn/rev
'SVN-Txn-Root-Stub', // /svn/delivery_notes/!svn/vtxr
'SVN-Txn-Stub', // /svn/delivery_notes/!svn/txn
'SVN-Me-Resource', // /svn/delivery_notes/!svn/me
'SVN-Rev-Root-Stub', // /svn/delivery_notes/!svn/rvr
'SVN-Youngest-Rev', // Integer
'SVN-Allow-Bulk-Updates', // Prefer
'SVN-Supported-Posts', // create-txn create-txn-with-props
'SVN-Repository-MergeInfo' // yes
];
function ignore() {}
/**
* URL sheme 'svn+https' svn over https
*/
class SVNHTTPSScheme extends urlResolverFs.HTTPSScheme {
/**
* Execute options request
* @param context {Context} execution context
* @param url {URL}
* @param body {string[]} xml lines
* @return {Promise}
*/
options(context, url, body) {
return this.fetch(context, url, {
method: 'OPTIONS',
body: [XML_HEADER, ...body].join(''),
headers: {
dav: this.davHeader,
'content-type': XML_CONTENT_TYPE
}
});
}
/**
* query the activity collection set.
* @param context {Context} execution context
* @param url {URL}
* @return {Promise}
*/
activityCollectionSet(context, url) {
var _this = this;
return _asyncToGenerator(function* () {
const options = yield _this.options(context, url, ['<D:options xmlns:D="DAV:">', '<D:activity-collection-set></D:activity-collection-set>', '</D:options>']);
const attributes = {};
const davFeatures = new Set();
const allowedMethods = new Set();
SVNHeaders.forEach(function (h) {
const v = options.headers.get(h);
if (v) {
attributes[h] = v;
}
});
headerIntoSet(options.headers.get('dav'), davFeatures);
headerIntoSet(options.headers.get('allow'), allowedMethods);
attributes['SVN-Youngest-Rev'] = parseInt(options.headers.get('SVN-Youngest-Rev'), 10);
return {
attributes,
davFeatures,
allowedMethods
};
})();
}
// TODO why is this not taken from the base class ?
get type() {
return 'svn+https';
}
static get name() {
return 'svn+https';
}
/**
* Delivers svn user agent
* @return {string} user agent identifier
*/
get userAgent() {
return 'SVN/1.9.4 (x86_64-apple-darwin15.0.0) serf/1.3.8';
}
/**
* Delivers svn client version
* @return {string} version
*/
get clientVersion() {
return '1.9.4';
}
get davHeader() {
return [NS_SVN_DAV_DEPTH, NS_SVN_DAV_MERGINFO, NS_SVN_DAV_LOG_REVPROPS].join(',');
}
/*
MKCOL /svn/delivery_notes/!svn/txr/1485-1cs/data/comp2 HTTP/1.1
Host subversion.assembla.com
Authorization Basic YXJsYWM3NzpzdGFydDEyMw==
User-Agent SVN/1.9.4 (x86_64-apple-darwin15.0.0) serf/1.3.8
Accept-Encoding gzip
DAV http://subversion.tigris.org/xmlns/dav/svn/depth
DAV http://subversion.tigris.org/xmlns/dav/svn/mergeinfo
DAV http://subversion.tigris.org/xmlns/dav/svn/log-revprops
*/
mkcol(url, tx) {
return _asyncToGenerator(function* () {})();
}
startTransaction(context, url, message) {
var _this2 = this;
return _asyncToGenerator(function* () {
const {
attributes,
davFeatures,
allowedMethods
} = yield _this2.activityCollectionSet(context, url);
const response = yield _this2.fetch(context, 'https://subversion.assembla.com/svn/delivery_notes/' + '!svn/me', {
method: 'POST',
body: encodeProperties({
'create-txn-with-props': {
'svn:txn-user-agent': _this2.userAgent,
'svn:log': message,
'svn:txn-client-compat-version': _this2.clientVersion
}
}),
headers: {
dav: _this2.davHeader,
'content-type': SVN_SKEL_CONTENT_TYPE
}
});
const txn = response.headers.get('SVN-Txn-Name');
if (!txn) {
return Promise.reject(new Error('Can`t create transaction'));
}
return txn;
})();
}
/**
* http://svn.apache.org/repos/asf/subversion/trunk/notes/svndiff
* http://stackoverflow.com/questions/24865265/how-to-do-svn-http-request-checkin-commit-within-html
*/
put(context, url, stream, options) {
var _this3 = this;
return _asyncToGenerator(function* () {
/*this.activityCollectionSet(url).then(acs => {
}).then(() =>
this.options(url, ['<D:options xmlns:D="DAV:"/>'])
);*/
const response = yield _this3.fetch(context, 'https://subversion.assembla.com/svn/delivery_notes/' + '!svn/me', {
method: 'POST',
body: encodeProperties({
'create-txn-with-props': {
'svn:txn-user-agent': _this3.userAgent,
'svn:log': options.message,
'svn:txn-client-compat-version': _this3.clientVersion
}
}),
headers: {
dav: _this3.davHeader,
'content-type': SVN_SKEL_CONTENT_TYPE
}
});
const txn = response.headers.get('SVN-Txn-Name');
if (!txn) {
return Promise.reject(new Error('Can`t create transaction'));
}
const [versionName] = txn.split(/\-/);
return _this3.fetch(context, `https://subversion.assembla.com/svn/delivery_notes/!svn/txr/${txn}/data/config.json`, {
method: 'PUT',
body: '{ffffff}',
headers: {
dav: _this3.davHeader,
'content-type': SVN_SVNDIFF_CONTENT_TYPE,
'X-SVN-Version-Name': versionName,
'X-SVN-Base-Fulltext-MD5': '7f407419826ad120a3c9374947770470',
'X-SVN-Result-Fulltext-MD5': '03d6350bb46a63e86f1c5db703af403c'
}
}).then(function (response) {
console.log(response);
}).catch(function (e) {
return console.error(e);
});
/*
POST /svn/delivery_notes/!svn/me HTTP/1.1
Content-Type application/vnd.svn-skel
DAV http://subversion.tigris.org/xmlns/dav/svn/depth
DAV http://subversion.tigris.org/xmlns/dav/svn/mergeinfo
DAV http://subversion.tigris.org/xmlns/dav/svn/log-revprops
(create-txn-with-props (svn:txn-user-agent 48 SVN/1.9.4 (x86_64-apple-darwin15.0.0) serf/1.3.8 svn:log 19 this is the message svn:txn-client-compat-version 5 1.9.4))
Response:
SVN-Txn-Name: 1483-1a1
*/
/*
PUT /svn/delivery_notes/!svn/txr/1483-1a1/data/config.json HTTP/1.1
Content-Type: application/vnd.svn-svndiff
DAV: http://subversion.tigris.org/xmlns/dav/svn/depth
DAV: http://subversion.tigris.org/xmlns/dav/svn/mergeinfo
DAV: http://subversion.tigris.org/xmlns/dav/svn/log-revprops
X-SVN-Version-Name: 1483
X-SVN-Base-Fulltext-MD5: 7f407419826ad120a3c9374947770470
X-SVN-Result-Fulltext-MD5: 03d6350bb46a63e86f1c5db703af403c
*/
/*
MERGE /svn/delivery_notes/data HTTP/1.1
Content-Type: text/xml
DAV: http://subversion.tigris.org/xmlns/dav/svn/depth
DAV: http://subversion.tigris.org/xmlns/dav/svn/mergeinfo
DAV: http://subversion.tigris.org/xmlns/dav/svn/log-revprops
X-SVN-Options: release-locks
<?xml version="1.0" encoding="utf-8"?>
<D:merge xmlns:D="DAV:">
<D:source>
<D:href>/svn/delivery_notes/!svn/txn/1483-1a1</D:href>
</D:source>
<D:no-auto-merge/>
<D:no-checkout/>
<D:prop>
<D:checked-in/>
<D:version-name/>
<D:resourcetype/>
<D:creationdate/>
<D:creator-displayname/>
</D:prop>
</D:merge>
Reponse:
HTTP/1.1 200 OK
Content-Type: text/xml
<?xml version="1.0" encoding="utf-8"?>
<D:merge-response xmlns:D="DAV:">
<D:updated-set>
<D:response>
<D:href>/svn/delivery_notes/!svn/vcc/default</D:href>
<D:propstat><D:prop>
<D:resourcetype><D:baseline/></D:resourcetype>
<D:version-name>1484</D:version-name>
<D:creationdate>2016-12-28T20:24:49.296311Z</D:creationdate>
<D:creator-displayname>arlac77</D:creator-displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/svn/delivery_notes/data/config.json</D:href>
<D:propstat><D:prop>
<D:resourcetype/>
<D:checked-in><D:href>/svn/delivery_notes/!svn/ver/1484/data/config.json</D:href></D:checked-in>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:updated-set>
</D:merge-response>
*/
})();
}
stat(context, url, options) {
var _this4 = this;
return _asyncToGenerator(function* () {
const acs = yield _this4.activityCollectionSet(context, url);
const path = url.href.substring(url.origin.length + acs.attributes['SVN-Repository-Root'].length);
const u2 = new URL(`${url.origin}${acs.attributes['SVN-Rev-Root-Stub']}/${acs.attributes['SVN-Youngest-Rev']}${path}`);
const properties = yield _this4.propfind(context, u2, {
resourcetype: 'DAV:',
getcontentlength: 'DAV:',
'deadprop-count': 'http://subversion.tigris.org/xmlns/dav/',
'version-name': 'DAV:',
creationdate: 'DAV:',
'creator-displayname': 'DAV:'
}, 0);
return properties[0];
})();
}
propfind(context, url, properties, depth = 1) {
var _this5 = this;
return _asyncToGenerator(function* () {
const xmls = [XML_HEADER, '<D:propfind xmlns:D="DAV:">'];
if (properties === undefined) {
xmls.push('<D:allprop/>');
} else {
xmls.push('<D:prop>');
for (const p in properties) {
xmls.push(`<${p} xmlns="${properties[p]}"/>`);
}
xmls.push('</D:prop>');
}
xmls.push('</D:propfind>');
const response = yield _this5.fetch(context, url, {
method: 'PROPFIND',
body: xmls.join('\n'),
headers: {
dav: _this5.davHeader,
depth: depth,
'content-type': XML_CONTENT_TYPE
}
});
return new Promise(function (fullfill, reject) {
const entries = [];
let entry;
let consume = ignore;
let rootPathPrefixLength;
const saxStream = sax.createStream(true, {
xmlns: true,
position: false,
trim: true
});
saxStream.on('opentag', function (node) {
switch (node.local) {
case 'response':
break;
case 'prop':
entry = {};
consume = ignore;
break;
case 'collection':
entry.collection = true;
break;
case 'version-name':
consume = function (text) {
entry.version = parseInt(text, 10);
consume = ignore;
};
break;
case 'getcontentlength':
consume = function (text) {
entry.size = parseInt(text, 10);
consume = ignore;
};
break;
case 'creator-displayname':
consume = function (text) {
entry.creator = text;
consume = ignore;
};
break;
case 'creationdate':
consume = function (text) {
entry.creationDate = new Date(text);
consume = ignore;
};
break;
case 'baseline-relative-path':
consume = function (text) {
if (rootPathPrefixLength) {
entry.name = text.substring(rootPathPrefixLength);
} else {
rootPathPrefixLength = text.length + 1;
entry = undefined;
}
consume = ignore;
};
break;
/* case 'resourcetype':
consume = text => {
console.log(`resourcetype: ${text}`);
//consume = ignore;
};
break;
default:
console.log(`${node.name} ${node.local} ${node.uri}`);
*/
}
});
saxStream.on('closetag', function (name) {
switch (name) {
case 'D:prop':
if (entry !== undefined) {
entries.push(entry);
}
break;
}
});
saxStream.on('text', function (text) {
return consume(text);
});
saxStream.on('end', function () {
return fullfill(entries);
});
saxStream.on('error', reject);
response.body.pipe(saxStream);
});
})();
}
list(context, url, options) {
var _this6 = this;
return _asyncToGenerator(function* () {
return _this6.propfind(context, url, options);
})();
}
_list(context, url, options) {
var _this7 = this;
return _asyncGenerator.wrap(function* () {
const list = yield _asyncGenerator.await(_this7.propfind(context, url, options));
for (const entry of list) {
yield entry;
}
})();
}
history(context, url, options = {}) {
const p = options.version === undefined ? this.list(context, url).then(entries => Promise.resolve(entries[0].version)) : Promise.resolve(options.version);
return p.then(start => {
const direction = options.direction || options.version === undefined ? 'backward' : 'forward';
const chunkSize = options.chunkSize || 1000;
let end = direction === 'forward' ? start + chunkSize : start - chunkSize;
if (end < 0) {
end = 0;
}
if (start > end) {
const t = start;
start = end;
end = t;
}
return this._history(context, url, start, end).then(entries => {
const self = this;
return Promise.resolve(function* () {
for (const i in entries) {
yield Promise.resolve(entries[i]);
}
let i = 0;
const p = self._history(url, end + 1, end + chunkSize);
for (let j = 0; j < 10; j++) {
yield p.then(entries => {
return Promise.resolve(entries[i++]);
});
}
/*
yield p.then(entries => {
return Promise.resolve(entries[i++]);
});
*/
});
});
});
}
_history(context, url, start, end) {
const xmls = [XML_HEADER, '<S:log-report xmlns:S="svn:">', `<S:start-revision>${start}</S:start-revision>`, `<S:end-revision>${end}</S:end-revision>`];
['svn:author', 'svn:date', 'svn:log'].forEach(item => xmls.push(`<S:revprop>${item}</S:revprop>`));
xmls.push('<S:path/>');
xmls.push('</S:log-report>');
return this.fetch(context, url, {
method: 'REPORT',
body: xmls.join('\n'),
headers: {
dav: this.davHeader,
'content-type': XML_CONTENT_TYPE
}
}).then(response => new Promise((fullfill, reject) => {
/*
<S:log-report xmlns:S="svn:" xmlns:D="DAV:">
<S:log-item>
<D:version-name>0</D:version-name>
<S:date>2011-09-18T13:20:54.561302Z</S:date>
</S:log-item>
<S:log-item>
*/
const saxStream = sax.createStream(true, {
xmlns: true,
position: false,
trim: true
});
const entries = [];
let entry;
let consume = ignore;
saxStream.on('opentag', node => {
switch (node.local) {
case 'log-item':
entry = {};
consume = ignore;
break;
case 'version-name':
consume = text => {
entry.version = parseInt(text, 10);
consume = ignore;
};
break;
case 'date':
consume = text => {
entry.date = new Date(text);
consume = ignore;
};
break;
case 'comment':
consume = text => {
entry.message = entry.message ? entry.message + text : text;
};
break;
case 'creator-displayname':
consume = text => {
entry.creator = text;
consume = ignore;
};
break;
default:
consume = ignore;
}
});
saxStream.on('closetag', name => {
switch (name) {
case 'S:log-item':
entries.push(entry);
break;
}
});
saxStream.on('text', text => {
consume(text);
});
saxStream.on('end', () => fullfill(entries));
saxStream.on('error', reject);
response.body.pipe(saxStream);
}));
}
}
exports.SVNHTTPSScheme = SVNHTTPSScheme;
exports.headerIntoSet = headerIntoSet;
exports.encodeProperties = encodeProperties;