Roxen.git / server / tools / git-rxnpatch

version» Context lines:

Roxen.git/server/tools/git-rxnpatch:1: + #!/usr/bin/env pike + // -*- Pike -*- + // + // Tool to create Roxen patches from git repositories. + // + // 2013-04-25 Henrik Grubbström    -  + // #define DISABLE_PUSH +  + constant common_options = ({ +  ({ "help", Getopt.NO_ARG, ({ "-h", "--help" }) }), +  ({ "version", Getopt.NO_ARG, ({ "-v", "--version" }) }), + }); +  + constant specific_options = ([ +  "delete": ({}), +  "help": ({}), +  "init": ({ +  ({ "path_prefix", Getopt.HAS_ARG, ({ "--path-prefix" }) }), +  ({ "subtree_prefix", Getopt.HAS_ARG, ({ "--relative" }) }), +  ({ "path_remap_rule", Getopt.HAS_ARG, ({ "--path-remap-rule" }) }), +  +  ({ "force", Getopt.NO_ARG, ({ "-f", "--force" }) }), +  }), +  "list": ({}), +  "make": ({ +  ({ "path_prefix", Getopt.HAS_ARG, ({ "--path-prefix" }) }), +  ({ "subtree_prefix", Getopt.HAS_ARG, ({ "--relative" }) }), +  ({ "path_remap_rule", Getopt.HAS_ARG, ({ "--path-remap-rule" }) }), +  }), +  "new": ({ +  ({ "path_prefix", Getopt.HAS_ARG, ({ "--path-prefix" }) }), +  ({ "subtree_prefix", Getopt.HAS_ARG, ({ "--relative" }) }), +  ({ "path_remap_rule", Getopt.HAS_ARG, ({ "--path-remap-rule" }) }), +  +  ({ "depends", Getopt.HAS_ARG, ({ "-d", "--depends" }) }), +  ({ "flag", Getopt.HAS_ARG, ({ "-F", "--flag" }) }), +  ({ "originator", Getopt.HAS_ARG, ({ "-O", "--originator" }) }), +  ({ "subject", Getopt.HAS_ARG, ({ "-s", "--subject" }) }), +  ({ "message", Getopt.HAS_ARG, ({ "-m", "--message" }) }), +  ({ "patchid", Getopt.HAS_ARG, ({ "--patchid" }) }), +  ({ "restart", Getopt.NO_ARG, ({ "--restart" }) }), +  ({ "norestart", Getopt.NO_ARG, ({ "--no-restart" }) }), +  }), +  "cluster": ({ +  ({ "force", Getopt.NO_ARG, ({ "-f", "--force" }) }), +  }), +  "status": ({}), + ]); +  + constant cmd_args = ([ +  0:" [\xa0<args>\xa0]", +  "delete": " <patch-id>", +  "help": " [\xa0<command>\xa0]", +  "init": " <start-point>", +  "list": "", +  "make": " [\xa0<patchid>\xa0]", +  "new": " [\xa0<commit>\xa0]", +  "cluster": "", +  "status": " [\xa0<commit>\xa0]", + ]); +  + constant cmd_doc = ([ +  0: "Create and manage RXN patches in git.\n", +  "delete": "Delete the latest patch.\n" +  "Note that the patch id must be specified as a precaution.\n", +  "help": "Display help about a command.\n", +  "init": "Initialize rxnpatch for the current branch.\n", +  "list": "List existing RXN patches.\n", +  "make": "Make the RXP file for an RXN patch.\n", +  "new": "Create a new RXN patch.\n", +  "cluster": "Make a tar file with all current patches for the branch.\n", +  "status": "Display patch status for the current git branch.\n", + ]); +  + string git_binary = getenv("_") || "git"; +  + string committer; +  + #if constant(Git.Export) + // Pike 7.9 and later. + constant GIT_MODE_FILE = Git.MODE_FILE; + constant GIT_MODE_EXE = Git.MODE_EXE; + constant GIT_MODE_SYMLINK = Git.MODE_SYMLINK; + constant GIT_MODE_GITLINK = Git.MODE_GITLINK; + constant GIT_MODE_DIR = Git.MODE_DIR; + constant GIT_NULL_SHA1 = Git.NULL_SHA1; + constant GitExport = Git.Export; + #else +  + constant GIT_MODE_FILE = 0100644; + constant GIT_MODE_EXE = 0100755; + constant GIT_MODE_SYMLINK = 0120000; + constant GIT_MODE_GITLINK = 0160000; + constant GIT_MODE_DIR = 040000; + constant GIT_NULL_SHA1 = "0000000000000000000000000000000000000000"; +  + //! This is a stripped Git.Export containing just the stuff that we need. + //! + //! @note + //! The APIs that are used MUST be compatible with the corresponding + //! in Git.Export. + class GitExport { +  protected Stdio.File export_fd; +  +  protected mapping(string:string|int) requested_features = ([]); +  +  protected void create(Stdio.File fd) +  { +  export_fd = fd || Stdio.stdout; +  } +  +  void command(sprintf_format cmd, sprintf_args ... args) +  { +  export_fd->write(cmd, @args); +  } +  +  int done() +  { +  if (requested_features["done"]) { +  command("done\n"); +  } +  if (export_fd) { +  export_fd->close(); +  export_fd = UNDEFINED; +  } +  return 0; +  } +  +  void reset(string ref, string|void committish) +  { +  command("reset %s\n", ref); +  if (committish) { +  command("from %s\n", committish); +  } +  } +  +  void mark(string marker) +  { +  command("mark %s\n", marker); +  } +  +  void data(string data) +  { +  command("data %d\n" +  "%s\n", sizeof(data), data); +  } +  +  void blob(string blob, string|void marker) +  { +  command("blob\n"); +  if (marker) mark(marker); +  data(blob); +  } +  +  void checkpoint() +  { +  command("checkpoint\n"); +  } +  +  void progress(string message) +  { +  foreach(message/"\n", string line) { +  command("progress %s\n", line); +  } +  } +  +  void feature(string feature, string|void arg) +  { +  if (arg) { +  command("feature %s=%s\n", feature, arg); +  } else { +  command("feature %s\n", feature); +  } +  requested_features[feature] = arg || 1; +  } +  +  void option(string option) +  { +  command("option %s\n", option); +  } +  +  void tag(string name, string committish, string tagger_info, string message) +  { +  command("tag %s\n" +  "from %s\n" +  "tagger %s\n", +  name, committish, tagger_info); +  data(message); +  } +  +  void commit(string ref, string|void commit_marker, +  string|void author_info, string committer_info, +  string message, string|void ... parents) +  { +  command("commit %s\n", ref); +  if (commit_marker) { +  mark(commit_marker); +  } +  if (author_info) { +  command("author %s\n", author_info); +  } +  command("committer %s\n", committer_info); +  data(message); +  if (sizeof(parents)) { +  command("from %s\n" +  "%{merge %s\n%}", +  parents[0], parents[1..]); +  } +  } +  +  void filedeleteall() +  { +  command("deleteall\n"); +  } +  +  void filemodify(int mode, string path, string|void dataref) +  { +  path = combine_path_unix("/", path)[1..]; +  if (path == "") { +  error("Invalid path.\n"); +  } +  command("M %06o %s %q\n", +  mode, dataref || "inline", path); +  } +  +  void filedelete(string path) +  { +  path = combine_path_unix("/", path)[1..]; +  if (path == "") { +  error("Invalid path.\n"); +  } +  command("D %q\n", path); +  } +  +  void filecopy(string from, string to) +  { +  from = combine_path_unix("/", from)[1..]; +  to = combine_path_unix("/", to)[1..]; +  if ((from == "") || (to == "")) { +  error("Invalid path.\n"); +  } +  command("C %q %q\n", from, to); +  } +  +  void filerename(string from, string to) +  { +  from = combine_path_unix("/", from)[1..]; +  to = combine_path_unix("/", to)[1..]; +  if ((from == "") || (to == "")) { +  error("Invalid path.\n"); +  } +  command("C %q %q\n", from, to); +  } +  +  void notemodify(string commit, string|void dataref) +  { +  if (!requested_features["notes"]) { +  error("The notes feature has not been requested.\n"); +  } +  command("N %s %s\n", dataref || "inline", commit); +  } +  +  void export(string file_name, string|void git_name) +  { +  Stdio.Stat st = file_stat(file_name); +  if (!st) return; +  int mode = st->mode; +  if (mode & GIT_MODE_DIR) { +  mode = GIT_MODE_DIR; +  } else if (mode & 0111) { +  mode = GIT_MODE_EXE; +  } else if (mode & 0666) { +  mode = GIT_MODE_FILE; +  } else { +  error("Unsupported filesystem mode for %O: %03o\n", file_name, mode); +  } +  if (mode == GIT_MODE_DIR) { +  foreach(get_dir(file_name), string fn) { +  export(combine_path(file_name, fn), +  combine_path(git_name || file_name, fn)); +  } +  } else { +  filemodify(mode, git_name); +  data(Stdio.read_bytes(file_name)); +  } +  } + } +  + #endif +  + string git(string command, string ... args) + { +  array(string) cmd = ({ git_binary, command, @args }); +  mapping(string:string|int) res = Process.run(cmd); +  if (res->exitcode) { +  werror("Git command '%s' failed with code %d:\n" +  "%s\n\n" +  "stdout:\n" +  "%s\n", cmd*"' '", res->exitcode, res->stderr||"", res->stdout||""); +  exit(1); +  } +  return res->stdout||""; + } +  + string git_try(string command, string ... args) + { +  array(string) cmd = ({ git_binary, command, @args }); +  mapping(string:string|int) res = Process.run(cmd); +  if (res->exitcode) { +  werror("git failure in command '%s' '%s':\n" +  " stderr: %s" +  " stdout: %s\n", +  command, args * "', '", res->stderr, res->stdout); +  return UNDEFINED; +  } +  return res->stdout||""; + } +  + string git_cat_file(string treeish, string path, int|void try) + { +  string blob_sha1 = git_try("ls-tree", "-z", "--full-tree", treeish, path); +  if (blob_sha1) { +  blob_sha1 = (((blob_sha1/"\0")[0]/"\t")[0]/" ")[-1]; +  } +  string res = blob_sha1 && sizeof(blob_sha1) && +  git_try("cat-file", "blob", blob_sha1); +  if (res || try) return res; +  werror("File %O not found in tree %O.\n", path, treeish); +  exit(1); + } +  + void git_push(string remote, string ref, int|void force) + { +  string res; + #ifndef DISABLE_PUSH +  if (force) { +  res = git_try("push", "-f", remote, ref); +  } else { +  res = git_try("push", remote, ref); +  } + #endif +  if (!res) { +  werror("Warning: Failed to push reference %O to remote %s.\n", +  ref, remote); +  } + } +  + void git_save_file(string branch, string path, string data, string message) + { +  if (!committer) { +  string user_name = getenv("GIT_AUTHOR_NAME") || +  git("config", "--get", "user.name") - "\n"; +  string user_email = getenv("GIT_AUTHOR_EMAIL") || +  git("config", "--get", "user.email") - "\n"; +  +  committer = sprintf("%s <%s> now", user_name, user_email); +  } +  +  if (!has_prefix(branch, "refs/heads/")) branch = "refs/heads/" + branch; +  +  string parent = branch + "^0"; +  +  if (!git_try("rev-list", "--max-count=1", branch)) { +  // First commit on branch. +  parent = UNDEFINED; +  } else if (git_cat_file(branch, path, 1) == data) { +  // Already up to date. +  return; +  } +  +  Stdio.File export_fd = Stdio.File(); +  Process.Process fast_importer = +  Process.Process(({ git_binary, "fast-import", "--date-format=now" }), +  ([ +  "stdin":export_fd->pipe(), +  "stderr":Stdio.File("/dev/null"), +  ])); +  GitExport exporter = GitExport(export_fd); +  +  // exporter->feature("done"); +  exporter->commit(branch, UNDEFINED, UNDEFINED, committer, message, +  @(parent?({ parent }):({}))); +  exporter->filemodify(GIT_MODE_FILE, path); +  exporter->data(data); +  +  int code; +  if ((code = (exporter->done() || fast_importer->wait()))) { +  werror("Failed to save file %O on branch %O (exit code %d).\n", +  path, branch, code); +  exit(1); +  } +  git_push("origin", branch); + } +  + void git_delete_files(string branch, array(string) files, string message) + { +  if (!committer) { +  string user_name = getenv("GIT_AUTHOR_NAME") || +  git("config", "--get", "user.name") - "\n"; +  string user_email = getenv("GIT_AUTHOR_EMAIL") || +  git("config", "--get", "user.email") - "\n"; +  +  committer = sprintf("%s <%s> now", user_name, user_email); +  } +  +  if (!has_prefix(branch, "refs/heads/")) branch = "refs/heads/" + branch; +  +  string parent = branch + "^0"; +  +  if (!git_try("rev-list", "--max-count=1", branch)) { +  // First commit on branch. +  return; +  } +  +  Stdio.File export_fd = Stdio.File(); +  Process.Process fast_importer = +  Process.Process(({ git_binary, "fast-import", "--date-format=now" }), +  ([ +  "stdin":export_fd->pipe(), +  "stderr":Stdio.File("/dev/null"), +  ])); +  GitExport exporter = GitExport(export_fd); +  +  // exporter->feature("done"); +  exporter->commit(branch, UNDEFINED, UNDEFINED, committer, message, +  @(parent?({ parent }):({}))); +  foreach(files, string path) { +  exporter->filedelete(path); +  } +  +  int code; +  if ((code = (exporter->done() || fast_importer->wait()))) { +  werror("Failed to delete files on branch %O (exit code %d).\n", +  branch, code); +  exit(1); +  } +  git_push("origin", branch); + } +  + void git_delete_file(string branch, string file, string message) + { +  git_delete_files(branch, ({ file }), message); + } +  + int main(int argc, array(string) argv) + { +  int help = 0; +  +  foreach(Getopt.find_all_options(argv, common_options, 1), +  array(string) opt) { +  switch(opt[0]) { +  case "help": +  help = 1; +  break; +  case "version": +  display_version(); +  exit(0); +  } +  } +  +  argv = Getopt.get_args(argv, 1); +  if (sizeof(argv) < 2) { +  display_usage(); +  exit(0); +  } +  argv = argv[1..]; +  if (!specific_options[argv[0]]) { +  werror("Unknown command: %O.\n\n", argv[0]); +  display_usage(); +  exit(1); +  } else if (help) { +  display_usage(argv[0]); +  exit(0); +  } +  +  mapping(string:int|string|array(string)) options = ([ +  "flag": ({ "restart" }), +  ]); +  foreach(Getopt.find_all_options(argv, specific_options[argv[0]], 1), +  array(string) opt) { +  switch(opt[0]) { +  case "flag": +  opt[1] = lower_case(opt[1]); +  if (has_prefix(opt[1], "no-")) { +  // Negated option. +  options[opt[1]] -= ({ opt[1][sizeof("no-")..] }); +  break; +  } +  // FALL_THROUGH +  case "depends": +  // Multiple OK. +  options[opt[0]] += ({ opt[1] }); +  break; +  case "restart": +  options->flag += ({ "restart" }); +  break; +  case "norestart": +  options->flag -= ({ "restart" }); +  break; +  case "force": +  case "message": +  default: +  // Singleton option. +  if (options[opt[0]]) { +  werror("Option %s specified multiple times.\n", opt[0]); +  exit(1); +  } +  options[opt[0]] = opt[1]; +  break; +  } +  } +  +  argv = Getopt.get_args(argv, 1); +  switch(argv[0]) { +  case "help": +  if (sizeof(argv) < 2) { +  display_usage(); +  exit(0); +  } +  if (!specific_options[argv[1]]) { +  werror("Unknown command: %O.\n\n", argv[1]); +  display_usage(); +  exit(1); +  } +  display_usage(argv[1]); +  exit(0); +  case "delete": +  delete_patch(options, argv); +  break; +  case "init": +  init_patch_branch(options, argv); +  break; +  case "list": +  list_pending_commits(options, argv); +  break; +  case "make": +  make_patch(options, argv); +  break; +  case "new": +  create_new_patch(options, argv); +  break; +  case "cluster": +  create_cluster(options, argv); +  break; +  case "status": +  display_status(options, argv); +  break; +  } + } +  + void display_usage(string|void cmd) + { +  string prefix = " git rxnpatch"; +  array(array(string|int|array(string))) options = common_options; +  if (cmd) { +  prefix += " [\xa0<opts>\xa0] " + cmd; +  options = specific_options[cmd]; +  } +  +  Stdio.stdout.write("Usage:\n"); +  string doc = ""; +  foreach(options, array(string|int|array(string)) opt) { +  doc += " [\xa0" + (opt[2]*"\xa0|\xa0"); +  if (opt[1] == Getopt.HAS_ARG) { +  doc += " <" + opt[0] + ">"; +  } +  doc += "\xa0]"; +  } +  if (!cmd) { +  doc += " [\xa0" + (sort(indices(specific_options)) * "\xa0|\xa0") + "\xa0]"; +  } +  doc += cmd_args[cmd]; +  doc = sprintf("%#*s%-=*s\n", +  sizeof(prefix), prefix, +  70 - sizeof(prefix), doc)[1..]; +  Stdio.stdout.write(replace(doc, "\xa0", " ")); +  +  doc = cmd_doc[cmd]; +  if (sizeof(doc)) { +  Stdio.stdout.write("\n" + doc); +  } +  return; + } +  + void display_version() + { +  Stdio.stdout.write("git-rxnpatch 1.0\n"); + } +  + //! @returns + //! Mapping from tag reference to commit sha1. + mapping(string:string) tags(string|void commit) + { +  return (mapping)map(git("show-ref", "--tags")/"\n" - ({""}), +  lambda(string line) { +  array(string) a = line/" "; +  return ({ a[1..]*" ", a[0] }); +  }); + } +  + //! Assert that @[commit] is on the first-parent path from @[head]. + //! + //! @returns + //! Returns the number of commits on the path. + int assert_first_parent(string head, string commit) + { +  string sha1 = git("rev-list", "--max-count=1", commit) - "\n"; +  array(string) commits = +  map(git("rev-list", "--first-parent", "--parents", +  sha1 + ".." + head)/"\n" - ({""}), +  lambda(string line) { +  array(string) a = line/" "; +  if (sizeof(a)) return a[1]; +  }); +  if (!sizeof(commits)) { +  string head_sha1 = git("rev-list", "--max-count=1", commit) - "\n"; +  if (sha1 == head_sha1) { +  // They are the same commit. +  return 0; +  } +  } +  if (!sizeof(commits) || (commits[-1] != sha1)) { +  werror("Commit %s is not on the first-parent path from %s.\n", +  commit, head); +  exit(1); +  } +  return sizeof(commits); + } +  + //! Bring the specified up to date with respect to the remote "origin". + void git_update(string branch) + { +  if (has_prefix(branch, "refs/heads/")) { +  branch = branch[sizeof("refs/heads/")..]; +  } +  if (!git_try("fetch", "origin", branch)) return; +  +  string head = +  git_try("rev-list", "--max-count=1", "refs/heads/" + branch); +  if (head) { +  array(string) a = +  map((git("rev-list", "--first-parent", "--parents", +  "refs/heads/" + branch + +  "..refs/remotes/origin/" + branch) || "")/"\n" - +  ({""}), +  lambda(string line) { +  array(string) a = line/" "; +  if (sizeof(a) > 1) return a[1]; +  }) - ({ 0 }); +  if(!sizeof(a)) return; // Up to date. +  if (head) { +  head -= "\n"; +  if (a[-1] != head) { +  werror("Branch %s has diverged from upstream.\n", branch); +  exit(1); +  } +  } +  } else if (!git_try("rev-list", "--max-count=1", +  "refs/remotes/origin/" + branch)) { +  // Not existing at origin either. +  return; +  } +  // Fast-forward. +  werror("Fast-forwarding branch %s.\n", branch); +  git("branch", "--track", "-f", branch, "refs/remotes/origin/" + branch); + } +  + //! Allocate a new patchid. + string new_patchid() + { +  do { +  int t = time(); +  mapping(string:int) lt = localtime(t); +  string patchid = +  sprintf("%04d-%02d-%02dT%02d%02d%02d", +  lt->year + 1900, lt->mon + 1, lt->mday, +  lt->hour, lt->min, lt->sec); +  if (git_try("show-ref", "refs/tags/rxnpatch/" + patchid)) { +  werror("Patchid %s already exists!\n" +  "Waiting a second for the next...\n", +  patchid); +  sleep(1); +  continue; +  } +  return patchid; +  } while (1); + } +  + //! Directory containing the repository information. + string git_dir; +  + //! Master branch to generate patches for. + string branch; +  + //! Branch indicating the current patch level. + string patch_branch; +  + //! SHA1 for the commit corresponding to @[patch_branch]. + string patch_sha1; +  + //! Dependencies corresponding to the current patch head. + array(string) depends = ({}); +  + //! Prefix for release tags on @[branch]. + string release_tag_prefix; +  + //! Prefix for cluster files in rxnpatch/rxnpatch for @[branch]. + //! + //! This is generated from @[branch] and @[release_tag_prefix]. + string cluster_path_prefix; +  + //! Prefix for release cluster files in rxnpatch/rxnpatch for @[branch]. + //! + //! This is generated from @[branch] and @[release_tag_prefix]. + string release_cluster_path_prefix; +  + //! Prefix where the repository is extracted at the user. + string path_prefix; +  + //! Repository subtree to extract. + string subtree_prefix; +  + //! Path remapping rules. + string path_remapping_rules; +  + //! Load dependencies corresponsing to @[sha1]. + array(string) load_deps(string sha1) + { +  array(string) ret = ({}); +  foreach(git("show-ref", "--tags", "--dereference", "--")/"\n", string line) { +  array(string) a = line/" "; +  if (a[0] != sha1) continue; +  string dep = a[1]; +  if (has_prefix(dep, "refs/tags/rxnpatch/")) { +  dep = dep[sizeof("refs/tags/rxnpatch/")..]; +  } else if (release_tag_prefix && has_prefix(dep, release_tag_prefix)) { +  dep = replace(dep[sizeof("refs/tags/")..], +  ({ "-", "_" }), ({ "/", "/" })); +  if (!has_value(dep, "/") && has_prefix(dep, "v")) { +  // Pike. +  dep = "pike/" + dep[1..]; +  } +  } else continue; +  ret += ({ dep }); +  } +  return ret; + } +  + array(array(string)) parse_remapping_rules(string rules) + { +  array(array(string)) res = ({}); +  foreach(rules/"\n", string line) { +  line = (line/"#")[0]; +  if (!sizeof(String.trim_all_whites(line))) continue; +  array(string) pair = line/":"; +  if (sizeof(pair) != 2) { +  werror("Warning: Invalid remapping rule: %O (ignored)\n", line); +  continue; +  } +  if (pair[0] == pair[1]) { +  werror("Warning: Noop remapping rule: %O (ignored)\n", line); +  continue; +  } +  res += ({ pair }); +  } +  return res; + } +  + //! Initialize common git information. + int init(mapping(string:string|int|array(string)) options) + { +  git_dir = git("rev-parse", "--git-dir") - "\n"; +  +  branch = git("symbolic-ref", "HEAD") - "\n"; +  +  if (!sizeof(branch)) { +  werror("Detached HEAD!\n"); +  exit(1); +  } +  +  if (has_prefix(branch, "refs/heads/")) { +  branch = branch[sizeof("refs/heads/")..]; +  } +  +  if (has_prefix(branch, "rxnpatch/")) { +  if (branch == "rxnpatch/rxnpatch") { +  werror("The current branch is the master patch branch (%s).\n" +  "Please switch to a development branch.\n", +  branch); +  exit(1); +  } +  +  werror("The current is a patch branch (%s).\n" +  "Please switch to a development branch (%s?).\n", +  branch, branch[sizeof("rxnpatch/")..]); +  exit(1); +  } +  +  patch_branch = "refs/heads/rxnpatch/" + branch; +  +  // Bring the rxnpatch branches up to date. +  werror("Syncing...\n"); +  git_update("refs/heads/rxnpatch/rxnpatch"); +  git_update(patch_branch); +  +  if (!(patch_sha1 = git_try("rev-list", "--max-count=1", patch_branch))) { +  return 0; +  } +  patch_sha1 -= "\n"; +  +  release_tag_prefix = +  git_cat_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/release_tag_prefix.txt", 1); +  if (release_tag_prefix) { +  release_tag_prefix -= "\n"; +  +  string repo = +  replace(release_tag_prefix[sizeof("refs/tags/")..], "/", "-"); +  +  if (repo == "v") { +  // Pike. +  repo = "pike"; +  } +  +  cluster_path_prefix = "refs/heads/" + branch + "/clusters/patches-" + +  repo + "-" + replace(branch, "/", "-") + "-"; +  +  release_cluster_path_prefix = "refs/heads/" + branch + "/clusters/patches-" + +  repo + "-"; +  } +  +  path_prefix = +  git_cat_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_prefix.txt", 1) || ""; +  path_prefix -= "\n"; +  +  if (options->path_prefix && (options->path_prefix != path_prefix)) { +  if (!has_suffix(options->path_prefix, "/") && +  (options->path_prefix != "")) { +  // Normalize the prefix to contain the directory separator. +  options->path_prefix += "/"; +  } +  if (options->path_prefix != path_prefix) { +  // They still differ. +  path_prefix = options->path_prefix; +  write("Updating default path prefix for branch %s to %s.\n", +  branch, path_prefix); +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_prefix.txt", +  path_prefix + "\n", +  "New path prefix: " + path_prefix + ".\n"); +  } +  } +  +  subtree_prefix = +  git_cat_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/subtree_prefix.txt", 1) || ""; +  subtree_prefix -= "\n"; +  +  if (options->subtree_prefix && (options->subtree_prefix != subtree_prefix)) { +  if (!has_suffix(options->subtree_prefix, "/") && +  (options->subtree_prefix != "")) { +  // Normalize the prefix to contain the directory separator. +  options->subtree_prefix += "/"; +  } +  if (subtree_prefix != options->subtree_prefix) { +  // They still differ. +  subtree_prefix = options->subtree_prefix; +  write("Updating default repository subtree prefix " +  "for branch %s to %s.\n", +  branch, subtree_prefix); +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/subtree_prefix.txt", +  subtree_prefix + "\n", +  "New repository subtree prefix: " + +  subtree_prefix + ".\n"); +  } +  } +  +  path_remapping_rules = +  git_cat_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_remapping_rules.txt", 1) || ""; +  +  if (options->path_remap_rule) { +  array(array(string)) remapping_rules = +  parse_remapping_rules(path_remapping_rules); +  array(string) new_rule = options->path_remap_rule/":"; +  if (sizeof(new_rule) != 2) { +  werror("Invalid remapping rule syntax: %O\n", options->path_remap_rule); +  exit(1); +  } +  int found; +  foreach(remapping_rules, array(string) old_rule) { +  if (old_rule[0] != new_rule[0]) continue; +  found = 1; +  if (old_rule[1] != new_rule[1]) { +  if (new_rule[1] == new_rule[0]) { +  // NOOP ==> Remove rule. +  write("Removing old path remapping rule: %O ==> %O.\n", +  old_rule[0], old_rule[1]); +  remapping_rules -= ({ old_rule }); +  } else { +  write("Updating old path remapping rule: %O ==> %O to %O.\n", +  old_rule[0], old_rule[1], new_rule[1]); +  old_rule[1] = new_rule[1]; +  } +  found = 2; // Dirty. +  } +  break; +  } +  if (!found && (new_rule[0] != new_rule[1])) { +  write("Adding new path remapping rule: %O ==> %O.\n", +  new_rule[0], new_rule[1]); +  remapping_rules += ({ new_rule }); +  found = 2; +  } +  if (found == 2) { +  write("Updating default path remapping rules for branch %s.\n", branch); +  path_remapping_rules = sprintf("%{%s:%s\n%}", remapping_rules); +  if (path_remapping_rules != "") { +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_remapping_rules.txt", +  path_remapping_rules, +  "New path remapping rules.\n"); +  } else { +  git_delete_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_remapping_rules.txt", +  "Removed path remapping rules.\n"); +  } +  } +  write("Path remapping rules:\n" +  "%{ %O ==> %O\n%}", +  remapping_rules); +  } +  +  depends = load_deps(patch_sha1); +  +  if (!sizeof(depends)) { +  werror("No dependency tags at the current patch branch HEAD!\n"); +  exit(1); +  } +  +  return 1; + } +  + void init_patch_branch(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (init(options)) { +  werror("Patch branch %s already exists!\n", patch_branch); +  if (!options->force) exit(1); +  } +  +  if (sizeof(argv) < 2) { +  werror("Missing argument: start-point.\n"); +  exit(1); +  } +  +  string current_patch = argv[1]; +  +  assert_first_parent(branch, current_patch); +  +  string current_tag; +  +  foreach (git("show-ref", "--tags", current_patch)/"\n" - ({""}), +  string line) { +  array(string) a = line/" "; +  string tag = a[1..]*" "; +  current_tag = tag; +  break; +  } +  if (!current_tag) { +  werror("%s is not a valid tag.\n", argv[1]); +  exit(1); +  } +  +  string rxnpatch_branch = "refs/heads/rxnpatch/rxnpatch"; +  // Check whether the refs/heads/rxnpatch/rxnpatch branch exists. +  if (!git_try("rev-list", "--max-count=1", "--heads", rxnpatch_branch)) { +  // Not available locally, what about remotely? +  string other = git_try("show-ref", "--max-count=1", "rxnpatch/rxnpatch"); +  string remote; +  foreach((other || "")/"\n" - ({""}), string line) { +  array(string) a = line/" "; +  string tag = a[1..]*" "; +  if (has_prefix(tag, "refs/remotes/")) { +  remote = tag; +  // FIXME: Look up the tracking remote for the current branch? +  if (tag == "refs/remotes/origin/rxnpatch/rxnpatch") { +  break; +  } +  } +  } +  if (remote) { +  // Clone the existing remote branch. +  git("branch", "--track", "rxnpatch/rxnpatch", remote); +  } +  } +  +  if (current_tag && !has_prefix(current_tag, "refs/tags/rxnpatch/")) { +  // Not a rxnpatch tag. +  // We've probably got a release tag. +  array(string) a = replace(current_tag, "-", "_")/"_"; +  string tag_prefix; +  if (sizeof(a) > 1) { +  tag_prefix = a[0] + "\n"; +  } else { +  a = current_tag/"/"; +  if (has_prefix(a[-1], "v")) { +  // Pike. +  tag_prefix = a[..sizeof(a)-2]*"/" + "/v"; +  } +  } +  if (tag_prefix) { +  string prev_prefix = +  git_cat_file(rxnpatch_branch, +  "refs/heads/" + branch + "/release_tag_prefix.txt", 1); +  if (tag_prefix != prev_prefix) { +  write("Updating release tag prefix for branch %s to %s...\n", +  branch, tag_prefix - "\n"); +  git_save_file(rxnpatch_branch, +  "refs/heads/" + branch + "/release_tag_prefix.txt", +  tag_prefix, +  sprintf("Updated release tag prefix for branch %s to %s.\n", +  branch, tag_prefix - "\n")); +  } +  } else { +  werror("Reference %s does not look like a release tag.\n", +  current_patch); +  exit(1); +  } +  } +  +  if (options->path_prefix) { +  if ((options->path_prefix != "") && +  !has_suffix(options->path_prefix, "/")) { +  options->path_prefix += "/"; +  } +  write("Setting default path prefix for branch %s to %s.\n", +  branch, options->path_prefix); +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_prefix.txt", +  options->path_prefix + "\n", +  "New path prefix: " + options->path_prefix + ".\n"); +  } else { +  werror("Warning: The path prefix for this branch has not been set.\n"); +  } +  +  path_prefix = options->path_prefix || ""; +  +  if (options->subtree_prefix) { +  if (!has_suffix(options->subtree_prefix, "/") && +  (options->subtree_prefix != "")) { +  // Normalize the prefix to contain the directory separator. +  options->subtree_prefix += "/"; +  } +  write("Setting default repository subtree prefix " +  "for branch %s to %s.\n", +  branch, options->subtree_prefix); +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/subtree_prefix.txt", +  options->subtree_prefix + "\n", +  "New repository subtree prefix: " + +  options->subtree_prefix + ".\n"); +  } +  +  subtree_prefix = options->subtree_prefix || ""; +  +  path_remapping_rules = ""; +  if (options->path_remap_rule) { +  array(string) new_rule = options->path_remap_rule/":"; +  if (sizeof(new_rule) != 2) { +  werror("Invalid remapping rule syntax: %O\n", options->path_remap_rule); +  exit(1); +  } +  if (new_rule[0] != new_rule[1]) { +  write("Adding new path remapping rule: %O ==> %O.\n", +  new_rule[0], new_rule[1]); +  +  write("Setting default path remapping rules for branch %s.\n", branch); +  path_remapping_rules = sprintf("%s:%s\n", new_rule[0], new_rule[1]); +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/path_remapping_rules.txt", +  path_remapping_rules, +  "Added path remapping rules.\n"); +  write("Path remapping rules:\n" +  " %O ==> %O\n", +  new_rule[0], new_rule[1]); +  } +  } +  +  git("branch", "-f", patch_branch[sizeof("refs/heads/")..], current_patch); +  write("Created patch branch %s.\n", patch_branch); + } +  + void display_status(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (!init(options)) { +  werror("rxnpatch not initialized on this branch.\n"); +  exit(0); +  } +  +  string head = branch; +  if (sizeof(argv) > 1) { +  head = argv[1]; +  } +  +  // Assert that the commits are in order on the first-parent path. +  +  assert_first_parent(branch, head); +  +  int pending = assert_first_parent(head, patch_branch); +  +  write("Branch: %s\n" +  "Patch-level: %s\n", +  branch, depends * " | "); +  if (release_tag_prefix) { +  write("Release tag prefix: %s\n", release_tag_prefix); +  } +  if (sizeof(subtree_prefix)) { +  write("Subtree path prefix: %s\n", subtree_prefix); +  } +  if (sizeof(path_prefix)) { +  write("Path prefix: %s\n", path_prefix); +  } +  if (sizeof(path_remapping_rules)) { +  write("Path remapping rules:\n" +  "%{ %s ==> %s\n%}", +  parse_remapping_rules(path_remapping_rules)); +  } +  +  mapping(string:string) patches = ([]); +  string prefix = "refs/heads/" + branch + "/patches"; +  foreach(git("ls-tree", "-r", "-z", "--full-tree", "rxnpatch/rxnpatch", +  "--", prefix)/"\0", string entry) { +  if (entry == "") continue; +  string path = (entry/"\t")[1..] * "\t"; +  if (!has_prefix(path, prefix + "/")) continue; +  string subpath = path[sizeof(prefix + "/")..]; +  string patchid = (subpath/"/")[0]; +  if (subpath == (patchid + "/" + patchid + ".rxp")) { +  patches[patchid] = "ok"; +  +  // FIXME: Add detection of stale patches. +  } else if (patches[patchid] != "ok") { +  if (subpath == (patchid + "/metadata.txt")) { +  patches[patchid] = "pending"; +  } else if (!patches[patchid]) { +  // Note that this is a normal state at this point, +  // since there may be other files than the main two +  // in the directory (notably "affected-files.txt"). +  patches[patchid] = "suspect"; +  } +  } +  } +  +  int num_pending; +  int num_suspect; +  foreach(patches; string patchid; string mode) { +  switch(mode) { +  case "ok": break; +  case "pending": +  num_pending++; +  break; +  default: +  case "suspect": +  patches[patchid] = "suspect"; +  num_suspect++; +  break; +  } +  } +  if (sizeof(patches)) { +  write("Num patches: %d\n", sizeof(patches) - num_pending); +  if (num_suspect) { +  write("Suspect patches found:\n"); +  foreach(sort(indices(patches)), string patchid) { +  if (patches[patchid] == "suspect") { +  write(" %s\n", patchid); +  } +  } +  } +  if (num_pending) { +  if (num_pending == 1) { +  write("Pending patch:\n"); +  } else { +  write("Pending patches:\n"); +  } +  foreach(sort(indices(patches)), string patchid) { +  if (patches[patchid] == "pending") { +  write(" %s\n", patchid); +  } +  } +  } +  } +  +  int pending_cluster; +  if (sizeof(patches)) { +  // Check if there's an up-to-date patch cluster for the branch. +  string prefix = "refs/heads/" + branch + "/clusters"; +  string cluster; +  foreach(git("ls-tree", "-r", "-z", "--full-tree", "rxnpatch/rxnpatch", +  "--", prefix)/"\0", string entry) { +  if (entry == "") continue; +  string path = (entry/"\t")[1..] * "\t"; +  if (!has_prefix(path, cluster_path_prefix)) continue; +  if (!has_suffix(path, ".tgz")) continue; +  // NB: The suffix ".tgz" is sorted after "-02.tgz", +  // so we need to compensate here. +  string new_cluster = path[sizeof(cluster_path_prefix)..<4]; +  if (!cluster || (cluster < new_cluster)) cluster = new_cluster; +  } +  if (cluster) { +  // There's apparently a cluster. +  string base = git_try("rev-list", "--max-count=1", +  "refs/tags/rxnpatch/clusters/" + branch + +  "/" + cluster); +  if (base) { +  // Check if any patches have been added or changed for the +  // branch since then. +  base = (base/"\n")[0]; +  pending_cluster = 0; +  foreach(git("diff-tree", "-r", "-z", "--name-only", +  base, "rxnpatch/rxnpatch")/"\0", string fname) { +  if (!has_prefix(fname, "refs/heads/" + branch + "/patches/")) +  continue; +  if (!has_suffix(fname, ".rxp")) continue; +  pending_cluster++; +  } +  } else { +  werror("Warning: Tag not found for cluster %s.\n", cluster); +  pending_cluster = sizeof(patches); +  } +  +  cluster = ((cluster_path_prefix + cluster + ".tgz")/"/")[-1]; +  +  if (pending_cluster) { +  write("Latest cluster: %s (%d patches pending)\n", +  cluster, pending_cluster); +  } else { +  write("Latest cluster: %s (Up to date)\n", cluster); +  } +  } else { +  pending_cluster = sizeof(patches); +  write("No patch clusters found.\n"); +  } +  } +  +  if (pending) { +  write("Pending commits: %d\n", pending); +  } else if (!num_pending && !pending_cluster) { +  write("Up to date.\n"); +  } + } +  + string format_description(string old_sha1, string head_sha1) + { +  // Note: We build the description backwards (ie the oldest entry +  // will appear at the top). +  string description = ""; +  string log = git("log", "--no-merges", "--date-order", "--dense", +  "--pretty=raw", "-s", old_sha1 + ".." + head_sha1); +  catch { log = utf8_to_string(log); }; +  array(string) a = log/"\n\n"; +  int num_blocks; +  foreach(a, string block) { +  if (has_prefix(block, " ")) num_blocks++; +  } +  foreach(a, string block) { +  if (!has_prefix(block, " ")) continue; +  // Normalize bug references ([bug ####] et al), +  // and keep them from getting linebreaks. +  block = +  replace(block, +  ({ "[bug ", "[Bug ", +  "[InfoKOM ", "[Infokom ", +  "[LysKOM ", "[Lyskom ", "[lyskom ", +  "[LysLysKOM ", "[Lyslyskom ", +  "[lyslyskom ", +  }), +  ({ "[bug\xa0", "[bug\xa0", +  "[InfoKOM\xa0", "[InfoKOM\xa0", +  "[LysKOM\xa0", "[LysKOM\xa0", "[LysKOM\xa0", +  "[LysLysKOM\xa0", "[LysLysKOM\xa0", +  "[LysLysKOM\xa0", +  })); +  block = map(block/"\n", +  lambda(string line) { +  if (has_prefix(line, " ")) return line[4..]; +  return line; +  }) * "\n"; +  if (!has_suffix(block, "\n")) block += "\n"; +  if (num_blocks > 1) { +  if (!has_prefix(block, "\t")) { +  string new_block = ""; +  foreach((block/"\n")[..<1]; int i; string line) { +  if (has_prefix(block, " ") || +  has_prefix(block, "*")) { +  line = " " + line; +  } else if (i) { +  line = " " + line; +  } else { +  line = " * " + line; +  } +  new_block += line + "\n"; +  } +  block = new_block; +  } +  } +  block = replace(block, "\xa0", " "); +  +  if (description == "") { +  description = string_to_utf8(block); +  } else { +  description = string_to_utf8(block) + "\n" + description; +  } +  } +  if (num_blocks > 1) { +  description = "Multiple fixes:\n\n" + description; +  } +  +  return description; + } +  + void list_pending_commits(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (!init(options)) { +  werror("rxnpatch not initialized on this branch.\n"); +  exit(1); +  } +  +  string head = branch; +  if (sizeof(argv) > 1) { +  head = argv[1]; +  } +  +  string head_sha1 = git("rev-list", "--max-count=1", head) - "\n"; +  +  assert_first_parent(branch, head_sha1); +  +  int count = assert_first_parent(head, patch_sha1); +  +  if (!count) { +  write("Up to date.\n"); +  return; +  } +  +  array(string) revs = git("rev-list", "--first-parent", +  patch_sha1 + ".." + head_sha1)/"\n" - ({ "" }); +  +  string prev_sha1 = patch_sha1; +  foreach(reverse(revs); int i; string sha1) { +  write("Commit %s (aka %s%s):\n", +  sha1, head, (count == (i+1))?"":"~"+(count-i)); +  write(format_description(prev_sha1, sha1)); +  prev_sha1 = sha1; +  } + } +  + void create_new_patch(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (!init(options)) { +  werror("rxnpatch not initialized on this branch.\n"); +  exit(1); +  } +  +  werror("Release tag prefix: %O\n", release_tag_prefix); +  +  string head = branch; +  if (sizeof(argv) > 1) { +  head = argv[1]; +  } +  +  string head_sha1 = git("rev-list", "--max-count=1", head) - "\n"; +  +  assert_first_parent(branch, head_sha1); +  +  int count = assert_first_parent(head, patch_sha1); +  +  if (!count) { +  werror("Fully patched.\n"); +  exit(1); +  } +  +  string user_email = +  options->originator || git("config", "--get", "user.email") - "\n"; +  +  string header = sprintf("from: %s\n" +  "to: %s\n" +  "originator: %s\n" +  "depends: %s\n", +  patch_sha1, +  head_sha1, +  user_email, +  depends * "|"); +  +  if (options->depends) { +  header += sprintf("%{depends: %s\n%}", options->depends); +  } +  +  if (options->flag) { +  if (has_value(options->flag, "restart")) { +  header += "restart: true\n"; +  options->flag -= ({ "restart" }); +  } +  header += sprintf("%{flag: %s\n%}", Array.uniq(options->flag)); +  } +  +  string subject = options->subject; +  +  string description = options->message || +  format_description(patch_sha1, head_sha1); +  +  if (!subject) { +  subject = ""; +  if (release_tag_prefix) { +  string display_name = +  map(release_tag_prefix[sizeof("refs/tags/")..]/"/", +  String.capitalize) * " "; +  +  // Convert to marketing names. +  display_name = ([ +  "V": "Pike", +  "Print": "EP", +  "Sitebuilder": "CMS", +  "Feedimport": "FeedImport", +  ])[display_name] || replace(display_name, "_", " "); +  +  subject = display_name + " " + branch + ": "; +  } +  subject += (description/"\n")[0]; +  } +  +  string patchid = options->patchid; +  if (patchid) { +  if (git_try("show-ref", "refs/tags/rxnpatch/" + patchid)) { +  werror("Patchid %s already exists!\n", patchid); +  exit(1); +  } +  } else { +  patchid = new_patchid(); +  } +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/patches/" + patchid + "/metadata.txt", +  sprintf("subject: %s\n" +  "%s\n" +  "%s", +  subject, +  header, +  description), +  sprintf("Created new patch %s on branch %s.\n" +  "\n" +  "%s\n" +  "From commit %s\n" +  "to commit %s.\n", +  patchid, branch, subject, patch_sha1, head_sha1)); +  git("tag", "-f", "rxnpatch/" + patchid, head_sha1); +  git_push("origin", "refs/tags/rxnpatch/" + patchid); +  +  write("Created patchid %s (%d commits).\n", patchid, count); +  +  git("branch", "-f", patch_branch[sizeof("refs/heads/")..], head_sha1); +  git_push("origin", patch_branch); + } +  + void check_ident(string block, string path, string commit) + { +  // Convert the path to a repository path. +  string repo_path = path; +  if (sizeof(path_prefix || "") && has_prefix(path, path_prefix)) { +  repo_path = repo_path[sizeof(path_prefix)..]; +  } +  if (sizeof(subtree_prefix || "")) { +  repo_path = subtree_prefix + repo_path; +  } +  +  string tree_info = +  (git("ls-tree", "--full-tree", "-z", commit, "--", repo_path)/"\0")[0]; +  if (!sizeof(tree_info)) { +  // Deleted? +  werror("File %s (%s) deleted?\n", path, repo_path); +  return; +  } +  sscanf(tree_info, "%o %s %s\t%s", +  int mode, string type, string sha1, string path2); +  if ((repo_path != path2) || (type != "blob")) { +  error("Unexpected output from git ls-tree --full-tree -z %s -- %s:\n" +  " %O\n", +  commit, repo_path, tree_info); +  } +  foreach(block/"\n", string line) { +  if (!has_prefix(line, "+") || !has_value(line, "$Id"": ")) continue; +  string frag = (line/("$Id"": "))[1]; +  if (!has_prefix(frag, sha1) && has_value(frag, "$")) { +  error("Unexpected $Id" +  "$ expansion for %s (%s):\n" +  "Expected: $Id" +  ": %s $\n" +  "Got: %O\n", +  path, repo_path, sha1, frag); +  } +  werror("%s: $Id: %s" +  " $\n", +  path, sha1); +  break; // We assume that all the idents get the same expansion. +  } + } +  + array(mapping(string:array(string))|string) parse_metadata(string patchid, +  string metadata) + { +  array(string) a = metadata/"\n\n"; +  if (sizeof(a) < 2) { +  werror("Missing patch description for patch %s.\n", patchid); +  exit(1); +  } +  +  mapping(string:array(string)) header = ([]); +  foreach(a[0]/"\n", string line) { +  line = (line/"#")[0]; +  if (line == "") continue; +  array(string) pair = line/":"; +  if (sizeof(pair) < 2) { +  werror("Invalid metadata syntax for patch %s:\n" +  "%s\n", patchid, line); +  exit(1); +  } +  header[lower_case(String.trim_all_whites(pair[0]))] += +  ({ String.trim_all_whites(pair[1..] * ":") }); +  } +  +  if (header->flags) { +  // Common typo. +  werror("Invalid header field %O.\n" +  "Did you perhaps mean %O?\n", +  "flags", "flag"); +  } +  +  string description = a[1..]*"\n\n"; +  +  return ({ header, description }); + } +  + void make_patch(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (!init(options)) { +  werror("rxnpatch not initialized on this branch.\n"); +  exit(1); +  } +  +  string patchid; +  if (sizeof(argv) < 2) { +  foreach(depends, string dep) { +  if (!has_value(dep, "/")) { +  patchid = dep; +  break; +  } +  } +  if (!patchid) { +  werror("Missing argument: patchid.\n"); +  exit(1); +  } +  } else { +  patchid = argv[1]; +  } +  +  write("Patch: %s\n", patchid); +  +  string metadata = +  git_cat_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/patches/" + patchid + "/metadata.txt"); +  +  [mapping(string:array(string)) header, string description] = +  parse_metadata(patchid, metadata); +  +  string tail_sha1; +  string head_sha1; +  string originator; +  string subject; +  if (header->from && (sizeof(header->from) == 1)) { +  tail_sha1 = header->from[0]; +  } else { +  werror("Invalid header field %O.\n", "from"); +  } +  if (header->to && (sizeof(header->to) == 1)) { +  head_sha1 = header->to[0]; +  } else { +  werror("Invalid header field %O.\n", "to"); +  } +  if (header->originator && (sizeof(header->originator) == 1)) { +  originator = header->originator[0]; +  } else { +  werror("Invalid header field %O.\n", "originator"); +  } +  if (header->subject && (sizeof(header->subject) == 1)) { +  subject = header->subject[0]; +  } else { +  werror("Invalid header field %O.\n", "subject"); +  } +  +  string tmpdir = "/var/tmp/rxnpatch-" + patchid; +  if (!mkdir(tmpdir) && (errno() != System.EEXIST)) { +  werror("Failed to create temporary directory %O (%s).\n", +  tmpdir, strerror(errno())); +  exit(1); +  } +  +  array(array(string)) remapping_rules = +  parse_remapping_rules(path_remapping_rules); +  +  write("Preparing patch...\n"); +  +  // Description. +  Stdio.write_file(tmpdir + "/description.txt", description); +  +  // Command. +  array(string) cmd = ({ +  "rxnpatch", "create", +  "--id=" + patchid, +  "--originator=" + originator, +  "--name=" + (subject || (description/"\n")[0]), +  "--description=" + tmpdir + "/description.txt", +  @map(header->depends || ({}), +  lambda(string dep) { +  return "--depends=" + dep; +  }), +  @(header->restart?({ "--flag=restart" }):({})), +  @map(header->flag || ({}), +  lambda(string flag) { +  return "--flag=" + flag; +  }), +  "--target-dir=" + tmpdir, +  }); +  +  // .distignore +  // NB: We only care about the recent .distignores. +  array(string) distignore_prefixes = ({}); +  foreach(git("ls-tree", "--full-tree", "-r", "-z", head_sha1, +  "--", subtree_prefix)/"\0", +  string line) { +  if (!has_suffix(line, ".distignore")) continue; +  array(string) a = line/"\t"; +  string path = a[1..]*"\t"; +  if (!has_suffix(path, "/.distignore") && (path != ".distignore")) continue; +  string distignore = replace(git_cat_file(head_sha1, path), "\r", "\n"); +  path = path[sizeof(subtree_prefix)..]; +  string dir = path_prefix + dirname(path); +  foreach(distignore/"\n" - ({""}), string path) { +  path = (path/"#")[0]; +  if (path == "") continue; +  if (has_suffix(path, "*")) path = path[..sizeof(path)-2]; +  distignore_prefixes += ({ combine_path(dir, path) }); +  } +  } +  distignore_prefixes = Array.uniq(distignore_prefixes); +  +  // Set up suitable diff options, so that we can expand Id-strings. +  if (!git_try("config", "diff.expand-ident.textconv")) { +  // NB: Use string concat to avoid erroneous self-replacement here. +  git("config", "diff.expand-ident.textconv", +  "sed -e \"s/\\\\\\$" +  "Id\\\\\\$/\\$" +  "Id: `git hash-object \"$1\"` \\$/g\" < \"$1\""); +  // git("config", "xfuncname", ""); +  } +  string attrs = Stdio.read_bytes(git_dir + "/info/attributes"); +  if (!attrs || !has_value(attrs, "expand-ident")) { +  attrs = "[attr]ident ident diff=expand-ident\n" + (attrs || ""); +  if (!Stdio.is_dir(git_dir + "/info/.")) { +  mkdir(git_dir + "/info"); +  } +  Stdio.write_file(git_dir + "/info/attributes", attrs); +  } +  +  // Diff. +  string raw_diff = git("diff", "--src-prefix=" + path_prefix, +  "--dst-prefix=" + path_prefix, "--no-ext-diff", +  "--no-color", "--textconv", "--no-renames", +  "--full-index", +  "--relative=" + subtree_prefix, +  tail_sha1 + ".." + head_sha1); +  string diff = ""; +  array(string) added = ({}); +  array(string) modified = ({}); +  array(string) deleted = ({}); +  foreach(("\n" + raw_diff + "diff --git ")/"\ndiff --git ", string block) { +  block = "diff --git " + block + "\n"; +  if (block == "diff --git \n") continue; +  mapping(string:array(string)) header = ([]); +  array(string) lines = block/"\n"; +  foreach(lines; int lineno; string line) { +  array a = line/" "; +  switch(a[0]) { +  case "diff": +  // Extract the path names and perform unquoting. +  header->a = ({ a[2] }); +  header->b = ({ a[3] }); +  if (has_prefix(header->a[0], "\"")) { +  sscanf(a[2..]*" ", "%O %s", header->a[0], header->b[0]); +  if (has_prefix(header->b[0], "\"")) { +  sscanf(a[2..]*" ", "%O %O", header->a[0], header->b[0]); +  } +  } else if (has_prefix(header->b[0], "\"")) { +  sscanf(a[3..]*" ", "%O", header->b[0]); +  } else if (sizeof(a) > 4) { +  // Some versions of git forget that strings containing +  // spaces need quoting... +  +  werror("Warning: Unquoted spaces in filename: %O\n", line); +  +  if (a[2] == "/dev/null") { +  header->b = ({ a[3..] * " " }); +  } else if (a[-1] == "/dev/null") { +  header->a = ({ a[2..<1] * " " }); +  header->b = ({ a[-1] }); +  } else if (sizeof(path_prefix)) { +  // FIXME: Assumes path_prefix doesn't contain spaces. +  int i; +  for(i = 3; i < sizeof(a); i++) { +  if (has_prefix(a[i], path_prefix)) { +  // Found! +  header->a = ({ a[2..i-1] * " " }); +  header->b = ({ a[i..] * " " }); +  break; +  } +  } +  } else { +  // Assume same number of spaces in both filenames. +  header->a = ({ a[2..1 + (sizeof(a)-2)/2] * " " }); +  header->b = ({ a[2 + (sizeof(a)-2)/2..] * " " }); +  } +  } +  // Perform .distignore filtering. +  string path = header->b[0]; +  foreach(distignore_prefixes, string prefix) { +  if (has_prefix(path, prefix)) { +  path = UNDEFINED; +  break; +  } +  } +  // Filter git and distmaker control files as well. +  // There are also some files in Pike that don't get installed. +  if (!path || has_prefix(path, ".git") || has_value(path, "/.git") || +  has_suffix(path, ".distignore") || +  has_suffix(path, "/testsuite.in")) { +  break; +  } +  // Perform any additional path remapping. +  int remapped; +  foreach(remapping_rules, array(string) rule) { +  if (has_prefix(header->a[0], rule[0])) { +  header->a = ({ rule[1] + header->a[0][sizeof(rule[0])..] }); +  remapped = 1; +  } +  if (has_prefix(header->b[0], rule[0])) { +  header->b = ({ rule[1] + header->b[0][sizeof(rule[0])..] }); +  remapped = 1; +  } +  } +  if (remapped) { +  string old_line = line; +  lines[lineno] = line = +  sprintf("diff --git %s %s", +  has_value(header->a[0], " ")? +  sprintf("%q", header->a[0]):header->a[0], +  has_value(header->b[0], " ")? +  sprintf("%q", header->b[0]):header->b[0]); +  block = lines * "\n"; +  write("Remapping\n" +  " %s\n" +  "to\n" +  " %s\n", +  old_line, line); +  } +  if (has_prefix(path, "pike/lib/") && +  !has_value(cmd, "--depends=roxenpatch/pike-support")) { +  // Support for patching pike files required. +  cmd += ({ "--depends=roxenpatch/pike-support" }); +  } +  continue; +  +  case "---": +  case "+++": +  // Perform any additional path remapping. +  foreach(remapping_rules, array(string) rule) { +  if (has_prefix(a[1], rule[0])) { +  write("Remapping path %s to %s.\n", +  a[1], rule[1] + a[1][sizeof(rule[0])..]); +  a[1] = rule[1] + a[1][sizeof(rule[0])..]; +  lines[lineno] = line = sprintf("%s %s", a[0], a[1]); +  block = lines * "\n"; +  } +  } +  header[a[0]] = a[1..]; +  if (a[0] == "---") continue; +  +  if ((header["---"][0] != "/dev/null") && (a[1] != "/dev/null")) { +  // Not a new or deleted file, and not binary. +  diff += block; +  modified += ({ a[1] }); +  if (has_value(block, "$Id: ")) { +  check_ident(block, a[1], head_sha1); +  } +  break; +  } +  // FALL_THROUGH +  case "Binary": +  string new_blob_sha1 = (header->index[0]/"..")[1]; +  if (new_blob_sha1 == GIT_NULL_SHA1) { +  // Deleted file. +  cmd += ({ +  "--delete=" + header->b[0], +  }); +  deleted += ({ header->b[0] }); +  break; +  } +  string old_blob_sha1 = (header->index[0]/"..")[0]; +  string blob = git("cat-file", "blob", new_blob_sha1); +  // FIXME: We assume that the checked out .gitattributes is correct +  // with respect to this file. +  array(string) ident_info = +  git("check-attr", "ident", "--", header->b[0])/": ident: "; +  if (ident_info[-1] == "set\n") { +  // NB: Use string concat to avoid erroneous self-replacement here. +  blob = replace(blob, "$""Id""$", +  "$""Id: " + new_blob_sha1 + " $"); +  // NB: We also assume that the same blob will not appear as new +  // for several files in the same diff, but with different +  // ident expansion state or mode. +  } +  Stdio.write_file(tmpdir + "/" + new_blob_sha1 + ".blob", blob); +  +  // Note: We need to reverse the prefixes so that +  // we get a repository relative path. +  string repo_path = +  subtree_prefix + header->b[0][sizeof(path_prefix)..]; +  +  int blob_mode = 0644; +  sscanf(git("ls-tree", head_sha1, "--", repo_path), +  "%o %*s", blob_mode); +  if (blob_mode & 0111) { +  blob_mode = 0755; +  } else { +  blob_mode = 0644; +  } +  chmod(tmpdir + "/" + new_blob_sha1 + ".blob", blob_mode); +  +  if (old_blob_sha1 == GIT_NULL_SHA1) { +  // New file. +  cmd += ({ +  "--new-file=" + tmpdir + "/" + new_blob_sha1 + ".blob" +  ":" + header->b[0], +  }); +  added += ({ header->b[0] }); +  } else { +  // Altered file. +  cmd += ({ +  "--replace=" + tmpdir + "/" + new_blob_sha1 + ".blob" +  ":" + header->b[0], +  }); +  modified += ({ header->b[0] }); +  } +  break; +  +  default: +  if (header[a[0]]) { +  werror("Multiple %s headers not supported.\n" +  "%O\n", a[0], block); +  exit(1); +  } +  header[a[0]] = a[1..]; +  continue; +  } +  break; +  } +  } +  +  if (sizeof(diff)) { +  Stdio.write_file(tmpdir + "/diff.patch", diff); +  +  cmd += ({ +  "--patch=" + tmpdir + "/diff.patch", +  }); +  } +  +  Process.Process rxnpatch = Process.Process(cmd); +  int code; +  if ((code = rxnpatch->wait())) { +  exit(code); +  } +  string patchdata = Stdio.read_bytes(tmpdir + "/" + patchid + ".rxp"); +  if (!patchdata) { +  werror("Resulting patch file (%O) not found.\n", +  tmpdir + "/" + patchid + ".rxp"); +  exit(1); +  } +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/patches/" + +  patchid + "/" + patchid + ".rxp", +  patchdata, +  "Created " + patchid + ".rxp on branch " + branch + ".\n\n" + +  metadata); +  +  string affected_files = ""; +  if (sizeof(added)) { +  affected_files += sprintf("Added files:\n" +  "%{ %s\n%}", +  added); +  } +  if (sizeof(modified)) { +  affected_files += sprintf("Modified files:\n" +  "%{ %s\n%}", +  modified); +  } +  if (sizeof(deleted)) { +  affected_files += sprintf("Deleted files:\n" +  "%{ %s\n%}", +  deleted); +  } +  git_save_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/patches/" + +  patchid + "/affected-files.txt", +  affected_files, +  "Updated affected-files.txt for patch " + patchid +".\n"); + } +  + void delete_patch(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (!init(options)) { +  werror("rxnpatch not initialized on this branch.\n"); +  exit(1); +  } +  +  string patchid; +  foreach(depends, string dep) { +  if (!has_value(dep, "/")) { +  patchid = dep; +  break; +  } +  } +  +  if (!patchid) { +  werror("No patches to delete on this branch.\n"); +  exit(1); +  } +  +  if (sizeof(argv) != 2) { +  werror("Missing argument: patchid (expected: %s).\n", patchid); +  exit(1); +  } +  if (argv[1] != patchid) { +  werror("Patch %s is not the latest patch on this branch.\n" +  "Latest patch id is %s\n", +  argv[1], patchid); +  exit(1); +  } +  +  write("Deleting patch: %s...\n", patchid); +  +  // Find previous patch. +  +  string metadata = +  git_cat_file("refs/heads/rxnpatch/rxnpatch", +  "refs/heads/" + branch + "/patches/" + patchid + "/metadata.txt"); +  +  array(string) a = metadata/"\n\n"; +  if (sizeof(a) < 2) { +  werror("Missing patch description for patch %s.\n", patchid); +  exit(1); +  } +  +  mapping(string:array(string)) header = ([]); +  foreach(a[0]/"\n", string line) { +  line = (line/"#")[0]; +  if (line == "") continue; +  array(string) pair = line/":"; +  if (sizeof(pair) < 2) { +  werror("Invalid metadata syntax:\n" +  "%s\n", line); +  exit(1); +  } +  header[lower_case(String.trim_all_whites(pair[0]))] += +  ({ String.trim_all_whites(pair[1..] * ":") }); +  } +  +  string tail_sha1; +  if (header->from && (sizeof(header->from) == 1)) { +  tail_sha1 = header->from[0]; +  } else { +  werror("Invalid header field %O.\n", "from"); +  } +  +  assert_first_parent(branch, tail_sha1); +  +  // Reset the patch head for the branch. +  +  git("branch", "-f", "rxnpatch/" + branch, tail_sha1); +  git_push("origin", "+rxnpatch/" + branch, 1); +  +  // Clear the patch tag. +  +  git("tag", "-d", "rxnpatch/" + patchid); +  git_push("origin", ":refs/tags/rxnpatch/" + patchid); +  +  // Clear the patch directory for the patch. +  string patch_dir = "refs/heads/" + branch + "/patches/" + patchid; +  array(string) files = ({}); +  foreach(git("ls-tree", "-r", "-z", "--full-tree", "rxnpatch/rxnpatch", +  "--", patch_dir)/"\0", string entry) { +  if (entry == "") continue; +  string path = (entry/"\t")[1..] * "\t"; +  if (!has_prefix(path, patch_dir)) continue; +  files += ({ path }); +  } +  +  git_delete_files("rxnpatch/rxnpatch", files, +  "Deleted patch " + patchid + ".\n"); + } +  + class FileBuf(String.Buffer buf) + { +  int write(string data) +  { +  buf->add(data); +  return sizeof(data); +  } +  +  int close() +  { +  return 1; +  } + } +  + string low_create_cluster(string cluster, mapping(string:string) patches) + { +  // Build the tar archive, and compress it incrementally. +  String.Buffer buf = String.Buffer(); +  Gz.File gzfile = Gz.File(FileBuf(buf), "wb"); +  +  string cluster_dir = (cluster_path_prefix[..<1]/"/")[-1] + "/"; +  +  foreach(sort(indices(patches)), string patch) { +  string blob_sha1 = patches[patch]; +  +  string blob = git("cat-file", "blob", blob_sha1); +  +  string patch_name = (patch/"/")[-1]; +  +  if (sizeof(cluster_dir) + sizeof(patch_name) >= 100) { +  // This is unlikely, but better safe than sorry... +  werror("Too long filename (%d characters) to be added to cluster:\n" +  "\t%O.\n", +  sizeof(cluster_dir) + sizeof(patch_name), +  cluster_dir + patch_name); +  exit(1); +  } +  +  write(" %s (%d bytes)\n", patch_name, sizeof(blob)); +  +  int mtime = Calendar.ISO.parse("%Y-%M-%DT%t", patch_name)->unix_time(); +  +  string name_pad = +  "\0" * (100 - (sizeof(cluster_dir) + sizeof(patch_name))); +  +  // NB: We use the ancient-style tar header here for +  // maximum compatibility. +  // +  // cf FreeBSD tar(5) compat notes: +  // Early tar implementations varied in how they terminated +  // these fields. The tar command in Version 7 AT&T UNIX used +  // the following conventions (this is also documented in early +  // BSD manpages): the pathname must be null-terminated; the +  // mode, uid, and gid fields must end in a space and a null +  // byte; the size and mtime fields must end in a space; the +  // checksum is terminated by a null and a space. +  // +  // Early implementations filled the numeric fields with +  // leading spaces. This seems to have been common practice +  // until the IEEE Std 1003.1-1988 (``POSIX.1'') standard was +  // released. For best portability, modern implementations +  // should fill the numeric fields with leading zeros. +  +  string header = sprintf("%100s%06o \0%06o \0%06o \0%011o %011o ", +  cluster_dir + patch_name + name_pad, +  0644, 0 /* uid */, 0 /* gid */, +  sizeof(blob), mtime); +  +  // NB: The max checksum for an ancient-style header is +  // less than 148*128 + 8*32 == 19200 == 045400, and +  // thus fits into the 6 digit octal field below. +  int csum = `+(@((array(int))header), ' '*8); +  string check = sprintf("%06o\0 ", csum); +  +  string pad = "\0" * (512 - (sizeof(header) + sizeof(check))); +  +  gzfile->write(header); +  gzfile->write(check); +  gzfile->write(pad); +  +  gzfile->write(blob); +  if (sizeof(blob) & 511) { +  gzfile->write("\0"*(512 - (sizeof(blob) & 511))); +  } +  } +  // Add tar EOF marker. +  gzfile->write("\0" * (512 * 2)); +  gzfile->close(); +  +  return buf->get(); + } +  + void create_cluster(mapping(string:string|int|array(string)) options, +  array(string) argv) + { +  if (!init(options)) { +  werror("rxnpatch not initialized on this branch.\n"); +  exit(1); +  } +  +  mapping(string:string) patches = ([]); +  string prefix = "refs/heads/" + branch + "/patches"; +  foreach(git("ls-tree", "-r", "-z", "--full-tree", "rxnpatch/rxnpatch", +  "--", prefix)/"\0", string entry) { +  // FIXME: Detect patches that haven't been made (ie lack *.rxp)? +  if (!has_suffix(entry, ".rxp")) continue; +  array(string) splitted = entry/"\t"; +  if (sizeof(splitted) < 2) continue; +  string blob = (splitted[0]/" ")[2]; +  string fname = splitted[1..]*"\t"; +  patches[fname] = blob; +  } +  +  string cluster; +  if (!options->force) { +  if (!sizeof(patches)) { +  write("No patches available to cluster.\n"); +  exit(0); +  } +  +  int pending_cluster = sizeof(patches); +  // Check if there's an up-to-date patch cluster for the branch. +  string cluster_prefix = "refs/heads/" + branch + "/clusters"; +  foreach(git("ls-tree", "-r", "-z", "--full-tree", "rxnpatch/rxnpatch", +  "--", cluster_prefix)/"\0", string entry) { +  if (entry == "") continue; +  string path = (entry/"\t")[1..] * "\t"; +  if (!has_prefix(path, cluster_path_prefix)) continue; +  if (!has_suffix(path, ".tgz")) continue; +  // NB: The suffix ".tgz" is sorted after "-02.tgz", +  // so we need to compensate here. +  string new_cluster = path[sizeof(cluster_path_prefix)..<4]; +  if (!cluster || (cluster < new_cluster)) cluster = new_cluster; +  } +  if (cluster) { +  // There's apparently a cluster. +  string base = git_try("rev-list", "--max-count=1", +  "refs/tags/rxnpatch/clusters/" + branch + +  "/" + cluster); +  if (base) { +  // Check if any patches have been added or changed for the +  // branch since then. +  base = (base/"\n")[0]; +  pending_cluster = 0; +  foreach(git("diff-tree", "-r", "-z", "--name-only", +  base, "rxnpatch/rxnpatch")/"\0", string fname) { +  if (!has_prefix(fname, "refs/heads/" + branch + "/patches/")) +  continue; +  if (!has_suffix(fname, ".rxp")) continue; +  pending_cluster++; +  } +  } else { +  werror("Warning: Tag not found for cluster %s.\n", cluster); +  pending_cluster = sizeof(patches); +  } +  +  if (!pending_cluster) { +  write("Cluster %s is up-to-date.\n", cluster); +  exit(0); +  } +  } +  } +  +  // Release dependency prefix. cf load_deps() above. +  string rel_dep_prefix; +  if (release_tag_prefix) { +  rel_dep_prefix = replace(release_tag_prefix[sizeof("refs/tags/")..], +  ({ "-", "_" }), ({ "/", "/" })); +  if (rel_dep_prefix == "v") rel_dep_prefix = "pike/"; +  else if (!has_suffix(rel_dep_prefix, "/")) rel_dep_prefix += "/"; +  } +  +  // Get the patchid of the most recent patch in the cluster. +  string timestamp_base = (sort(indices(patches))[-1]/"/")[-2]; +  +  // Find the next available timestamp and tag. +  string timestamp = timestamp_base; +  string tag; +  while(1) { +  tag = "rxnpatch/clusters/" + branch + "/" + timestamp; +  if (!git_try("rev-list", "--max-count=1", tag)) break; +  +  // Bump the timestamp by a second. +  // NB: This code may generate seconds 60 or larger, but +  // we don't care as we just want a unique identifier. +  array(string) a = timestamp/"T"; +  if (sizeof(a[1]) < 6) { +  // Most recent patch is from the time before seconds in patchids. +  // Pad the time field to 6 digits. +  a[1] = (a[1] + "000000")[..5]; +  } else { +  a[1] = sprintf("%06d", ((int)a[1]) + 1); +  } +  timestamp = a * "T"; +  } +  +  cluster = sprintf("%s%s.tgz", cluster_path_prefix, timestamp); +  +  // Create release-specific clusters... +  string release; +  mapping(string:string) release_patches = ([]); +  foreach(({ UNDEFINED }) + reverse(sort(indices(patches))), string file) { +  mapping(string:array(string)) header; +  if (file) { +  string patchid = (file/"/")[-2]; +  string metadata = git_cat_file("rxnpatch/rxnpatch", +  combine_path(file, "../metadata.txt"), 1); +  if (metadata) { +  [header, string description] = parse_metadata(patchid, metadata); +  } +  release_patches[file] = patches[file]; +  } else { +  // Start sentinel. +  // NB: This is used to ensure that a generic cluster +  // is generated even if there are no patches for +  // the current release. Cf [IS-26]. +  header = ([ "depends" : ({ depends * "|" }) ]); +  } +  if (header) { +  find_dep: +  foreach(header->depends || ({}), string dep) { +  foreach(dep/"|", string d) { +  if (has_prefix(d, rel_dep_prefix)) { +  // Found. +  string next_release = d[sizeof(rel_dep_prefix)..]; +  // NB: next_release and release may be equal when there +  // are derived patches. +  if (next_release != release) { +  if (release) { +  cluster = sprintf("%s%s-%s.tgz", +  release_cluster_path_prefix, +  next_release, release); +  } +  write("Creating cluster %s containing %d patch%s...\n", +  cluster, sizeof(release_patches), +  (sizeof(release_patches) != 1)?"es":""); +  string tgz = low_create_cluster(cluster, release_patches); +  git_save_file("refs/heads/rxnpatch/rxnpatch", cluster, tgz, +  "Created patch cluster " + (cluster/"/")[-1] + +  " on branch " + branch + ".\n"); +  release_patches = ([]); +  release = next_release; +  } +  // NB: We can break here since we know that there can only +  // be one release tag at the parent commit due to a +  // new commit being generated every time the version +  // is bumped. +  break find_dep; +  } +  } +  } +  } +  } +  +  git("tag", tag, "refs/heads/rxnpatch/rxnpatch"); +  git_push("origin", "refs/tags/" + tag); + } +    Newline at end of file added.