Skip to content
| Marketplace
Sign in
Visual Studio Code>Other>version482New to Visual Studio Code? Get it now.
version482

version482

pmchen

|
516 installs
| (0) | Free
Keeps versions of files in EECS 482 github repos
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info
//
// 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 crypto = require('crypto');
const child_process = require('child_process');
const version = 'vscode-20251019';
const minTimeInterval = 10;     // minimum time between versions
const minTagInterval = 60;      // minimum time between tags
const maxAge = 60*60;           // maximum age of version482 file
const maxSize = 50*1024*1024;   // maximum size of version482 file
const username = os.userInfo().username.replace(/[^a-zA-Z0-9]/g, "");

let hash = {};          // last hash value for each version file
let size = {};          // last size for each version file
let prior = {};         // prior contents for each file
let priorTime = {};     // last time each file was written
let priorTag = {};      // last time each repo was tagged

let sessionStart = time();
let hashInitial = crypto.createHash('sha256').update(username + ' ' + sessionStart.toString());

let versionDir = {};    // .version482 directory name for each source file directory
let proj = {};          // project 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, creating the .version482
// directory if needed.  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();
	// console.log('top=' + top);

	// 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 a main project repository");
	}
	proj[dir] = found[2];
	const repo = found[1] + '.' + proj[dir];
	console.log('proj=' + proj[dir] + ', repo=' + repo);

	versionDir[dir] = top + path.sep + '.version482';

	if (proj[dir].match(/[012]/)) {
	    // create the .version482 directory if needed
	    try {
		if (! fs.statSync(versionDir[dir]).isDirectory) {
		    versionDir[dir] = '';
		}
	    } catch (err) {
		// .version482 directory doesn't exist
		fs.mkdirSync(versionDir[dir]);
	    }
	} else {
	    // clone Project 3-4 .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");
	    }

	    // 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, "");

	    // am I on the correct branch?
	    let branch1 = child_process.execSync('cd "' + versionDir[dir] + '"; git branch --show-current').toString().trimEnd();
	    if (branch1 != branch) {
		// try to create the branch
		try {
		    child_process.execSync('cd "' + versionDir[dir] + '"; git checkout -b ' + branch + ' > /dev/null 2>&1');
		    // git checkout -b worked.  Try pulling from remote, in
		    // case branch already exists.
		    try {
			child_process.execSync('export SSH_ASKPASS=echo; export SSH_ASKPASS_REQUIRE=force; cd "' + versionDir[dir] + '"; git pull origin ' + branch);
		    } catch (err) {
			// pull will fail if branch doesn't exist on remote
			// (this is normal)
		    }
		    // add upstream reference to github
		    child_process.execSync('cd "' + versionDir[dir] + '"; git push --set-upstream origin ' + branch);
		} catch (err) {
		    // git checkout -b fails if the branch already exists
		    // locally.  Try checking out the existing branch.
		    console.log("warning: checking out existing branch " + branch);
		    child_process.execSync('cd "' + versionDir[dir] + '"; git checkout ' + branch + ' > /dev/null 2>&1');
		}
	    }
	}
    } catch (err) {
	console.log("initVersionDir 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 minTimeInterval.
    if (doc.fileName in priorTime
            && Math.abs(now - priorTime[doc.fileName]) < minTimeInterval) {
        // 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, minTimeInterval*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;
    }

    // make sure file isn't too big
    if (doc.getText().length > 10 * 1024 * 1024) {
        return;
    }

    const versionDirname = versionDir[path.dirname(doc.fileName)];

    if (proj[path.dirname(doc.fileName)].match(/[012]/)) {
        // Project 0-2

        // check if version file has grown too old or too big
        if (now - sessionStart > maxAge || (versionDirname in size && size[versionDirname] > maxSize)) {
            // start new version file by mimicking restarting vscode
            hash = {};
            size = {};
            prior = {};
            priorTime = {};
            sessionStart = now;
            hashInitial = crypto.createHash('sha256').update(username + ' ' + sessionStart.toString());
        }

        if (! (versionDirname in hash)) {
            hash[versionDirname] = hashInitial.copy();
        }
        if (! (versionDirname in size)) {
            size[versionDirname] = 0;
        }

        const priorName = versionDirname + path.sep + sessionStart.toString() + '.' + username + '.prior';
        const currentName = versionDirname + path.sep + sessionStart.toString() + '.' + username + '.current';

        if (! (doc.fileName in prior)) {
            prior[doc.fileName] = '';
        }

        fs.writeFileSync(priorName, prior[doc.fileName]);

        const current = doc.getText();
        fs.writeFileSync(currentName, current);
        const versionfile = versionDirname + path.sep + sessionStart.toString() + '.' + username;
        const diff = child_process.execSync('diff "' + priorName + '" "' + currentName + '"; rm "' + priorName + '" "' + currentName + '"').toString();
        let dict = {};
        dict['file'] = doc.fileName;
        dict['diff'] = diff;
        const line = version + ' ' + now + ' '
                        + size[versionDirname] + ' '
                        + hash[versionDirname].copy().digest('hex') + ' '
                        + JSON.stringify(dict);
        try {
            fs.appendFileSync(versionfile, line + "\n", () => {});
        } catch (err) {
            return;
        }
        prior[doc.fileName] = current;
        priorTime[doc.fileName] = now;

        hash[versionDirname] = crypto.createHash('sha256').update(line);
        size[versionDirname] += line.length + 1;

    } else {
        // Project 3-4

        // 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');

        priorTime[doc.fileName] = now;
    }
}

// try to create edit tag and push it to github
function saveFile(doc) {
    // add version482 entry, so it's as new as the saved file
    // force the entry by clearing priorTime
    priorTime = {};
    textChanged(doc);

    const now = time();

    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 tagging/pushing
    if (versionDirname in priorTag
            && now - priorTag[versionDirname] < minTagInterval) {
        // make sure this event is eventually tagged
        timer[versionDirname] = setTimeout(saveFile,
            (minTagInterval-(now-priorTag[versionDirname]))*1000, doc);
        return;
    }

    if (proj[path.dirname(doc.fileName)].match(/[012]/)) {
        // Project 0-2
        const d = new Intl.DateTimeFormat("en-US",
            {
                timeZone: 'America/Detroit',
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                hourCycle: 'h23',
                hour: 'numeric',
                minute: 'numeric',
                second: 'numeric',
            }).format(new Date);
        const found = d.match(/(\d+)\/(\d+)\/(\d+)\D+(\d+)\D+(\d+)\D+(\d+)/);
        const dateString = found[3] + '.' + found[1] + '.' + found[2] + '_' + found[4] + '.' + found[5] + '.' + found[6];

        // Make sure this git repository has at least one commit, so HEAD~ refers
        // to something when undoing the first temporary commit
        try {
            child_process.execSync('cd "' + versionDirname + '"; git log > /dev/null 2>&1');
        } catch (err) {
            child_process.execSync('cd "' + versionDirname + '"; git commit -m "initial commit" --allow-empty > /dev/null 2>&1');
        }

        // create first temporary commit
        try {
            child_process.execSync('cd "' + versionDirname + '"; git commit -m edit-' + dateString + '-tmp1 --allow-empty > /dev/null 2>&1');

            // add version files to repo (only for the second temporary commit)
            child_process.execSync('cd "' + versionDirname + '"; git add -f . > /dev/null 2>&1');

            // create second temporary commit
            try {
                child_process.execSync('cd "' + versionDirname + '"; git commit -am edit-' + dateString + '-tmp2 --allow-empty > /dev/null 2>&1');

                // create edit tag for the second temporary commit
                try {
                    child_process.execSync('cd "' + versionDirname + '"; git tag -a edit-' + dateString + ' -m "" > /dev/null 2>&1');

                    priorTag[versionDirname] = now;

                    // remove version482 files that are guaranteed not to change
                    for (const f of fs.globSync(versionDirname + path.sep + "*")) {
                        if (now - path.basename(f).replace(/\..*/, '') > maxAge) {
                            fs.unlink(f, () => {});
                        }
                    }
                } catch (err) {
                    // edit tag failed
                }

                // Undo second temporary commit.  Use --mixed to unstage the files
                // that were staged because of the -a argument to git commit
                child_process.execSync('cd "' + versionDirname + '"; git reset --mixed HEAD~ > /dev/null 2>&1');

            } catch (err) {
                // second temporary commit failed
            }

            // Undo first temporary commit.  Use --soft to preserve the files that
            // that were already staged.
            child_process.execSync('cd "' + versionDirname + '"; git reset --soft HEAD~ > /dev/null 2>&1');

        } catch (err) {
            // first temporary commit failed
        }

        // push tags
        child_process.execSync('export SSH_ASKPASS=echo; export SSH_ASKPASS_REQUIRE=force; cd "' + versionDirname + '"; git push --tags --quiet > /dev/null 2>&1 &');

    } else {
        // Project 3-4
	child_process.execSync('export SSH_ASKPASS=echo; export SSH_ASKPASS_REQUIRE=force; cd "' + versionDirname + '"; git push --quiet > /dev/null 2>&1 &');
    }
}
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2025 Microsoft