{"thread":{"id":"25227","subject":"git-show-merge-path v1.0","startedAt":"2010-09-24T19:03:54Z","lastAt":"2010-09-24T19:03:54Z","messageCount":1,"participants":["Artur Skawina"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"151527","messageId":"4C9CF61A.6060300@gmail.com","threadId":"25227","inReplyTo":null,"subject":"git-show-merge-path v1.0","fromName":"Artur Skawina","fromEmail":"art.08.09@gmail.com","sentAt":"2010-09-24T19:03:54Z","receivedAt":"2010-09-24T19:03:54Z","isPatch":false,"sender":{"key":"art.08.09@gmail.com","avatar":null},"body":"This started as a quick graph walking hack, but evolved into something that\ncan actually be useful.\n\ngit-show-merge-path can tell /if/, /how/ and /when/ a change became visible\nfrom a certain branch or tag. Or a few hundred thereof.\n\n\"git-show-merge-path <commit> <targets>\".\n\n$ git-show-merge-path <commit> heads    # checks all local branches\n$ git-show-merge-path <commit> tags     # checks all tags\n$ git-show-merge-path <commit> heads/as # checks all local branches named 'as/...'\n$ git-show-merge-path <commit> remotes/name\n                                        # checks all refs of the named remote\n$ git-show-merge-path <commit> remotes  # checks all refs of all remotes\n$ git-show-merge-path <commit> heads/master\n                                        # checks the local 'master' branch\n$ git-show-merge-path <commit> master   # checks all 'master' branches\n                                        # (incl. remotes etc)\n$ git-show-merge-path <commit> 'doh*'   # checks all branches named 'doh...'\n$ git-show-merge-path <commit> 'tags/v2.*' # checks all tags beginning w/ \"v2.\"\n$ git-show-merge-path <commit> '*'      # checks every reference it finds\n$ git-show-merge-path <commit> refs/heads/master # for nonhumans, or situations\n                                        # where the DWIM approach fails.\n\nEg inside a git.git clone \"git-show-merge-path 829ef38 next\" will say\nhow 829ef38 merged into 'origin/next' and \"git-show-merge-path 829ef38 origin\"\nwill report the status vs all the branches in that repo;\n\n$ git-show-merge-path 829ef38 origin\n12f7559e0634 M: 'dm/mergetool-vimdiff' => next                        100922 16:36\n          \\_ Merged into next and pu [v1.7.3-60-g12f7559]\n             Not reachable from HEAD, html, maint, man, master and todo\n\nChecking which release included some change could look like this:\n\n$ git-show-merge-path 657b6245b tags\n44f9e6c6bc50 M: 'nouveau/for-airlied' => drm-linus                    091223 00:28\nf42ecb2808db M: 'drm-linus'@$KO/airlied/drm-2.6                       091223 16:59\n          \\_ Merged into v2.6.33, v2.6.33-rc2, v2.6.33-rc3, v2.6.33-rc4, v2.6.33-rc5,\n v2.6.33-rc6, v2.6.33-rc7, v2.6.33-rc8, v2.6.33.1, v2.6.33.2, v2.6.34, v2.6.34-rc1,\n v2.6.34-rc2, v2.6.34-rc3, v2.6.34-rc4, v2.6.34-rc5, v2.6.34-rc6, v2.6.34-rc7, v2.6.34.1,\n v2.6.35, v2.6.35-rc1, v2.6.35-rc2, v2.6.35-rc3, v2.6.35-rc4, v2.6.35-rc5, v2.6.35-rc6,\n v2.6.35.1, v2.6.35.2, v2.6.35.3, v2.6.36-rc1 and v2.6.36-rc2 [v2.6.33-rc1-266-gf42ecb2]\n[followed by some uninteresting merges, and a long list of tagged releases which\n do not contain this commit]\n\nNote that the ref names it prints are simplified; if unsure, do not\nrely on the DWIM target selection, just give it a full \"refs/...\" name.\n\nartur\n\n----------------------------------------------------------------------\n#! /usr/bin/env pike\n// git-show-merge-path <rev> [long-lived-branch(es)]\n// v. 1.0\n// Will show all external merge commits starting at <rev> until\n// this commit appears on the specified branches. When that happens\n// \"Merged into <branchlist>\" is printed. If <rev> is still\n// unreachable from some of the branches then the search continues.\n// If at least one of the branches does not contain <rev> then $0\n// can and will print *all* merges (ie it won't stop at the last\n// of the given branches containing this commit), followed by \n// \"Not reachable from <branchlist>\". This is a feature (can be\n// used to find leaks outside of the given branches).\n//\n#define die(a...) exit(1, \"Aborting; %s\"+a)\n\nstatic mapping commits = ([]);\n\narray parsecommits(string ... delim) {\n   array res = ({});\n   string id;\n   array lines = run(\"git\", \"rev-list\", \"--format=raw\", \"--ancestry-path\",\n                                        \"--date-order\", @delim)/\"\\n\";\n   foreach (lines, string line) {\n      array words = line/\" \";\n      string h = words[0];\n      if (h==\"commit\") {\n         id = words[1];\n         if (!commits[id])\n            commits[id] = ([]);\n         res += ({id});\n         if (mapping bs = livebranches[id])\n            commits[id][\"Branch\"] += bs;\n      } else if (h==\"\") {\n         if (commits[id])\n            commits[id][\"\"] += ({line});\n      }\n      else {\n         if (h==\"parent\" && !commits[id][\"parent\"] && commits[id][\"Branch\"]) {\n            string firstparent = words[1];\n            if (!commits[firstparent])\n               commits[firstparent] = ([ \"Branch\" : commits[id][\"Branch\"] ]);\n            else\n               commits[firstparent][\"Branch\"] += commits[id][\"Branch\"];\n         }\n         commits[id][h] += words[1..];\n      }\n   }\n   return res;\n}\n\nstatic mapping desc = ([]);\n\nstatic mapping livebranches = ([]); // id : mapping(name:id)\nstatic mapping branchnames = ([]);  // name : id\n\nint main(int argc, array argv) {\n   argv[1] = (run(\"git\", \"rev-parse\", argv[1])/\"\\n\")[0];\n   if (argc==2)\n      argv += ({\"master\"});\n   branchnames = git_refs(argv[2..]);\n   if (sizeof(branchnames)==0)\n      die(\"refs not found:%{ \\\"%s\\\"%}\\n\", \"\", argv[2..]);\n   foreach (branchnames; string b; string v)\n      livebranches[v] += ([b:v]);\n   array commit_list = parsecommits(\"^\"+argv[1], @indices(livebranches));\n   commit_list = reverse(commit_list);\n   desc[argv[1]] = 1;\n   foreach (commit_list, string id) {\n      if (commits[id][\"parent\"]) {\n         foreach (commits[id][\"parent\"], string parent)\n            if (desc[parent])\n                desc[id] = 1;\n         if (sizeof(commits[id][\"parent\"])>1)\n            if (!desc[commits[id][\"parent\"][0]]) {\n               int comtime = (int)commits[id][\"committer\"][-2];\n               write(\"%.12s %-56.56s %.12s\\n\", id,\n                      squeeze_subject(commits[id][\"\"][1]),\n                      cal->Second(comtime)->format_time_xshort());\n            }\n         if (mapping reached = commits[id][\"Branch\"]) {\n            reached = reached&branchnames;\n            if (sizeof(reached)>0) {\n               branchnames -= reached;\n\t       array refs = Array.sort_array(indices(reached));\n               write(\"          \\\\_ Merged into %s [%s]\\n\",\n\t               String.implode_nicely(refs),\n\t\t       git_describe(id) );\n               if (sizeof(branchnames)==0)\n                  exit(0);\n            }\n         }\n      }\n      m_delete(commits, id);\n   }\n   array refs = Array.sort_array(indices(branchnames));\n   write(\"             Not reachable from %s\\n\", String.implode_nicely(refs));\n}\n\n// This can slow us down almost twice; open coding it\n// into the history walk would be possible, but i'm\n// not doing that for only a few 100ms gain (total)...\nstring git_describe(string id) {\n   return (run(\"git\", \"describe\", id)/\"\\n\")[0];\n}\n\n// Given glob pattern(s) (\"m?st*r\") return a mapping of\n// all matching existing refs (symbolic:dereferenced_id)\nmapping git_refs(array patterns) {\n   mapping res = ([]);\n   array tags = ({});\n\n   foreach (patterns; int i; string pattern)\n      if (pattern[0..4]!=\"refs/\")\n         patterns[i] = \"*/\"+pattern;\n      \n   foreach (run(\"git\", \"show-ref\", \"-d\")/\"\\n\", string line) {\n      array words = line/\" \";\n      \n      if (sizeof(words)<2)\n         break;\n      foreach (patterns, string pattern)\n         if (glob(pattern, words[1]) || glob(pattern+\"/*\", words[1])) {\n            if (words[1][0..9]!=\"refs/tags/\")\n               res += ([ words[1] : words[0] ]);\n            else\n               tags += ({words[1]});\n            break;\n         }\n   }\n   \n   if (sizeof(tags)) {\n      foreach (run(\"git\", \"show-ref\", \"-d\", @tags)/\"\\n\", string line)\n         if (line[sizeof(line)-3..]==\"^{}\") {\n            array words = line/\" \";\n            res += ([ words[1][..sizeof(words[1])-4] : words[0] ]);\n         }\n   }\n   \n   string prefix = String.common_prefix(indices(res));\n   if (prefix!=\"\") {\n      int preflen = sizeof(prefix);\n      while (preflen && prefix[preflen-1]!='/')\n         preflen--;\n      foreach (res; string in; string val)\n         res[in[preflen..]] = m_delete(res, in);\n   }\n   return res;\n}\n\nstring squeeze_subject(string subject) {\n\n   subject = String.trim_all_whites(subject);\n   subject = String.expand_tabs(subject);\n   foreach (sub_from_to, mapping m)\n      subject = replace(subject, m);\n   return subject;\n}\n\nstatic array(mapping) sub_from_to =\n({\n   ([ \n      \"Merge branch \" : \"Merge \",\n      \"Merge remote branch \" : \"Merge \",\n      \"Merge branches \" : \"MM:\",\n   ]),\n   ([ \n      \"Merge \" : \"M: \",\n      \"' of git:\": \"'@git:\",\n      \"' into \": \"' => \",\n   ]),\n   ([ \n       \"git://git.kernel.org/pub/scm/linux/kernel/git/\" : \"$KO/\",\n       \"commit '\" : \"C'\"\n   ]),\n});\n\nstring run(string ... cmdline) {\n#if __REAL_MAJOR__<7 || __REAL_MAJOR__==7 && __REAL_MINOR__<8\n   string s = Process.popen(cmdline*\" \");\n   if (s==\"\")\n      die(\"\\n\", cmdline*\" \");\n   return s;\n#else\n   mapping r;\n   mixed e = catch { r = Process.run( ({@cmdline}) ); };\n   if (e || r[\"exitcode\"])\n      die(\"\", e?e:r[\"stderr\"]);\n   return r[\"stdout\"];\n#endif\n}\n\nstatic object cal = Calendar.ISO.set_timezone(Calendar.Timezone.UTC);\n----------------------------------------------------------------------\n"}]}