Skip to content

Instantly share code, notes, and snippets.

@naholyr
Last active August 25, 2022 17:44
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save naholyr/4676585 to your computer and use it in GitHub Desktop.
Save naholyr/4676585 to your computer and use it in GitHub Desktop.
Git(Hub)-backed comments system

General context

  • A static website using a generator (like Jekyll)
  • Comments are in a folder, one comment = one JSON file (attached to article based on path + filename)
  • website is versionned using git, repository hosted by github (cool for pull-requests)

Posting a new comment

OK here we need some dynamism ;)

  • A light server listening on port X, Nginx redirects "/comments" to this port (or whatever)
  • When receiving a new comment, here are the steps:
  1. validate input
  2. create the comment:
  • go to a local git clone of website repository (not the served one, that could be dangerous)
  • git stash -u -k
  • git checkout -b comment-XXX (may use a UUID here)
  • create the new JSON file for comment, unicity must be guaranteed to ease merging (use UUID)
  • git add
  • git commit
  • git checkout original-branch
  • git stash pop
  • git push origin comment-XXX
  1. create the pull request:
  • use github API to create a PR comment-XXX → master (with some nice info in title & body, like "comment from … about …")
  • as we're here, assign the PR and add a nice label ("comments")

Moderation

Moderation consists of:

  • Accept comment = merge PR
  • Edit comment = local checkout, edit, commit, push, then merge
  • Refuse comment = close PR

Edition is not nice in this scenario, but it's the most rare action.

Display comments

We need to regenerate the website, but this could be automated!

  • The same light server could also accept GitHub payloads
  • We just have to declare the web hook in github repository
  • When a push is received from merging a pull-request with head like "comment-…", we know it's a new comment
  • go to the specific repository (you know, not the one in production)
  • git pull
  • regenerate files
  • copy new files (typically 2: the blog post and the comments rss) to production

And yes, this is the right place to talk about continuous integration of your static website ;)

Caveats

  • Beware the dead locks when working on git repository. We may duplicate repositories, clone, keep a lock, whatever, but think about it.
  • All comments must be moderated. Extra work :( but we could easily add another bot here that will automatically merge pull requests from trusted users.
  • GitHub is notably unreliable: APIs not responding quite often, so when a PR cannot be created for some reason, it must be kept somewhere to try again later.

About accessibility?

It's all good:

  • Posting a comment can be done with no JS at all (the server just has to answer with a 302)
  • List of comments is static HTML, glory :)
// POST /new
// → validate input
// → references a valid blog post
// → contains email, website, message
// → go to comments git repository
// → create new branch
// → create new comment file
// → git commit
// → github pull-request
var fs = require('fs');
var path = require('path');
var uuid = require('node-uuid');
var async = require('async');
var git = require('./git');
var GitHubAPI = require('github');
var config = require('./config.json');
// POST data
var article = 'lol-rofl-plop';
var content = new Buffer('LOL');
var author_name = 'John Lebowitz';
var author_email = 'j@lebowitz.com';
// Configuration
var id = uuid();
var dir = path.resolve(config.comments_repo);
var file = path.join(dir, 'posts', id + '.md');
var branch = 'comment-' + id;
var repo = new git.Repository(dir);
var github = new GitHubAPI({
version: "3.0.0"
});
github.authenticate(config.github_authentication);
if (!repo.isRepository) {
throw new Error('Invalid value for "comments_repo": not a repository');
}
var originalBranch;
// Let's the real process start
async.series([
function (cb) {
console.error('check github labels for label', '"' + config.tag_name + '"');
github.issues.getLabels({
"user": config.repo_user,
"repo": config.repo_name
}, function (err, labels) {
if (err) return cb(err);
if (labels.some(function (l) { return l.name === 'comment'; })) return cb();
github.issues.createLabel({
"user": config.repo_user,
"repo": config.repo_name,
"name": config.tag_name,
"color": config.tag_color
}, cb);
});
},
function (cb) {
repo.currentBranch(function (err, branch) {
originalBranch = branch;
cb(err);
});
},
function (cb) {
console.error('git stash');
repo.stashSave(cb);
},
function (cb) {
console.error('git checkout -b', branch);
repo.createBranchAndCheckout(branch, cb);
},
function (cb) {
console.error('write', file);
fs.writeFile(file, content, cb);
},
function (cb) {
console.error('git add', file);
repo.add([file], cb);
},
function (cb) {
console.error('git commit');
repo.commit('New comment: ' + id, cb);
},
function (cb) {
console.error('git checkout', originalBranch);
repo.checkout(originalBranch, cb);
},
function (cb) {
console.error('git stash', 'pop');
repo.stashPop(cb);
},
function (cb) {
console.error('git push', 'origin', branch);
repo.pushOrigin(branch, cb);
},
function (cb) {
console.error('git branch -d', branch);
repo.deleteBranch(branch, cb);
},
function (cb) {
console.error('github pull-request', branch);
github.pullRequests.create({
"user": config.repo_user,
"repo": config.repo_name,
"title": "Comment from " + author_name + " <" + author_email + ">",
"body": "Commenting " + article + ":\n\n" + content.toString(),
"base": "master",
"head": branch
}, function (err, pr) {
if (err) return cb(err);
console.error('assign & tag pull-request', pr.number);
github.issues.edit({
"user": config.repo_user,
"repo": config.repo_name,
"number": pr.number,
"title": pr.title,
"assignee": config.repo_user,
"labels": [config.tag_name]
}, function (err) {
if (err) console.error('tagging failed, overall process is still OK');
cb();
})
});
}
], function (err) {
if (err) {
console.error(err);
process.exit(1);
}
});
var git = module.exports = require('gitty');
function callback (ctx, cb) {
return function (err, stdout, stderr) {
return cb.call(ctx, err || stderr, stdout);
};
}
git.Repository.prototype.stashSave = function (cb) {
new git.Command(this.path, 'stash', ['-u'], '').
exec(callback(this, cb));
};
git.Repository.prototype.currentBranch = function (cb) {
new git.Command(this.path, 'branch', ['--list'], '').
exec(callback(this, function (err, out) {
if (err) return cb(err);
var branch = out.
split(/[\r\n]+/g).
filter(function (line) {
return line.match(/^\s*\*/);
})[0];
if (!branch) return cb('Cannot fetch branch: invalid output');
cb(null, branch.replace(/^\s*\*\s*(.+)\s*$/, '$1'));
}));
};
git.Repository.prototype.stashPop = function (cb) {
new git.Command(this.path, 'stash', ['pop'], '').
exec(callback(this, cb));
};
git.Repository.prototype.createBranchAndCheckout = function (name, cb) {
new git.Command(this.path, 'checkout', ['-b'], name).
exec(callback(this, function (err, out) {
if (typeof err === 'string') {
out = err + '\n' + (out || '')
err = null;
}
cb(err, out);
}));
};
git.Repository.prototype.deleteBranch = function (name, cb) {
new git.Command(this.path, 'branch', ['-D'], name).
exec(callback(this, cb));
};
// Do not use gitty's buggy "push" (because of wrong event listened
// from pty.js, and I don't need this because I rely on SSH key)
// Note: my PR has already been accepted, this patch shouldn't stay here longer
git.Repository.prototype.pushOrigin = function (name, cb) {
new git.Command(this.path, 'push', ['origin'], name).
exec(callback(this, function (err, out) {
if (typeof err === 'string') {
out = err + '\n' + (out || '')
err = null;
}
cb(err, out);
}));
};
check github labels for label "comment"
git stash
git checkout -b comment-fe46c3e0-8211-4467-9f8e-501c5f899506
write /home/nchambrier/Projects/Perso/naholyr.fr/posts/fe46c3e0-8211-4467-9f8e-501c5f899506.md
git add /home/nchambrier/Projects/Perso/naholyr.fr/posts/fe46c3e0-8211-4467-9f8e-501c5f899506.md
git commit
git checkout comments
git stash pop
git push origin comment-fe46c3e0-8211-4467-9f8e-501c5f899506
git branch -d comment-fe46c3e0-8211-4467-9f8e-501c5f899506
github pull-request comment-fe46c3e0-8211-4467-9f8e-501c5f899506
assign & tag pull-request 9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment