thegit
Version:
Implementation of git in node.js.
477 lines (448 loc) • 16.4 kB
JavaScript
const { execSync } = require("child_process");
const globals = require("./../globals");
const {
createEmptyFile,
deleteFile,
deleteDir,
createDir,
listPathsInDir,
writeToFile,
readFile,
pathExists,
isFile,
isDir
} = require('../util/fs_util');
const {
isInit,
createObjectFromFileContent,
debug,
logMessage,
setRootDir,
hash,
compress,
decompress,
getObjectFromHash,
getLastCommit,
createCommitStr,
updateRefsWithCommit,
getBranches,
getCurrentBranch,
parseCommit
} = require('../util/util.js');
const {
updateFilesFromTrees, checkoutCommit
} = require('./checkout.js');
const {
addAll
} = require('./add');
const {
mergedFile,
findmaxnum,
addchanges
} = require('./conflict')
const {
createTree
} = require('./commit')
function mergeCaller(...args) {
// branch name is args[0]
if(args[0]=="--continue"){
if(!(pathExists(".legit/MERGE_HEAD") && isFile(".legit/MERGE_HEAD"))){
console.log("merge not in progress, nothing to merge");
return;
}
addAll();
let root = createTree();
let currCommit = getLastCommit();
let incomingCommit = readFile(".legit/MERGE_HEAD");
writeToFile(".legit/COMMIT_EDITMSG", "merge commit");
execSync(globals.commitFileCommand);
globals.commitMessage = readFile(".legit/COMMIT_EDITMSG")
.split("\n")
.filter((line) => {
return line.trim()[0] != "#";
}).join("");
// new commit
let commitStr = createCommitStr(root.hash,{currCommit,incomingCommit});
console.log(commitStr);
let commitHash = hash(commitStr);
createObjectFromFileContent(commitStr);
// update refs
let currBranch = getCurrentBranch();
writeToFile(globals.headsDir + currBranch, commitHash);
// update working dir
let currTreeHash = parseCommit(currCommit).tree;
let nextTreeHash = root.hash;
updateFilesFromTrees(currTreeHash, nextTreeHash);
// delete merge head
deleteFile('.legit/MERGE_HEAD');
}
else if(args[0]=="--abort"){
checkoutCommit(getLastCommit());
deleteFile('.legit/MERGE_HEAD');
}
else
merge(args[0]);
}
function merge(branchName) {
let currCommit = getLastCommit();
let incomingCommit = readFile(globals.headsDir + branchName);
let mergeBase = getCommonAncestor(currCommit, incomingCommit);
if (mergeBase == currCommit) {
// ! fast forword merge
// A -- B -- C - main (.legit/HEAD)
// \
// D -- E - feature (passed as arg)
// A -- B -- C -- D -- E - feature,Main
//
console.log("Fast Farwording Merge");
let currBranch = getCurrentBranch();
writeToFile(globals.headsDir + currBranch, incomingCommit);
} else {
// ! true merge
// A -- B -- C -- F
// \
// D -- E -feature
// A -- B -- C -- F -- E' (Tree of E,F) - Main (running merge from main)
// \ /
// D -- E -feature
//
// ! steps for when no merge conflict
// ! conflict only occurs when the same file is modified
// find merge base
// get trees from merge base, current commit, and incoming commit
let baseTree = getTreeFromHash(parseCommit(mergeBase).tree, "root");
let currTree = getTreeFromHash(parseCommit(currCommit).tree, "root");
let incomingTree = getTreeFromHash(parseCommit(incomingCommit).tree, "root");
// calculate hashes for new tree
let conflictObj = { conflict:false };
let mergedTree = getMergedTree(baseTree,currTree,incomingTree,conflictObj);
if(conflictObj.conflict != true){
// create a new commit while asking for commit message,
writeToFile(".legit/COMMIT_EDITMSG", "merge commit");
execSync(globals.commitFileCommand);
globals.commitMessage = readFile(".legit/COMMIT_EDITMSG")
.split("\n")
.filter((line) => {
return line.trim()[0] != "#";
}).join("");
// new commit
let commitStr = createCommitStr(mergedTree.hash,{currCommit,incomingCommit});
console.log(commitStr);
let commitHash = hash(commitStr);
createObjectFromFileContent(commitStr);
// update refs
let currBranch = getCurrentBranch();
writeToFile(globals.headsDir + currBranch, commitHash);
// update working dir
let currTreeHash = parseCommit(currCommit).tree;
let nextTreeHash = mergedTree.hash;
updateFilesFromTrees(currTreeHash, nextTreeHash);
}
else{
// ! here there is true conflict
createEmptyFile('.legit/MERGE_HEAD');
writeToFile(".legit/MERGE_HEAD",incomingCommit);
// update working dir
let currTreeHash = parseCommit(currCommit).tree;
let nextTreeHash = mergedTree.hash;
updateFilesFromTrees(currTreeHash, nextTreeHash);
console.log("conflict present, please resolve conflicts and then do git merge --continue");
}
}
}
function getMergedTree(baseTree,currTree,incomingTree,conflict){
// ! !!!!!!! need to deal with case where there is an empty line in treeNode
// with the copy of merge base tree, recursively,
let mergeTree = {name: baseTree.name, type:'tree'};
// if blob
// * existing files
// ? different hashes in base, incoming and curr for same file - (both curr/incoming modified same file) -
// conflict
// ? existing file has same hash in one of incoming/curr, and a new hash in the other - (one of curr/incoming modified) -
// take the modified hash
// ? file has same hash in incoming and curr
// take the hash
// * creation
// ? different hashes in incoming and curr for same file - (both curr/incoming created same file with diff content -
// conflict
// ? same hash for file in incoming and curr - (both curr/incoming, created same exact file)
// take the hash and add file
// ? if a filename only is in curr xor incoming -
// take the hash and add the file to tree
// * deletion
// ? if file name is present in base and one of curr or incoming with a different hash - (deleted by one and modified but other)
// conflict
// ? if a filename from base is present in curr or incoming with same hash - (has been deleted by one or both of incoming or curr)
// do not add file
let mergeBlobs = [];
let baseBlobs = {};
baseTree && baseTree.children.filter(child=>{
return child.type=='blob';
}).forEach(blobObj=>{
baseBlobs[blobObj.name] = blobObj.hash;
})
let currBlobs = {};
currTree && currTree.children.filter(child=>{
return child.type=='blob';
}).forEach(blobObj=>{
currBlobs[blobObj.name] = blobObj.hash;
})
let incomingBlobs = {};
incomingTree && incomingTree.children.filter(child=>{
return child.type=='blob';
}).forEach(blobObj=>{
incomingBlobs[blobObj.name] = blobObj.hash;
})
for(let file in baseBlobs){
if((file in currBlobs) && (file in incomingBlobs)){
// ! files are modified
if(currBlobs[file] == incomingBlobs[file]){
mergeBlobs.push({'type': 'blob', 'hash': currBlobs[file], 'name': file});
}
else if((currBlobs[file] != baseBlobs[file]) && (incomingBlobs[file] == baseBlobs[file])){
mergeBlobs.push({'type': 'blob', 'hash': currBlobs[file], 'name': file});
}
else if((currBlobs[file] == baseBlobs[file]) && (incomingBlobs[file] != baseBlobs[file])){
mergeBlobs.push({'type': 'blob', 'hash': incomingBlobs[file], 'name': file});
}
else{
let newFile = mergedFile(getObjectFromHash(baseBlobs[file]), getObjectFromHash(currBlobs[file]), getObjectFromHash(incomingBlobs[file]));
if(newFile.conflict) {
console.log(`Conflict in file ${file}`);
conflict.conflict = true;
}
createObjectFromFileContent(newFile.newContent);
mergeBlobs.push({'type': 'blob', 'hash': hash(newFile.newContent), 'name': file});
}
}
// ! when files are deleted
else if(!(file in currBlobs) && (file in incomingBlobs)){
if(incomingBlobs[file]==baseBlobs[file]){
continue;
}
else{
let newFile = mergedFile(getObjectFromHash(baseBlobs[file]), '', getObjectFromHash(incomingBlobs[file]));
if(newFile.conflict) {
console.log(`Conflict in file ${file}`);
conflict.conflict = true;
}
createObjectFromFileContent(newFile.newContent);
mergeBlobs.push({'type': 'blob', 'hash': hash(newFile.newContent), 'name': file});
}
}
else if((file in currBlobs) && !(file in incomingBlobs)){
if(currBlobs[file]==baseBlobs[file]){
continue;
}
else{
let newFile = mergedFile(getObjectFromHash(baseBlobs[file]), getObjectFromHash(currBlobs[file]), '');
if(newFile.conflict) {
console.log(`Conflict in file ${file}`);
conflict.conflict = true;
}
createObjectFromFileContent(newFile.newContent);
mergeBlobs.push({'type': 'blob', 'hash': hash(newFile.newContent), 'name': file});
}
}
else {
continue;
}
}
// ! when files are created
for(let file in currBlobs){
if(!(file in baseBlobs) && !(file in incomingBlobs)){
mergeBlobs.push({'type': 'blob', 'hash': currBlobs[file], 'name': file});
}
else if(!(file in baseBlobs) && (file in incomingBlobs)){
if(incomingBlobs[file] != currBlobs[file]) {
let newFile = mergedFile('', getObjectFromHash(currBlobs[file]), getObjectFromHash(incomingBlobs[file]));
if(newFile.conflict) {
console.log(`Conflict in file ${file}`);
conflict.conflict = true;
}
createObjectFromFileContent(newFile.newContent);
mergeBlobs.push({'type': 'blob', 'hash': hash(newFile.newContent), 'name': file});
}
else {
mergeBlobs.push({'type': 'blob', 'hash': currBlobs[file], 'name': file});
}
}
}
for(let file in incomingBlobs){
if(!(file in baseBlobs) && !(file in currBlobs)){
mergeBlobs.push({'type': 'blob', 'hash': incomingBlobs[file], 'name': file});
}
// other conditions taken care in previous loop
}
let mergeTrees = [];
let baseTrees = {};
baseTree && baseTree.children.filter(child=>{
return child.type=='tree';
}).forEach(treeObj=>{
baseTrees[treeObj.name] = treeObj;
})
let currTrees = {};
currTree && currTree.children.filter(child=>{
return child.type=='tree';
}).forEach(treeObj=>{
currTrees[treeObj.name] = treeObj;
})
let incomingTrees = {};
incomingTree && incomingTree.children.filter(child=>{
return child.type=='tree';
}).forEach(treeObj=>{
incomingTrees[treeObj.name] = treeObj;
})
for(let dir in baseTrees){
if((dir in currTrees) && (dir in incomingTrees)){
// ! dir are modified
if(currTrees[dir].hash == incomingTrees[dir].hash){
mergeTrees.push(currTrees[dir]);
}
else if((currTrees[dir].hash != baseTrees[dir].hash) && (incomingTrees[dir].hash == baseTrees[dir].hash)){
mergeTrees.push(currTrees[dir]);
}
else if((currTrees[dir].hash == baseTrees[dir].hash) && (incomingTrees[dir].hash != baseTrees[dir].hash)){
mergeTrees.push(incomingTrees[dir]);
}
else{
mergeTrees.push(getMergedTree(baseTrees[dir], currTrees[dir], incomingTrees[dir], conflict));
}
}
// ! when dirs are deleted
else if(!(dir in currTrees) && (dir in incomingTrees)){
if(incomingTrees[dir].hash == baseTrees[dir].hash){
continue;
}
else{
mergeTrees.push(getMergedTree(baseTrees[dir],null,incomingTrees[dir],conflict));
}
}
else if((dir in currTrees) && !(dir in incomingTrees)){
if(currTrees[dir].hash == baseTrees[dir].hash){
continue;
}
else{
mergeTrees.push(getMergedTree(baseTrees[dir],currTrees[dir],null,conflict));
}
}
else {
continue;
}
}
// ! when dirs are created
for(let dir in currTrees){
if(!(dir in baseTrees) && !(dir in incomingTrees)){
mergeTrees.push(currTrees[dir]);
}
else if(!(dir in baseTrees) && (dir in incomingTrees)){
if(incomingTrees[dir] != currTrees[dir]) {
mergeTrees.push(getMergedTree(null,currTrees[dir],incomingTrees[dir],conflict));
}
else {
mergeTrees.push(currTrees[dir]);
}
}
}
for(let dir in incomingTrees){
if(!(dir in baseTrees) && !(dir in currTrees)){
mergeTrees.push(incomingTrees[dir]);
}
// other conditions taken care in previous loop
}
// if tree
// * existing trees
// different hashes in base, incoming and curr for same tree - (both curr/incoming modified same file) -
// call recursively with node of same file name
// existing tree has same hash in one of incoming/curr, and a new hash in the other - (one of curr/incoming modified) -
// call recursively with node of same file name
// existing file has same hash in all three,
// take existing hash
// * creation
// different hashes in incoming and curr for same tree - (both curr/incoming created same file with diff content -
// call recursively
// same hash for tree in incoming and curr - (both curr/incoming, created same exact file)
// take the hash
// if a filename only is in curr xor incoming -
// take the hash
// * deletion
// if file name is present in base and one of curr or incoming with a different hash - (deleted by one and modified but other)
// conflict
// if a filename from base is present in curr or incoming with same hash - (has been deleted by one or both of incoming or curr)
// dont take hash
mergeTree.children = [...mergeBlobs,...mergeTrees];
let treeStr = [];
for(let child of mergeTree.children){
treeStr.push(`${child.type} ${child.hash} ${child.name}`);
}
treeStr = treeStr.join("\n");
createObjectFromFileContent(treeStr);
mergeTree.hash = hash(treeStr);
return mergeTree;
}
function getTreeFromHash(treeHash, nodeName){
// make empty object to return, with name as nodeName and type: tree, hash: treehash
// read the contents of hash,
// make a temp list to accumulate children
// for each child
// if its a blob, add to list
// {name:childname, hash:childhash, type:blob}
// if its a tree add to list
// getTreeFromHash(childHash,childname)
// add list to empty object in children property
// return obj
let treeObject = {name: nodeName, hash: treeHash, type:"tree"};
let objs = getObjectFromHash(treeHash).split("\n").filter(e=>e).map((line) => {
let [type, hash, name] = line.split(" ");
return {
type, hash, name
}
});
let children = [];
for(let obj of objs){
if(obj.type == 'blob'){
children.push(obj);
}
else{
children.push(getTreeFromHash(obj.hash, obj.name))
}
}
treeObject.children = children;
return treeObject;
}
function getCommonAncestor(commitA, commitB) {
if(commitA==commitB) return commitA;
let commits = new Set();
commits.add(commitA);
commits.add(commitB);
while ("parent" in parseCommit(commitA) || "parent" in parseCommit(commitB)) {
let parsedA = parseCommit(commitA);
let parsedB = parseCommit(commitB);
if("parent" in parsedA){
if(commits.has(parsedA.parent)){
return parsedA.parent;
}
else{
commits.add(parsedA.parent);
commitA = parsedA.parent;
}
}
if("parent" in parsedB){
if(commits.has(parsedB.parent)){
return parsedB.parent;
}
else{
commits.add(parsedB.parent);
commitA = parsedB.parent;
}
}
}
return "";
}
module.exports = {
merge,
mergeCaller,
getMergedTree,
getTreeFromHash,
getCommonAncestor
}