59cd492010-09-05Marcus Comstedt #! /usr/bin/env pike
dc003e2016-12-07Henrik Grubbström (Grubba) #if 0 /* /bin/sh code below */ #! The following is debug code to simplify identifying the #! pike binary that is used when running the helper. #! It is enabled by prepending the top line of the file #! with #!/bin/sh type pike echo "$""0: $0" echo "$""@:" "$@" exec /usr/bin/env pike "$0" "$@" #endif /* /bin/sh code above */
c528962010-09-26Marcus Comstedt #define DOLLAR "$" constant unexpanded_id = DOLLAR"Id"DOLLAR;
0c9c482010-09-25Marcus Comstedt mapping(string:program) hooks = ([
5179182010-09-25Marcus Comstedt  "pre-commit" : PreCommitHook,
efde062010-09-25Marcus Comstedt  "pre-receive" : PreReceiveHook,
fa4f8f2010-09-27Marcus Comstedt  "post-commit" : PostCommitHook,
c21d882010-10-04Marcus Comstedt  "post-rewrite" : PostRewriteHook,
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; }
bb72622010-09-26Marcus Comstedt array(string) split_lf(string data) { array(string) a = data / "\n"; 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) {
404f402010-10-04Martin Stjernholm  Stdio.File mystdout = Stdio.File(); Stdio.File mystderr = Stdio.File(); Process.Process p = Process.Process (({"git"})+args, ([ "stdout":mystdout->pipe(), "stderr":mystderr->pipe(), ])); string gotstdout="", gotstderr=""; mystdout->set_read_callback( lambda( mixed i, string data) { gotstdout += data; } ); mystderr->set_read_callback( lambda( mixed i, string data) { gotstderr += data; } );
4a53ba2010-11-01Marcus Comstedt  mystdout->set_close_callback( lambda () { mystdout->set_read_callback(0); mystdout = 0; }); mystderr->set_close_callback( lambda () { mystderr->set_read_callback(0); mystderr = 0; }); while( mystdout || mystderr )
404f402010-10-04Martin Stjernholm  Pike.DefaultBackend( 1.0 ); int exitcode = p->wait(); if (exitcode > max_exitcode) { werror(gotstderr); fail("git exited with code %d\n", exitcode);
59cd492010-09-05Marcus Comstedt  }
404f402010-10-04Martin Stjernholm  return gotstdout;
59cd492010-09-05Marcus Comstedt } string run_git(string ... args) { return run_git_ex(0, @args); }
b668f12010-09-26Marcus Comstedt string get_staged_file(string filename, int|void allow_empty)
efde062010-09-25Marcus Comstedt {
b668f12010-09-26Marcus Comstedt  string blob; string treeentry = run_git("ls-files", "--stage", "--", filename); if (allow_empty && !sizeof(treeentry)) return ""; if (2 != sscanf(treeentry, "%*o %s ", blob))
efde062010-09-25Marcus Comstedt  fail("Unable to parse output from git ls-files...\n");
b668f12010-09-26Marcus Comstedt  return run_git("cat-file", "blob", blob);
efde062010-09-25Marcus Comstedt }
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;
b668f12010-09-26Marcus Comstedt  string treeentry = run_git("ls-tree", sha, "--", filename); if (allow_empty && !sizeof(treeentry))
aeab9a2010-09-25Marcus Comstedt  return "";
c275d42011-01-23Marcus Comstedt  if (allow_empty == 2 && 2 == sscanf(treeentry, "%*o tree %s\t", blob)) return "";
b668f12010-09-26Marcus Comstedt  if (2 != sscanf(treeentry, "%*o blob %s\t", blob))
efde062010-09-25Marcus Comstedt  fail("Unexpected output from git ls-tree\n"); return run_git("cat-file", "blob", blob); }
5179182010-09-25Marcus Comstedt 
e6367f2010-10-02Marcus Comstedt string check_commit_timestamps(string commit) { int cct, cat, pct, pat; string parents; int sysclock = time() + 60; if(3 != sscanf(run_git("log", "-n", "1", "--format=%ct %at %P", commit), "%d %d %s\n", cct, cat, parents)) fail("Unexpected output from git log"); if (cat > sysclock) return "Author time is in the future"; if (cct > sysclock) return "Commit time is in the future"; if (cat > cct) return "Author time is later than commit time"; foreach(parents/" ", string parent) { if(2 != sscanf(run_git("log", "-n", "1", "--format=%ct %at", parent), "%d %d", pct, pat)) fail("Unexpected output from git log"); if (cct < pct) return "Commit time is before that of parent "+parent; } return 0; }
bdb9782010-10-03Marcus Comstedt int is_encoding_utf8(string name) { return (!name) || (<"utf-8", "utf8">)[lower_case(name)]; } string check_encoding(string data, string|void encoding) { if(is_encoding_utf8(encoding)) encoding = "UTF-8"; mixed err = catch {
7750fc2016-11-30Henrik Grubbström (Grubba) #if constant(Charset)
aca3872016-02-07Chris Angelico  Charset.Decoder decoder = Charset.decoder(encoding);
7750fc2016-11-30Henrik Grubbström (Grubba) #else
bdb9782010-10-03Marcus Comstedt  Locale.Charset.Decoder decoder = Locale.Charset.decoder(encoding);
7750fc2016-11-30Henrik Grubbström (Grubba) #endif
3b6d0f2010-10-03Marcus Comstedt  foreach(Array.uniq(values(decoder->feed(data)->drain())), int c) switch(c) { case 0xfffd: return "Undefinied character detected\n"; case '\t': case '\n': case '\r': /* ? */ /* Allowed control character */ break; default: if (c<32 || (c>=0x80 && c<0xa0)) return sprintf("Forbidden control character 0x%02x detected\n", c); }
bdb9782010-10-03Marcus Comstedt  }; return err && err[0]; } string check_commit_msg(string commit) { string message = run_git("cat-file", "commit", commit); string encoding = 0; string headers = (message/"\n\n")[0]; foreach(headers/"\n", string headerline) { if(has_prefix(headerline, "encoding ")) encoding = headerline[9..]; } return check_encoding(message, encoding); }
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) {
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string _sprintf(int type) {
efde062010-09-25Marcus Comstedt  return type=='O' && sprintf("AttrState(%O, %O)\n", attr, setto); } }; class MatchAttr(string name, int is_macro, array(AttrState) states) {
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string _sprintf(int type) {
efde062010-09-25Marcus Comstedt  return type=='O' && sprintf("MatchAttr(%O, %d, %O)\n", name, is_macro, states); } };
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected array(MatchAttr) attrs = ({}); protected mapping(string:MatchAttr) macros = ([]);
efde062010-09-25Marcus Comstedt 
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected 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  }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected 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 
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected MatchAttr parse_attr_line(string line, int macro_ok)
efde062010-09-25Marcus Comstedt  { 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  }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected void handle_attr_line(string line, int macro_ok)
efde062010-09-25Marcus Comstedt  { MatchAttr a = parse_attr_line(line, macro_ok); if(a) attrs += ({ a }); }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected void create(string data)
efde062010-09-25Marcus Comstedt  { foreach(data/"\n", string line) handle_attr_line(line, 1);
19bf732010-09-26Marcus Comstedt  attrs = reverse(attrs);
8b2c1e2010-09-27Marcus Comstedt  foreach(attrs, MatchAttr a) if(a->is_macro) macros[a->name] = a; attrs = filter(attrs, lambda(MatchAttr a) { return !a->is_macro; });
efde062010-09-25Marcus Comstedt  }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected int path_matches(string path, string pattern)
efde062010-09-25Marcus Comstedt  { if(search(pattern, "/")<0) return glob(pattern, (path/"/")[-1]); if(has_prefix(pattern, "/")) pattern = pattern[1..]; return glob(pattern, path); }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected void macroexpand_one(string attrname, array(MatchAttr) attrs,
67699d2014-10-24Henrik Grubbström (Grubba)  mapping(string:string|int) all_attr)
efde062010-09-25Marcus Comstedt  { if(all_attr[attrname] != ATTR_TRUE) return;
8b2c1e2010-09-27Marcus Comstedt  MatchAttr ma = macros[attrname];
19bf732010-09-26Marcus Comstedt  if(ma) fill_one(ma, attrs, all_attr);
efde062010-09-25Marcus Comstedt  }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected void fill_one(MatchAttr attr, array(MatchAttr) attrs,
67699d2014-10-24Henrik Grubbström (Grubba)  mapping(string:string|int) all_attr)
efde062010-09-25Marcus Comstedt  {
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  } }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected void fill(string path, array(MatchAttr) attrs,
67699d2014-10-24Henrik Grubbström (Grubba)  mapping(string:string|int) all_attr)
efde062010-09-25Marcus Comstedt  {
19bf732010-09-26Marcus Comstedt  foreach(attrs, MatchAttr a)
8b2c1e2010-09-27Marcus Comstedt  if(/*!a->is_macro &&*/ path_matches(path, a->name))
19bf732010-09-26Marcus Comstedt  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 = ({});
8b2c1e2010-09-27Marcus Comstedt  foreach(attrs+values(macros), MatchAttr attr) {
aeab9a2010-09-25Marcus Comstedt  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; }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string _sprintf(int type) {
efde062010-09-25Marcus Comstedt  return type=='O' && sprintf("GitAttributes(%O)\n", attrs); } } /* Hooks */ class CommitHookUtils {
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected array(string) files_to_commit;
b668f12010-09-26Marcus Comstedt  GitAttributes attrs;
d645802010-09-26Marcus Comstedt 
24e2112013-08-09Henrik Grubbström (Grubba)  private string git_dir; string get_git_dir() { return git_dir || (git_dir = String.trim_all_whites(run_git("rev-parse", "--git-dir"))); }
b668f12010-09-26Marcus Comstedt  string get_file(string filename, int|void allow_empty);
2c8b302010-09-26Marcus Comstedt  string get_old_file(string filename, int|void allow_empty);
b668f12010-09-26Marcus Comstedt  int entry_is_new(string filename) { return 0; }
d645802010-09-26Marcus Comstedt 
0ef2fc2010-09-25Marcus Comstedt  int find_expanded_ident(string data)
08686f2010-09-25Marcus Comstedt  { int p=0;
c528962010-09-26Marcus Comstedt  while ((p = search(data, DOLLAR"Id", p))>=0) {
4409b42011-04-25Martin Stjernholm  if (data[p..p+3] != unexpanded_id) { int p2 = search(data, DOLLAR, p+3), p3 = search(data, "\n", p+3); if (p2 > p && (p3 < 0 || p2 < p3)) return 1; }
0ef2fc2010-09-25Marcus Comstedt  p += 4; } return 0; }
efde062010-09-25Marcus Comstedt 
d645802010-09-26Marcus Comstedt  int check_ident(string filename)
efde062010-09-25Marcus Comstedt  {
c275d42011-01-23Marcus Comstedt  if (find_expanded_ident(get_file(filename, 2))) {
d645802010-09-26Marcus Comstedt  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  }
b668f12010-09-26Marcus Comstedt  int check_blocker_attributes() {
2c95142010-10-10Martin Stjernholm  int err = 0;
b668f12010-09-26Marcus Comstedt  foreach(files_to_commit, string filename) { mapping(string:string|int) a = attrs->checkattr(filename); if(a->foreign_ident == GitAttributes.ATTR_TRUE) { if (!entry_is_new(filename)) { write("File %s has the foreign_ident attribute. Please remove it before commit.\n", filename);
2c95142010-10-10Martin Stjernholm  err = 1; continue;
b668f12010-09-26Marcus Comstedt  } } if(stringp(a->block_commit) || a->block_commit == GitAttributes.ATTR_TRUE) { if (!entry_is_new(filename)) { write("File %s is blocked from committing: %s\n", filename, replace((stringp(a->block_commit)? a->block_commit : "no explanation given"), "-", " "));
2c95142010-10-10Martin Stjernholm  err = 1; continue;
b668f12010-09-26Marcus Comstedt  } } if(a->ident && a->ident != GitAttributes.ATTR_FALSE && a->ident != GitAttributes.ATTR_UNSET) {
2c95142010-10-10Martin Stjernholm  if (check_ident(filename)) { err = 1; continue; }
b668f12010-09-26Marcus Comstedt  } }
2c95142010-10-10Martin Stjernholm  return err;
b668f12010-09-26Marcus Comstedt  }
2c8b302010-09-26Marcus Comstedt  int check_gitattributes_files() {
2c95142010-10-10Martin Stjernholm  int err = 0;
4ef5022013-03-05Henrik Grubbström (Grubba) #if 0
2c8b302010-09-26Marcus Comstedt  foreach(files_to_commit, string filename) if(has_suffix(filename, "/.gitattributes")) {
2768612010-10-10Martin Stjernholm  write(".gitattributes are not allowed in subdirectories; " "please remove %s\n", filename);
2c95142010-10-10Martin Stjernholm  err = 1;
2c8b302010-09-26Marcus Comstedt  }
4ef5022013-03-05Henrik Grubbström (Grubba) #endif
2c8b302010-09-26Marcus Comstedt  if(search(files_to_commit, ".gitattributes")>=0) { GitAttributes old_attrs = GitAttributes(get_old_file(".gitattributes", 1));
2481672010-09-26Marcus Comstedt  array(string) new_f_i = sort(attrs->findattr("foreign_ident")); array(string) old_f_i = sort(old_attrs->findattr("foreign_ident")); array(string) added_fi = new_f_i - old_f_i; array(string) removed_fi = old_f_i - new_f_i;
2c8b302010-09-26Marcus Comstedt 
2481672010-09-26Marcus Comstedt  foreach(added_fi, string path) {
2c8b302010-09-26Marcus Comstedt  if(!has_prefix(path, "/") || search(path, "*")>=0) {
2768612010-10-10Martin Stjernholm  write("Commit adds unsupported foreign_ident: %s\n", path);
2c95142010-10-10Martin Stjernholm  err = 1; continue;
2c8b302010-09-26Marcus Comstedt  } path = path[1..]; if (!entry_is_new(path)) {
2768612010-10-10Martin Stjernholm  write("Commit adds foreign_ident to existing file %s\n", path);
2c95142010-10-10Martin Stjernholm  err = 1;
2c8b302010-09-26Marcus Comstedt  } }
2481672010-09-26Marcus Comstedt  foreach(removed_fi, string path) {
2c8b302010-09-26Marcus Comstedt  if(has_prefix(path, "/")) path = path[1..];
c4a0832010-10-05Marcus Comstedt  if (search(files_to_commit, path)<0 &&
ce62642013-01-23Marcus Comstedt  find_expanded_ident(get_file(path, 2))) {
2768612010-10-10Martin Stjernholm  write("Commit removes foreign_ident from unchanged file %s\n", path);
2c95142010-10-10Martin Stjernholm  err = 1;
2c8b302010-09-26Marcus Comstedt  } } }
2c95142010-10-10Martin Stjernholm  return err;
2c8b302010-09-26Marcus Comstedt  }
efde062010-09-25Marcus Comstedt }
c0b2b32010-10-04Marcus Comstedt class CommitHookUtilsRepo { inherit CommitHookUtils;
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string sha;
c0b2b32010-10-04Marcus Comstedt  string get_file(string filename, int|void allow_empty) { return get_committed_file(sha, filename, allow_empty); } string get_old_file(string filename, int|void allow_empty) { return get_committed_file(sha+"^", filename, allow_empty); } int entry_is_new(string filename) { return (!sizeof(run_git("ls-tree", sha+"^", "--", filename))) && sizeof(run_git("ls-tree", sha, "--", filename)); } int check_commit(string sha) { if (!has_prefix(sha, "HEAD")) write("Checking commit %s\n", sha); this_program::sha = sha; files_to_commit = split_z(run_git("diff", "--name-only", "-z", sha, sha+"^")); attrs = GitAttributes(get_file(".gitattributes", 1)); string ts_test = check_commit_timestamps(sha);
2c95142010-10-10Martin Stjernholm  int err = 0;
c0b2b32010-10-04Marcus Comstedt  if (ts_test) { write("Invalid timestamps: %s\n", ts_test);
2c95142010-10-10Martin Stjernholm  err = 1;
c0b2b32010-10-04Marcus Comstedt  } string cm_test = check_commit_msg(sha); if (cm_test) { write("Commit message encoding problem:\n%s", cm_test);
2c95142010-10-10Martin Stjernholm  err = 1;
c0b2b32010-10-04Marcus Comstedt  }
2c95142010-10-10Martin Stjernholm  return check_blocker_attributes() | check_gitattributes_files() | err;
c0b2b32010-10-04Marcus Comstedt  } }
efde062010-09-25Marcus Comstedt /* Checks run before editing a commit message */ class PreCommitHook { inherit CommitHookUtils;
b668f12010-09-26Marcus Comstedt  string get_file(string filename, int|void allow_empty)
d645802010-09-26Marcus Comstedt  {
b668f12010-09-26Marcus Comstedt  return get_staged_file(filename, allow_empty);
d645802010-09-26Marcus Comstedt  }
2c8b302010-09-26Marcus Comstedt  string get_old_file(string filename, int|void allow_empty) { return get_committed_file("HEAD", filename, allow_empty); }
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...
12fbdc2010-10-02Marcus Comstedt  if (sizeof(run_git("diff", "--name-only", "--", ".gitattributes"))) {
efde062010-09-25Marcus Comstedt  write("You have unstaged changes to .gitattributes.\n" "Please add or stash them before commit.\n"); return 1; } }
0ef2fc2010-09-25Marcus Comstedt 
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"));
b668f12010-09-26Marcus Comstedt  attrs = GitAttributes(get_file(".gitattributes", 1));
0c9c482010-09-25Marcus Comstedt  return
2c95142010-10-10Martin Stjernholm  check_attributes_staged() | check_blocker_attributes() |
d645802010-09-26Marcus Comstedt  check_gitattributes_files();
0c9c482010-09-25Marcus Comstedt  }
59cd492010-09-05Marcus Comstedt }
efde062010-09-25Marcus Comstedt /* Checks run before accepting a push */ class PreReceiveHook {
c0b2b32010-10-04Marcus Comstedt  inherit CommitHookUtilsRepo;
efde062010-09-25Marcus Comstedt 
24e2112013-08-09Henrik Grubbström (Grubba)  enum AccessLevel { ACCESS_NONE = 0, ACCESS_BASIC = 1, ACCESS_FULL = 2, // rebase/delete branch, move/delete tag, etc. };
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected mapping(string:AccessLevel) groups = ([
24e2112013-08-09Henrik Grubbström (Grubba)  "scratch": ACCESS_FULL, ]);
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected array(string) commits_to_check = ({});
4594fa2010-10-04Marcus Comstedt 
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected void parse_groups(string user)
efde062010-09-25Marcus Comstedt  {
24e2112013-08-09Henrik Grubbström (Grubba)  string git_dir = get_git_dir(); groups[user] = ACCESS_FULL; string group_data = Stdio.read_bytes(combine_path(git_dir, "info/group")); if (!group_data) return; foreach(replace(group_data, "\r", "\n")/"\n", string line) { array(string) fields = map(line/":", String.trim_all_whites); // NB: We currently only care about fields 0 (group name) // and 3 (member list). The first member of a group is // considered the primary member, and has full access. if ((sizeof(fields) != 4) || (fields[0] == "")) continue; array(string) members = map(fields[3]/",", String.trim_all_whites); if (members[0] == user) { // Primary member. groups[fields[0]] = ACCESS_FULL; continue; } if (groups[fields[0]]) continue; // Already a member. if (has_value(members, user)) { groups[fields[0]] = ACCESS_BASIC; continue; } groups[fields[0]] = ACCESS_NONE; } } AccessLevel check_access(string ref_name, string user) { /* Return ACCESS_NONE (0) for no access, * ACCESS_BASIC (1) for basic access, and * ACCESS_FULL (2) for full access * (including rebase/delete branch, and move/delete tag) */
e2915a2011-03-20Marcus Comstedt  string shortref = ref_name; sscanf(shortref, "refs/%*[^/]/%s", shortref);
24e2112013-08-09Henrik Grubbström (Grubba)  foreach(groups; string group; AccessLevel ac) {
bd0f1f2016-11-30Henrik Grubbström (Grubba)  if (has_prefix(shortref, group + "/")) { if (!ac) { write("Access to %s denied for user %s\n", ref_name, user); }
24e2112013-08-09Henrik Grubbström (Grubba)  return ac;
bd0f1f2016-11-30Henrik Grubbström (Grubba)  }
24e2112013-08-09Henrik Grubbström (Grubba)  }
e2915a2011-03-20Marcus Comstedt  if (search(ref_name, "/x-") >= 0) { write("The ref %s can only be modified by its owner\n", ref_name);
24e2112013-08-09Henrik Grubbström (Grubba)  return ACCESS_NONE;
e2915a2011-03-20Marcus Comstedt  }
24e2112013-08-09Henrik Grubbström (Grubba)  return ACCESS_BASIC;
e2915a2011-03-20Marcus Comstedt  } int check_tag_push(string old_sha, string new_sha, string ref_name,
24e2112013-08-09Henrik Grubbström (Grubba)  AccessLevel access_level)
e2915a2011-03-20Marcus Comstedt  {
24e2112013-08-09Henrik Grubbström (Grubba)  if (access_level >= ACCESS_FULL)
e2915a2011-03-20Marcus Comstedt  return 0;
eecd8a2010-09-27Marcus Comstedt  string oldtag = String.trim_all_whites(run_git_ex(1, "rev-parse", "--verify", "-q", ref_name)); if (sizeof(oldtag) && oldtag != new_sha) {
e2915a2011-03-20Marcus Comstedt  write("Tag %s already exists with value %s, will not %s it\n", ref_name, oldtag, (new_sha == "0"*40? "delete":"move")); return 1; } if (!sizeof(oldtag) && search(ref_name[10..], "/") >= 0) { write("Common tags are not allowed to contain /.\n");
eecd8a2010-09-27Marcus Comstedt  return 1; } return 0; }
e2915a2011-03-20Marcus Comstedt  int check_branch_push(string old_sha, string new_sha, string ref_name,
24e2112013-08-09Henrik Grubbström (Grubba)  AccessLevel access_level)
eecd8a2010-09-27Marcus Comstedt  { if (old_sha == "0"*40) {
e2915a2011-03-20Marcus Comstedt  // New branch, check if the name is allowed... if (sscanf(ref_name, "refs/heads/%*[0-9.]%*c") < 2) { write("Main version branches can not be created remotely.\n"); return 1; } if (access_level < 2 && search(ref_name[11..], "/")>=0) { write("Common topic branch names are not allowed to contain /.\n"); return 1; } return 0; } else if (new_sha == "0"*40) { // Delete old branch if (access_level < 2) { write("You may not delete branches which do not belong to you.\n"); return 1; }
efde062010-09-25Marcus Comstedt  return 0; } else {
24e2112013-08-09Henrik Grubbström (Grubba)  if (access_level >= ACCESS_FULL)
e2915a2011-03-20Marcus Comstedt  /* Skip checks */ return 0;
eecd8a2010-09-27Marcus Comstedt  string merge_base = String.trim_all_whites(run_git("merge-base", old_sha, new_sha)); if (merge_base != old_sha) { write("Push to %s is not fast-forward.\n", ref_name); return 1; }
18ca842013-01-23Marcus Comstedt  array(string) old_depth = split_lf(run_git("rev-list", "--first-parent", "-n", "2", old_sha));
6034962011-01-13Marcus Comstedt  array(string) fp_path = split_lf(run_git("rev-list", "--first-parent",
18ca842013-01-23Marcus Comstedt  (sizeof(old_depth)<2? new_sha : old_sha+"^.."+new_sha)));
6034962011-01-13Marcus Comstedt  if (search(fp_path, old_sha)<0) { write("Commit %s does not contain %s in its first-parent ancestry.\nDid you pull with merge instead of rebase?\n", new_sha, old_sha); return 1; }
eecd8a2010-09-27Marcus Comstedt 
4594fa2010-10-04Marcus Comstedt  commits_to_check += split_lf(run_git("rev-list", old_sha+".."+new_sha));
efde062010-09-25Marcus Comstedt  return 0; } }
24e2112013-08-09Henrik Grubbström (Grubba)  int check_push(string git_user, string old_sha, string new_sha, string ref_name)
eecd8a2010-09-27Marcus Comstedt  {
24e2112013-08-09Henrik Grubbström (Grubba)  AccessLevel access_level;
e2915a2011-03-20Marcus Comstedt  if(!(access_level = check_access(ref_name, git_user))) return 1;
eecd8a2010-09-27Marcus Comstedt  if (has_prefix(ref_name, "refs/tags/")) {
e2915a2011-03-20Marcus Comstedt  return check_tag_push(old_sha, new_sha, ref_name, access_level);
eecd8a2010-09-27Marcus Comstedt  } else if (has_prefix(ref_name, "refs/heads/")) {
e2915a2011-03-20Marcus Comstedt  return check_branch_push(old_sha, new_sha, ref_name, access_level);
eecd8a2010-09-27Marcus Comstedt  } else { write("Trying to push a ref which is neither under refs/tags/ or refs/heads/...\n"); return 1; } }
efde062010-09-25Marcus Comstedt  int hook() {
24e2112013-08-09Henrik Grubbström (Grubba)  string git_user = getenv("GIT_USER")||getenv("USER")||"nobody"; parse_groups(git_user);
bb72622010-09-26Marcus Comstedt  foreach(split_lf(Stdio.stdin->read()), string line) { array(string) args = line / " "; if(sizeof(args) != 3) fail("Unexpected input line to pre-receive hook: %s\n", line);
24e2112013-08-09Henrik Grubbström (Grubba)  if(check_push(git_user, @args))
bb72622010-09-26Marcus Comstedt  return 1; }
4594fa2010-10-04Marcus Comstedt  foreach(Array.uniq(commits_to_check), string sha) if(check_commit(sha)) return 1;
efde062010-09-25Marcus Comstedt  return 0; } }
fa4f8f2010-09-27Marcus Comstedt /* Do housekeeping after a commit */ class PostCommitHook {
c0b2b32010-10-04Marcus Comstedt  inherit CommitHookUtilsRepo;
fa4f8f2010-09-27Marcus Comstedt  void cleanup(string filename, mapping(string:string|int) attr) { if(attr->ident && attr->ident != GitAttributes.ATTR_FALSE && attr->ident != GitAttributes.ATTR_UNSET && search(get_committed_file("HEAD", filename, 1), unexpanded_id)>=0) if(sizeof(run_git("diff", "--name-only", "--", filename))) { write("NOTICE: The file %s has a stale ident,\n but I won't touch it since you have unstaged changes.\n", filename); } else { // write("Checking out %s, to fix stale ident...\n", filename); run_git("checkout", "HEAD", "--", filename); } } int hook() {
c0b2b32010-10-04Marcus Comstedt  if (check_commit("HEAD")) write("NOTICE: Your commit has errors, see above messages. Please amend before push.\n"); foreach(files_to_commit, string filename)
fa4f8f2010-09-27Marcus Comstedt  cleanup(filename, attrs->checkattr(filename));
e6367f2010-10-02Marcus Comstedt  string ts_test = check_commit_timestamps("HEAD"); if (ts_test) { write("NOTICE: Your commit has invalid timestamps: %s\n", ts_test); write("Please amend it before pushing.\n"); }
bdb9782010-10-03Marcus Comstedt  string cm_test = check_commit_msg("HEAD"); if (cm_test) { write("NOTICE: Your commit message has an encoding problem:\n%s", cm_test); write("Please ament it before pushing.\n"); }
fa4f8f2010-09-27Marcus Comstedt  return 0; } }
c21d882010-10-04Marcus Comstedt class PostRewriteHook { inherit CommitHookUtilsRepo; int hook(string command) { if (command == "rebase") { int errs = 0; foreach(split_lf(Stdio.stdin->read()), string line) { string old_sha, new_sha, extra; if(sscanf(line, "%s %s%*[ ]%s", old_sha, new_sha, extra) != 4) fail("Unparsable input line %O!\n", line); errs += check_commit(new_sha); } if (errs) write("NOTICE: %d of the commits contain errors. Please amend before pushing.\n", errs); } return 0; } }
59cd492010-09-05Marcus Comstedt /* Filters */
5179182010-09-25Marcus Comstedt /* A sample filter, not really useful... */
59cd492010-09-05Marcus Comstedt class NiceIdentFilter {
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string replace_id(string f, function(string:string) replace) {
80f91d2010-09-05Marcus Comstedt  int p=0;
c528962010-09-26Marcus Comstedt  while((p=search(f, DOLLAR"Id", p)) >= 0) { int p2 = search(f, DOLLAR, p+3), p3 = search(f, "\n", p+3);
4409b42011-04-25Martin Stjernholm  if (p2 > p && (p3 < 0 || p2 < p3)) {
80f91d2010-09-05Marcus Comstedt  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; }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string clean_ident(string i)
80f91d2010-09-05Marcus Comstedt  {
c528962010-09-26Marcus Comstedt  if(has_prefix(i, DOLLAR"Id:") && sizeof(i/" ")==13) return unexpanded_id;
80f91d2010-09-05Marcus Comstedt  }
fe0ca62014-08-28Henrik Grubbström (Grubba)  protected string smudge_ident(string i)
80f91d2010-09-05Marcus Comstedt  {
c528962010-09-26Marcus Comstedt  return DOLLAR"Id: some nice ident perhaps, but based on what? "DOLLAR;
80f91d2010-09-05Marcus Comstedt  } 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; } }