#! /usr/bin/env pike |
|
#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 */ |
|
#define DOLLAR "$" |
constant unexpanded_id = DOLLAR"Id"DOLLAR; |
|
mapping(string:program) hooks = ([ |
"pre-commit" : PreCommitHook, |
"pre-receive" : PreReceiveHook, |
"post-commit" : PostCommitHook, |
"post-rewrite" : PostRewriteHook, |
]); |
|
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; |
} |
|
array(string) split_lf(string data) |
{ |
array(string) a = data / "\n"; |
if (sizeof(a) && a[-1] == "") |
a = a[..sizeof(a)-2]; |
return a; |
} |
|
string run_git_ex(int max_exitcode, string ... args) |
{ |
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; |
} ); |
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 ) |
Pike.DefaultBackend( 1.0 ); |
|
int exitcode = p->wait(); |
|
if (exitcode > max_exitcode) { |
werror(gotstderr); |
fail("git exited with code %d\n", exitcode); |
} |
return gotstdout; |
} |
|
string run_git(string ... args) |
{ |
return run_git_ex(0, @args); |
} |
|
string get_staged_file(string filename, int|void allow_empty) |
{ |
string blob; |
string treeentry = run_git("ls-files", "--stage", "--", filename); |
if (allow_empty && !sizeof(treeentry)) |
return ""; |
if (2 != sscanf(treeentry, "%*o %s ", blob)) |
fail("Unable to parse output from git ls-files...\n"); |
return run_git("cat-file", "blob", blob); |
} |
|
string get_committed_file(string sha, string filename, int|void allow_empty) |
{ |
string blob; |
string treeentry = run_git("ls-tree", sha, "--", filename); |
if (allow_empty && !sizeof(treeentry)) |
return ""; |
if (allow_empty == 2 && 2 == sscanf(treeentry, "%*o tree %s\t", blob)) |
return ""; |
if (2 != sscanf(treeentry, "%*o blob %s\t", blob)) |
fail("Unexpected output from git ls-tree\n"); |
return run_git("cat-file", "blob", blob); |
} |
|
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; |
} |
|
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 { |
#if constant(Charset) |
Charset.Decoder decoder = Charset.decoder(encoding); |
#else |
Locale.Charset.Decoder decoder = Locale.Charset.decoder(encoding); |
#endif |
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': |
|
break; |
default: |
if (c<32 || (c>=0x80 && c<0xa0)) |
return sprintf("Forbidden control character 0x%02x detected\n", c); |
} |
}; |
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); |
} |
|
class GitAttributes |
{ |
enum { |
ATTR_TRUE = 1, |
ATTR_FALSE = 2, |
ATTR_UNSET = 3 |
}; |
|
class AttrState(string attr, string|int setto) { |
protected string _sprintf(int type) { |
return type=='O' && sprintf("AttrState(%O, %O)\n", attr, setto); |
} |
}; |
|
class MatchAttr(string name, int is_macro, array(AttrState) states) { |
protected string _sprintf(int type) { |
return type=='O' && sprintf("MatchAttr(%O, %d, %O)\n", |
name, is_macro, states); |
} |
}; |
|
protected array(MatchAttr) attrs = ({}); |
protected mapping(string:MatchAttr) macros = ([]); |
|
protected 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); |
} |
|
protected 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); |
} |
|
protected 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))); |
} |
|
protected void handle_attr_line(string line, int macro_ok) |
{ |
MatchAttr a = parse_attr_line(line, macro_ok); |
if(a) attrs += ({ a }); |
} |
|
protected void create(string data) |
{ |
foreach(data/"\n", string line) |
handle_attr_line(line, 1); |
attrs = reverse(attrs); |
foreach(attrs, MatchAttr a) |
if(a->is_macro) |
macros[a->name] = a; |
attrs = filter(attrs, lambda(MatchAttr a) { return !a->is_macro; }); |
} |
|
protected 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); |
} |
|
protected void macroexpand_one(string attrname, array(MatchAttr) attrs, |
mapping(string:string|int) all_attr) |
{ |
if(all_attr[attrname] != ATTR_TRUE) |
return; |
MatchAttr ma = macros[attrname]; |
if(ma) fill_one(ma, attrs, all_attr); |
} |
|
protected 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); |
} |
} |
} |
|
protected void fill(string path, array(MatchAttr) attrs, |
mapping(string:string|int) all_attr) |
{ |
foreach(attrs, MatchAttr a) |
if( 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+values(macros), 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; |
} |
|
protected string _sprintf(int type) { |
return type=='O' && sprintf("GitAttributes(%O)\n", attrs); |
} |
} |
|
|
|
|
|
class CommitHookUtils |
{ |
protected array(string) files_to_commit; |
GitAttributes attrs; |
|
private string git_dir; |
|
string get_git_dir() |
{ |
return git_dir || |
(git_dir = String.trim_all_whites(run_git("rev-parse", "--git-dir"))); |
} |
|
string get_file(string filename, int|void allow_empty); |
string get_old_file(string filename, int|void allow_empty); |
int entry_is_new(string filename) { return 0; } |
|
int find_expanded_ident(string data) |
{ |
int p=0; |
while ((p = search(data, DOLLAR"Id", p))>=0) { |
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; |
} |
p += 4; |
} |
return 0; |
} |
|
int check_ident(string filename) |
{ |
if (find_expanded_ident(get_file(filename, 2))) { |
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; |
} |
|
int check_blocker_attributes() |
{ |
int err = 0; |
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); |
err = 1; |
continue; |
} |
} |
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"), "-", " ")); |
err = 1; |
continue; |
} |
} |
if(a->ident && a->ident != GitAttributes.ATTR_FALSE && |
a->ident != GitAttributes.ATTR_UNSET) { |
if (check_ident(filename)) { |
err = 1; |
continue; |
} |
} |
} |
return err; |
} |
|
int check_gitattributes_files() |
{ |
int err = 0; |
#if 0 |
foreach(files_to_commit, string filename) |
if(has_suffix(filename, "/.gitattributes")) { |
write(".gitattributes are not allowed in subdirectories; " |
"please remove %s\n", filename); |
err = 1; |
} |
#endif |
|
if(search(files_to_commit, ".gitattributes")>=0) { |
GitAttributes old_attrs = |
GitAttributes(get_old_file(".gitattributes", 1)); |
|
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; |
|
foreach(added_fi, string path) { |
if(!has_prefix(path, "/") || search(path, "*")>=0) { |
write("Commit adds unsupported foreign_ident: %s\n", path); |
err = 1; |
continue; |
} |
path = path[1..]; |
if (!entry_is_new(path)) { |
write("Commit adds foreign_ident to existing file %s\n", path); |
err = 1; |
} |
} |
|
foreach(removed_fi, string path) { |
if(has_prefix(path, "/")) |
path = path[1..]; |
if (search(files_to_commit, path)<0 && |
find_expanded_ident(get_file(path, 2))) { |
write("Commit removes foreign_ident from unchanged file %s\n", path); |
err = 1; |
} |
} |
} |
return err; |
} |
} |
|
class CommitHookUtilsRepo |
{ |
inherit CommitHookUtils; |
|
protected string sha; |
|
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); |
int err = 0; |
if (ts_test) { |
write("Invalid timestamps: %s\n", ts_test); |
err = 1; |
} |
string cm_test = check_commit_msg(sha); |
if (cm_test) { |
write("Commit message encoding problem:\n%s", cm_test); |
err = 1; |
} |
return check_blocker_attributes() | |
check_gitattributes_files() | |
err; |
} |
} |
|
|
|
class PreCommitHook |
{ |
inherit CommitHookUtils; |
|
string get_file(string filename, int|void allow_empty) |
{ |
return get_staged_file(filename, allow_empty); |
} |
|
string get_old_file(string filename, int|void allow_empty) |
{ |
return get_committed_file("HEAD", filename, allow_empty); |
} |
|
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 hook() |
{ |
files_to_commit = |
split_z(run_git("diff", "--staged", "--name-only", "-z")); |
attrs = GitAttributes(get_file(".gitattributes", 1)); |
return |
check_attributes_staged() | |
check_blocker_attributes() | |
check_gitattributes_files(); |
} |
} |
|
|
|
class PreReceiveHook |
{ |
inherit CommitHookUtilsRepo; |
|
enum AccessLevel { |
ACCESS_NONE = 0, |
ACCESS_BASIC = 1, |
ACCESS_GROUP = 2, |
ACCESS_FULL = 3, |
}; |
|
protected mapping(string:AccessLevel) groups = ([ |
"scratch": ACCESS_FULL, |
]); |
|
protected array(string) commits_to_check = ({}); |
|
protected void parse_groups(string user) |
{ |
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); |
|
|
|
|
|
if ((sizeof(fields) != 4) || (fields[0] == "")) continue; |
array(string) members = map(fields[3]/",", String.trim_all_whites); |
if (members[0] == user) { |
|
groups[fields[0]] = ACCESS_FULL; |
continue; |
} |
if (groups[fields[0]]) continue; |
if (has_value(members, user)) { |
groups[fields[0]] = ACCESS_GROUP; |
continue; |
} |
groups[fields[0]] = ACCESS_NONE; |
} |
|
|
groups[user] = ACCESS_FULL; |
} |
|
AccessLevel check_access(string ref_name, string user) |
{ |
|
|
|
|
|
string shortref = ref_name; |
sscanf(shortref, "refs/%*[^/]/%s", shortref); |
foreach(groups; string group; AccessLevel ac) { |
if (has_prefix(shortref, group + "/")) { |
if (!ac) { |
write("Access to %s denied for user %s\n", ref_name, user); |
} |
return ac; |
} |
} |
if (search(ref_name, "/x-") >= 0) { |
write("The ref %s can only be modified by its owner\n", ref_name); |
return ACCESS_NONE; |
} |
return ACCESS_BASIC; |
} |
|
int check_tag_push(string old_sha, string new_sha, string ref_name, |
AccessLevel access_level) |
{ |
if (access_level >= ACCESS_FULL) |
return 0; |
|
string oldtag = |
String.trim_all_whites(run_git_ex(1, "rev-parse", "--verify", |
"-q", ref_name)); |
if (sizeof(oldtag) && oldtag != new_sha) { |
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) { |
if (access_level >= ACCESS_GROUP) { |
|
return 0; |
} |
write("Common tags are not allowed to contain /.\n"); |
return 1; |
} |
|
return 0; |
} |
|
int check_branch_push(string old_sha, string new_sha, string ref_name, |
AccessLevel access_level) |
{ |
if (old_sha == "0"*40) { |
|
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 < ACCESS_GROUP && 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) { |
|
if (access_level < ACCESS_FULL) { |
write("You may not delete branches which do not belong to you.\n"); |
return 1; |
} |
return 0; |
} else { |
if (access_level >= ACCESS_FULL) |
|
return 0; |
|
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; |
} |
array(string) old_depth = split_lf(run_git("rev-list", "--first-parent", |
"-n", "2", old_sha)); |
array(string) fp_path = split_lf(run_git("rev-list", "--first-parent", |
(sizeof(old_depth)<2? |
new_sha : |
old_sha+"^.."+new_sha))); |
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; |
} |
|
commits_to_check += split_lf(run_git("rev-list", old_sha+".."+new_sha)); |
return 0; |
} |
} |
|
int check_push(string git_user, |
string old_sha, string new_sha, string ref_name) |
{ |
AccessLevel access_level; |
if(!(access_level = check_access(ref_name, git_user))) return 1; |
if (has_prefix(ref_name, "refs/tags/")) { |
return check_tag_push(old_sha, new_sha, ref_name, access_level); |
} else if (has_prefix(ref_name, "refs/heads/")) { |
return check_branch_push(old_sha, new_sha, ref_name, access_level); |
} else { |
write("Trying to push a ref which is neither under refs/tags/ or refs/heads/...\n"); |
return 1; |
} |
} |
|
int hook() |
{ |
string git_user = getenv("GIT_USER")||getenv("USER")||"nobody"; |
parse_groups(git_user); |
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); |
if(check_push(git_user, @args)) |
return 1; |
} |
foreach(Array.uniq(commits_to_check), string sha) |
if(check_commit(sha)) |
return 1; |
return 0; |
} |
} |
|
|
|
class PostCommitHook |
{ |
inherit CommitHookUtilsRepo; |
|
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 { |
|
run_git("checkout", "HEAD", "--", filename); |
} |
} |
|
int hook() |
{ |
if (check_commit("HEAD")) |
write("NOTICE: Your commit has errors, see above messages. Please amend before push.\n"); |
foreach(files_to_commit, string filename) |
cleanup(filename, attrs->checkattr(filename)); |
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"); |
} |
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"); |
} |
return 0; |
} |
} |
|
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; |
} |
} |
|
|
|
|
|
class NiceIdentFilter |
{ |
protected string replace_id(string f, function(string:string) replace) { |
int p=0; |
while((p=search(f, DOLLAR"Id", p)) >= 0) { |
int p2 = search(f, DOLLAR, p+3), p3 = search(f, "\n", p+3); |
if (p2 > p && (p3 < 0 || 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; |
} |
|
protected string clean_ident(string i) |
{ |
if(has_prefix(i, DOLLAR"Id:") && sizeof(i/" ")==13) |
return unexpanded_id; |
} |
|
protected string smudge_ident(string i) |
{ |
return DOLLAR"Id: some nice ident perhaps, but based on what? "DOLLAR; |
} |
|
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; |
} |
} |
|
|