#! /usr/bin/env pike |
|
mapping(string:program) hooks = ([ |
"pre-commit" : PreCommitHook, |
"pre-receive" : PreReceiveHook, |
]); |
|
mapping(string:program) filters = ([ |
#if 0 |
"nice_ident" : NiceIdentFilter, |
#endif |
]); |
|
|
|
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); |
} |
|
array(string) split_z(string data) |
{ |
array(string) a = data / "\0"; |
if (sizeof(a) && a[-1] == "") |
a = a[..sizeof(a)-2]; |
return a; |
} |
|
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); |
} |
|
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); |
} |
|
string get_committed_file(string sha, string filename, int|void allow_empty) |
{ |
string blob; |
string attrentry = run_git("ls-tree", sha, "--", filename); |
if (allow_empty && !sizeof(attrentry)) |
return ""; |
if (2 != sscanf(attrentry, "%*o blob %s\t", blob)) |
fail("Unexpected output from git ls-tree\n"); |
return run_git("cat-file", "blob", blob); |
} |
|
class GitAttributes |
{ |
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) |
{ |
int n; |
if(name == "" || name[0] == '-') |
return 1; |
sscanf(name, "%*[-._0-9a-zA-Z]%n", n); |
return n != sizeof(name); |
} |
|
static AttrState parse_attr(string src) |
{ |
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); |
} |
|
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); |
} |
return MatchAttr(name, is_macro, reverse(map(line/" "-({""}), parse_attr))); |
} |
|
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); |
attrs = reverse(attrs); |
} |
|
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) |
{ |
MatchAttr ma = 0; |
if(all_attr[attrname] != ATTR_TRUE) |
return; |
foreach(attrs, MatchAttr a) |
if(a->is_macro && a->name == attrname) |
ma = a; |
if(ma) fill_one(ma, attrs, all_attr); |
} |
|
static void fill_one(MatchAttr attr, array(MatchAttr) attrs, |
mapping(string:string|int) all_attr) |
{ |
foreach(attr->states, AttrState s) { |
if(!all_attr[s->attr]) { |
all_attr[s->attr] = s->setto; |
macroexpand_one(s->attr, attrs, all_attr); |
} |
} |
} |
|
static void fill(string path, array(MatchAttr) attrs, |
mapping(string:string|int) all_attr) |
{ |
foreach(attrs, MatchAttr a) |
if(!a->is_macro && path_matches(path, a->name)) |
fill_one(a, attrs, all_attr); |
} |
|
mapping(string:string|int) checkattr(string path) |
{ |
mapping(string:string|int) all_attr = ([]); |
fill(path, attrs, all_attr); |
return all_attr; |
} |
|
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; |
} |
|
static string _sprintf(int type) { |
return type=='O' && sprintf("GitAttributes(%O)\n", attrs); |
} |
} |
|
|
|
|
|
class CommitHookUtils |
{ |
static array(string) files_to_commit; |
|
string get_file(string filename); |
|
int find_expanded_ident(string data) |
{ |
int p=0; |
while ((p = search(data, "$Id", p))>=0) { |
if (data[p..p+3] != "$Id$") |
return 1; |
p += 4; |
} |
return 0; |
} |
|
int check_ident(string filename) |
{ |
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; |
} |
} |
|
|
|
class PreCommitHook |
{ |
inherit CommitHookUtils; |
|
string get_file(string filename) |
{ |
return get_staged_file(filename); |
} |
|
int check_attributes_staged() |
{ |
|
|
|
|
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; |
} |
} |
|
int check_blocker_attributes() |
{ |
constant attrs_to_check = ({ "foreign_ident", "block_commit", "ident" }); |
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; |
case "ident": |
if (value == "unset") |
break; |
if (check_ident(filename)) |
return 1; |
break; |
} |
} |
} |
return 0; |
} |
|
int check_gitattributes_files() |
{ |
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) { |
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 && |
search(line, "[attr]") != 1 && |
(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; |
} |
} |
} |
} |
return 0; |
} |
|
int hook() |
{ |
files_to_commit = |
split_z(run_git("diff", "--staged", "--name-only", "-z")); |
return |
check_attributes_staged() || |
check_blocker_attributes() || |
check_gitattributes_files(); |
} |
} |
|
|
|
class PreReceiveHook |
{ |
inherit CommitHookUtils; |
|
static string sha; |
|
string get_file(string filename) |
{ |
return get_committed_file(sha, filename); |
} |
|
int check_blocker_attributes(GitAttributes attrs) |
{ |
foreach(files_to_commit, string filename) { |
mapping(string:string|int) a = attrs->checkattr(filename); |
if(a->foreign_ident == GitAttributes.ATTR_TRUE) { |
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; |
} |
} |
if(stringp(a->block_commit) || a->block_commit == GitAttributes.ATTR_TRUE) { |
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"), "-", " ")); |
return 1; |
} |
} |
if(a->ident && a->ident != GitAttributes.ATTR_FALSE && |
a->ident != GitAttributes.ATTR_UNSET) { |
if (check_ident(filename)) |
return 1; |
} |
} |
return 0; |
} |
|
int check_gitattributes_files(GitAttributes attrs) |
{ |
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) { |
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; |
} |
} |
} |
return 0; |
} |
|
int check_commit(string sha) |
{ |
write("Checking commit %s\n", sha); |
this_program::sha = sha; |
files_to_commit = |
split_z(run_git("diff", "--name-only", "-z", sha, sha+"^")); |
string attrtext = get_committed_file(sha, ".gitattributes", 1); |
GitAttributes attrs = GitAttributes(attrtext); |
return check_blocker_attributes(attrs) || |
check_gitattributes_files(attrs); |
} |
|
int check_push(string old_sha, string new_sha, string ref_name) |
{ |
if(old_sha == "0"*40) { |
|
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; |
} |
} |
|
|
|
|
|
class NiceIdentFilter |
{ |
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) { |
|
f = f[..p-1]+r+f[p2+1..]; |
p += sizeof(r); |
} else { |
|
p = p2+1; |
} |
} else p += 3; |
} |
return f; |
} |
|
static string clean_ident(string i) |
{ |
if(has_prefix(i, "$Id:") && sizeof(i/" ")==13) |
return "$Id$"; |
} |
|
static string smudge_ident(string i) |
{ |
return "$Id$"; |
} |
|
int clean() |
{ |
write(replace_id(Stdio.stdin->read(), clean_ident)); |
return 0; |
} |
|
int smudge() |
{ |
write(replace_id(Stdio.stdin->read(), smudge_ident)); |
return 0; |
} |
} |
|
|
|
class GitHelper |
{ |
void setup_hooks() |
{ |
constant hooksdir = "hooks"; |
if (!sizeof(hooks)) |
return; |
if (!file_stat(hooksdir)) { |
write("Creating the hooks directory\n"); |
if (!mkdir(hooksdir)) |
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); |
} else if (s->islnk) { |
|
} else { |
write("Hook %s already exists, so won't overwrite it...\n", name); |
} |
} |
} |
|
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; |
if (old == "") { |
write("Installing filter operation %s\n", confname); |
run_git("config", confname, cmd); |
} else if(old == cmd) { |
|
} 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) |
if (filter[op]) |
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; |
return 0; |
} |
|
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]) { |
werror("Filter %s does not implement %s!\n", filter, fop); |
return 1; |
} 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; |
} |
} |
|
|