Created
December 17, 2015 12:34
-
-
Save PhiLhoSoft/091f05e78e6df3472096 to your computer and use it in GitHub Desktop.
atom-perforce.js fixed for Windows -- see https://github.com/mattsawyer77/atom-perforce/issues/46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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