node-red/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js

661 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";
} else if (/nothing (add )?to commit/i.test(stdout)) {
return stdout;
}
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) {
var args = ["init", "--initial-branch", "main"];
return runGitCommand(args, cwd).catch(function () {
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", "--no-rebase"];
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
}