Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
atom-perforce.js fixed for Windows -- see https://github.com/mattsawyer77/atom-perforce/issues/46
'use strict';
var atomPerforce = module, // sugary alias
path = require('path'),
os = require('os'),
p4 = require('node-perforce'),
Q = require('q'),
$ = require('jquery'),
environment = require('./environment'),
clientStatusBarElement = $('<div/>')
.addClass('git-branch inline-block')
.append('<span class="icon icon-git-branch"></span>')
.append('<span class="branch-label"></span>'),
changeHunkDescriptorRegex = /^[\d,]+(\w)(\d+)(,)?(\d+)?$/,
envVarsToExtract = [
'P4CONFIG',
'P4IGNORE',
'P4PORT',
'P4USER',
'P4TICKETS',
'P4PASSWD',
'HOME'
],
clientStatusBarTile,
environmentReady;
function escapePathSpaces(filepath) {
if(os.platform() === 'win32') {
return filepath.replace(/ /g, '^ ');
} else {
return filepath.replace(/ /g, '\\ ');
}
}
function escapeBackSlashes(filepath) {
return filepath.replace(/\\/g, '\\\\');
}
function execP4Command(command, options) {
var p4Fn = p4[command];
if(p4Fn && p4Fn.call) {
return Q.when(environmentReady || atomPerforce.exports.setupEnvironment())
.then(function(p4Env) {
var defaultOptions = { env: p4Env };
return Q.nfcall(p4Fn, $.extend(true, {}, defaultOptions, options));
});
}
else {
throw new Error('unknown node-perforce method: ' + command);
}
}
function setStatusClient(p4ClientName) {
var statusBar = document.querySelector('status-bar'),
statusElement;
if(clientStatusBarTile && clientStatusBarTile.destroy) {
clientStatusBarTile.destroy();
}
if(p4ClientName) {
statusElement = clientStatusBarElement.clone();
statusElement.find('.branch-label').text(p4ClientName);
clientStatusBarTile = statusBar.addRightTile({
item: statusElement,
priority: 0
});
}
}
/**
* build a list of change hunks, where each hunk is a descriptor, a list of additions, and a list of deletions
* @param {string} p4DiffOutput the output of a p4 diff command
* @return {array} list of hunk objects
*/
function processDiff(p4DiffOutput) {
var changes = [],
change, firstChar;
p4DiffOutput.match(/[^\r\n]+/gm).forEach(function(line) {
if(line.length > 1) {
firstChar = line.substr(0, 1);
if(!isNaN(parseInt(firstChar, 10))) {
if(change) {
changes.push(change);
}
change = {
descriptor: line,
added: [],
removed: []
};
}
else if(firstChar === '<') {
change.removed.push(line);
}
else if(firstChar === '>') {
change.added.push(line);
}
}
});
if(change) {
changes.push(change);
}
return changes;
}
function normalizePath(path) {
return path.toLowerCase().replace(/\\/g, '/');
}
function checkIsInWorkspace(p4Info) {
return !p4Info['clientUnknown.'] && normalizePath(p4Info.currentDirectory).startsWith(normalizePath(p4Info.clientRoot));
}
/**
* transform p4 depot format output to a local file path given the client root path
* @param {string} clientPath the client path (i.e. starting with //<client name>/...)
* @param {object} p4Info the result of p4.info
*/
function transformClientPathToLocalPath(clientPath, p4Info) {
var clientPathRegex = new RegExp('^//' + p4Info.clientName + '/(.+)$'),
match = clientPathRegex.exec(clientPath);
if(match) {
return path.join(p4Info.clientRoot, match[1]);
}
else {
throw new Error('could not parse client path ', clientPath);
}
}
atomPerforce.exports = {
/**
* setup the perforce environment by using environment.js
* to extract environment variables and optionally overriding the PATH
* if the user has specified a custom p4 executable path.
* this is called lazily when the 1st perforce command is attempted,
* or when the default p4 executable setting is altered
* @return {object} promise for when the environment is setup
*/
setupEnvironment: function setupEnvironment() {
var pathElements,
defaultPath = atom.config.get('atom-perforce.defaultP4Location');
environmentReady = environment.extractVarsFromEnvironment(envVarsToExtract);
// make sure the default p4 location is in the path
pathElements = process.env.PATH.split(path.delimiter);
if(pathElements.indexOf(defaultPath) === -1) {
pathElements.unshift(defaultPath);
process.env.PATH = pathElements.join(path.delimiter);
}
return environmentReady;
},
/**
* p4 edit a file
* @return {object} promise for completion of p4 edit
*/
edit: function edit() {
var editor = atom.workspace.getActivePaneItem(),
openedBufferFilePath,
openedBufferFilename;
if(editor && editor.getPath && editor.getPath()) {
openedBufferFilePath = path.dirname(editor.getPath());
openedBufferFilename = path.basename(editor.getPath());
// call p4 info to make sure perforce is available
return execP4Command('info', { cwd: openedBufferFilePath })
.then(function(p4Info) {
if(checkIsInWorkspace(p4Info)) {
return execP4Command('edit', { cwd: openedBufferFilePath, files: [escapePathSpaces(openedBufferFilename)] })
.then(function(result) {
// p4 edit returns a 0 exit code even if the file is already opened
if((/currently opened/).test(result)) {
atom.notifications.addWarning('Perforce: file already opened', { detail: result, dismissable: true });
}
else {
atom.notifications.addSuccess('Perforce: file opened for edit', { detail: result });
}
})
.catch(function(err) {
atom.notifications.addError('Perforce: failed to open for edit', { detail: err.message, dismissable: true });
console.error(err);
return false;
});
}
else {
console.info(openedBufferFilePath + ' is outside any known perforce workspace');
}
})
.catch(function(err) {
console.err(err);
return false;
});
}
else {
atom.notifications.addWarning('Perforce: cannot edit an unsaved file', { dismissable: true });
console.warn('cannot edit an unsaved file');
return Q.when(false);
}
},
/**
* execute p4 add to add the currently opened file in perforce
* @return {object} promise for completion of p4 add
*/
add: function add() {
var editor = atom.workspace.getActivePaneItem(),
openedBufferFilePath,
openedBufferFilename;
if(editor && editor.getPath && editor.getPath()) {
openedBufferFilePath = path.dirname(editor.getPath());
openedBufferFilename = path.basename(editor.getPath());
// call p4 info to make sure perforce is available
return execP4Command('info', { cwd: openedBufferFilePath })
.then(function(p4Info) {
if(checkIsInWorkspace(p4Info)) {
return execP4Command('add', { cwd: openedBufferFilePath, files: [escapePathSpaces(openedBufferFilename)] })
.then(function(result) {
// for some unfortunate reason, p4 add <existing file> returns a 0 exit code
if((/can't add existing file/).test(result)) {
atom.notifications.addWarning('Perforce: file already exists', { detail: result, dismissable: true });
}
else if((/already opened|currently opened/).test(result)) {
atom.notifications.addWarning('Perforce: file already opened', { detail: result, dismissable: true });
}
else {
atom.notifications.addSuccess('Perforce: file opened for add', { detail: result });
console.log(result);
}
})
.catch(function(err) {
atom.notifications.addError('Perforce: failed to open for add', { detail: err.message, dismissable: true });
console.error(err);
return false;
});
}
else {
console.info(openedBufferFilePath + ' is outside any known perforce workspace');
}
})
.catch(function(err) {
console.err(err);
return false;
});
}
else {
atom.notifications.addWarning('Perforce: cannot add an unsaved file', { dismissable: true });
console.warn('cannot add an unsaved file');
return Q.when(false);
}
},
/**
* execute p4 sync
*/
sync: function sync() {
var promises = [],
directories = [],
successDirectories = [];
function checkResolved(dir) {
function handleResolveResult(result) {
var fileList;
if(!(/No file\(s\) to resolve/i).test(result)) {
// parse the filename from each line
fileList = result.trim().split('\n').map(function(line) {
var match = (/^(.*) - (.*)$/).exec(line);
if(match) {
return match[1];
}
else {
return false;
}
})
// filter out blanks
.filter(function(line) {
return !!line;
})
// translate to relative path
.map(function(filename) {
return path.relative(dir, filename);
});
if(fileList.length > 1) {
atom.notifications.addWarning('Perforce: some file(s) need to be resolved in ' + dir, {
detail: fileList.join('\n'),
dismissable: true
});
}
else {
atom.notifications.addWarning('Perforce: ' + fileList[0] + ' needs to be resolved in ' + dir, {
dismissable: true
});
}
}
}
// do p4 resolve -n to check if files need to be resolved post-sync
return execP4Command('resolve', { cwd: dir, files: ['-n ./...'] })
.then(handleResolveResult)
.catch(function(err) {
handleResolveResult(err.message);
});
}
directories = atom.project.getDirectories().map(function(projectRoot) {
return projectRoot.realPath;
});
if(directories && directories.length) {
directories.forEach(function(dir) {
var syncDeferred = Q.defer(),
synced = syncDeferred.promise;
promises.push(synced);
// call p4 info to make sure perforce is available
execP4Command('info', { cwd: dir })
.then(function(p4Info) {
if (checkIsInWorkspace(p4Info)) {
return execP4Command('sync', { cwd: dir, files: ['./...'] });
}
})
.then(function() {
successDirectories.push(dir);
return checkResolved(dir)
.then(function() {
syncDeferred.resolve();
});
})
.catch(function(err) {
// this message is returned on stderr, so node-perforce treats it as a failure
if(err.message && (/file\(s\) up-to-date/i).test(err.message)) {
console.log('p4 sync completed in ' + dir);
successDirectories.push(dir);
return checkResolved(dir)
.then(function() {
syncDeferred.resolve();
});
}
else {
atom.notifications.addError('Perforce: sync failed', { detail: err.message, dismissable: true });
console.error('could not p4 sync', err);
syncDeferred.reject(err.message);
}
});
}); // per directory
return Q.all(promises)
.finally(function() {
if(successDirectories.length) {
atom.notifications.addSuccess('Perforce: sync complete', {
detail: "paths synced:\n" + successDirectories.join('\n')
});
}
});
} // if there were directories
},
/**
* execute p4 revert
* @param {string=} filepath optional filepath or event object
* @param {boolean=} confirm (default true) whether to confirm before reverting
* @return {object} a promise for when the operation is complete
*/
revert: function revert(filename, confirm) {
var deferred = Q.defer(),
editor = atom.workspace.getActivePaneItem(),
filepath;
confirm = confirm !== false; // default to true
filename = filename ? filename : editor.getPath();
filepath = path.dirname(filename);
function executeRevert() {
// call p4 info to make sure perforce is available
return execP4Command('info', { cwd: filepath })
.then(function(p4Info) {
if (checkIsInWorkspace(p4Info)) {
return execP4Command('revert', {
cwd: filepath,
files: [escapePathSpaces(path.basename(filename))]
});
}
})
.then(function(result) {
if(editor && editor.buffer) {
editor.buffer.reload();
}
atom.notifications.addSuccess('Perforce: file reverted', { detail: result });
console.log('p4 revert completed');
deferred.resolve(true);
})
.catch(function(err) {
atom.notifications.addError('Perforce: revert failed', { detail: err.message, dismissable: true});
console.error('could not p4 revert', err);
deferred.reject(err);
});
}
function executeCancel() {
console.log('revert canceled');
deferred.resolve(false);
}
if(confirm) {
atom.confirm({
message: 'Revert?',
detailedMessage: 'Are you sure you want to revert your changes to ' + path.basename(filename) + '?',
buttons: {
Revert: executeRevert,
Cancel: executeCancel
}
});
}
else {
executeRevert()
.then(function() {
deferred.resolve(true);
})
.catch(function(err) {
deferred.reject(err);
});
}
return deferred.promise;
},
/**
* get a list of changes to the current file compared to the depot version
* @param {object=} an editor instance
* @return {object} a promise for an array of hunks, where each hunk is an object containing:
* - {string} descriptor: a descriptor denoting the range of lines affected and which type of operation
* - {array} added: a list of lines added
* - {array} removed: a list of lines removed
*/
getChanges: function getChanges(editor) {
var deferred = Q.defer(),
openedBufferFilePath,
openedBufferFilename;
editor = editor || atom.workspace.getActivePaneItem();
if(editor && editor.getPath && editor.getPath()) {
openedBufferFilePath = path.dirname(editor.getPath());
openedBufferFilename = path.basename(editor.getPath());
// call p4 info to make sure perforce is available
execP4Command('info', { cwd: openedBufferFilePath })
.then(function(p4Info) {
if (checkIsInWorkspace(p4Info)) {
// call p4 diff on the file
return execP4Command('diff', {
cwd: openedBufferFilePath,
files: [escapePathSpaces(openedBufferFilename)]
})
.then(function(result) {
deferred.resolve(processDiff(result));
})
.catch(function(err) {
if(!/not opened on this client/.test(err) && !/not opened for edit/.test(err)) {
console.error(err);
deferred.reject(err);
}
else {
deferred.resolve([]);
}
});
}
})
.catch(function(err) {
console.error(err);
deferred.reject(err);
});
}
else {
deferred.reject('no file currently open');
}
return deferred.promise;
},
/**
* show diff marks in the file (regardless of which pane(s) it's open in)
* @param {string} filepath the full path of the file
* @param {array} changes a list of change hunks produced by processDiff()
*/
showDiffMarks: function showDiffMarks(filepath, changes) {
atom.workspace.getPaneItems().forEach(function(editor) {
if(editor && editor.getPath && editor.getPath() === filepath) {
// clear any pre-existing perforce markers
editor.getDecorations({perforce: true}).forEach(function(decoration) {
var marker = decoration.getMarker();
if(marker && marker.destroy) {
marker.destroy();
}
});
// mark each change in the list
changes.forEach(function(change) {
var startLine, endLine, changeType, descriptor, marker;
descriptor = changeHunkDescriptorRegex.exec(change.descriptor);
if(descriptor) {
switch(descriptor[1]) {
case 'c': changeType = 'modified'; break;
case 'd': changeType = 'removed'; break;
case 'a': changeType = 'added'; break;
default: throw new Error('unrecognized change hunk type ' + descriptor[1]);
}
startLine = parseInt(descriptor[2], 10);
if(descriptor[4] && descriptor[3] === ',') {
endLine = parseInt(descriptor[4], 10);
}
else {
endLine = startLine;
}
marker = editor.markBufferRange([[startLine - 1, 0], [endLine, 0]]);
editor.decorateMarker(marker, {
type: 'line-number',
class: 'git-line-' + changeType,
perforce: true
});
}
});
}
});
},
/**
* show the p4 client (a.k.a. workspace) name in the right side of the status bar
*/
showClientName: function showClientName() {
var editor = atom.workspace.getActiveTextEditor(),
dir;
if(editor) {
dir = path.dirname(editor.getPath());
}
else if(atom.project.getDirectories() && atom.project.getDirectories().length) {
dir = atom.project.getDirectories()[0].realPath;
}
if(dir && !dir.startsWith('atom:')) {
// call p4 info to make sure perforce is available
execP4Command('info', { cwd: dir })
.then(function(p4Info) {
if(!checkIsInWorkspace(p4Info)) {
setStatusClient(false);
}
else {
setStatusClient(p4Info.clientName);
}
})
.catch(function(err) {
if((/command not found/).test(err)) {
atom.notifications.addError('Perforce: p4 command not found on path', {
detail: [
'Your path does not contain the p4 command. You can either specify the ',
'p4 command\'s directory in the atom-perforce settings, or set your PATH ',
'environment variable to include the directory that contains the p4 command.'
].join(''),
dismissable: true
});
}
console.error(err);
setStatusClient(false);
});
}
else {
setStatusClient(false);
}
},
/**
* get a list of files that are currently opened in this workspace
* @return {array} a promise for a array of p4 fstat objects from node-perforce's opened()
*/
getOpenedFiles: function getOpenedFiles() {
var projectRoots = atom.project.getDirectories(),
promises = [],
p4Info;
if(projectRoots && projectRoots.length) {
projectRoots.forEach(function(projectRoot) {
if (projectRoot.path.startsWith('atom:'))
return; // To ignore
var deferred = Q.defer();
promises.push(deferred.promise);
// call p4 info to make sure perforce is available
Q.when(p4Info || execP4Command('info', { cwd: projectRoot.path }))
.then(function(p4InfoResult) {
p4Info = p4InfoResult;
return execP4Command('opened', { files: ['./...'], cwd: projectRoot.path });
})
.then(function(p4Opened) {
deferred.resolve(p4Opened
.filter(function(fileinfo) {
return fileinfo && fileinfo.clientFile;
})
.map(function(fileinfo) {
fileinfo.localPath = transformClientPathToLocalPath(fileinfo.clientFile, p4Info);
return fileinfo;
})
);
})
.catch(function(err) {
console.error(err, 'for', projectRoot.path);
deferred.reject(err);
});
});
return Q.allSettled(promises)
// combine the results for each root directory
.then(function(results) {
return results.reduce(function(memo, result) {
if(result.state === 'fulfilled') {
memo = [].concat(memo, result.value);
}
return memo;
}, []);
});
}
else {
return Q.when([]);
}
},
/**
* load all files currently opened for add/edit into buffers
*/
loadAllOpenFiles: function loadAllOpenFiles() {
return atomPerforce.exports.getOpenedFiles()
.then(function(p4OpenedFiles) {
var editors = atom.workspace.getTextEditors();
p4OpenedFiles.filter(function(fileinfo) {
return fileinfo.type !== 'binary' && !(/delete/).test(fileinfo.action);
})
.forEach(function(fileinfo) {
// is the file already opened in a buffer?
if(!editors.some(function(editor) {
return fileinfo.localPath === editor.getPath();
})) {
atom.workspace.open(fileinfo.localPath, {
activatePane: false
});
}
});
})
.catch(function(err) {
atom.notifications.addError('Perforce: failed to load all currently opened files', { detail : err, dismissable: true});
});
},
/**
* mark files that are opened for edit or add in the tree
* TODO: get away from this jQueryish non-API code if possible
* @param {array=} openedFiles optional array of p4-opened (fstat format) objects
* (see getOpenedFiles return value)
*/
markOpenFiles: function markOpenFiles(openedFiles) {
Q.when(openedFiles || atomPerforce.exports.getOpenedFiles())
.then(function(p4OpenedFiles) {
// clear all markers first
var elements = document.querySelectorAll('.perforce.status-modified, .perforce.status-added');
[].forEach.call(elements, function(element) {
['perforce', 'status-modified', 'status-added'].forEach(function(className) {
element.classList.remove(className);
});
});
// add back markers
p4OpenedFiles.forEach(function(fileinfo) {
elements = document.querySelectorAll('[data-path="' + escapeBackSlashes(fileinfo.localPath) + '"]');
[].forEach.call(elements, function(element) {
var className;
switch(fileinfo.action) {
case 'edit':
case 'move/add':
case 'branch':
case 'integrate':
className = 'status-modified'; break;
case 'add':
case 'import':
className = 'status-added'; break;
case 'delete':
case 'move/delete':
case 'purge':
case 'archive':
className = 'status-removed'; break;
}
if(className) {
element.classList.add('perforce');
element.classList.add(className);
}
});
});
})
.catch(function(err) {
atom.notifications.addError('Perforce: failed to indicate currently opened files', { detail: err.message, dismissable: true });
});
},
/**
* check whether a file is tracked (i.e. has been added to) perforce
* @param {string} filepath the full file path
* @param {object} a promise for either:
* boolean: false if the file is not tracked in perforce, OR
* object: the fstat object from p4 fstat
* NOTE: the promise will be rejected if the file is not inside a perforce workspace
*/
fileIsTracked: function fileIsTracked(filepath) {
var deferred = Q.defer();
var dir = path.dirname(filepath);
execP4Command('info', { cwd: dir })
.then(function(p4Info) {
if (checkIsInWorkspace(p4Info)) {
return execP4Command('fstat', {
cwd: dir,
files: [escapePathSpaces(path.basename(filepath))]
})
.then(function(fileinfo) {
if(fileinfo) {
deferred.resolve(fileinfo);
}
else {
deferred.resolve(false);
}
});
}
})
.catch(function(err) {
console.log(err);
if(/no such file/.test(err)) {
// the file is within a p4 workspace, but is not added
deferred.resolve(false);
}
else {
// the file is outside a p4 workspace
deferred.reject(err);
}
});
return deferred.promise;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment