59cd492010-09-05Marcus Comstedt #! /usr/bin/env pike
0c9c482010-09-25Marcus Comstedt mapping(string:program) hooks = ([
5179182010-09-25Marcus Comstedt  "pre-commit" : PreCommitHook,
efde062010-09-25Marcus Comstedt  "pre-receive" : PreReceiveHook,
59cd492010-09-05Marcus Comstedt ]);
0c9c482010-09-25Marcus Comstedt mapping(string:program) filters = ([
7b6b1b2010-09-25Marcus Comstedt #if 0
59cd492010-09-05Marcus Comstedt  "nice_ident" : NiceIdentFilter,
7b6b1b2010-09-25Marcus Comstedt #endif
59cd492010-09-05Marcus Comstedt ]); constant filterops = ({ "clean", "smudge" }); void fail(string msg, mixed ... args) { werror(msg, @args); exit(1); } void iofailn(int errno, string msg, mixed ... args) { fail(msg+": %s\n", @args, strerror(errno)); } void iofail(string msg, mixed ... args) { iofailn(errno(), msg, @args); }
0c9c482010-09-25Marcus Comstedt array(string) split_z(string data) { array(string) a = data / "\0"; if (sizeof(a) && a[-1] == "") a = a[..sizeof(a)-2]; return a; }
59cd492010-09-05Marcus Comstedt string run_git_ex(int max_exitcode, string ... args) { mapping res = Process.run(({"git"})+args); if (res->exitcode > max_exitcode) { werror(res->stderr); fail("git exited with code %d\n", res->exitcode); } return res->stdout; } string run_git(string ... args) { return run_git_ex(0, @args); }
efde062010-09-25Marcus Comstedt string get_staged_file(string filename) { string sha; if (2 != sscanf(run_git("ls-files", "--stage", "--", filename), "%*o %s ", sha)) fail("Unable to parse output from git ls-files...\n"); return run_git("cat-file", "blob", sha); }
59cd492010-09-05Marcus Comstedt 
aeab9a2010-09-25Marcus Comstedt string get_committed_file(string sha, string filename, int|void allow_empty)
efde062010-09-25Marcus Comstedt { string blob; string attrentry = run_git("ls-tree", sha, "--", filename);
aeab9a2010-09-25Marcus Comstedt  if (allow_empty && !sizeof(attrentry)) return "";
efde062010-09-25Marcus Comstedt  if (2 != sscanf(attrentry, "%*o blob %s\t", blob)) fail("Unexpected output from git ls-tree\n"); return run_git("cat-file", "blob", blob); }
5179182010-09-25Marcus Comstedt 
efde062010-09-25Marcus Comstedt class GitAttributes
59cd492010-09-05Marcus Comstedt {
efde062010-09-25Marcus Comstedt  enum { ATTR_TRUE = 1, ATTR_FALSE = 2, ATTR_UNSET = 3 }; class AttrState(string attr, string|int setto) { static string _sprintf(int type) { return type=='O' && sprintf("AttrState(%O, %O)\n", attr, setto); } }; class MatchAttr(string name, int is_macro, array(AttrState) states) { static string _sprintf(int type) { return type=='O' && sprintf("MatchAttr(%O, %d, %O)\n", name, is_macro, states); } }; static array(MatchAttr) attrs = ({}); static int invalid_attr_name(string name)
08686f2010-09-25Marcus Comstedt  {
efde062010-09-25Marcus Comstedt  int n; if(name == "" || name[0] == '-') return 1; sscanf(name, "%*[-._0-9a-zA-Z]%n", n); return n != sizeof(name);
08686f2010-09-25Marcus Comstedt  }
efde062010-09-25Marcus Comstedt  static AttrState parse_attr(string src)
0c9c482010-09-25Marcus Comstedt  {
efde062010-09-25Marcus Comstedt  string equals = 0; string|int setto; sscanf(src, "%s=%s", src, equals); if(src[0] == '-' || src[0] == '!') { setto = (src[0]=='-'? ATTR_FALSE : ATTR_UNSET); src = src[1..]; } else setto = equals || ATTR_TRUE; if(invalid_attr_name(src)) fail("%s is not a valid attribute name\n", src); return AttrState(src, setto); }
0c9c482010-09-25Marcus Comstedt 
efde062010-09-25Marcus Comstedt  static MatchAttr parse_attr_line(string line, int macro_ok) { int is_macro=0; string name; line = String.trim_whites(replace(line, ({"\t","\r","\n"}), ({" ", " ", " "}))); if(!sizeof(line) || line[0] == '#') return 0; if(has_prefix(line, "[attr]")) { if(!macro_ok) fail("%s not allowed\n", name); is_macro=1; sscanf(line, "[attr]%*[ ]%s%*[ ]%s", name, line); } else { sscanf(line, "%s%*[ ]%s", name, line); }
19bf732010-09-26Marcus Comstedt  return MatchAttr(name, is_macro, reverse(map(line/" "-({""}), parse_attr)));
efde062010-09-25Marcus Comstedt  } static void handle_attr_line(string line, int macro_ok) { MatchAttr a = parse_attr_line(line, macro_ok); if(a) attrs += ({ a }); } static void create(string data) { foreach(data/"\n", string line) handle_attr_line(line, 1);
19bf732010-09-26Marcus Comstedt  attrs = reverse(attrs);
efde062010-09-25Marcus Comstedt  } static int path_matches(string path, string pattern) { if(search(pattern, "/")<0) return glob(pattern, (path/"/")[-1]); if(has_prefix(pattern, "/")) pattern = pattern[1..]; return glob(pattern, path); } static void macroexpand_one(string attrname, array(MatchAttr) attrs, mapping(string:string|int) all_attr) {
19bf732010-09-26Marcus Comstedt  MatchAttr ma = 0;
efde062010-09-25Marcus Comstedt  if(all_attr[attrname] != ATTR_TRUE) return;
19bf732010-09-26Marcus Comstedt  foreach(attrs, MatchAttr a) if(a->is_macro && a->name == attrname) ma = a; if(ma) fill_one(ma, attrs, all_attr);
efde062010-09-25Marcus Comstedt  } static void fill_one(MatchAttr attr, array(MatchAttr) attrs, mapping(string:string|int) all_attr) {
19bf732010-09-26Marcus Comstedt  foreach(attr->states, AttrState s) {
efde062010-09-25Marcus Comstedt  if(!all_attr[s->attr]) { all_attr[s->attr] = s->setto; macroexpand_one(s->attr, attrs, all_attr); }
0c9c482010-09-25Marcus Comstedt  } }
efde062010-09-25Marcus Comstedt  static void fill(string path, array(MatchAttr) attrs, mapping(string:string|int) all_attr) {
19bf732010-09-26Marcus Comstedt  foreach(attrs, MatchAttr a) if(!a->is_macro && path_matches(path, a->name)) fill_one(a, attrs, all_attr);
efde062010-09-25Marcus Comstedt  } mapping(string:string|int) checkattr(string path) { mapping(string:string|int) all_attr = ([]); fill(path, attrs, all_attr); return all_attr; }
aeab9a2010-09-25Marcus Comstedt  array(string) findattr(string attrname) { array(string) r = ({}); foreach(attrs, MatchAttr attr) { int z=0; foreach(attr->states, AttrState state) if(state->attr == attrname && state->setto == ATTR_TRUE) { z = 1; break; } if (z) r += ({ attr->name }); } return r; }
efde062010-09-25Marcus Comstedt  static string _sprintf(int type) { return type=='O' && sprintf("GitAttributes(%O)\n", attrs); } } /* Hooks */ class CommitHookUtils {
d645802010-09-26Marcus Comstedt  static array(string) files_to_commit; string get_file(string filename);
0ef2fc2010-09-25Marcus Comstedt  int find_expanded_ident(string data)
08686f2010-09-25Marcus Comstedt  { int p=0; while ((p = search(data, "$Id", p))>=0) {
0ef2fc2010-09-25Marcus Comstedt  if (data[p..p+3] != "$Id$") return 1; p += 4; } return 0; }
efde062010-09-25Marcus Comstedt 
d645802010-09-26Marcus Comstedt  int check_ident(string filename)
efde062010-09-25Marcus Comstedt  {
d645802010-09-26Marcus Comstedt  if (find_expanded_ident(get_file(filename))) { write("File %s contains an expanded ident.\n", filename); if(this_program == PreCommitHook) { write("Try 'git reset %s; git add %s', " "or remove the ident manually.\n", @({filename})*2);; } return 1; } return 0;
efde062010-09-25Marcus Comstedt  } } /* Checks run before editing a commit message */ class PreCommitHook { inherit CommitHookUtils;
d645802010-09-26Marcus Comstedt  string get_file(string filename) { return get_staged_file(filename); }
efde062010-09-25Marcus Comstedt  int check_attributes_staged() { // We don't allow .gitattributes to differ between wt and index, // because that could mean the committed stuff ends up with different // attributes than they have right now... if (sizeof(run_git("diff", "--name-only", ".gitattributes"))) { write("You have unstaged changes to .gitattributes.\n" "Please add or stash them before commit.\n"); return 1; } }
0ef2fc2010-09-25Marcus Comstedt 
d645802010-09-26Marcus Comstedt  int check_blocker_attributes()
59cd492010-09-05Marcus Comstedt  {
08686f2010-09-25Marcus Comstedt  constant attrs_to_check = ({ "foreign_ident", "block_commit", "ident" });
0c9c482010-09-25Marcus Comstedt  foreach(run_git("check-attr", @attrs_to_check, "--", @files_to_commit) / "\n" - ({""}), string line) { array(string) parts = line / ": "; if (sizeof(parts) != 3) fail("Unexpected output from git check-attr, please fix check_blocker_attributes()\n"); [string filename, string attribute, string value] = parts; if (value != "unspecified") { switch (attribute) { case "foreign_ident": write("File %s has the foreign_ident attribute. Please remove it before commit.\n", filename); return 1; case "block_commit": write("File %s is blocked from committing: %s\n", filename, replace(value, "-", " ")); return 1;
08686f2010-09-25Marcus Comstedt  case "ident": if (value == "unset") break; if (check_ident(filename)) return 1; break;
0c9c482010-09-25Marcus Comstedt  } } }
80f91d2010-09-05Marcus Comstedt  return 0;
59cd492010-09-05Marcus Comstedt  }
0c9c482010-09-25Marcus Comstedt 
d645802010-09-26Marcus Comstedt  int check_gitattributes_files()
0ef2fc2010-09-25Marcus Comstedt  { foreach(files_to_commit, string filename)
1b00922010-09-25Marcus Comstedt  if(has_suffix(filename, "/.gitattributes")) { write(".gitattributes are not allowed in subdirectories\n"); return 1; } if(search(files_to_commit, ".gitattributes")>=0) { string diff = run_git("diff", "-p", "--cached", "--", ".gitattributes"); if (sizeof(diff)) { int pos = search(diff, "\n@@"); if (pos >= 0) diff = diff[pos+1..]; foreach(diff/"\n", string line) if(sizeof(line) && search(line, "foreign_ident")>=0 &&
a10aeb2010-09-25Marcus Comstedt  search(line, "[attr]") != 1 &&
1b00922010-09-25Marcus Comstedt  (line[0]=='+' || line[0]=='-')) { int code, len; string fn; if(sscanf(line, "%c/%s foreign_ident%n", code, fn, len) != 3 || len != sizeof(line)) { write("Unsupported change of foreign_ident in .gitattributes\n"); return 1; } if (code=='-' && search(files_to_commit, fn)<0) { write("Removed foreign_ident from unstaged file %s\n", fn); return 1; }
0ef2fc2010-09-25Marcus Comstedt  }
1b00922010-09-25Marcus Comstedt  } }
0ef2fc2010-09-25Marcus Comstedt  return 0; }
0c9c482010-09-25Marcus Comstedt  int hook() {
d645802010-09-26Marcus Comstedt  files_to_commit =
0c9c482010-09-25Marcus Comstedt  split_z(run_git("diff", "--staged", "--name-only", "-z")); return check_attributes_staged() ||
d645802010-09-26Marcus Comstedt  check_blocker_attributes() || check_gitattributes_files();
0c9c482010-09-25Marcus Comstedt  }
59cd492010-09-05Marcus Comstedt }
efde062010-09-25Marcus Comstedt /* Checks run before accepting a push */ class PreReceiveHook { inherit CommitHookUtils;
d645802010-09-26Marcus Comstedt  static string sha; string get_file(string filename)
efde062010-09-25Marcus Comstedt  {
d645802010-09-26Marcus Comstedt  return get_committed_file(sha, filename);
efde062010-09-25Marcus Comstedt  }
d645802010-09-26Marcus Comstedt  int check_blocker_attributes(GitAttributes attrs)
efde062010-09-25Marcus Comstedt  { foreach(files_to_commit, string filename) { mapping(string:string|int) a = attrs->checkattr(filename); if(a->foreign_ident == GitAttributes.ATTR_TRUE) {
aeab9a2010-09-25Marcus Comstedt  if (sizeof(run_git("ls-tree", sha+"^", "--", filename)) || !sizeof(run_git("ls-tree", sha, "--", filename))) { write("File %s has the foreign_ident attribute. Please remove it before commit.\n", filename); return 1; }
efde062010-09-25Marcus Comstedt  } if(stringp(a->block_commit) || a->block_commit == GitAttributes.ATTR_TRUE) {
aeab9a2010-09-25Marcus Comstedt  if (sizeof(run_git("ls-tree", sha+"^", "--", filename)) || !sizeof(run_git("ls-tree", sha, "--", filename))) { write("File %s is blocked from committing: %s\n", filename, replace((stringp(a->block_commit)? a->block_commit : "no explanation given"), "-", " "));
efde062010-09-25Marcus Comstedt  return 1;
aeab9a2010-09-25Marcus Comstedt  }
efde062010-09-25Marcus Comstedt  } if(a->ident && a->ident != GitAttributes.ATTR_FALSE && a->ident != GitAttributes.ATTR_UNSET) {
d645802010-09-26Marcus Comstedt  if (check_ident(filename))
efde062010-09-25Marcus Comstedt  return 1; } } return 0; }
d645802010-09-26Marcus Comstedt  int check_gitattributes_files(GitAttributes attrs)
efde062010-09-25Marcus Comstedt  { foreach(files_to_commit, string filename) if(has_suffix(filename, "/.gitattributes")) { write(".gitattributes are not allowed in subdirectories\n"); return 1; } if(search(files_to_commit, ".gitattributes")>=0) {
aeab9a2010-09-25Marcus Comstedt  GitAttributes old_attrs = GitAttributes(get_committed_file(sha+"^", ".gitattributes", 1)); array(string) new_f_e = sort(attrs->findattr("foreign_ident")); array(string) old_f_e = sort(old_attrs->findattr("foreign_ident")); array(string) added_fe = new_f_e - old_f_e; array(string) removed_fe = old_f_e - new_f_e; foreach(added_fe, string path) { if(!has_prefix(path, "/") || search(path, "*")>=0) { write("Added unsupported foreign_ident: %s\n", path); return 1; } path = path[1..]; if (sizeof(run_git("ls-tree", sha+"^", "--", path)) || !sizeof(run_git("ls-tree", sha, "--", path))) { write("Added foreign_ident to unadded file %s\n", path); return 1; } } foreach(removed_fe, string path) { if(has_prefix(path, "/")) path = path[1..]; if (search(files_to_commit, path)<0) { write("Removed foreign_ident from unchanged file %s\n", path); return 1; }
efde062010-09-25Marcus Comstedt  } } return 0; } int check_commit(string sha) { write("Checking commit %s\n", sha);
d645802010-09-26Marcus Comstedt  this_program::sha = sha; files_to_commit =
efde062010-09-25Marcus Comstedt  split_z(run_git("diff", "--name-only", "-z", sha, sha+"^"));
aeab9a2010-09-25Marcus Comstedt  string attrtext = get_committed_file(sha, ".gitattributes", 1);
efde062010-09-25Marcus Comstedt  GitAttributes attrs = GitAttributes(attrtext);
d645802010-09-26Marcus Comstedt  return check_blocker_attributes(attrs) || check_gitattributes_files(attrs);
efde062010-09-25Marcus Comstedt  } int check_push(string old_sha, string new_sha, string ref_name) { if(old_sha == "0"*40) { // New ref, maybe check if the name is allowed... return 0; } else { foreach(run_git("rev-list", old_sha+".."+new_sha)/"\n", string sha) if(sizeof(sha) && check_commit(sha)) return 1; return 0; } } int hook() { foreach(Stdio.stdin->read() / "\n", string line) if(sizeof(line)) { array(string) args = line / " "; if(sizeof(args) != 3) fail("Unexpected input line to pre-receive hook: %s\n", line); if(check_push(@args)) return 1; } return 0; } }
59cd492010-09-05Marcus Comstedt /* Filters */
5179182010-09-25Marcus Comstedt /* A sample filter, not really useful... */
59cd492010-09-05Marcus Comstedt class NiceIdentFilter {
80f91d2010-09-05Marcus Comstedt  static string replace_id(string f, function(string:string) replace) { int p=0; while((p=search(f, "$Id", p)) >= 0) { int p2 = search(f, "$", p+3), p3 = search(f, "\n", p+3); if (p2 > p && p2 < p3) { string r = replace(f[p..p2]); if (r) {
5940512010-09-05Marcus Comstedt  // werror("Replacing %O with %O\n", f[p..p2], r);
80f91d2010-09-05Marcus Comstedt  f = f[..p-1]+r+f[p2+1..]; p += sizeof(r);
5940512010-09-05Marcus Comstedt  } else { // werror("Not replacing %O\n", f[p..p2]); p = p2+1; }
80f91d2010-09-05Marcus Comstedt  } else p += 3; } return f; } static string clean_ident(string i) {
5940512010-09-05Marcus Comstedt  if(has_prefix(i, "$Id:") && sizeof(i/" ")==13)
80f91d2010-09-05Marcus Comstedt  return "$Id$"; } static string smudge_ident(string i) { return "$Id$"; } int clean()
59cd492010-09-05Marcus Comstedt  {
80f91d2010-09-05Marcus Comstedt  write(replace_id(Stdio.stdin->read(), clean_ident)); return 0;
59cd492010-09-05Marcus Comstedt  }
c1f54a2010-09-05Marcus Comstedt  int smudge()
59cd492010-09-05Marcus Comstedt  {
80f91d2010-09-05Marcus Comstedt  write(replace_id(Stdio.stdin->read(), smudge_ident)); return 0;
59cd492010-09-05Marcus Comstedt  } } /* Main helper */ class GitHelper { void setup_hooks() { constant hooksdir = "hooks"; if (!sizeof(hooks)) return; if (!file_stat(hooksdir)) { write("Creating the hooks directory\n");
52d8152010-09-05Marcus Comstedt  if (!mkdir(hooksdir))
59cd492010-09-05Marcus Comstedt  iofail("Failed to create %s", hooksdir); } foreach (hooks; string name; ) { string path = combine_path(hooksdir, name); Stdio.Stat s = file_stat(path, 1); if (!s) { write("Installing %s\n", path); System.symlink(__FILE__, path);
52d8152010-09-05Marcus Comstedt  } else if (s->islnk) {
59cd492010-09-05Marcus Comstedt  /* Already setup ok, it seems */ } else {
5179182010-09-25Marcus Comstedt  write("Hook %s already exists, so won't overwrite it...\n", name);
59cd492010-09-05Marcus Comstedt  } } } void setup_filter(string name, string op) { string confname = "filter."+name+"."+op; string old = String.trim_all_whites(run_git_ex(1, "config", "--get", confname)); string cmd = __FILE__+" filter_"+name+"_"+op;
52d8152010-09-05Marcus Comstedt  if (old == "") {
59cd492010-09-05Marcus Comstedt  write("Installing filter operation %s\n", confname); run_git("config", confname, cmd); } else if(old == cmd) { /* Already has correct value */ } else { write("Filter operation %s is already set to %s, not modifying\n", confname, old); } } void setup_filters() { foreach (filters; string name; program fprog) { object filter = fprog(); foreach (filterops; ; string op)
52d8152010-09-05Marcus Comstedt  if (filter[op])
59cd492010-09-05Marcus Comstedt  setup_filter(name, op); } } int setup(array(string) args) { if (sizeof(args)) { werror("githelper.pike should be invoked without arguments...\n"); return 1; } if (!cd(String.trim_all_whites(run_git("rev-parse", "--git-dir")))) iofail("Failed to cd to .git directory"); setup_hooks(); setup_filters(); return 0; } } string get_filter_op(string arg) { if (!has_prefix(arg, "filter_")) return 0; foreach (filterops; ; string op) if (has_suffix(arg, "_"+op)) return op;
070acd2010-09-05Marcus Comstedt  return 0;
59cd492010-09-05Marcus Comstedt } int main(int argc, array(string) argv) { string command_name = basename(argv[0]); if (hooks[command_name]) return hooks[command_name]()->hook(@argv[1..]); else if (command_name == "githelper.pike") { string fop; if (argc>1 && (fop = get_filter_op(argv[1]))) { string filter = argv[1][7..sizeof(argv[1])-(sizeof(fop)+2)]; if (filters[filter]) { object f = filters[filter](); if (!f[fop]) {
52d8152010-09-05Marcus Comstedt  werror("Filter %s does not implement %s!\n", filter, fop); return 1;
59cd492010-09-05Marcus Comstedt  } else return f[fop](@argv[2..]); } else { werror("Unknown filter %s!\n", filter); return 1; } } else return GitHelper()->setup(argv[1..]); } else { werror("Unknown invocation method %s!\n", command_name); return 1; } }