myvcs 0.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -0
- package/Controllers/addRepo.js +38 -22
- package/Controllers/commitRepo.js +77 -23
- package/Controllers/config.js +2 -3
- package/Controllers/diffLines.js +41 -0
- package/Controllers/initRepo.js +1 -0
- package/Controllers/pullRepo.js +80 -55
- package/Controllers/pushRepo.js +128 -41
- package/Controllers/revertRepo.js +49 -16
- package/Controllers/specificCommit.js +16 -0
- package/README.md +10 -0
- package/config/aws-config.js +1 -1
- package/config/vcsConfig.js +4 -6
- package/index.js +60 -8
- package/package.json +10 -3
- package/utils/db.js +11 -0
package/.env.example
ADDED
package/Controllers/addRepo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises"
|
|
2
2
|
import path from "path"
|
|
3
3
|
async function readIgnoreFile() {
|
|
4
|
-
const ignorePath = path.
|
|
4
|
+
const ignorePath = path.resolve(process.cwd(), ".myvcsignore");
|
|
5
5
|
|
|
6
6
|
try {
|
|
7
7
|
// Check if file exists
|
|
@@ -9,7 +9,6 @@ async function readIgnoreFile() {
|
|
|
9
9
|
|
|
10
10
|
// Read file content
|
|
11
11
|
const content = await fs.readFile(ignorePath, "utf-8");
|
|
12
|
-
|
|
13
12
|
// Split into lines and clean up
|
|
14
13
|
const patterns = content
|
|
15
14
|
.split("\n") // Split by newline
|
|
@@ -24,7 +23,7 @@ async function readIgnoreFile() {
|
|
|
24
23
|
return null;
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
|
-
|
|
26
|
+
let ignoreList=[];
|
|
28
27
|
async function checkExists(path) {
|
|
29
28
|
try {
|
|
30
29
|
await fs.access(path);
|
|
@@ -37,6 +36,7 @@ async function addRepo(filePath){
|
|
|
37
36
|
const repoPath=path.resolve(process.cwd(),".myvcs");
|
|
38
37
|
const stagingPath=path.resolve(repoPath,"staging");
|
|
39
38
|
const repoExists=await checkExists(repoPath);
|
|
39
|
+
ignoreList = await readIgnoreFile();
|
|
40
40
|
if (!repoExists) {
|
|
41
41
|
console.log("Not a repository. Run 'myvcs init' first.");
|
|
42
42
|
return;
|
|
@@ -44,7 +44,7 @@ async function addRepo(filePath){
|
|
|
44
44
|
try{
|
|
45
45
|
await fs.mkdir(stagingPath,{recursive:true});
|
|
46
46
|
const fileName=path.basename(filePath);
|
|
47
|
-
if(fileName==".") addAllFiles(process.cwd(),stagingPath);
|
|
47
|
+
if(fileName==".") await addAllFiles(process.cwd(),stagingPath);
|
|
48
48
|
else if(ignoreList && ignoreList.includes(fileName)){
|
|
49
49
|
console.log("File not added to staging area Ignored!!")
|
|
50
50
|
}
|
|
@@ -56,37 +56,53 @@ async function addRepo(filePath){
|
|
|
56
56
|
console.error("Failed to add file to staging area!! ",err)
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
async function shouldIgnore(filename) {
|
|
60
|
-
|
|
61
|
-
// Ignore specific files/folders
|
|
59
|
+
async function shouldIgnore(filename, fullPath) {
|
|
60
|
+
// Check against ignore patterns
|
|
62
61
|
if (ignoreList && ignoreList.includes(filename)) return true;
|
|
63
62
|
|
|
64
|
-
//
|
|
63
|
+
// Check if path contains ignored directories
|
|
64
|
+
if (ignoreList) {
|
|
65
|
+
for (const pattern of ignoreList) {
|
|
66
|
+
if (fullPath.includes(pattern)) return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ignore hidden files
|
|
65
71
|
if (filename.startsWith(".")) return true;
|
|
66
72
|
|
|
67
73
|
return false;
|
|
68
74
|
}
|
|
69
|
-
async function addAllFiles(directory,stagingPath,baseDir=process.cwd()){
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
async function addAllFiles(directory, stagingPath, baseDir=process.cwd()){
|
|
76
|
+
// Check if this directory itself should be ignored
|
|
77
|
+
const dirName = path.basename(directory);
|
|
78
|
+
const shouldSkip = await shouldIgnore(dirName, directory);
|
|
79
|
+
|
|
80
|
+
if(shouldSkip) {
|
|
81
|
+
console.log(` X SKIPPING ${directory}`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const items = await fs.readdir(directory);
|
|
86
|
+
let addedCount = 0;
|
|
72
87
|
for(const item of items){
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
const fullPath = path.join(directory, item);
|
|
89
|
+
if(await shouldIgnore(item, fullPath)) continue;
|
|
90
|
+
|
|
91
|
+
const stat = await fs.stat(fullPath);
|
|
76
92
|
if(stat.isDirectory()){
|
|
77
|
-
const count=await addAllFiles(fullPath,stagingPath);
|
|
78
|
-
addedCount+=count;
|
|
93
|
+
const count = await addAllFiles(fullPath, stagingPath, baseDir); // recursive call will check it
|
|
94
|
+
addedCount += count;
|
|
79
95
|
}
|
|
80
|
-
else{
|
|
81
|
-
const relativePath=path.relative(baseDir,fullPath);
|
|
82
|
-
const destPath=path.join(stagingPath,relativePath);
|
|
83
|
-
await fs.mkdir(path.dirname(destPath),{recursive:true});
|
|
84
|
-
await fs.copyFile(fullPath,destPath);
|
|
96
|
+
else {
|
|
97
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
98
|
+
const destPath = path.join(stagingPath, relativePath);
|
|
99
|
+
await fs.mkdir(path.dirname(destPath), {recursive: true});
|
|
100
|
+
await fs.copyFile(fullPath, destPath);
|
|
85
101
|
addedCount++;
|
|
86
102
|
console.log(`added file ${relativePath}`);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
|
-
if(directory==baseDir){
|
|
105
|
+
if(directory == baseDir){
|
|
90
106
|
console.log(`Added ${addedCount} files to staging area`);
|
|
91
107
|
}
|
|
92
108
|
return addedCount;
|
|
@@ -2,30 +2,82 @@ import fs from "fs/promises"
|
|
|
2
2
|
import path from "path"
|
|
3
3
|
import { v5 as uuidv5 } from 'uuid';
|
|
4
4
|
import dotenv from "dotenv"
|
|
5
|
+
import { diffLines } from "./diffLines.js";
|
|
5
6
|
dotenv.config();
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
function generateCommitID(message, parent) {
|
|
9
|
+
const unique = message + (parent || "") + Date.now();
|
|
10
|
+
return uuidv5(unique, process.env.MY_NAMESPACE);
|
|
8
11
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await
|
|
12
|
+
|
|
13
|
+
async function processFiles(stagedPath, thisCommitPath, commitPath, parent, diffs, baseDir = stagedPath) {
|
|
14
|
+
const items = await fs.readdir(stagedPath);
|
|
15
|
+
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
const sourcePath = path.join(stagedPath, item);
|
|
18
|
+
const stat = await fs.stat(sourcePath);
|
|
19
|
+
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
// Recurse into subdirectory
|
|
22
|
+
const destDir = path.join(thisCommitPath, path.relative(baseDir, sourcePath));
|
|
23
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
24
|
+
await processFiles(sourcePath, thisCommitPath, commitPath, parent, diffs, baseDir);
|
|
25
|
+
} else {
|
|
26
|
+
// Process file
|
|
27
|
+
const relativePath = path.relative(baseDir, sourcePath);
|
|
28
|
+
const newContent = await fs.readFile(sourcePath, "utf-8");
|
|
29
|
+
|
|
30
|
+
let oldContent = "";
|
|
31
|
+
if (parent) {
|
|
32
|
+
const parentSnapshotPath = path.join(commitPath, parent, relativePath);
|
|
33
|
+
try {
|
|
34
|
+
oldContent = await fs.readFile(parentSnapshotPath, "utf-8");
|
|
35
|
+
} catch {
|
|
36
|
+
oldContent = "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const diff = diffLines(oldContent, newContent);
|
|
41
|
+
diffs[relativePath] = diff;
|
|
42
|
+
|
|
43
|
+
// Copy file to commit snapshot
|
|
44
|
+
const destPath = path.join(thisCommitPath, relativePath);
|
|
45
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
46
|
+
await fs.copyFile(sourcePath, destPath);
|
|
22
47
|
}
|
|
23
|
-
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function commitRepo(message) {
|
|
52
|
+
const repoPath = path.resolve(process.cwd(), ".myvcs");
|
|
53
|
+
const headPath = path.join(repoPath, "HEAD");
|
|
54
|
+
const commitPath = path.resolve(repoPath, "commits");
|
|
55
|
+
const stagedPath = path.resolve(repoPath, "staging");
|
|
56
|
+
const commitsJsonPath = path.join(commitPath, "commits.json");
|
|
57
|
+
|
|
58
|
+
let parent = null;
|
|
59
|
+
try {
|
|
60
|
+
parent = (await fs.readFile(headPath, "utf-8")).trim() || null;
|
|
61
|
+
} catch {
|
|
62
|
+
parent = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const diffs = {};
|
|
66
|
+
const commitId = generateCommitID(message, parent);
|
|
67
|
+
const thisCommitPath = path.resolve(path.join(commitPath, commitId));
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await fs.mkdir(commitPath, { recursive: true });
|
|
71
|
+
await fs.mkdir(thisCommitPath, { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Process all files recursively
|
|
74
|
+
await processFiles(stagedPath, thisCommitPath, commitPath, parent, diffs);
|
|
75
|
+
|
|
76
|
+
let commits = [];
|
|
24
77
|
try {
|
|
25
78
|
const data = await fs.readFile(commitsJsonPath, 'utf-8');
|
|
26
79
|
commits = JSON.parse(data);
|
|
27
80
|
} catch (err) {
|
|
28
|
-
// File doesn't exist yet, start with empty array
|
|
29
81
|
if (err.code !== 'ENOENT') throw err;
|
|
30
82
|
}
|
|
31
83
|
|
|
@@ -33,17 +85,19 @@ async function commitRepo(message){
|
|
|
33
85
|
commits.push({
|
|
34
86
|
commitId,
|
|
35
87
|
message,
|
|
36
|
-
date: new Date().toISOString()
|
|
88
|
+
date: new Date().toISOString(),
|
|
89
|
+
parent
|
|
37
90
|
});
|
|
38
91
|
|
|
39
92
|
// Write back to file
|
|
93
|
+
const diffsPath = path.join(thisCommitPath, "diffs.json");
|
|
94
|
+
await fs.writeFile(diffsPath, JSON.stringify(diffs, null, 2));
|
|
40
95
|
await fs.writeFile(commitsJsonPath, JSON.stringify(commits, null, 2));
|
|
96
|
+
await fs.writeFile(headPath, commitId);
|
|
41
97
|
console.log(`commit done ${commitId} with message ${message}`);
|
|
42
|
-
}catch(err){
|
|
43
|
-
console.error("Commit failed!! ",err);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("Commit failed!! ", err);
|
|
44
100
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
101
|
}
|
|
48
102
|
|
|
49
|
-
export {commitRepo}
|
|
103
|
+
export { commitRepo }
|
package/Controllers/config.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {readConfig,setConfigValue} from "../config/vcsConfig.js"
|
|
2
|
-
|
|
3
2
|
export function showConfig(){
|
|
4
3
|
try{
|
|
5
4
|
const config=readConfig();
|
|
@@ -16,14 +15,14 @@ export function showConfig(){
|
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
export function setConfig(key,value){
|
|
18
|
+
export async function setConfig(key,value){
|
|
20
19
|
try{
|
|
21
20
|
const validKeys=["username","email"];
|
|
22
21
|
if(!validKeys.includes(key)){
|
|
23
22
|
console.log(`Invalid Config Key ${key}`);
|
|
24
23
|
process.exit(1);
|
|
25
24
|
}
|
|
26
|
-
if(key
|
|
25
|
+
if(key==="username"){
|
|
27
26
|
if(!/^[a-zA-Z0-9_-]+$/.test(value)){
|
|
28
27
|
console.log("username must only contain letters,underscores and hyphen");
|
|
29
28
|
process.exit(1);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function lcs(a,b){
|
|
2
|
+
const m=a.length,n=b.length;
|
|
3
|
+
const dp=Array.from({length:m+1},()=>Array(n+1).fill(0));
|
|
4
|
+
|
|
5
|
+
for(let i=1;i<m;i++){
|
|
6
|
+
for(let j=1;j<n;j++){
|
|
7
|
+
if(a[i-1]===b[j-1]){
|
|
8
|
+
dp[i][j]=dp[i-1][j-1]+1;
|
|
9
|
+
}
|
|
10
|
+
else dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return dp;
|
|
15
|
+
}
|
|
16
|
+
function backtrack(dp,a,b,i,j){
|
|
17
|
+
if(i===0 && j==0) return [];
|
|
18
|
+
if(i>0 && j>0 && a[i-1]===b[j-1]){
|
|
19
|
+
const result=backtrack(dp,a,b,i-1,j-1);
|
|
20
|
+
result.push({type:"same",line:a[i-1]});
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
else if(j>0 && (i==0||dp[i][j-1]>=dp[i-1][j])){
|
|
24
|
+
const result=backtrack(dp,a,b,i,j-1);
|
|
25
|
+
result.push({type:"added",line:b[j-1]});
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
else{
|
|
29
|
+
const result=backtrack(dp,a,b,i-1,j);
|
|
30
|
+
result.push({type:"removed",line:a[i-1]});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function diffLines(oldContent,newContent){
|
|
35
|
+
const a = oldContent.split("\n").filter(line => line.length > 0);
|
|
36
|
+
const b = newContent.split("\n").filter(line => line.length > 0);
|
|
37
|
+
const dp=lcs(a,b);
|
|
38
|
+
return backtrack(dp,a,b,a.length,b.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {diffLines}
|
package/Controllers/initRepo.js
CHANGED
|
@@ -10,6 +10,7 @@ async function initRepo(){
|
|
|
10
10
|
try{
|
|
11
11
|
await fs.mkdir(repoPath,{recursive:true});
|
|
12
12
|
await fs.mkdir(commitPath,{recursive:true});
|
|
13
|
+
await fs.writeFile(path.join(repoPath, "HEAD"), "");
|
|
13
14
|
await fs.writeFile(
|
|
14
15
|
path.join(repoPath,"config.json"),
|
|
15
16
|
JSON.stringify({bucket:process.env.S3_BUCKET})
|
package/Controllers/pullRepo.js
CHANGED
|
@@ -1,64 +1,89 @@
|
|
|
1
1
|
import fs from "fs/promises"
|
|
2
2
|
import path from "path"
|
|
3
|
-
import {S3,S3_BUCKET} from "../config/aws-config.js"
|
|
4
|
-
import {getUsername,getConfigValue} from "../config/vcsConfig.js"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await fs.writeFile(localFilePath,fileContent.Body);
|
|
32
|
-
console.log(`Downloaded: ${relativePath}`);
|
|
3
|
+
import { S3, S3_BUCKET } from "../config/aws-config.js"
|
|
4
|
+
import { getUsername, getConfigValue } from "../config/vcsConfig.js"
|
|
5
|
+
|
|
6
|
+
async function pullRepo() {
|
|
7
|
+
const repoPath = path.resolve(process.cwd(), ".myvcs");
|
|
8
|
+
const commitsPath = path.resolve(repoPath, "commits");
|
|
9
|
+
const commitsJsonPath = path.join(commitsPath, "commits.json");
|
|
10
|
+
const headPath = path.join(repoPath, "HEAD");
|
|
11
|
+
const username = getUsername();
|
|
12
|
+
const repoName = getConfigValue("repoName");
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Download repo payload from S3
|
|
16
|
+
const params = {
|
|
17
|
+
Bucket: S3_BUCKET,
|
|
18
|
+
Key: `repos/${username}/${repoName}/repo.json`
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const data = await S3.getObject(params).promise();
|
|
22
|
+
const payload = JSON.parse(data.Body.toString());
|
|
23
|
+
|
|
24
|
+
// Read local commits
|
|
25
|
+
let localCommits = [];
|
|
26
|
+
try {
|
|
27
|
+
const localData = await fs.readFile(commitsJsonPath, "utf-8");
|
|
28
|
+
localCommits = JSON.parse(localData);
|
|
29
|
+
} catch {
|
|
30
|
+
localCommits = [];
|
|
33
31
|
}
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
|
|
33
|
+
const localHashes = new Set(localCommits.map(c => c.commitId));
|
|
34
|
+
|
|
35
|
+
// Find new commits
|
|
36
|
+
const newCommits = payload.commits.filter(c => !localHashes.has(c.commitId));
|
|
37
|
+
|
|
38
|
+
if (newCommits.length === 0) {
|
|
39
|
+
console.log("✓ Already up to date");
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
|
|
43
|
+
console.log(`Pulling ${newCommits.length} commit(s)...`);
|
|
44
|
+
|
|
45
|
+
// Apply new commits
|
|
46
|
+
for (const commit of newCommits) {
|
|
47
|
+
const commitDir = path.join(commitsPath, commit.commitId);
|
|
48
|
+
await fs.mkdir(commitDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
// Write snapshot files
|
|
51
|
+
const snapshot = payload.snapshots[commit.commitId] || {};
|
|
52
|
+
for (const [filePath, content] of Object.entries(snapshot)) {
|
|
53
|
+
const destPath = path.join(commitDir, filePath);
|
|
54
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
55
|
+
await fs.writeFile(destPath, content, "utf-8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Write diffs
|
|
59
|
+
const diff = payload.diffs[commit.commitId] || {};
|
|
60
|
+
const diffsPath = path.join(commitDir, "diffs.json");
|
|
61
|
+
await fs.writeFile(diffsPath, JSON.stringify(diff, null, 2));
|
|
62
|
+
|
|
63
|
+
console.log(`Applied [${commit.commitId.slice(0, 7)}] ${commit.message}`);
|
|
57
64
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
|
|
66
|
+
// Update commits.json
|
|
67
|
+
const allCommits = [...localCommits, ...newCommits];
|
|
68
|
+
await fs.writeFile(commitsJsonPath, JSON.stringify(allCommits, null, 2));
|
|
69
|
+
|
|
70
|
+
// Update HEAD
|
|
71
|
+
await fs.writeFile(headPath, payload.head);
|
|
72
|
+
|
|
73
|
+
// Restore working files from latest commit
|
|
74
|
+
const latestSnapshot = payload.snapshots[payload.head] || {};
|
|
75
|
+
for (const [filePath, content] of Object.entries(latestSnapshot)) {
|
|
76
|
+
const destPath = path.join(process.cwd(), filePath);
|
|
77
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
78
|
+
await fs.writeFile(destPath, content, "utf-8");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`Pulled ${newCommits.length} commit(s)`);
|
|
82
|
+
console.log(`HEAD -> ${payload.head.slice(0, 7)}`);
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("Pull failed:", err);
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
export {pullRepo}
|
|
89
|
+
export { pullRepo }
|
package/Controllers/pushRepo.js
CHANGED
|
@@ -1,49 +1,136 @@
|
|
|
1
1
|
import fs from "fs/promises"
|
|
2
2
|
import path from "path"
|
|
3
|
-
import {S3,S3_BUCKET} from "../config/aws-config.js"
|
|
4
|
-
import {getUsername,getConfigValue} from "../config/vcsConfig.js"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
3
|
+
import { S3, S3_BUCKET } from "../config/aws-config.js"
|
|
4
|
+
import { getUsername, getConfigValue } from "../config/vcsConfig.js"
|
|
5
|
+
import repoModel from "../models/repoModel.js"
|
|
6
|
+
import userModel from "../models/userModel.js"
|
|
7
|
+
|
|
8
|
+
async function pushRepo() {
|
|
9
|
+
const repoPath = path.resolve(process.cwd(), ".myvcs");
|
|
10
|
+
const commitsPath = path.resolve(repoPath, "commits");
|
|
11
|
+
const commitsJsonPath = path.join(commitsPath, "commits.json");
|
|
12
|
+
const headPath = path.join(repoPath, "HEAD");
|
|
13
|
+
const username = getUsername();
|
|
14
|
+
const repoName = getConfigValue("repoName");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Read commits metadata
|
|
18
|
+
const commitsData = await fs.readFile(commitsJsonPath, "utf-8");
|
|
19
|
+
const commits = JSON.parse(commitsData);
|
|
20
|
+
|
|
21
|
+
if (commits.length === 0) {
|
|
22
|
+
console.log("No commits to push");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read HEAD
|
|
27
|
+
const head = (await fs.readFile(headPath, "utf-8")).trim();
|
|
28
|
+
|
|
29
|
+
// Create S3 key
|
|
30
|
+
const s3Key = `repos/${username}/${repoName}/repo.json`;
|
|
31
|
+
|
|
32
|
+
// Check if repo exists in MongoDB, create if not
|
|
33
|
+
const existingRepo = await repoModel.findOne({ name: repoName });
|
|
34
|
+
|
|
35
|
+
if (!existingRepo) {
|
|
36
|
+
const existingUser = await userModel.findOne({ email: username });
|
|
37
|
+
|
|
38
|
+
if (!existingUser) {
|
|
39
|
+
throw new Error("User not found. Please register first.");
|
|
30
40
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
const newRepo = new repoModel({
|
|
43
|
+
name: repoName,
|
|
44
|
+
visibility: false,
|
|
45
|
+
owner: existingUser._id,
|
|
46
|
+
s3Key: s3Key,
|
|
47
|
+
lastCommitHash: head,
|
|
48
|
+
totalCommits: commits.length
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await newRepo.save();
|
|
52
|
+
console.log(`Created new repo: ${repoName}`);
|
|
53
|
+
} else {
|
|
54
|
+
// Update existing repo metadata
|
|
55
|
+
const latestCommit = commits[commits.length - 1];
|
|
56
|
+
existingRepo.s3Key = s3Key;
|
|
57
|
+
existingRepo.lastCommitHash = head;
|
|
58
|
+
existingRepo.lastCommitMessage = latestCommit.message;
|
|
59
|
+
existingRepo.lastCommitDate = new Date(latestCommit.date);
|
|
60
|
+
existingRepo.totalCommits = commits.length;
|
|
61
|
+
await existingRepo.save();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Bundle all commit snapshots and diffs
|
|
65
|
+
const snapshots = {};
|
|
66
|
+
const diffs = {};
|
|
67
|
+
|
|
68
|
+
for (const commit of commits) {
|
|
69
|
+
const commitDir = path.join(commitsPath, commit.commitId);
|
|
70
|
+
|
|
71
|
+
// Read snapshot files recursively
|
|
72
|
+
snapshots[commit.commitId] = await readCommitFiles(commitDir);
|
|
73
|
+
|
|
74
|
+
// Read diffs
|
|
75
|
+
const diffsPath = path.join(commitDir, "diffs.json");
|
|
76
|
+
try {
|
|
77
|
+
const diffData = await fs.readFile(diffsPath, "utf-8");
|
|
78
|
+
diffs[commit.commitId] = JSON.parse(diffData);
|
|
79
|
+
} catch {
|
|
80
|
+
diffs[commit.commitId] = {};
|
|
41
81
|
}
|
|
42
82
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
|
|
84
|
+
// Create payload
|
|
85
|
+
const payload = {
|
|
86
|
+
repoName,
|
|
87
|
+
username,
|
|
88
|
+
pushedAt: new Date().toISOString(),
|
|
89
|
+
head,
|
|
90
|
+
commits,
|
|
91
|
+
snapshots,
|
|
92
|
+
diffs
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Upload as single JSON file
|
|
96
|
+
const params = {
|
|
97
|
+
Bucket: S3_BUCKET,
|
|
98
|
+
Key: s3Key,
|
|
99
|
+
Body: JSON.stringify(payload, null, 2),
|
|
100
|
+
ContentType: "application/json"
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await S3.upload(params).promise();
|
|
104
|
+
console.log(`✓ Pushed ${commits.length} commit(s) to S3`);
|
|
105
|
+
console.log(` HEAD → ${head.slice(0, 7)}`);
|
|
106
|
+
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error("Push failed:", err);
|
|
109
|
+
throw err; // re-throw so withDB can handle cleanup
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readCommitFiles(commitDir, baseDir = commitDir) {
|
|
114
|
+
const files = {};
|
|
115
|
+
const items = await fs.readdir(commitDir);
|
|
116
|
+
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
if (item === "diffs.json") continue;
|
|
119
|
+
|
|
120
|
+
const fullPath = path.join(commitDir, item);
|
|
121
|
+
const stat = await fs.stat(fullPath);
|
|
122
|
+
|
|
123
|
+
if (stat.isDirectory()) {
|
|
124
|
+
const nested = await readCommitFiles(fullPath, baseDir);
|
|
125
|
+
Object.assign(files, nested);
|
|
126
|
+
} else {
|
|
127
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
128
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
129
|
+
files[relativePath] = content;
|
|
130
|
+
}
|
|
46
131
|
}
|
|
132
|
+
|
|
133
|
+
return files;
|
|
47
134
|
}
|
|
48
135
|
|
|
49
|
-
export {pushRepo}
|
|
136
|
+
export { pushRepo }
|
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
import fs from "fs/promises"
|
|
2
2
|
import path from "path"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
for(const file of files){
|
|
13
|
-
await fs.copyFile(path.join(thisCommit,file),path.join(parentDir,file));
|
|
3
|
+
import {addRepo} from "./addRepo.js"
|
|
4
|
+
import { commitRepo } from "./commitRepo.js"
|
|
5
|
+
async function applyInverseDiff(diff, filePath) {
|
|
6
|
+
const invertedLines = [];
|
|
7
|
+
|
|
8
|
+
for(const d of diff) {
|
|
9
|
+
if(d.type === "same") {
|
|
10
|
+
invertedLines.push(d.line); // keep it
|
|
14
11
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
console.log("Unable to Revert!! ",err);
|
|
12
|
+
else if(d.type === "removed") {
|
|
13
|
+
invertedLines.push(d.line); // it was removed, bring it back
|
|
14
|
+
}
|
|
15
|
+
// skip "added"
|
|
20
16
|
}
|
|
17
|
+
|
|
18
|
+
const content = invertedLines.join("\n");
|
|
19
|
+
await fs.writeFile(filePath, content);
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
async function revertRepo(commitID) {
|
|
23
|
+
const repoPath = path.resolve(process.cwd(), ".myvcs");
|
|
24
|
+
const commitPath = path.resolve(repoPath, "commits");
|
|
25
|
+
const diffsPath = path.join(commitPath, commitID, "diffs.json");
|
|
26
|
+
const commitJSON = path.join(commitPath, "commits.json");
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Read diffs
|
|
30
|
+
const diffs = JSON.parse(await fs.readFile(diffsPath, "utf-8"));
|
|
31
|
+
|
|
32
|
+
// Read original commit message
|
|
33
|
+
const commits = JSON.parse(await fs.readFile(commitJSON, "utf-8"));
|
|
34
|
+
const originalCommit = commits.find(c => c.commitId === commitID);
|
|
35
|
+
|
|
36
|
+
if (!originalCommit) {
|
|
37
|
+
console.error(`Commit ${commitID} not found`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Apply inverse diffs and stage files
|
|
42
|
+
for (const [file, diff] of Object.entries(diffs)) {
|
|
43
|
+
const filePath = path.join(process.cwd(), file);
|
|
44
|
+
await applyInverseDiff(diff, filePath);
|
|
45
|
+
await addRepo(filePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create revert commit
|
|
49
|
+
await commitRepo(`Revert: ${originalCommit.message}`);
|
|
50
|
+
|
|
51
|
+
console.log(`Reverted commit ${commitID} successfully!`);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error("Unable to revert:", err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
23
56
|
export {revertRepo}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import fs from "fs/promises"
|
|
3
|
+
async function showSpecificCommit(commitId) {
|
|
4
|
+
const repoPath=path.resolve(process.cwd(),".myvcs");
|
|
5
|
+
const commitsPath=path.join(repoPath,"commits");
|
|
6
|
+
const diffsPath = path.join(commitsPath, commitId, "diffs.json");
|
|
7
|
+
const diffs = JSON.parse(await fs.readFile(diffsPath, "utf-8"));
|
|
8
|
+
for(const [file, diff] of Object.entries(diffs)) {
|
|
9
|
+
console.log(`\n${file}:`);
|
|
10
|
+
for(const line of diff) {
|
|
11
|
+
if(line.type === "added") console.log(`+ ${line.line}`);
|
|
12
|
+
if(line.type === "removed") console.log(`- ${line.line}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export {showSpecificCommit}
|
package/README.md
CHANGED
|
@@ -8,6 +8,16 @@ npm install -g myvcs
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## Setup
|
|
11
|
+
Create a .env file in your project:
|
|
12
|
+
|
|
13
|
+
AWS_ACCESS_KEY_ID=...
|
|
14
|
+
AWS_SECRET_ACCESS_KEY=...
|
|
15
|
+
S3_BUCKET=...
|
|
16
|
+
MY_NAMESPACE=...
|
|
17
|
+
|
|
18
|
+
Then run:
|
|
19
|
+
|
|
20
|
+
myvcs init
|
|
11
21
|
```
|
|
12
22
|
Initialize a repository:
|
|
13
23
|
```bash
|
package/config/aws-config.js
CHANGED
package/config/vcsConfig.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
2
|
import path from "path"
|
|
3
|
-
|
|
4
3
|
function getConfigPath(){
|
|
5
4
|
return path.join(process.cwd(),".myvcs","config.json");
|
|
6
5
|
}
|
|
@@ -8,7 +7,6 @@ function configDirExists(){
|
|
|
8
7
|
const configDir=path.join(process.cwd(),".myvcs");
|
|
9
8
|
return fs.existsSync(configDir);
|
|
10
9
|
}
|
|
11
|
-
|
|
12
10
|
function initConfig(){
|
|
13
11
|
const defaultConfig={
|
|
14
12
|
"username":null,
|
|
@@ -50,7 +48,7 @@ function writeConfig(config){
|
|
|
50
48
|
throw new Error("Not a myvcs repository. Run 'myvcs init' first.")
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
|
-
function setConfigValue(key,value){
|
|
51
|
+
async function setConfigValue(key,value){
|
|
54
52
|
const config=readConfig();
|
|
55
53
|
config[key]=value;
|
|
56
54
|
writeConfig(config);
|
|
@@ -61,11 +59,11 @@ function getConfigValue(key) {
|
|
|
61
59
|
return config[key];
|
|
62
60
|
}
|
|
63
61
|
function getUsername(){
|
|
64
|
-
const username=getConfigValue("
|
|
62
|
+
const username=getConfigValue("email");
|
|
65
63
|
if(!username){
|
|
66
64
|
throw new Error(
|
|
67
|
-
"Username not configured. Please set your
|
|
68
|
-
" myvcs config set
|
|
65
|
+
"Username not configured. Please set your email with:\n" +
|
|
66
|
+
" myvcs config set email <your-registered-mail>"
|
|
69
67
|
);
|
|
70
68
|
}
|
|
71
69
|
return username;
|
package/index.js
CHANGED
|
@@ -9,8 +9,24 @@ import { pullRepo } from "./Controllers/pullRepo.js";
|
|
|
9
9
|
import {revertRepo} from "./Controllers/revertRepo.js"
|
|
10
10
|
import {showCommits} from "./Controllers/showCommits.js"
|
|
11
11
|
import {showConfig,setConfig} from "./Controllers/config.js"
|
|
12
|
+
import { showSpecificCommit } from "./Controllers/specificCommit.js";
|
|
13
|
+
import { connectDB, disconnectDB } from "./utils/db.js";
|
|
14
|
+
|
|
12
15
|
yargs(hideBin(process.argv))
|
|
13
|
-
.command("init","initialise a new repository",{},
|
|
16
|
+
.command("init","initialise a new repository",{},
|
|
17
|
+
async ()=>{
|
|
18
|
+
try{
|
|
19
|
+
await connectDB();
|
|
20
|
+
await initRepo()
|
|
21
|
+
}
|
|
22
|
+
catch(e){
|
|
23
|
+
console.log('ERROR:',e);
|
|
24
|
+
}
|
|
25
|
+
finally{
|
|
26
|
+
await disconnectDB();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
)
|
|
14
30
|
.command("add <file>","Add a file to repo/staging area use 'add .' to add all files",
|
|
15
31
|
(yargs)=>{
|
|
16
32
|
yargs.positional("file",{
|
|
@@ -28,11 +44,26 @@ yargs(hideBin(process.argv))
|
|
|
28
44
|
type:"string"
|
|
29
45
|
})
|
|
30
46
|
},
|
|
31
|
-
(argv)=>{
|
|
32
|
-
commitRepo(argv.message);
|
|
47
|
+
async (argv)=>{
|
|
48
|
+
await commitRepo(argv.message);
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
.command("push","push commits to cloud storage",{},
|
|
52
|
+
async ()=>{
|
|
53
|
+
try{
|
|
54
|
+
await connectDB();
|
|
55
|
+
pushRepo();
|
|
56
|
+
}
|
|
57
|
+
catch(e){
|
|
58
|
+
console.log('ERROR:',e);
|
|
59
|
+
}
|
|
60
|
+
finally{
|
|
61
|
+
await disconnectDB();
|
|
62
|
+
}
|
|
33
63
|
}
|
|
64
|
+
|
|
65
|
+
|
|
34
66
|
)
|
|
35
|
-
.command("push","push commits to cloud storage",{},pushRepo)
|
|
36
67
|
.command("pull","pull commits from cloud to local system",{},pullRepo)
|
|
37
68
|
.command("revert <commitID>","revert back to specified commitID",
|
|
38
69
|
(yargs)=>{
|
|
@@ -41,11 +72,22 @@ yargs(hideBin(process.argv))
|
|
|
41
72
|
type:"string"
|
|
42
73
|
})
|
|
43
74
|
},
|
|
44
|
-
(argv)=>{
|
|
45
|
-
revertRepo(argv.commitID);
|
|
75
|
+
async (argv)=>{
|
|
76
|
+
await revertRepo(argv.commitID);
|
|
46
77
|
}
|
|
47
78
|
)
|
|
48
79
|
.command("list commits","show all commmits",{},showCommits)
|
|
80
|
+
.command("show <commitID>","show specific CommitID",
|
|
81
|
+
(yargs)=>{
|
|
82
|
+
yargs.positional("commitID",{
|
|
83
|
+
describe:"commitID to revert",
|
|
84
|
+
type:"string"
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
,async (argv)=>{
|
|
88
|
+
await showSpecificCommit(argv.commitID);
|
|
89
|
+
}
|
|
90
|
+
)
|
|
49
91
|
.command("config","configure myVCS on your system",
|
|
50
92
|
(yargs)=>{
|
|
51
93
|
return yargs
|
|
@@ -61,8 +103,18 @@ yargs(hideBin(process.argv))
|
|
|
61
103
|
type:"string"
|
|
62
104
|
})
|
|
63
105
|
},
|
|
64
|
-
(argv)=>{
|
|
65
|
-
|
|
106
|
+
async (argv)=>{
|
|
107
|
+
try{
|
|
108
|
+
await connectDB();
|
|
109
|
+
await setConfig(argv.key,argv.value);
|
|
110
|
+
}
|
|
111
|
+
catch(e){
|
|
112
|
+
console.log('Error while configuring:',e);
|
|
113
|
+
}
|
|
114
|
+
finally{
|
|
115
|
+
await disconnectDB();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
66
118
|
}
|
|
67
119
|
)
|
|
68
120
|
.command("list","show config details",{},showConfig)
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myvcs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
7
|
},
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"vcs",
|
|
10
|
+
"version-control",
|
|
11
|
+
"git",
|
|
12
|
+
"s3",
|
|
13
|
+
"cloud-storage"
|
|
14
|
+
],
|
|
9
15
|
"type": "module",
|
|
10
16
|
"author": "Amarnath Vasanth amarnathvasanth39@gmail.com",
|
|
11
17
|
"license": "ISC",
|
|
@@ -15,11 +21,12 @@
|
|
|
15
21
|
},
|
|
16
22
|
"repository": {
|
|
17
23
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/
|
|
24
|
+
"url": "https://github.com/amar1009/VCStool.git"
|
|
19
25
|
},
|
|
20
26
|
"dependencies": {
|
|
21
27
|
"aws-sdk": "^2.1693.0",
|
|
22
28
|
"dotenv": "^17.2.3",
|
|
29
|
+
"mongoose": "^9.2.2",
|
|
23
30
|
"uuid": "^13.0.0",
|
|
24
31
|
"yargs": "^18.0.0"
|
|
25
32
|
}
|
package/utils/db.js
ADDED