b0VIM 8.0wZ:zllexoyolexoyo-thinkpad~lexoyo/Documents/unifile/lib/unifile-github.jsutf-8 3210#"! Utp n\oypD h 8> ct e o<ad nrM/.SRO { :    c 1 s < !   S d wMc&k_[0`$[*D@(# * @param {Object} config - Configuration object * @constructor /**class GitHubConnector { */ * {@link https://developer.github.com/v3/oauth/#web-application-flow|here} * learn more about GitHub OAuth Web application flow * You can register a new application {@link https://github.com/settings/applications/new|here} and * This will need a registered GitHub application with valid redirection for your server. * * Service connector for {@link https://github.com|GitHub} plateform./**const callAPI = Symbol('callAPI');const commitBlob = Symbol('commitBlob');const assignSessionAccount = Symbol('assignSessionAccount');const createBlob = Symbol('createBlob');const transformTree = Symbol('transformTree');const commit = Symbol('commit');const createBranch = Symbol('createBranch');} else done(null, filteredTree); done(new UnifileError(UnifileError.ENOENT, 'Not Found')); if(filteredTree.length === treeRes.tree.length) }); return !regex.test(file.path); const filteredTree = treeRes.tree.filter(function(file) { const regex = new RegExp('^' + path + '$|^' + path + '(/)');function removeFile(path, treeRes, done) { */ * @private * @param {string} [branch=master] - Branch containing file/folder * @param {Object} treeRes[].tree - Commit tree * @param {Object} treeRes - Result of a GET request to the tree API * @param {string} path - Path to the file/folder to delete * Remove a file/folder by transforming the given tree/**} }); return Object.assign({}, file, {path: file.path.replace(regex, dest + '$1')}); // Overrides file path const regex = new RegExp('^' + src + '$|^' + src + '(/)'); return treeRes.tree.map(function(file) {function move(src, dest, treeRes) { */ * @private * @param {string} [branch=master] - Branch containing file/folder * @param {Object} treeRes[].tree - Commit tree * @param {string} dest - Destination path relative to branch root * @param {string} src - Source path relative to branch root * Move a folder or a file in a branch by transforming the given tree/**} }); }); paginate(reqOptions, res.headers.link, memo.concat(JSON.parse(body))).then(resolve); request(reqOptions, function(err, res, body) { reqOptions.url = matches[1]; return new Promise(function(resolve, reject) { } return Promise.resolve(memo); if(!matches) { // End of pagination }); return matches !== null; matches = link.trim().match(/<(.+)>;\s*rel="next"/); links.some(function(link) { let matches; const links = link.split(/,\s*/);function paginate(reqOptions, link, memo) { */ * @private * @return {Promise} a Promise of aggregated result * @param {Object[]} memo - Aggregator of result * @param {string} link - Link header * @param {Object} reqOptions - Options to pass to the request. Url will be overidden * Handle GitHub pagination/**} return cleanPath.split('/').filter((s) => s !== ''); const cleanPath = path.startsWith('/') ? path.substr(1) : path;function getPathTokens(path) { */ * @private * @return {Array} an array with path levels as elements * @param {String} path - Path to split * Remove first '/', split the path and remove empty tokens/*const {UnifileError, BatchError} = require('./error.js');const APP_PERMISSION = 'scope=repo,delete_repo,user';const SERVICE_HOST = 'github.com';const NAME = 'github';const Tools = require('unifile-common-tools');const Mime = require('mime');const Promise = require('bluebird');const request = require('request');const {Writable, Transform, PassThrough} = require('stream');const Url = require('url');'use strict';ad[ r,zn81& k M : ) "    module.exports = GitHubConnector;} } } }); }); } reject(e); } catch (e) { } else resolve(result); paginate(reqOptions, res.headers.link, result).then(resolve); if(res.headers.hasOwnProperty('link')) { const result = res.statusCode !== 204 ? JSON.parse(body) : null; try { } return reject(new UnifileError(code, message)); })(); } return {code: UnifileError.EIO, message: defaultMessage}; default: return {code: UnifileError.ENOENT, message: 'Not Found'}; case 404: return {code: UnifileError.EACCES, message: defaultMessage}; case 403: return {code: UnifileError.EACCES, message: 'Bad credentials'}; case 401: switch (res.statusCode) {ad^  lZ<( f J  i ]  L & "   g y Z (  s_KbT< l> R? C<0#m-{?87 for(const currentAction of fileActions) { const newTrees = {}; actionsChain = actionsChain.then(() => this[transformTree](session, splitPath[0], (treeRes, done) => { } if(sameBranch) fileActions.push(actionQueue.shift()); sameBranch = nextSplitPath.length > 2 && lastRepo === nextSplitPath[0] && lastBranch === nextSplitPath[1]; const [lastRepo, lastBranch] = getPathTokens(action.path); const nextSplitPath = getPathTokens(actionQueue[0].path); while(actionQueue.length > 0 && sameBranch) { let sameBranch = true; // Get all the file action on this branch to group in a commit const fileActions = [action]; default: break; } console.warn(`Unsupported batch action on repo/branch: ${actionName}`); default: break; }); `Could not complete action ${actionName}: ${err.message}`); UnifileError.EINVAL, throw new BatchError( (err) => { .catch((err) => err.name !== 'BatchError', actionsChain = actionsChain.then(() => this.rename(session, action.path, action.destination)) 'Rename actions should have a destination')); UnifileError.EINVAL, return Promise.reject(new BatchError( if(!action.destination) case 'rename': `Could not complete action ${actionName}: Cannot create file here.`)); UnifileError.ENOTSUP, return Promise.reject(new UnifileError( case 'writefile': break; (err) => {throw new Error(`Could not complete action ${actionName}: ${err.message}`);}); .catch((err) => err.name !== 'BatchError', actionsChain = actionsChain.then(() => this[actionName](session, action.path)) case 'mkdir': case 'rmdir': switch (actionName) { const actionName = action.name.toLowerCase(); case 2: case 1: 'Cannot execute batch action without a path')); UnifileError.EINVAL, case 0: return Promise.reject(new BatchError( switch (splitPath.length) { const splitPath = getPathTokens(action.path); const action = actionQueue.shift(); while(actionQueue.length > 0) { .filter((action) => ['rmdir', 'unlink', 'mkdir', 'writefile', 'rename'].indexOf(action.name.toLowerCase()) > -1); const actionQueue = actions.slice() // Filter invalid batch actions le console.log('xxxxxxxxxxxxxxxx console.log('xxxxxxxxxxxxxxxxxxxxxx batch(session, actions, message) { } } 'Remove ' + path, splitPath[1]); return this[transformTree](session, splitPath[0], removeFile.bind(undefined, path), const path = splitPath.slice(2).join('/'); default: // Remove file/folder }); } throw new UnifileError(UnifileError.INVAL, 'You cannot leave this folder empty.'); else { return this[callAPI](session, repoPath + '/git/refs/heads/' + splitPath[1], null, 'DELETE'); if(branches.length > 1) .then((branches) => { return this[callAPI](session, repoPath + '/branches', null, 'GET') case 2: // Remove branch return this[callAPI](session, repoPath, null, 'DELETE'); case 1: // Remove repo return Promise.reject(new UnifileError(UnifileError.INVAL, 'Cannot remove path with an empty name.')); case 0: // Error switch (splitPath.length) { const repoPath = '/repos/' + session.account.login + '/' + splitPath[0]; const splitPath = getPathTokens(path); rmdir(session, path) { } 'Remove ' + filePath, splitPath[1]); return this[transformTree](session, splitPath[0], removeFile.bind(undefined, filePath), const filePath = splitPath.slice(2).join('/'); return Promise.reject(new UnifileError(UnifileError.EISDIR, 'Path is a folder. Use rmdir()'));ad8Q7 t  } e 6  g I  { h I * l e Y L ) Vha` for(const currentAction of fileActions) { const newTrees = {}; actionsChain = actionsChain.then(() => this[transformTree](session, splitPath[0], (treeRes, done) => { } if(sameBranch) fileActions.push(actionQueue.shift()); sameBranch = nextSplitPath.length > 2 && lastRepo === nextSplitPath[0] && lastBranch === nextSplitPath[1]; const [lastRepo, lastBranch] = getPathTokens(action.path); const nextSplitPath = getPathTokens(actionQueue[0].path); while(actionQueue.length > 0 && sameBranch) { let sameBranch = true; // Get all the file action on this branch to group in a commit const fileActions = [action]; default: break; } console.warn(`Unsupported batch action on repo/branch: ${actionName}`); default: break; }); `Could not complete action ${actionName}: ${err.message}`); UnifileError.EINVAL, throw new BatchError( (err) => { .catch((err) => err.name !== 'BatchError', actionsChain = actionsChain.then(() => this.rename(session, action.path, action.destination)) 'Rename actions should have a destination')); UnifileError.EINVAL, return Promise.reject(new BatchError( if(!action.destination) case 'rename': `Could not complete action ${actionName}: Cannot create file here.`)); UnifileError.ENOTSUP, return Promise.reject(new UnifileError( case 'writefile': break; (err) => {throw new Error(`Could not complete action ${actionName}: ${err.message}`);}); .catch((err) => err.name !== 'BatchError', actionsChain = actionsChain.then(() => this[actionName](session, action.path)) case 'mkdir': case 'rmdir': switch (actionName) { const actionName = action.name.toLowerCase(); case 2: case 1: 'Cannot execute batch action without a path')); UnifileError.EINVAL, case 0: return Promise.reject(new BatchError( switch (splitPath.length) { const splitPath = getPathTokens(action.path); const action = actionQueue.shift(); while(actionQueue.length > 0) { .filter((action) => ['rmdir', 'unlink', 'mkdir', 'writefile', 'rename'].indexOf(action.name.toLowerCase()) > -1); const actionQueue = actions.slice() // Filter invalid batch actions let actionsChain = Promise.resolve();ad\r&O >  r A @   f c b M 4 o l k M 6 B  `W~i\<7bZT>o;p# return this[callAPI](sessionCopy, '/user', null, 'GET') sessionCopy.token = token; const sessionCopy = Object.assign({}, session); // Create a copy to only set the true token when we know it's the good one 'Invalid token. It must start with either "token" or "Basic".')); UnifileError.EACCES, return Promise.reject(new UnifileError( if(!token.startsWith('token ') && !token.startsWith('Basic ')) // Check if token is a valid OAuth or Basic token setAccessToken(session, token) { } } return Promise.reject(new UnifileError(UnifileError.EACCES, 'Invalid credentials')); } else { }); return this.setAccessToken(session, `token ${token}`); .then((token) => { }) }); else resolve(body.access_token); reject(new UnifileError(UnifileError.EACCES, 'Unable to get access token. Please check your credentials.')); else if(response.statusCode >= 400 || 'error' in body) if(err) reject(new UnifileError(UnifileError.EINVAL, 'Error while calling GitHub API. ' + err)); }, function(err, response, body) { json: true }, state: session.state code: loginInfos.code, client_secret: this.clientSecret, client_id: this.clientId, body: { method: 'POST', url: this.oauthCallbackUrl + '/access_token', request({ return new Promise((resolve, reject) => { return Promise.reject(new UnifileError(UnifileError.EACCES, 'Invalid request (cross-site request)')); if(loginInfos.state !== session.state) } else if('state' in loginInfos && 'code' in loginInfos) { // OAuth return this.setAccessToken(session, `Basic ${auth}`); const auth = new Buffer(loginInfos.user + ':' + loginInfos.password).toString('base64'); } else if('user' in loginInfos && 'password' in loginInfos) { // Basic auth return this.setAccessToken(session, `Basic ${new Buffer(url.auth).toString('base64')}`); this.serviceHost = url.host || this.serviceHost; 'Invalid URL. You must provide authentication: http://user:pwd@host')); UnifileError.EACCES, return Promise.reject(new UnifileError( if(!url.auth) const url = Url.parse(loginInfos); if(loginInfos.constructor === String) { // Authenticated URL login(session, loginInfos) { } }, this.infos); username: (session && session.account) ? session.account.display_name : undefined isOAuth: true, isLoggedIn: !!(session && ('token' in session) || ('basic' in session)), return Object.assign({ getInfos(session) { } this.name = this.infos.name; }); description: 'Edit files from your GitHub repository.' icon: '../assets/github.png', displayName: 'GitHub', name: NAME, this.infos = Tools.mergeInfos(config.infos, { this.redirectUri = config.redirectUri || null; this.oauthCallbackUrl = `https://${this.serviceHost}/login/oauth`; this.serviceHost = config.serviceHost || SERVICE_HOST; this.clientSecret = config.clientSecret; this.clientId = config.clientId; throw new Error('Invalid configuration. Please refer to the documentation to get the required fields.'); if(!config || !config.clientId || !config.clientSecret) constructor(config) { */ * @param {ConnectorStaticInfos} [config.infos] - Connector infos to override * @param {string} [config.serviceHost=github.com] - Hostname of the service * @param {string} [config.name=github] - Name of the connector * You still need to register it in your GitHub App * @param {string} [config.redirectUri] - GitHub application redirect URI. * @param {string} config.clientSecret - GitHub application client secret * @param {string} config.clientId - GitHub application client IDad%%ytnkjM. pA { z ` 7 "  i D 5  f  k M %   u l c [ P ( {ZK3}uqpXUT=]R6oN7$d#bZRG%$ default: // Get a content stat break; }); }; mime: 'application/git-branch' isDir: true, name: branch.name, modified: branch.commit.commit.author.date, size: 'N/A', return { .then(function(branch) { resultPromise = this[callAPI](session, apiPath, null, 'GET') apiPath = '/repos/' + session.account.login + '/' + splitPath[0] + '/branches/' + splitPath[1]; case 2: // Get branch stat break; }); }; mime: 'application/git-repo' isDir: true, name: repo.name, modified: repo.updated_at, size: repo.size, return { .then(function(repo) { resultPromise = this[callAPI](session, apiPath, null, 'GET') apiPath = '/repos/' + session.account.login + '/' + splitPath[0]; case 1: // Get repo stat break; case 0: resultPromise = Promise.reject(new UnifileError(UnifileError.EINVAL, 'You must provide a path to stat')); switch (splitPath.length) { let apiPath; let resultPromise; const splitPath = getPathTokens(path); stat(session, path) { } return resultPromise; } }); }); }; mime: isDir ? 'application/directory' : Mime.getType(item.name) isDir: isDir, name: item.name, modified: commits[0].commit.author.date, size: item.size, return { const isDir = item.type === 'dir'; .then(function(commits) { return this[callAPI](session, apiPath + '/commits', {path: item.path, sha: splitPath[1]}, 'GET') .map((item) => { resultPromise = this[callAPI](session, apiPath + '/contents/' + filePath, reqData, 'GET') }; ref: splitPath[1] const reqData = { const filePath = splitPath.slice(2).join('/'); apiPath = '/repos/' + session.account.login + '/' + splitPath[0]; default: // List files of one branch break; }); }); }; mime: 'application/git-branch' isDir: true, name: item.name, modified: date, size: 'N/A', return { .then(function(date) { }) return result.commit.author.date; .then(function(result) { return this[callAPI](session, Url.parse(item.commit.url).path, null, 'GET') .map((item) => { resultPromise = this[callAPI](session, apiPath, null, 'GET') apiPath = '/repos/' + session.account.login + '/' + splitPath[0] + '/branches'; case 1: // List all branches break; }); }); }; mime: 'application/git-repo' isDir: true, name: item.name, modified: item.updated_at, size: item.size, return { return res.map(function(item) { .then(function(res) { resultPromise = this[callAPI](session, '/user/repos', {affiliation: 'owner'}, 'GET') case 0: // List repos switch (splitPath.length) { let apiPath; let resultPromise; const splitPath = getPathTokens(path); readdir(session, path) { //Filesystem commands } + (this.redirectUri ? '&redirect_uri=' + this.redirectUri : '')); + '&state=' + session.state + '&client_id=' + this.clientId + '/authorize?' + APP_PERMISSION return Promise.resolve(this.oauthCallbackUrl session.state = (+new Date() * Math.random()).toString(36).replace('.', ''); // Generate a random string for the state getAuthorizeURL(session) { } return Promise.resolve(session); Tools.clearSession(session); clearAccessToken(session) { } }); return session.token; session.token = sessionCopy.token; .then(() => { .then(this[assignSessionAccount].bind(undefined, session))ad pqZS]I$  w ^ .  } T > /  v Y I 0   @ " { H G ha~}[2o0-,T.keP;'"gfE0  write(chunk, encoding, callback) { const aggregator = new Writable({ const chunks = []; // Catch Blob request response transformer.pipe(stream); const stream = this[callAPI](session, apiPath, {}, 'POST', true); // Make the request and pipe the transformer as input transformer.first = true; }); } callback(null); this.push('"}'); flush(callback) { }, callback(null, chunk.toString('base64')); } this.first = false; this.push('{"encoding": "base64", "content": "'); if(this.first) { transform(chunk, encoding, callback) { const transformer = new Transform({ // This will encapsulate the raw content into an acceptable Blob request const apiPath = '/repos/' + session.account.login + '/' + splitPath[0] + '/git/blobs'; } return stream; }); stream.emit('error', new UnifileError(UnifileError.ENOTSUP, 'This folder can only contain folders.')); process.nextTick(() => { const stream = new PassThrough(); if(splitPath.length < 3) { const splitPath = getPathTokens(path); createWriteStream(session, path) { } .then((blob) => this[commitBlob](session, splitPath, blob)); return this[createBlob](session, splitPath[0], data) } return Promise.reject(new UnifileError(UnifileError.ENOTSUP, 'This folder can only contain folders.')); if(splitPath.length < 3) { const splitPath = getPathTokens(path); writeFile(session, path, data) { } } }); else throw err; if(err.message.startsWith('Invalid request')) throw new Error('Reference already exists'); .catch((err) => { return this[callAPI](session, apiPath + '/.gitkeep', reqData, 'PUT') }; branch: splitPath[1] content: new Buffer('').toString('base64'), message: 'Create ' + filePath, reqData = { apiPath = '/repos/' + session.account.login + '/' + splitPath[0] + '/contents/' + filePath; const filePath = splitPath.slice(2).join('/'); default: // Create a folder (with a .gitkeep file in it because git doesn't track empty folder) return this[createBranch](session, splitPath[0], splitPath[1]); case 2: // Create a branch .then(() => this.rename(session, path + '/master/README.md', path + '/master/.gitkeep')); // Renames default README to a more discreet .gitkeep return this[callAPI](session, apiPath, reqData, 'POST') }; auto_init: true name: splitPath[0], reqData = { apiPath = '/user/repos'; case 1: // Create a repo return Promise.reject(new UnifileError(UnifileError.EINVAL, 'Cannot create dir with an empty name.')); case 0: // Error switch (splitPath.length) { let apiPath; let reqData = null; const splitPath = getPathTokens(path); mkdir(session, path) { } return resultPromise; } }); } }); }; mime: Mime.getType(stat.name) isDir: false, name: stat.name, modified: commit[0].commit.author.date, size: stat.size, return { .then(function(commit) { return this[callAPI](session, apiPath + '/commits', {path: stat.path, sha: splitPath[1]}, 'GET') } else { }; mime: 'application/directory' isDir: true, name: filePath.split('/').pop(), modified: 'N/A', size: 'N/A', return { if(Array.isArray(stat)) { .then((stat) => { resultPromise = this[callAPI](session, apiPath + '/contents/' + filePath, reqData, 'GET') }; ref: splitPath[1] const reqData = { const filePath = splitPath.slice(2).join('/'); apiPath = '/repos/' + session.account.login + '/' + splitPath[0];ad'h}[W- g D C  K > K G F _ R . >  zd]WRL+sKS [Ih' if(splitPath.length < 3) const splitPath = getPathTokens(path); if(!path) return Promise.reject(new UnifileError(UnifileError.EINVAL, 'Cannot remove path with an empty name.')); unlink(session, path) { } } 'Move ' + fileSrc + ' to ' + fileDest, splitPath[1]); return this[transformTree](session, splitPath[0], (tree, done) => done(null, move(fileSrc, fileDest, tree)), const fileDest = splitPathDest.slice(2).join('/'); const fileSrc = splitPath.slice(2).join('/'); default: // Rename a file/folder }); return this[callAPI](session, apiPath + splitPath[1], null, 'DELETE'); .then(() => { return this[createBranch](session, splitPath[0], splitPathDest[1], splitPath[1]) apiPath = '/repos/' + session.account.login + '/' + splitPath[0] + '/git/refs/heads/'; case 2: // Rename branch (actually copy src to dest then remove src) return this[callAPI](session, apiPath, reqData, 'PATCH'); const reqData = {name: dest}; apiPath = '/repos/' + session.account.login + '/' + splitPath[0]; case 1: // Rename repo return Promise.reject(new UnifileError(UnifileError.EINVAL, 'Cannot rename path with an empty name.')); case 0: // Error switch (splitPath.length) { let apiPath; const splitPathDest = getPathTokens(dest); if(!dest) return Promise.reject(new Error('Cannot rename path with an empty destination')); const splitPath = getPathTokens(src); rename(session, src, dest) { } .pipe(transformer); return this.readFile(session, path, true) transformer.errorToken = 'message":"'; transformer.contentToken = 'content":"'; transformer.isContent = false; }); } } } callback(null); // Drop content } else { this.emit('error', new Error(extract(data, idx, this.errorToken))); // Request errored } else if((idx = data.indexOf(this.errorToken)) > -1) { callback(null, Buffer.from(extract(data, idx, this.contentToken), 'base64').toString()); // Content detected, returns it until " this.isContent = true; if((idx = data.indexOf(this.contentToken)) > -1) { let idx; // TODO better start detection } else { callback(null, data.split('"')[0]); // return all the content until a " shows up if(this.isContent) { const data = chunk.toString(); transform(chunk, encoding, callback) { const transformer = new Transform({ } return data.substr(idx + token.length).split('"')[0]; function extract(data, idx, token) { createReadStream(session, path) { } } }); } return Promise.reject(new UnifileError(UnifileError.EISDIR, 'Path is a directory.')); } else { return Buffer.from(res.content, res.encoding); if(res.type === 'file') { return promise.then(function(res) { else { if(isStream) return promise; var promise = this[callAPI](session, apiPath, {ref: splitPath[1]}, 'GET', isStream); + splitPath.slice(2).join('/'); + '/' + splitPath[0] + '/contents/' const apiPath = '/repos/' + session.account.login } return Promise.reject(new UnifileError(UnifileError.ENOTSUP, 'This folder only contain folders.')); if(!isStream && splitPath.length < 3) { const splitPath = getPathTokens(path); readFile(session, path, isStream = false) { } return transformer; }); }); transformer.emit('close'); .then(() => { this[commitBlob](session, splitPath, JSON.parse(Buffer.concat(chunks).toString())) aggregator.on('finish', () => { // Now commit the blob with the full response stream.pipe(aggregator); }); } callback(null); chunks.push(chunk);adclV#~E*  c O ; /   S  * q %  v ^ P E 5 $ e+sZ]A(o3)i`[W@=<.-(CN .then((res) => { return this[callAPI](session, apiPath + '/heads', null, 'GET') const apiPath = '/repos/' + session.account.login + '/' + repo + '/git/refs'; [createBranch](session, repo, branchName, fromBranch) { */ * @private * @return {Promise} a Promise of the API call result * @param {string} [fromBranch] - Branch to start the new branch from. Default to the default_branch of the repo * @param {string} branchName - Name for the newly created branch * @param {string} repo - Repository name where to create the branch * @param {GHSession} session - GH session * Create a branch with the given parameters /** // Internals } return actionsChain; } } }); throw new BatchError(UnifileError.EIO, `Error while batch: ${err.message}`); .catch((err) => err.name !== 'BatchError', (err) => { }, message || 'Batch update', splitPath[1])) }); done(new Error(`Could not create a new tree ${e}`)); .catch((e) => { }) done(null, treeRes.tree); .then(() => { })) }); }); sha: t.sha mode: '040000', type: 'tree', path: treePath, treeRes.tree.push({ treeRes.tree = treeRes.tree.filter((node) => node.path !== treePath); .then((t) => { }) return this[callAPI](session, apiPath + '/git/trees', tree, 'POST'); tree.tree.forEach((t, index) => t.sha = shas[index].sha); .then((shas) => { return Promise.all(newTrees[treePath].blobs) const apiPath = '/repos/' + session.account.login + '/' + splitPath[0]; const tree = {tree: newTrees[treePath].tree}; return Promise.all(treesToPlant.map((treePath) => { const treesToPlant = Object.keys(newTrees).filter((path) => newTrees[path].tree.length > 0); } } console.warn(`Unsupported batch action: ${currentAction.name}`); default: break; } }); type: 'blob' mode: '100644', content: currentAction.content.toString('base64'), path: path.replace(closestParent + '/', ''), treeRes.tree.push({ treeRes.tree = treeRes.tree.filter((node) => node.path !== path); } else { }); type: 'blob' mode: '100644', path: path.replace(closestParent + '/', ''), newTrees[closestParent].tree.push({ newTrees[closestParent].blobs.push(this[createBlob](session, splitPath[0], currentAction.content)); if(closestParent) { const closestParent = Object.keys(newTrees).filter((p) => path.includes(p)).sort().pop(); // Get the longest path matching this file parent 'WriteFile actions should have a content')); UnifileError.EINVAL, return new Promise.reject(new UnifileError( if(!currentAction.content) case 'writefile': break; }; blobs: [] tree: [], newTrees[path] = { case 'mkdir': break; treeRes.tree = move(src, dest, treeRes); const dest = getPathTokens(currentAction.destination).slice(2).join('/'); const src = path; 'Rename actions should have a destination')); UnifileError.EINVAL, return new Promise.reject(new UnifileError( if(!currentAction.destination) case 'rename': break; treeRes.tree = removeFile(path, treeRes); case 'rmdir': case 'unlink': switch (currentAction.name.toLowerCase()) { const path = getPathTokens(currentAction.path).slice(2).join('/');adeRb\YXS6  T  u ] Y  p [ Z E N I 6 $  }kM:%qkXQM]Hzw\TMH4 return this[callAPI](session, apiPath + '/git/trees', {tree: tree}, 'POST'); if(Array.isArray(tree) && tree.length > 0) { .then((tree) => { }) }); }); else resolve(result); if(err && err instanceof Error) reject(err); transformer(res, (err, result) => { return new Promise((resolve, reject) => { .then((res) => { }) return this[callAPI](session, apiPath + '/git/trees/' + head.object.sha, {recursive: 1}, 'GET'); lastCommitSha = head.object.sha; .then((head) => { return this[callAPI](session, apiPath + '/git/refs/heads/' + branch, null, 'GET') const apiPath = '/repos/' + session.account.login + '/' + repo; let lastCommitSha; [transformTree](session, repo, transformer, message, branch = 'master') { */ * @private * @see {@link https://developer.github.com/v3/git/trees/#create-a-tree|Create a tree} * * @return {Promise} a Promise of the server response * @param {string} [branch=master] - Branch containing the tree * @param {string} message - Commit message for the new tree * @param {Function} transformer - Function to apply on tree. Get the tree as first param and wait for an array in the callback. * @param {string} repo - Name of the repository to commit * @param {GHSession} session - GH session * Transform the git tree and commit the transformed tree /** } }); return this[callAPI](session, apiPath + '/refs/heads/' + branch, data, 'PATCH'); // Update head }; sha: res.sha const data = { .then((res) => { }) return this[callAPI](session, apiPath + '/commits', data, 'POST'); // Create a new commit with the new tree }; message: message tree: res.sha, parents: [lastCommitSha], const data = { .then((res) => { }) return this[callAPI](session, apiPath + '/trees', data, 'POST'); // Create a new tree }; tree: tree base_tree: res.tree.sha, const data = { .then((res) => { }) return this[callAPI](session, apiPath + '/commits/' + lastCommitSha, null, 'GET'); // Get last commit info lastCommitSha = res.object.sha; .then((res) => { return this[callAPI](session, apiPath + '/refs/heads/' + branch, null, 'GET') // Get branch head let lastCommitSha; const apiPath = '/repos/' + session.account.login + '/' + repo + '/git'; [commit](session, repo, tree, message, branch) { * */ * @private * @see {@link https://developer.github.com/v3/git/trees/#create-a-tree|Create a tree} * * @return {Promise} a Promise of the server response * @param {string} [branch=master] - Branch containing the tree * @param {string} message - Message of the commit * @param {string} [tree[].sha] - Sha of the object to put in the tree. Will be ignored if content is set * @param {string} [tree[].content] - Content to put into file. If set, sha will be ignored * @param {string} tree[].type - Object type (blob/commit/tree) * @param {string} tree[].mode - Object mode (100644 for files) * @param {string} tree[].path - Full path to the file to modify * @param {Object[]} tree - Array of objects to commit * @param {string} repo - Name of the repository to commit * @param {GHSession} session - GH session * Create and push a commit /** } }); return this[callAPI](session, apiPath, reqData, 'POST'); } reqData.sha = origin.object.sha; if(!origin) throw new Error('Unknown branch origin ' + fromBranch); })[0]; return branch.ref === 'refs/heads/' + fromBranch; const origin = res.filter(function(branch) { else { if(!fromBranch) reqData.sha = res[0].object.sha; }; ref: 'refs/heads/' + branchName const reqData = {adoqe9"q\V  s m j i d 5 x 8 4 S  2 u _ @ '  N sdR?/~e`M?f)(piH! const defaultMessage = JSON.parse(body).message; const {code, message} = (() => { if(res.statusCode >= 400) { } return reject(err); if(err) { request(reqOptions, (err, res, body) => { return new Promise((resolve, reject) => { else { if(isStream) return request(reqOptions); else if(!isStream) reqOptions.body = JSON.stringify(data); if(method === 'GET') reqOptions.qs = data; }; } 'X-OAuth-Scopes': 'delete_repo, repo, user' 'User-Agent': 'Unifile', 'Authorization': session.token, 'Accept': 'application/vnd.github.v3+json', headers: { method: method, url: `https://api.${this.serviceHost}${path}`, const reqOptions = { [callAPI](session, path, data, method, isStream = false, retry = true) { */ * @private * @return {Promise|Stream} a Promise of the result send by server or a stream to the endpoint * @param {boolean} retry - Allow the request to retry on error * @param {boolean} isStream - Access the API as a stream or not * @param {string} method - HTTP verb to use * @param {Object} data - Data to pass. Convert to querystring if method is GET or to the request body * @param {string} path - End point path * @param {Object} session - GitHub session storage * Make a call to the GitHub API /** } }], 'Create ' + path, splitPath[1]); type: 'blob' mode: '100644', sha: blob.sha, path: path, return this[commit](session, splitPath[0], [{ const path = splitPath.slice(2).join('/'); [commitBlob](session, splitPath, blob) { */ * @private * @param {Object} blob - Blob return by the blob creation route * @param {string[]} splitPath - Path tokens containing repo/branch/path * @param {Object} session - GH session * Commit a blob to the given repo, branch and path /** } }; num_repos: account.public_repos login: account.login, display_name: account.name, session.account = { [assignSessionAccount](session, account) { */ * @private * @return {Promise} an empty promise * @param {Object} account - GH account * @param {Object} session - GH session * Fetch the account information on the service and map them to the session /** } }, 'POST'); encoding: 'base64' content: buffer.toString('base64'), return this[callAPI](session, apiPath, { const apiPath = '/repos/' + session.account.login + '/' + repoName + '/git/blobs'; const buffer = Buffer.isBuffer(content) ? content : new Buffer(content); [createBlob](session, repoName, content) { */ * @private * @see {@link https://developer.github.com/v3/git/blobs/#create-a-blob|Create a blob} * * @return {Promise} a promise of result for the blob creation * @param {string|Buffer} content - Content of the blob * @param {string} repoName - Name of the repository where to create the blob * @param {Object} session - GitHub session storage * Create a blob in the designated repository /** } }); return this[callAPI](session, apiPath + '/git/refs/heads/' + branch, data, 'PATCH'); }; sha: res.sha const data = { .then((res) => { }) return this[callAPI](session, apiPath + '/git/commits', data, 'POST'); }; message: message tree: newTree.sha, parents: [lastCommitSha], const data = { .then((newTree) => { }) } 'Invalid tree transformation. Transformer must return an array.')); UnifileError.EIO, return Promise.reject(new UnifileError( } else { return Promise.reject(new UnifileError(UnifileError.ENOTSUP, 'You can not leave this folder empty.')); } else if(Array.isArray(tree)) {