1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00
node-red/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js
Nick O'Leary 6fb96fa3c1
Move exec and events components to util module
The exec and events components are common components that
are used by both runtime and registry. It makes sense to
move them into the util package.

This also adds some docs to the registry module
2020-12-07 11:49:01 +00:00

656 lines
23 KiB
JavaScript

/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var authResponseServer = require('./authServer').ResponseServer;
var sshResponseServer = require('./authServer').ResponseSSHServer;
var clone = require('clone');
var path = require("path");
var gitCommand = "git";
var gitVersion;
const {log,exec} = require("@node-red/util");
function runGitCommand(args,cwd,env,emit) {
log.trace(gitCommand + JSON.stringify(args));
args.unshift("credential.helper=")
args.unshift("-c");
return exec.run(gitCommand, args, {cwd:cwd, detached:true, env:env}, emit).then(result => {
return result.stdout;
}).catch(result => {
var stdout = result.stdout;
var stderr = result.stderr;
var err = new Error(stderr);
err.stdout = stdout;
err.stderr = stderr;
if (/Connection refused/i.test(stderr)) {
err.code = "git_connection_failed";
} else if (/Connection timed out/i.test(stderr)) {
err.code = "git_connection_failed";
} else if(/Host key verification failed/i.test(stderr)) {
// TODO: handle host key verification errors separately
err.code = "git_host_key_verification_failed";
} else if (/fatal: could not read/i.test(stderr)) {
// Username/Password
err.code = "git_auth_failed";
} else if(/HTTP Basic: Access denied/i.test(stderr)) {
err.code = "git_auth_failed";
} else if(/Permission denied \(publickey\)/i.test(stderr)) {
err.code = "git_auth_failed";
} else if(/Authentication failed/i.test(stderr)) {
err.code = "git_auth_failed";
} else if (/commit your changes or stash/i.test(stderr)) {
err.code = "git_local_overwrite";
} else if (/CONFLICT/.test(err.stdout)) {
err.code = "git_pull_merge_conflict";
} else if (/not fully merged/i.test(stderr)) {
err.code = "git_delete_branch_unmerged";
} else if (/remote .* already exists/i.test(stderr)) {
err.code = "git_remote_already_exists";
} else if (/does not appear to be a git repository/i.test(stderr)) {
err.code = "git_not_a_repository";
} else if (/Repository not found/i.test(stderr)) {
err.code = "git_repository_not_found";
} else if (/repository '.*' does not exist/i.test(stderr)) {
err.code = "git_repository_not_found";
} else if (/refusing to merge unrelated histories/i.test(stderr)) {
err.code = "git_pull_unrelated_history"
} else if (/Please tell me who you are/i.test(stderr)) {
err.code = "git_missing_user";
} else if (/name consists only of disallowed characters/i.test(stderr)) {
err.code = "git_missing_user";
}
throw err;
})
}
function runGitCommandWithAuth(args,cwd,auth,emit) {
log.trace("runGitCommandWithAuth "+JSON.stringify(auth).replace(/("pass.*?"\s*:\s*").+?"/g,'$1[hidden]"'));
return authResponseServer(auth).then(function(rs) {
var commandEnv = clone(process.env);
commandEnv.GIT_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh");
commandEnv.NODE_RED_GIT_NODE_PATH = process.execPath;
commandEnv.NODE_RED_GIT_SOCK_PATH = rs.path;
commandEnv.NODE_RED_GIT_ASKPASS_PATH = path.join(__dirname,"authWriter.js");
return runGitCommand(args,cwd,commandEnv,emit).then( result => {
rs.close();
return result;
}).catch(err => {
rs.close();
throw err;
});
})
}
function runGitCommandWithSSHCommand(args,cwd,auth,emit) {
log.trace("runGitCommandWithSSHCommand "+JSON.stringify(auth).replace(/("pass.*?"\s*:\s*").+?"/g,'$1[hidden]"'));
return sshResponseServer(auth).then(function(rs) {
var commandEnv = clone(process.env);
commandEnv.SSH_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh");
commandEnv.DISPLAY = "dummy:0";
commandEnv.NODE_RED_GIT_NODE_PATH = process.execPath;
commandEnv.NODE_RED_GIT_SOCK_PATH = rs.path;
commandEnv.NODE_RED_GIT_ASKPASS_PATH = path.join(__dirname,"authWriter.js");
// For git < 2.3.0
commandEnv.GIT_SSH = path.join(__dirname,"node-red-ssh.sh");
commandEnv.NODE_RED_KEY_FILE=auth.key_path;
// GIT_SSH_COMMAND - added in git 2.3.0
commandEnv.GIT_SSH_COMMAND = "ssh -i " + auth.key_path + " -F /dev/null";
// console.log('commandEnv:', commandEnv);
return runGitCommand(args,cwd,commandEnv,emit).then( result => {
rs.close();
return result;
}).catch(err => {
rs.close();
throw err;
});
})
}
function cleanFilename(name) {
if (name[0] !== '"') {
return name;
}
return name.substring(1,name.length-1);
}
function parseFilenames(name) {
var re = /([^ "]+|(".*?"))($| -> ([^ ]+|(".*"))$)/;
var m = re.exec(name);
var result = [];
if (m) {
result.push(cleanFilename(m[1]));
if (m[4]) {
result.push(cleanFilename(m[4]));
}
}
return result;
}
// function getBranchInfo(localRepo) {
// return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) {
// var lines = output.split("\n");
// var unknownDirs = [];
// var branchLineRE = /^## (No commits yet on )?(.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/m;
// console.log(output);
// console.log(lines);
// var m = branchLineRE.exec(output);
// console.log(m);
// var result = {}; //commits:{}};
// if (m) {
// if (m[1]) {
// result.empty = true;
// }
// result.local = m[2];
// if (m[4]) {
// result.remote = m[4];
// }
// }
// return result;
// });
// }
function getStatus(localRepo) {
// parseFilename('"test with space"');
// parseFilename('"test with space" -> knownFile.txt');
// parseFilename('"test with space" -> "un -> knownFile.txt"');
var result = {
files: {},
commits: {},
branches: {}
}
return runGitCommand(['rev-list', 'HEAD', '--count'],localRepo).then(function(count) {
result.commits.total = parseInt(count);
}).catch(function(err) {
if (/ambiguous argument/i.test(err.message)) {
result.commits.total = 0;
} else {
throw err;
}
}).then(function() {
return runGitCommand(["ls-files","--cached","--others","--exclude-standard"],localRepo).then(function(output) {
var lines = output.split("\n");
lines.forEach(function(l) {
if (l==="") {
return;
}
var fullName = cleanFilename(l);
// parseFilename(l);
var parts = fullName.split("/");
var p = result.files;
var name;
for (var i = 0;i<parts.length-1;i++) {
var name = parts.slice(0,i+1).join("/")+"/";
if (!p.hasOwnProperty(name)) {
p[name] = {
type:"d"
}
}
}
result.files[fullName] = {
type: /\/$/.test(fullName)?"d":"f"
}
})
return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) {
var lines = output.split("\n");
var unknownDirs = [];
var branchLineRE = /^## (?:(?:No commits yet on )|(?:Initial commit on))?(.+?)(?:$|\.\.\.(.+?)(?:$| \[(?:(?:ahead (\d+)(?:,\s*)?)?(?:behind (\d+))?|(gone))\]))/;
lines.forEach(function(line) {
if (line==="") {
return;
}
if (line[0] === "#") {
var m = branchLineRE.exec(line);
if (m) {
result.branches.local = m[1];
if (m[2]) {
result.branches.remote = m[2];
result.commits.ahead = 0;
result.commits.behind = 0;
}
if (m[3] !== undefined) {
result.commits.ahead = parseInt(m[3]);
}
if (m[4] !== undefined) {
result.commits.behind = parseInt(m[4]);
}
if (m[5] !== undefined) {
result.commits.ahead = result.commits.total;
result.branches.remoteError = {
code: "git_remote_gone"
}
}
}
return;
}
var status = line.substring(0,2);
var fileName;
var names;
if (status !== '??') {
names = parseFilenames(line.substring(3));
} else {
names = [cleanFilename(line.substring(3))];
}
fileName = names[0];
if (names.length > 1) {
fileName = names[1];
}
// parseFilename(fileName);
if (fileName.charCodeAt(0) === 34) {
fileName = fileName.substring(1,fileName.length-1);
}
if (result.files.hasOwnProperty(fileName)) {
result.files[fileName].status = status;
} else {
result.files[fileName] = {
type: "f",
status: status
};
}
if (names.length > 1) {
result.files[fileName].oldName = names[0];
}
if (status === "??" && fileName[fileName.length-1] === '/') {
unknownDirs.push(fileName);
}
})
var allFilenames = Object.keys(result.files);
allFilenames.forEach(function(f) {
var entry = result.files[f];
if (!entry.hasOwnProperty('status')) {
unknownDirs.forEach(function(uf) {
if (f.startsWith(uf)) {
entry.status = "??"
}
});
}
})
// console.log(files);
return result;
})
})
})
}
function parseLog(log) {
var lines = log.split("\n");
var currentCommit = {};
var commits = [];
lines.forEach(function(l) {
if (l === "-----") {
commits.push(currentCommit);
currentCommit = {}
return;
}
var m = /^(.*): (.*)$/.exec(l);
if (m) {
// git 2.1.4 (Debian Stable) doesn't support %D for refs - so filter out
if (m[1] === 'refs' && m[2]) {
if (m[2] !== '%D') {
currentCommit[m[1]] = m[2].split(",").map(function(v) { return v.trim() });
} else {
currentCommit[m[1]] = [];
}
} else {
if (m[1] === 'parents') {
currentCommit[m[1]] = m[2].split(" ");
} else {
currentCommit[m[1]] = m[2];
}
}
}
});
return commits;
}
function getRemotes(cwd) {
return runGitCommand(['remote','-v'],cwd).then(function(output) {
var result;
if (output.length > 0) {
result = {};
var remoteRE = /^(.+)\t(.+) \((.+)\)$/gm;
var m;
while ((m = remoteRE.exec(output)) !== null) {
result[m[1]] = result[m[1]]||{};
result[m[1]][m[3]] = m[2];
}
}
return result;
})
}
function getBranches(cwd, remote) {
var args = ['branch','-vv','--no-color'];
if (remote) {
args.push('-r');
}
var branchRE = /^([ \*] )(\S+) +(\S+)(?: \[(\S+?)(?:: (?:ahead (\d+)(?:, )?)?(?:behind (\d+))?)?\])? (.*)$/;
return runGitCommand(args,cwd).then(function(output) {
var branches = [];
var lines = output.split("\n");
branches = lines.map(function(l) {
var m = branchRE.exec(l);
var branch = null;
if (m) {
branch = {
name: m[2],
remote: m[4],
status: {
ahead: m[5]||0,
behind: m[6]||0,
},
commit: {
sha: m[3],
subject: m[7]
}
}
if (m[1] === '* ') {
branch.current = true;
}
}
return branch;
}).filter(function(v) { return !!v && v.commit.sha !== '->' });
return {branches:branches};
})
}
function getBranchStatus(cwd,remoteBranch) {
var commands = [
// #commits master ahead
runGitCommand(['rev-list', 'HEAD','^'+remoteBranch, '--count'],cwd),
// #commits master behind
runGitCommand(['rev-list', '^HEAD',remoteBranch, '--count'],cwd)
];
return Promise.all(commands).then(function(results) {
return {
commits: {
ahead: parseInt(results[0]),
behind: parseInt(results[1])
}
}
})
}
function addRemote(cwd,name,options) {
var args = ["remote","add",name,options.url]
return runGitCommand(args,cwd);
}
function removeRemote(cwd,name) {
var args = ["remote","remove",name];
return runGitCommand(args,cwd);
}
module.exports = {
init: function(_settings) {
return new Promise(function(resolve,reject) {
Promise.all([
runGitCommand(["--version"]),
runGitCommand(["config","--global","user.name"]).catch(err=>""),
runGitCommand(["config","--global","user.email"]).catch(err=>"")
]).then(function(output) {
var m = / (\d\S+)/.exec(output[0]);
gitVersion = m[1];
var globalUserName = output[1].trim();
var globalUserEmail = output[2].trim();
var result = {
version: gitVersion
};
if (globalUserName && globalUserEmail) {
result.user = {
name: globalUserName,
email: globalUserEmail
}
}
log.trace("git init: "+JSON.stringify(result));
resolve(result);
}).catch(function(err) {
log.trace("git init: git not found");
resolve(null);
});
});
},
initRepo: function(cwd) {
return runGitCommand(["init"],cwd);
},
setUpstream: function(cwd,remoteBranch) {
var args = ["branch","--set-upstream-to",remoteBranch];
return runGitCommand(args,cwd);
},
pull: function(cwd,remote,branch,allowUnrelatedHistories,auth,gitUser) {
var args = ["pull"];
if (remote && branch) {
args.push(remote);
args.push(branch);
}
if (gitUser && gitUser['name'] && gitUser['email']) {
args.unshift('user.name="'+gitUser['name']+'"');
args.unshift('-c');
args.unshift('user.email="'+gitUser['email']+'"');
args.unshift('-c');
}
if (allowUnrelatedHistories) {
args.push("--allow-unrelated-histories");
}
var promise;
if (auth) {
if ( auth.key_path ) {
promise = runGitCommandWithSSHCommand(args,cwd,auth,true);
}
else {
promise = runGitCommandWithAuth(args,cwd,auth,true);
}
} else {
promise = runGitCommand(args,cwd,undefined,true)
}
return promise;
// .catch(function(err) {
// if (/CONFLICT/.test(err.stdout)) {
// var e = new Error("pull failed - merge conflict");
// e.code = "git_pull_merge_conflict";
// throw e;
// } else if (/Please commit your changes or stash/i.test(err.message)) {
// var e = new Error("Pull failed - local changes would be overwritten");
// e.code = "git_pull_overwrite";
// throw e;
// }
// throw err;
// });
},
push: function(cwd,remote,branch,setUpstream, auth) {
var args = ["push"];
if (branch) {
if (setUpstream) {
args.push("-u");
}
args.push(remote);
args.push("HEAD:"+branch);
} else {
args.push(remote);
}
args.push("--porcelain");
var promise;
if (auth) {
if ( auth.key_path ) {
promise = runGitCommandWithSSHCommand(args,cwd,auth,true);
}
else {
promise = runGitCommandWithAuth(args,cwd,auth,true);
}
} else {
promise = runGitCommand(args,cwd,undefined,true)
}
return promise.catch(function(err) {
if (err.code === 'git_error') {
if (/^!.*non-fast-forward/m.test(err.stdout)) {
err.code = 'git_push_failed';
}
throw err;
} else {
throw err;
}
});
},
clone: function(remote, auth, cwd) {
var args = ["clone",remote.url];
if (remote.name) {
args.push("-o");
args.push(remote.name);
}
if (remote.branch) {
args.push("-b");
args.push(remote.branch);
}
args.push(".");
if (auth) {
if ( auth.key_path ) {
return runGitCommandWithSSHCommand(args,cwd,auth,true);
}
else {
return runGitCommandWithAuth(args,cwd,auth,true);
}
} else {
return runGitCommand(args,cwd,undefined,true);
}
},
getStatus: getStatus,
getFile: function(cwd, filePath, treeish) {
var args = ["show",treeish+":"+filePath];
return runGitCommand(args,cwd);
},
getFiles: function(cwd) {
return getStatus(cwd).then(function(status) {
return status.files;
})
},
revertFile: function(cwd, filePath) {
var args = ["checkout",filePath];
return runGitCommand(args,cwd);
},
stageFile: function(cwd,file) {
var args = ["add"];
if (Array.isArray(file)) {
args = args.concat(file);
} else {
args.push(file);
}
return runGitCommand(args,cwd);
},
unstageFile: function(cwd, file) {
var args = ["reset","--"];
if (file) {
args.push(file);
}
return runGitCommand(args,cwd);
},
commit: function(cwd, message, gitUser) {
var args = ["commit","-m",message];
var env;
if (gitUser && gitUser['name'] && gitUser['email']) {
args.unshift('user.name="'+gitUser['name']+'"');
args.unshift('-c');
args.unshift('user.email="'+gitUser['email']+'"');
args.unshift('-c');
}
return runGitCommand(args,cwd,env);
},
getFileDiff(cwd,file,type) {
var args = ["diff","-w"];
if (type === "tree") {
// nothing else to do
} else if (type === "index") {
args.push("--cached");
}
args.push(file);
return runGitCommand(args,cwd);
},
fetch: function(cwd,remote,auth) {
var args = ["fetch",remote];
if (auth) {
if ( auth.key_path ) {
return runGitCommandWithSSHCommand(args,cwd,auth);
}
else {
return runGitCommandWithAuth(args,cwd,auth);
}
} else {
return runGitCommand(args,cwd);
}
},
getCommits: function(cwd,options) {
var args = ["log", "--format=sha: %H%nparents: %p%nrefs: %D%nauthor: %an%ndate: %ct%nsubject: %s%n-----"];
var limit = parseInt(options.limit) || 20;
args.push("-n "+limit);
var before = options.before;
if (before) {
args.push(before);
}
var commands = [
runGitCommand(['rev-list', 'HEAD', '--count'],cwd),
runGitCommand(args,cwd).then(parseLog)
];
return Promise.all(commands).then(function(results) {
var result = results[0];
result.count = results[1].length;
result.before = before;
result.commits = results[1];
return {
count: results[1].length,
commits: results[1],
before: before,
total: parseInt(results[0])
};
})
},
getCommit: function(cwd,sha) {
var args = ["show",sha];
return runGitCommand(args,cwd);
},
abortMerge: function(cwd) {
return runGitCommand(['merge','--abort'],cwd);
},
getRemotes: getRemotes,
getRemoteBranch: function(cwd) {
return runGitCommand(['rev-parse','--abbrev-ref','--symbolic-full-name','@{u}'],cwd).catch(function(err) {
if (/no upstream configured for branch/i.test(err.message)) {
return null;
}
throw err;
})
},
getBranches: getBranches,
// getBranchInfo: getBranchInfo,
checkoutBranch: function(cwd, branchName, isCreate) {
var args = ['checkout'];
if (isCreate) {
args.push('-b');
}
args.push(branchName);
return runGitCommand(args,cwd);
},
deleteBranch: function(cwd, branchName, isRemote, force) {
if (isRemote) {
throw new Error("Deleting remote branches not supported");
}
var args = ['branch'];
if (force) {
args.push('-D');
} else {
args.push('-d');
}
args.push(branchName);
return runGitCommand(args, cwd);
},
getBranchStatus: getBranchStatus,
addRemote: addRemote,
removeRemote: removeRemote
}