reckless-node-perforce
Version:
Simplified fork of node-perforce with vulnerable dependencies removed and fixed commands
528 lines (450 loc) • 13.9 kB
JavaScript
var os = require('os');
const { exec, spawn } = require('child_process');
var p4options = require('./p4options');
const { stdout } = require('process');
var ztagRegex = /^\.\.\.\s+(\w+)\s+(.+)/;
var p4 = process.platform === 'win32' ? 'p4.exe' : 'p4';
function camelize(str)
{
return str
.split(/[-_\s]+/) // Split by dash, underscore, or space
.map((word, index) =>
index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
)
.join('');
}
// Build a list of options/arguments for the p4 command
function optionBuilder(options)
{
options = options || {};
var results = { stdin: [], args: [], files: [] };
Object.keys(options).map(function (option)
{
var p4option = p4options[option];
if (!p4option) return;
if (p4option.category !== 'unary')
{
if ((options[option] || {}).constructor !== p4option.type)
{
if (NodeP4.debug_mode)
{
console.log(`[P4 DEBUG] Rejected option parameter due to wrong argument type! Option ${option}, parameter value was ${options[option]}, required type was ${p4option.type.name}.`);
}
throw new Error(`[Perforce] Rejected option parameter due to wrong argument type! Option ${option}, parameter value was ${options[option]}, required type was ${p4option.type.name}.`);
return;
}
}
if (p4option.category === 'stdin')
{
results.stdin.push(p4option.cmd + options[option]);
if (results.args.indexOf('-i') < 0 && !p4option.omit_i) results.args.push('-i');
}
else if (p4option.cmd)
{
results.args.push(p4option.cmd);
if (p4option.category === 'mixed') results.args.push(options[option]);
}
else
{
results.files = results.files.concat(options[option]);
}
});
return results;
}
// Filter passed-in options to get a hash of child process options (i.e., not p4 command arguments)
function execOptionBuilder(options)
{
var validKeys =
{
cwd: true,
env: true,
encoding: true,
shell: true,
timeout: true,
maxBuffer: true,
killSignal: true,
uid: true,
gid: true
};
options = options || {};
return Object.keys(options).reduce(function (result, key)
{
if (validKeys[key])
{
result[key] = options[key];
}
return result;
}, {});
}
function execP4(p4cmd, options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
var ob = optionBuilder(options);
var childProcessOptions = execOptionBuilder(options);
var cmd = [p4, p4cmd, ob.args.join(' '), ob.files.join(' ')];
if (NodeP4.debug_mode)
{
console.log('[P4 DEBUG] ' + cmd.join(' '));
}
// flatten both flags _and_ file‐spec strings, then drop blanks
const flatArgs = ob.args.reduce((a, s) => a.concat(String(s).split(/\s+/)), []);
const flatFiles = ob.files.reduce((a, s) => a.concat(String(s).split(/\s+/)), []);
const argv = [p4cmd, ...flatArgs, ...flatFiles].filter(Boolean);
// use spawn to avoid buffer size issues
var child = spawn(p4, argv, { ...childProcessOptions, shell: true });
let stdout = '', stderr = '';
child.stdout.on('data', d => { stdout += d; });
child.stderr.on('data', d => { stderr += d; });
child.on('error', (err) =>
{
// Handle spawn errors (like command not found)
callback(err);
});
child.on('close', code =>
{
// For p4 info and some other commands, we want to return stdout even if
// the exit code is non-zero, as it often contains useful information
if (code !== 0)
{
// We'll still pass the stderr as an error property
return callback(null, stdout, {error: stderr, code: code});
}
return callback(null, stdout);
});
if (ob.stdin.length > 0)
{
ob.stdin.forEach(function (line)
{
// for multi-line inputs, the first line goes in as is, and the following need to start with a tab
let multiline = line.split('\n')
child.stdin.write(multiline[0] + '\n');
if (NodeP4.debug_mode && p4cmd.toLowerCase() != 'login')
{
console.log(" > " + multiline[0]);
}
multiline.shift();
multiline.forEach(function (theLine)
{
child.stdin.write('\t' + theLine + '\n');
if (NodeP4.debug_mode && p4cmd.toLowerCase() != 'login')
{
console.log(' > ' + theLine);
}
});
});
child.stdin.end();
}
}
// Process group of lines of output from a p4 command executed with -ztag
function processZtagOutput(output)
{
return output.split('\n').reduce(function (memo, line)
{
var match, key, value;
match = ztagRegex.exec(line);
if (match)
{
key = match[1];
value = match[2];
memo[key] = value;
}
return memo;
}, {});
}
function NodeP4() {}
NodeP4.prototype.changelist =
{
create: function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
var newOptions =
{
_change: 'new',
description: options.description || '<saved by node-perforce>'
};
execP4('change', newOptions, function (err, stdout)
{
if (err) return callback(err);
var matched = stdout.match(/([0-9]+)/g);
if (matched.length > 0) return callback(null, parseInt(matched[0], 10));
else return callback(new Error('Unknown error'));
});
},
edit: function (options, callback)
{
callback = callback || function () { };
if (!options || !options.changelist) return callback(new Error('Missing parameter/argument'));
if (!options.description) return callback();
var newOptions =
{
_change: options.changelist.toString(),
description: options.description
};
execP4('change', newOptions, function (err)
{
if (err) return callback(err);
return callback();
});
},
delete: function (options, callback)
{
callback = callback || function () { };
if (!options || !options.changelist) return callback(new Error('Missing parameter/argument'));
execP4('change', { _delete: options.changelist }, function (err)
{
if (err) return callback(err);
return callback();
});
},
view: function (options, callback)
{
if (!options || !options.changelist) return callback(new Error('Missing parameter/argument'));
execP4('change', { _output: options.changelist }, function (err, stdout)
{
if (err) return callback(err);
// preprocessing file status
stdout = stdout.replace(/(\t)+#(.)*/g, function (match)
{
return '@@@' + match.substring(3);
});
var result = {};
var lines = stdout.replace(/#(.)*\n/g, '').split(os.EOL + os.EOL);
lines.forEach(function (line)
{
var key = camelize(line.split(':')[0].toLowerCase().trim());
if (key)
{
result[key] = line.substring(line.indexOf(':') + 1).trim();
}
});
if (result.files)
{
result.files = result.files.split('\n').map(function (file)
{
var file = file.replace(/\t*/g, '').split('@@@');
return { file: file[0], action: file[1] };
});
}
else
{
result.files = [];
}
return callback(null, result);
});
},
submit: function (options, callback)
{
if (!options || !options.changelist) return callback(new Error('Missing parameter/argument'));
execP4('submit', options, function (err, stdout)
{
if (err) return callback(err);
});
}
};
NodeP4.prototype.info = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
execP4('info', options, function (err, stdout)
{
if (err) return callback(err);
var result = {};
stdout.split(/\r\n|\r|\n/).forEach(function (line)
{
if (!line) return;
var key = camelize(line.split(':')[0].toLowerCase());
result[key] = line.substring(line.indexOf(':') + 1).trim();
});
callback(null, result);
});
};
// Return an array of file info objects for each file opened in the workspace
NodeP4.prototype.opened = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
execP4('-ztag opened', options, function (err, stdout)
{
var result;
if (err) return callback(err);
// process each file
result = stdout.trim().split(/\r\n\r\n|\n\n/).reduce(function (memo, fileinfo)
{
// process each line of file info, transforming into a hash
memo.push(processZtagOutput(fileinfo));
return memo;
}, []);
callback(null, result);
});
};
NodeP4.prototype.fstat = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
execP4('fstat', options, function (err, stdout)
{
var result;
if (err) return callback(err);
// process each file fstat info
result = stdout.trim().split(/\r\n\r\n|\n\n/).reduce(function (memo, fstatinfo)
{
// process each line of file info, transforming into a hash
memo.push(processZtagOutput(fstatinfo));
return memo;
}, []);
callback(null, result);
});
};
NodeP4.prototype.changes = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
execP4('-ztag changes', options, function (err, stdout)
{
var result;
if (err) return callback(err);
// process each change
result = stdout.trim().split(/\r\n\r\n|\n\n(?=\.\.\.)/).reduce(function (memo, changeinfo)
{
// process each line of change info, transforming into a hash
var item = processZtagOutput(changeinfo);
// If object representing change is not empty, push it onto array
if (Object.keys(item).length != 0)
{
memo.push(item);
}
return memo;
}, []);
callback(null, result);
});
};
NodeP4.prototype.user = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
execP4('-ztag user', options, function (err, stdout)
{
var result;
if (err) return callback(err);
// process ztagged user information
result = processZtagOutput(stdout.trim());
callback(null, result);
});
};
NodeP4.prototype.users = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
execP4('-ztag users', options, function (err, stdout)
{
var result;
if (err) return callback(err);
// process each change
result = stdout.trim().split(/\r\n\r\n|\n\n(?=\.\.\.)/).reduce(function (memo, userinfo)
{
// process each line of user info, transforming into a hash
memo.push(processZtagOutput(userinfo));
return memo;
}, []);
callback(null, result);
});
};
NodeP4.prototype.diff2 = function (options, callback)
{
if (typeof options === 'function')
{
callback = options;
options = undefined;
}
// TODO: Check that no more than two file arguments are provided
execP4('-ztag diff2', options, function (err, stdout)
{
var result;
if (err) return callback(err);
// process each change
result = stdout.trim().split(/\r\n\r\n|\n\n(?=\.\.\.)/).reduce(function (memo, diff2info)
{
// process each line of change info, transforming into a hash
var item = processZtagOutput(diff2info);
// If object representing change is not empty, push it onto array
if (Object.keys(item).length != 0)
memo.push(item);
return memo;
}, []);
callback(null, result);
});
};
NodeP4.prototype.awaitCommand = function (command, options)
{
return new Promise((resolve, reject) =>
{
let commandPointer = this;
if (command.includes('.'))
{
commandPointer = this[command.substring(0, command.indexOf('.'))];
command = command.substring(command.indexOf('.') + 1);
}
commandPointer[command](options, (err, out) =>
{
// Error and output handling
if (err)
{
if (err.message && err.message.includes("file(s) up-to-date"))
{
resolve(err.message); // Not a real error, treat as success
}
else
{
reject(err); // Real error, reject the promise
}
}
else
{
resolve(out); // No errors, resolve with the actual output
}
});
});
};
NodeP4.prototype.setDebugMode = function (debug_active)
{
// change static property so that we can access it inside nested function defs like changelist.create,
// where we can't easily get an instance property using the 'this' variable since the context is nested
NodeP4.debug_mode = debug_active;
};
var commonCommands = ['add', 'delete', 'edit', 'revert', 'sync', 'diff', 'reconcile', 'reopen', 'resolved',
'shelve', 'unshelve', 'client', 'resolve', 'submit', 'describe', 'files', 'have', 'login', 'logout'];
commonCommands.forEach(function (command)
{
NodeP4.prototype[command] = function (options, callback)
{
execP4(command, options, callback);
};
});
module.exports = new NodeP4();
;