From e06ec920f7a5d784e674c4c4b4e6d1da3dc7391d Mon Sep 17 00:00:00 2001 From: Piotr Russ Date: Mon, 16 Nov 2020 00:10:28 +0100 Subject: api, login, auth --- node_modules/fs-write-stream-atomic/.npmignore | 3 + node_modules/fs-write-stream-atomic/.travis.yml | 11 ++ node_modules/fs-write-stream-atomic/LICENSE | 15 ++ node_modules/fs-write-stream-atomic/README.md | 35 ++++ node_modules/fs-write-stream-atomic/index.js | 176 +++++++++++++++++++++ node_modules/fs-write-stream-atomic/package.json | 63 ++++++++ node_modules/fs-write-stream-atomic/test/basic.js | 97 ++++++++++++ node_modules/fs-write-stream-atomic/test/chown.js | 44 ++++++ .../fs-write-stream-atomic/test/rename-eperm.js | 154 ++++++++++++++++++ .../fs-write-stream-atomic/test/rename-fail.js | 30 ++++ .../fs-write-stream-atomic/test/slow-close.js | 40 +++++ .../fs-write-stream-atomic/test/toolong.js | 29 ++++ 12 files changed, 697 insertions(+) create mode 100644 node_modules/fs-write-stream-atomic/.npmignore create mode 100644 node_modules/fs-write-stream-atomic/.travis.yml create mode 100644 node_modules/fs-write-stream-atomic/LICENSE create mode 100644 node_modules/fs-write-stream-atomic/README.md create mode 100644 node_modules/fs-write-stream-atomic/index.js create mode 100644 node_modules/fs-write-stream-atomic/package.json create mode 100644 node_modules/fs-write-stream-atomic/test/basic.js create mode 100644 node_modules/fs-write-stream-atomic/test/chown.js create mode 100644 node_modules/fs-write-stream-atomic/test/rename-eperm.js create mode 100644 node_modules/fs-write-stream-atomic/test/rename-fail.js create mode 100644 node_modules/fs-write-stream-atomic/test/slow-close.js create mode 100644 node_modules/fs-write-stream-atomic/test/toolong.js (limited to 'node_modules/fs-write-stream-atomic') diff --git a/node_modules/fs-write-stream-atomic/.npmignore b/node_modules/fs-write-stream-atomic/.npmignore new file mode 100644 index 0000000..2f24c57 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/.npmignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +.nyc_output/ diff --git a/node_modules/fs-write-stream-atomic/.travis.yml b/node_modules/fs-write-stream-atomic/.travis.yml new file mode 100644 index 0000000..6894662 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/.travis.yml @@ -0,0 +1,11 @@ +language: node_js +sudo: false +before_install: + - "npm -g install npm" +node_js: + - "0.8" + - "0.10" + - "0.12" + - "iojs" + - "4" + - "5" diff --git a/node_modules/fs-write-stream-atomic/LICENSE b/node_modules/fs-write-stream-atomic/LICENSE new file mode 100644 index 0000000..19129e3 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/node_modules/fs-write-stream-atomic/README.md b/node_modules/fs-write-stream-atomic/README.md new file mode 100644 index 0000000..9a15d05 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/README.md @@ -0,0 +1,35 @@ +# fs-write-stream-atomic + +Like `fs.createWriteStream(...)`, but atomic. + +Writes to a tmp file and does an atomic `fs.rename` to move it into +place when it's done. + +First rule of debugging: **It's always a race condition.** + +## USAGE + +```javascript +var fsWriteStreamAtomic = require('fs-write-stream-atomic') +// options are optional. +var write = fsWriteStreamAtomic('output.txt', options) +var read = fs.createReadStream('input.txt') +read.pipe(write) + +// When the write stream emits a 'finish' or 'close' event, +// you can be sure that it is moved into place, and contains +// all the bytes that were written to it, even if something else +// was writing to `output.txt` at the same time. +``` + +### `fsWriteStreamAtomic(filename, [options])` + +* `filename` {String} The file we want to write to +* `options` {Object} + * `chown` {Object} User and group to set ownership after write + * `uid` {Number} + * `gid` {Number} + * `encoding` {String} default = 'utf8' + * `mode` {Number} default = `0666` + * `flags` {String} default = `'w'` + diff --git a/node_modules/fs-write-stream-atomic/index.js b/node_modules/fs-write-stream-atomic/index.js new file mode 100644 index 0000000..1690ff5 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/index.js @@ -0,0 +1,176 @@ +var fs = require('graceful-fs') +var Writable = require('readable-stream').Writable +var util = require('util') +var MurmurHash3 = require('imurmurhash') +var iferr = require('iferr') +var crypto = require('crypto') + +function murmurhex () { + var hash = MurmurHash3('') + for (var ii = 0; ii < arguments.length; ++ii) { + hash.hash('' + arguments[ii]) + } + return hash.result() +} + +var invocations = 0 +function getTmpname (filename) { + return filename + '.' + murmurhex(__filename, process.pid, ++invocations) +} + +var setImmediate = global.setImmediate || setTimeout + +module.exports = WriteStreamAtomic + +// Requirements: +// 1. Write everything written to the stream to a temp file. +// 2. If there are no errors: +// a. moves the temp file into its final destination +// b. emits `finish` & `closed` ONLY after the file is +// fully flushed and renamed. +// 3. If there's an error, removes the temp file. + +util.inherits(WriteStreamAtomic, Writable) +function WriteStreamAtomic (path, options) { + if (!(this instanceof WriteStreamAtomic)) { + return new WriteStreamAtomic(path, options) + } + Writable.call(this, options) + + this.__isWin = options && options.hasOwnProperty('isWin') ? options.isWin : process.platform === 'win32' + + this.__atomicTarget = path + this.__atomicTmp = getTmpname(path) + + this.__atomicChown = options && options.chown + + this.__atomicClosed = false + + this.__atomicStream = fs.WriteStream(this.__atomicTmp, options) + + this.__atomicStream.once('open', handleOpen(this)) + this.__atomicStream.once('close', handleClose(this)) + this.__atomicStream.once('error', handleError(this)) +} + +// We have to suppress default finish emitting, because ordinarily it +// would happen as soon as `end` is called on us and all of the +// data has been written to our target stream. So we suppress +// finish from being emitted here, and only emit it after our +// target stream is closed and we've moved everything around. +WriteStreamAtomic.prototype.emit = function (event) { + if (event === 'finish') return this.__atomicStream.end() + return Writable.prototype.emit.apply(this, arguments) +} + +WriteStreamAtomic.prototype._write = function (buffer, encoding, cb) { + var flushed = this.__atomicStream.write(buffer, encoding) + if (flushed) return cb() + this.__atomicStream.once('drain', cb) +} + +function handleOpen (writeStream) { + return function (fd) { + writeStream.emit('open', fd) + } +} + +function handleClose (writeStream) { + return function () { + if (writeStream.__atomicClosed) return + writeStream.__atomicClosed = true + if (writeStream.__atomicChown) { + var uid = writeStream.__atomicChown.uid + var gid = writeStream.__atomicChown.gid + return fs.chown(writeStream.__atomicTmp, uid, gid, iferr(cleanup, moveIntoPlace)) + } else { + moveIntoPlace() + } + } + + function moveIntoPlace () { + fs.rename(writeStream.__atomicTmp, writeStream.__atomicTarget, iferr(trapWindowsEPERM, end)) + } + + function trapWindowsEPERM (err) { + if (writeStream.__isWin && + err.syscall && err.syscall === 'rename' && + err.code && err.code === 'EPERM' + ) { + checkFileHashes(err) + } else { + cleanup(err) + } + } + + function checkFileHashes (eperm) { + var inprocess = 2 + var tmpFileHash = crypto.createHash('sha512') + var targetFileHash = crypto.createHash('sha512') + + fs.createReadStream(writeStream.__atomicTmp) + .on('data', function (data, enc) { tmpFileHash.update(data, enc) }) + .on('error', fileHashError) + .on('end', fileHashComplete) + fs.createReadStream(writeStream.__atomicTarget) + .on('data', function (data, enc) { targetFileHash.update(data, enc) }) + .on('error', fileHashError) + .on('end', fileHashComplete) + + function fileHashError () { + if (inprocess === 0) return + inprocess = 0 + cleanup(eperm) + } + + function fileHashComplete () { + if (inprocess === 0) return + if (--inprocess) return + if (tmpFileHash.digest('hex') === targetFileHash.digest('hex')) { + return cleanup() + } else { + return cleanup(eperm) + } + } + } + + function cleanup (err) { + fs.unlink(writeStream.__atomicTmp, function () { + if (err) { + writeStream.emit('error', err) + writeStream.emit('close') + } else { + end() + } + }) + } + + function end () { + // We have to use our parent class directly because we suppress `finish` + // events fired via our own emit method. + Writable.prototype.emit.call(writeStream, 'finish') + + // Delay the close to provide the same temporal separation a physical + // file operation would have– that is, the close event is emitted only + // after the async close operation completes. + setImmediate(function () { + writeStream.emit('close') + }) + } +} + +function handleError (writeStream) { + return function (er) { + cleanupSync() + writeStream.emit('error', er) + writeStream.__atomicClosed = true + writeStream.emit('close') + } + function cleanupSync () { + try { + fs.unlinkSync(writeStream.__atomicTmp) + } finally { + return + } + } +} diff --git a/node_modules/fs-write-stream-atomic/package.json b/node_modules/fs-write-stream-atomic/package.json new file mode 100644 index 0000000..b95ae2b --- /dev/null +++ b/node_modules/fs-write-stream-atomic/package.json @@ -0,0 +1,63 @@ +{ + "_from": "fs-write-stream-atomic@^1.0.8", + "_id": "fs-write-stream-atomic@1.0.10", + "_inBundle": false, + "_integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "_location": "/fs-write-stream-atomic", + "_phantomChildren": {}, + "_requested": { + "type": "range", + "registry": true, + "raw": "fs-write-stream-atomic@^1.0.8", + "name": "fs-write-stream-atomic", + "escapedName": "fs-write-stream-atomic", + "rawSpec": "^1.0.8", + "saveSpec": null, + "fetchSpec": "^1.0.8" + }, + "_requiredBy": [ + "/copy-concurrently", + "/move-concurrently" + ], + "_resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "_shasum": "b47df53493ef911df75731e70a9ded0189db40c9", + "_spec": "fs-write-stream-atomic@^1.0.8", + "_where": "/home/pruss/Dev/3-minute-website/node_modules/move-concurrently", + "author": { + "name": "Isaac Z. Schlueter", + "email": "i@izs.me", + "url": "http://blog.izs.me/" + }, + "bugs": { + "url": "https://github.com/npm/fs-write-stream-atomic/issues" + }, + "bundleDependencies": false, + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "deprecated": false, + "description": "Like `fs.createWriteStream(...)`, but atomic.", + "devDependencies": { + "rimraf": "^2.4.4", + "standard": "^5.4.1", + "tap": "^2.3.1" + }, + "directories": { + "test": "test" + }, + "homepage": "https://github.com/npm/fs-write-stream-atomic", + "license": "ISC", + "main": "index.js", + "name": "fs-write-stream-atomic", + "repository": { + "type": "git", + "url": "git+https://github.com/npm/fs-write-stream-atomic.git" + }, + "scripts": { + "test": "standard && tap --coverage test/*.js" + }, + "version": "1.0.10" +} diff --git a/node_modules/fs-write-stream-atomic/test/basic.js b/node_modules/fs-write-stream-atomic/test/basic.js new file mode 100644 index 0000000..d0205e1 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/test/basic.js @@ -0,0 +1,97 @@ +var fs = require('graceful-fs') +var test = require('tap').test +var path = require('path') +var writeStream = require('../index.js') + +var rename = fs.rename +fs.rename = function (from, to, cb) { + setTimeout(function () { + rename(from, to, cb) + }, 100) +} + +test('basic', function (t) { + // open 10 write streams to the same file. + // then write to each of them, and to the target + // and verify at the end that each of them does their thing + var target = path.resolve(__dirname, 'test.txt') + var n = 10 + + // We run all of our assertions twice: + // once for finish, once for close + // There are 6 assertions, two fixed, plus 4 lines in the file. + t.plan(n * 2 * 6) + + var streams = [] + for (var i = 0; i < n; i++) { + var s = writeStream(target) + s.on('finish', verifier('finish', i)) + s.on('close', verifier('close', i)) + streams.push(s) + } + + function verifier (ev, num) { + return function () { + if (ev === 'close') { + t.equal(this.__emittedFinish, true, num + '. closed only after finish') + } else { + this.__emittedFinish = true + t.equal(ev, 'finish', num + '. finished') + } + + // make sure that one of the atomic streams won. + var res = fs.readFileSync(target, 'utf8') + var lines = res.trim().split(/\n/) + lines.forEach(function (line, lineno) { + var first = lines[0].match(/\d+$/)[0] + var cur = line.match(/\d+$/)[0] + t.equal(cur, first, num + '. line ' + lineno + ' matches') + }) + + var resExpr = /^first write \d+\nsecond write \d+\nthird write \d+\nfinal write \d+\n$/ + t.similar(res, resExpr, num + '. content matches') + } + } + + // now write something to each stream. + streams.forEach(function (stream, i) { + stream.write('first write ' + i + '\n') + }) + + // wait a sec for those writes to go out. + setTimeout(function () { + // write something else to the target. + fs.writeFileSync(target, 'brutality!\n') + + // write some more stuff. + streams.forEach(function (stream, i) { + stream.write('second write ' + i + '\n') + }) + + setTimeout(function () { + // Oops! Deleted the file! + fs.unlinkSync(target) + + // write some more stuff. + streams.forEach(function (stream, i) { + stream.write('third write ' + i + '\n') + }) + + setTimeout(function () { + fs.writeFileSync(target, 'brutality TWO!\n') + streams.forEach(function (stream, i) { + stream.end('final write ' + i + '\n') + }) + }, 50) + }, 50) + }, 50) +}) + +test('cleanup', function (t) { + fs.readdirSync(__dirname).filter(function (f) { + return f.match(/^test.txt/) + }).forEach(function (file) { + fs.unlinkSync(path.resolve(__dirname, file)) + }) + t.end() +}) diff --git a/node_modules/fs-write-stream-atomic/test/chown.js b/node_modules/fs-write-stream-atomic/test/chown.js new file mode 100644 index 0000000..1733cf2 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/test/chown.js @@ -0,0 +1,44 @@ +'use strict' +var fs = require('graceful-fs') +var path = require('path') +var test = require('tap').test +var rimraf = require('rimraf') +var writeStream = require('../index.js') + +var target = path.resolve(__dirname, 'test-chown') + +test('chown works', function (t) { + t.plan(1) + var stream = writeStream(target, {chown: {uid: process.getuid(), gid: process.getgid()}}) + var hadError = false + stream.on('error', function (er) { + hadError = true + console.log('#', er) + }) + stream.on('close', function () { + t.is(hadError, false, 'no errors before close') + }) + stream.end() +}) + +test('chown fails', function (t) { + t.plan(1) + fs.chown = function (file, uid, gid, cb) { + cb(new Error('TEST BREAK')) + } + var stream = writeStream(target, {chown: {uid: process.getuid(), gid: process.getgid()}}) + var hadError = false + stream.on('error', function (er) { + hadError = true + console.log('#', er) + }) + stream.on('close', function () { + t.is(hadError, true, 'error before close') + }) + stream.end() +}) + +test('cleanup', function (t) { + rimraf.sync(target) + t.end() +}) diff --git a/node_modules/fs-write-stream-atomic/test/rename-eperm.js b/node_modules/fs-write-stream-atomic/test/rename-eperm.js new file mode 100644 index 0000000..b1be0f3 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/test/rename-eperm.js @@ -0,0 +1,154 @@ +'use strict' +var fs = require('graceful-fs') +var path = require('path') +var test = require('tap').test +var rimraf = require('rimraf') +var writeStream = require('../index.js') + +var target = path.resolve(__dirname, 'test-rename-eperm1') +var target2 = path.resolve(__dirname, 'test-rename-eperm2') +var target3 = path.resolve(__dirname, 'test-rename-eperm3') + +test('rename eperm none existing file', function (t) { + t.plan(2) + + var _rename = fs.rename + fs.existsSync = function (src) { + return true + } + fs.rename = function (src, dest, cb) { + // simulate a failure during rename where the file + // is renamed successfully but the process encounters + // an EPERM error and the target file does not exist + _rename(src, dest, function (e) { + var err = new Error('TEST BREAK') + err.syscall = 'rename' + err.code = 'EPERM' + cb(err) + }) + } + + var stream = writeStream(target, { isWin: true }) + var hadError = false + var calledFinish = false + stream.on('error', function (er) { + hadError = true + console.log('#', er) + }) + stream.on('finish', function () { + calledFinish = true + }) + stream.on('close', function () { + t.is(hadError, true, 'error was caught') + t.is(calledFinish, false, 'finish was called before close') + }) + stream.end() +}) + +// test existing file with diff. content +test('rename eperm existing file different content', function (t) { + t.plan(2) + + var _rename = fs.rename + fs.existsSync = function (src) { + return true + } + fs.rename = function (src, dest, cb) { + // simulate a failure during rename where the file + // is renamed successfully but the process encounters + // an EPERM error and the target file that has another content than the + // destination + _rename(src, dest, function (e) { + fs.writeFile(src, 'dest', function (writeErr) { + if (writeErr) { + return console.log('WRITEERR: ' + writeErr) + } + + fs.writeFile(target2, 'target', function (writeErr) { + if (writeErr) { + return console.log('WRITEERR: ' + writeErr) + } + + var err = new Error('TEST BREAK') + err.syscall = 'rename' + err.code = 'EPERM' + cb(err) + }) + }) + }) + } + + var stream = writeStream(target2, { isWin: true }) + var hadError = false + var calledFinish = false + stream.on('error', function (er) { + hadError = true + console.log('#', er) + }) + stream.on('finish', function () { + calledFinish = true + }) + stream.on('close', function () { + t.is(hadError, true, 'error was caught') + t.is(calledFinish, false, 'finish was called before close') + }) + stream.end() +}) + +// test existing file with the same content +// test existing file with diff. content +test('rename eperm existing file different content', function (t) { + t.plan(2) + + var _rename = fs.rename + fs.existsSync = function (src) { + return true + } + fs.rename = function (src, dest, cb) { + // simulate a failure during rename where the file + // is renamed successfully but the process encounters + // an EPERM error and the target file that has the same content than the + // destination + _rename(src, dest, function (e) { + fs.writeFile(src, 'target2', function (writeErr) { + if (writeErr) { + return console.log('WRITEERR: ' + writeErr) + } + + fs.writeFile(target3, 'target2', function (writeErr) { + if (writeErr) { + return console.log('WRITEERR: ' + writeErr) + } + + var err = new Error('TEST BREAK') + err.syscall = 'rename' + err.code = 'EPERM' + cb(err) + }) + }) + }) + } + + var stream = writeStream(target3, { isWin: true }) + var hadError = false + var calledFinish = false + stream.on('error', function (er) { + hadError = true + console.log('#', er) + }) + stream.on('finish', function () { + calledFinish = true + }) + stream.on('close', function () { + t.is(hadError, false, 'error was caught') + t.is(calledFinish, true, 'finish was called before close') + }) + stream.end() +}) + +test('cleanup', function (t) { + rimraf.sync(target) + rimraf.sync(target2) + rimraf.sync(target3) + t.end() +}) diff --git a/node_modules/fs-write-stream-atomic/test/rename-fail.js b/node_modules/fs-write-stream-atomic/test/rename-fail.js new file mode 100644 index 0000000..7e27f0b --- /dev/null +++ b/node_modules/fs-write-stream-atomic/test/rename-fail.js @@ -0,0 +1,30 @@ +'use strict' +var fs = require('graceful-fs') +var path = require('path') +var test = require('tap').test +var rimraf = require('rimraf') +var writeStream = require('../index.js') + +var target = path.resolve(__dirname, 'test-rename') + +test('rename fails', function (t) { + t.plan(1) + fs.rename = function (src, dest, cb) { + cb(new Error('TEST BREAK')) + } + var stream = writeStream(target) + var hadError = false + stream.on('error', function (er) { + hadError = true + console.log('#', er) + }) + stream.on('close', function () { + t.is(hadError, true, 'error before close') + }) + stream.end() +}) + +test('cleanup', function (t) { + rimraf.sync(target) + t.end() +}) diff --git a/node_modules/fs-write-stream-atomic/test/slow-close.js b/node_modules/fs-write-stream-atomic/test/slow-close.js new file mode 100644 index 0000000..9840a6e --- /dev/null +++ b/node_modules/fs-write-stream-atomic/test/slow-close.js @@ -0,0 +1,40 @@ +'use strict' +var fs = require('graceful-fs') +var path = require('path') +var test = require('tap').test +var rimraf = require('rimraf') +var writeStream = require('../index.js') + +var target = path.resolve(__dirname, 'test-chown') + +test('slow close', function (t) { + t.plan(2) + // The goal here is to simulate the "file close" step happening so slowly + // that the whole close/rename process could finish before the file is + // actually closed (and thus buffers truely flushed to the OS). In + // previous versions of this module, this would result in the module + // emitting finish & close before the file was fully written and in + // turn, could break other layers that tried to read the new file. + var realEmit = fs.WriteStream.prototype.emit + var reallyClosed = false + fs.WriteStream.prototype.emit = function (event) { + if (event !== 'close') return realEmit.apply(this, arguments) + setTimeout(function () { + reallyClosed = true + realEmit.call(this, 'close') + }.bind(this), 200) + } + var stream = writeStream(target) + stream.on('finish', function () { + t.is(reallyClosed, true, "didn't finish before target was closed") + }) + stream.on('close', function () { + t.is(reallyClosed, true, "didn't close before target was closed") + }) + stream.end() +}) + +test('cleanup', function (t) { + rimraf.sync(target) + t.end() +}) diff --git a/node_modules/fs-write-stream-atomic/test/toolong.js b/node_modules/fs-write-stream-atomic/test/toolong.js new file mode 100644 index 0000000..f146cc5 --- /dev/null +++ b/node_modules/fs-write-stream-atomic/test/toolong.js @@ -0,0 +1,29 @@ +var path = require('path') +var test = require('tap').test +var writeStream = require('../index.js') + +function repeat (times, string) { + var output = '' + for (var ii = 0; ii < times; ++ii) { + output += string + } + return output +} + +var target = path.resolve(__dirname, repeat(1000, 'test')) + +test('name too long', function (t) { + t.plan(2) + var stream = writeStream(target) + var hadError = false + stream.on('error', function (er) { + if (!hadError) { + t.is(er.code, 'ENAMETOOLONG', target.length + ' character name results in ENAMETOOLONG') + hadError = true + } + }) + stream.on('close', function () { + t.ok(hadError, 'got error before close') + }) + stream.end() +}) -- cgit v1.2.3