//
// Keep versions of files in EECS 482 github repos
//
const vscode = require('vscode');
const os = require('os');
const fs = require('fs');
const path = require('path');
const child_process = require('child_process');
const version = 'vscode-20251228';
const minTimeVersion = 10; // minimum time between versions
const minTimePush = 60; // minimum time between pushes
const minTimeCheck = 600; // minimum time between checking version482 repo
const username = os.userInfo().username.replace(/[^a-zA-Z0-9]/g, "");
let versionTime = {}; // last time each file was written
let pushTime = {}; // last time each repo was pushed
let checkTime = 0; // last time version482 repo was checked
let sessionStart = time();
let versionDir = {}; // .version482 directory name for each source file directory
let timer = {};
exports.activate = () => {
vscode.workspace.textDocuments.forEach(doc => initFile(doc));
vscode.workspace.onDidOpenTextDocument(doc => {
initFile(doc);
});
vscode.workspace.onDidSaveTextDocument(doc => {
saveFile(doc);
});
vscode.workspace.onDidChangeTextDocument(e => {
textChanged(e.document);
});
}
// return current time in seconds
function time() {
return(Math.trunc(Date.now()/1000));
}
// See if directory is in an EECS 482 git repo.
// Store .version482 directory name in versionDir, and make sure the .version482
// directory exists and is properly configured. If the directory isn't in a
// git repo, then set the versionDir entry to the empty string.
function initVersionDir(dir) {
if (dir in versionDir) {
return;
}
versionDir[dir] = '';
let dirFound = "";
if (fs.existsSync(dir)) {
dirFound = dir;
} else {
// Windows VS Code may prepend extra directory components when WSL
// files are opened as local files). Fix this by removing directory
// components from the beginning of the directory name until I find a
// directory that exists.
// Parse the directory name into components.
let dirRoot = dir;
let dirRootLast;
let basename = "";
let components = [];
do {
dirRootLast = dirRoot;
basename = path.basename(dirRoot);
if (basename != "") {
components.unshift(basename);
dirRoot = path.dirname(dirRoot);
}
} while (basename != "" && dirRoot != dirRootLast);
// Look for the directory, removing prefixes if needed.
for (let i=0; i<components.length && dirFound == ""; i++) {
const dir1 = dirRoot + components.slice(i).join(path.sep);
if (fs.existsSync(dir1)) {
dirFound = dir1;
}
}
}
if (dirFound == "") {
return;
}
try {
// Get top level of working tree
// This will throw an exception if git rev-parse fails, e.g., if
// the directory is not in a work tree
let top = child_process.execSync('cd "' + dirFound + '"; git rev-parse --show-toplevel').toString().trimEnd();
// Get name of main repo and make sure it's not a version482 repo
const stdout = child_process.execSync('cd "' + dirFound + '"; git remote -v 2> /dev/null').toString();
if (stdout.indexOf('version482') >= 0) {
throw new Error("File is in a version482 repo");
}
const found = stdout.match(/(eecs482\/[a-z.]*)\.(\d+)/);
if (! found) {
throw new Error("File is not in an eecs482 project repository");
}
const repo = found[1] + '.' + found[2];
versionDir[dir] = top + path.sep + '.version482';
// clone version482 repo if needed
if (! fs.existsSync(versionDir[dir])) {
child_process.execSync('export SSH_ASKPASS=echo; export SSH_ASKPASS_REQUIRE=force; cd "' + top + '"; git clone git@github.com:' + repo + '.version482 .version482 > /dev/null 2>&1');
}
// make sure .version482 is a directory
if (! fs.statSync(versionDir[dir]).isDirectory) {
throw new Error(".version482 is not a directory");
}
// make sure .version482 is in its own repo
let version482_top = child_process.execSync('cd "' + versionDir[dir] + '"; git rev-parse --show-toplevel').toString().trimEnd();
if (version482_top == top) {
throw new Error("version482 is part of the main project repository");
}
// Make sure origin for version482 is consistent with origin for
// main repo (in case repos were renamed, or the repo somehow loses
// its origin definition).
try {
let url = child_process.execSync('cd "' + versionDir[dir] + '"; git remote get-url origin 2> /dev/null').toString().trimEnd();
if (url != 'git@github.com:' + repo + '.version482') {
child_process.execSync('cd "' + versionDir[dir] + '"; git remote set-url origin git@github.com:' + repo + '.version482');
}
} catch (err) {
// Somehow the repo lost its definition of origin. Add it back.
child_process.execSync('cd "' + versionDir[dir] + '"; git remote add origin git@github.com:' + repo + '.version482');
}
// compute correct branch for this local repo
let branch = username + child_process.execSync('uname -s').toString().trimEnd();
if (fs.existsSync('/etc/os-release')) {
branch += child_process.execSync('cat /etc/os-release | grep "^ID=" | sed "s/^.*=//"').toString().trimEnd();
}
branch += top;
branch = branch.replace(/[^a-zA-Z0-9]/g, "");
// make sure the version482 repo is on the correct branch
let branch1 = child_process.execSync('cd "' + versionDir[dir] + '"; git branch --show-current').toString().trimEnd();
if (branch1 != branch) {
// try to checkout branch, in case this branch already exists
try {
child_process.execSync('cd "' + versionDir[dir] + '"; git checkout ' + branch + ' > /dev/null 2>&1');
} catch (err) {
// Branch didn't exist (this is the common case). Try to
// create the branch (set up tracking below).
child_process.execSync('cd "' + versionDir[dir] + '"; git checkout -b ' + branch + ' --no-track > /dev/null 2>&1');
}
}
// make sure the version482 repo's upstream is set to the
// corresponding branch on github
let remote = '';
try {
remote = child_process.execSync('cd "' + versionDir[dir] + '"; git rev-parse --abbrev-ref "@{upstream}" 2> /dev/null').toString().trimEnd();
} catch (err) {
// rev-parse will fail if no upstream has been configured
// (this is normal)
remote = '';
}
if (remote != "origin/" + branch) {
// try to pull from github, in case branch already exists
// on github
try {
child_process.execSync('export SSH_ASKPASS=echo; export SSH_ASKPASS_REQUIRE=force; cd "' + versionDir[dir] + '"; git pull origin ' + branch + ' 2> /dev/null');
} catch (err) {
// pull will fail if branch doesn't exist on remote
// (this is normal)
}
// set upstream to github, and create branch on github if needed
child_process.execSync('cd "' + versionDir[dir] + '"; git push --set-upstream origin ' + branch + ' > /dev/null 2>&1');
}
} catch (err) {
console.log("initVersionDir(" + dir + ") caught exception");
console.log(err);
versionDir[dir] = '';
}
}
function initFile(doc) {
initVersionDir(path.dirname(doc.fileName));
}
function textChanged(doc) {
const now = time();
// Limit the rate of versioning events. Also save events where time has
// gone backward by more than minTimeVersion.
if (doc.fileName in versionTime
&& Math.abs(now - versionTime[doc.fileName]) < minTimeVersion) {
// replace any pending timer event for this file, so events don't pile up
if (doc.fileName in timer) {
clearTimeout(timer[doc.fileName]);
}
// make sure this version is eventually saved
timer[doc.fileName] = setTimeout(textChanged,
(minTimeVersion-(now-versionTime[doc.fileName]))*1000, doc);
return;
}
initVersionDir(path.dirname(doc.fileName));
// make sure file is in an EECS 482 git repo
if (versionDir[path.dirname(doc.fileName)] == '') {
return;
}
// make sure file is a program source file, i.e., has extension {cpp,cc,h,hpp,py}
const ext = path.extname(doc.fileName);
if (ext != '.cpp' && ext != '.cc' && ext != '.h' && ext != '.hpp' && ext != '.py') {
return;
}
const versionDirname = versionDir[path.dirname(doc.fileName)];
// create/update file
const basename = path.basename(doc.fileName);
fs.writeFileSync(versionDirname + path.sep + basename, doc.getText());
// commit changes
child_process.execSync('cd "' + versionDirname + '"; git add -f "' + basename + '" > /dev/null 2>&1; git commit --allow-empty -m "' + version + '" > /dev/null 2>&1');
versionTime[doc.fileName] = now;
}
// push commits to github
function saveFile(doc) {
const now = time();
// Clear versionDir every so often, so the version482 repo gets re-checked.
if (now - checkTime >= minTimeCheck) {
versionDir = {};
checkTime = now;
}
initVersionDir(path.dirname(doc.fileName));
// make sure file is in an EECS 482 git repo
if (versionDir[path.dirname(doc.fileName)] == '') {
return;
}
const versionDirname = versionDir[path.dirname(doc.fileName)];
// limit the rate of pushing
if (versionDirname in pushTime
&& now - pushTime[versionDirname] < minTimePush) {
// replace any pending push for this directory, so events don't pile up
if (versionDirname in timer) {
clearTimeout(timer[versionDirname]);
}
// make sure this event is eventually pushed
timer[versionDirname] = setTimeout(saveFile,
(minTimePush-(now-pushTime[versionDirname]))*1000, doc);
return;
}
// add version482 entry, so it's as new as the saved file
textChanged(doc);
child_process.execSync('export SSH_ASKPASS=echo; export SSH_ASKPASS_REQUIRE=force; cd "' + versionDirname + '"; git push --quiet > /dev/null 2>&1');
pushTime[versionDirname] = now;
}