{"thread":{"id":"25247","subject":"git-show-merge-path v2.0","startedAt":"2010-09-25T21:03:30Z","lastAt":"2010-09-25T21:03:30Z","messageCount":1,"participants":["Artur Skawina"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"151691","messageId":"4C9E63A2.9040204@gmail.com","threadId":"25247","inReplyTo":null,"subject":"git-show-merge-path v2.0","fromName":"Artur Skawina","fromEmail":"art.08.09@gmail.com","sentAt":"2010-09-25T21:03:30Z","receivedAt":"2010-09-25T21:03:30Z","isPatch":false,"sender":{"key":"art.08.09@gmail.com","avatar":null},"body":"I thought i was done with this thing, but decided to add the experimental\nfastforward detection, and while doing that also fixed a lot of corner\ncase bugs, which didn't appear when testing on real repos. Like breaking\nw/ used in repos that have no annotated tags or reporting that a commit is \nunreachable, when in fact the branch head points at it.\n\nThe one new experimental feature is the FF detection.\n\nIt received little testing and is off by default.\nFor the artificial \"fake\" repos, it puts every single commit on the\ncorrect branch. In practice i guess it depends on how creative people\nget and if there's some, preferably hook-enforced, policy in place.\n\nIs it useful? Well, I'm not sure yet, here are some examples:\n[s/origin/heads/ if that's where your master/next/maint etc lives]\n[all of these cmds show just one merge if ran w/o '-g']\n\n$ git-show-merge-path 442b3caaee origin \nf4198c9b7d31 M: 'master'@git://repo.or.cz/git-gui                   080316 06:07\n          \\_ Appears in HEAD, maint, master, next and pu [v1.5.4.4-616-gf4198c9]\n             Not reachable from html, man and todo\n\n$ git-show-merge-path 442b3caaee origin -g\n442b3caaee6d                                                        \n          \\_ Appears in master [gitgui-0.9.3-33-g442b3ca]\nf4198c9b7d31 M: 'master'@git://repo.or.cz/git-gui                   080316 06:07\n          \\_ Appears in HEAD, next and pu [v1.5.4.4-616-gf4198c9]\n319a36a5c2da M: 'maint'                                             080327 20:35\n          \\_ Appears in maint [v1.5.5-rc1-21-g319a36a]\n             Not reachable from html, man and todo\n\n$ git-show-merge-path 1b6ecbad3511 origin -g\n1b6ecbad3511                                                        \n          \\_ Appears in maint [v1.7.2.3-8-g1b6ecba]\n8ac8cf5bc17e M: 'maint'                                             100910 00:29\n          \\_ Appears in HEAD, master, next and pu [v1.7.3-rc0-35-g8ac8cf5]\n             Not reachable from html, man and todo\n\t     \n$ git-show-merge-path 4ce6fb805803 origin -g\n9f44723d1a2c M: 'en/d-f-conflict-fix'                               100908 15:54\n          \\_ Appears in HEAD, master, next and pu [v1.7.3-rc0-8-g9f44723]\n5879b6bbcaba M: 'maint'                                             100912 20:53\n          \\_ Appears in maint [v1.7.3-rc1-4-g5879b6b]\n             Not reachable from html, man and todo\n\n\nIt can easily get confused, and i didn't even bother to tweak the heuristic\nto fix specific cases (like 'master' being somewhat special).\nThat's why this is off by default and needs to be explicitly turned on (\"-g\").\nAlways run the script w/o '-g' and only turn it on to get a more detailed, but\npossibly misleading view later.\n\n\nThen, there's the hidden easter egg enabled by '-g2'. Added while writing this\nemail, so you can imagine just how much testing it has received...\nThis mode works best when you request only refs that are all reachable from\nthe commit in question, as this will prevent uninteresting merges (past the\nreal merge point) from being shown. (IOW pick one of the refs printed after\nthe last merge point shown w/o '-g2')\n\n$ git-show-merge-path 1b6ecbad3511 origin/master -g2\n1b6ecbad3511                                                        \n          \\_ Appears in $dr/maint-ls-tree-prefix-recursion-fix and $maint [v1.7.2.3-8-g1b6ecba]\n8ac8cf5bc17e M: 'maint'                                             100910 00:29\n          \\_ Appears in master [v1.7.3-rc0-35-g8ac8cf5]\n\n\n$ git-show-merge-path d5675bd204e heads/master    \n8bd39456bd5a M: 'vhost-net'@$KO/mst/vhost                           100703 05:29\n597e608a8492 M: 'master' of $KO/davem/net-2.6                       100707 22:59\n2aa72f612144 M: $KO/davem/net-2.6                                   100708 02:56\n          \\_ Appears in master [v2.6.35-rc4-86-g2aa72f6]\n\n$ git-show-merge-path d5675bd204e heads/master -g2\nd5675bd204ef                                                        \n          \\_ Appears in $vhost-net [v2.6.35-rc1-140-gd5675bd]\n8bd39456bd5a M: 'vhost-net'@$KO/mst/vhost                           100703 05:29\n          \\_ Appears in $davem/net-2.6 and $master [v2.6.35-rc1-182-g8bd3945]\n597e608a8492 M: 'master' of $KO/davem/net-2.6                       100707 22:59\n          \\_ Appears in $davem/net-next-2.6, $for-davem and $vhost-net-next [v2.6.35-rc1-1020-g597e608]\n2aa72f612144 M: $KO/davem/net-2.6                                   100708 02:56\n          \\_ Appears in master [v2.6.35-rc4-86-g2aa72f6]\n\nSo it can sometimes be used to easily dig out a little bit more info,\nw/o having to look at the rest of the history.\n\nAmazingly enough, this thing can still be almost twice as fast as the\nequivalent \"git tag --contains\". And I thought git was fast. :^)\n\nartur\n---------------------------------------------------------------------------------\n#! /usr/bin/env pike\n// git-show-merge-path <rev> [refs-glob ...]\n// v2.0\n// Will show all external merge commits starting at <rev> until\n// this commit appears on the specified branches. When that happens\n// \"Appears in <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\n#define ismerge(c) (sizeof( (c)[\"parent\"] )>1)\n\nvoid pp_commit(string id) {\n   mapping c = commits[id];\n   \n   if (!options[\"guessff\"] || !c )\n      return;\n      \n   if (sizeof(c[\"parent\"])==2) {\n      string b0, b1;\n      \n      foreach (mergesub, string form)  \n\t if (sscanf(c[\"\"][1], form, b0, b1)==3)\n            break;\n      if (!b0 || !b1) {\n         foreach (mmergesub, string form)  \n            if (sscanf(c[\"\"][1], form, b0)==2)\n\t       break;\n\t b1 = \"master\";\n      }\n\n      if (b0 && b1) {\n         if ((int)options[\"guessff\"]>=2) {\n\t    c[\"Branch\"] = ([\"$\"+b0:0, \"$\"+b1:0]) + (c[\"Branch\"]?c[\"Branch\"]:([]));\n\t    commits[ c[\"parent\"][0] ][\"Branch\"] = \n\t        ([\"$\"+b0:0, \"$\"+b1:0]) + (commits[ c[\"parent\"][0] ][\"Branch\"]||([]));\n\t    extrabranches = ([\"$\"+b0:0, \"$\"+b1:0]) + extrabranches;\n\t    \n\t    mapping bm = ([\"$\"+b0:0])&c[\"Branch\"];\n\n\t    if (sizeof(bm) && sizeof(([\"$\"+b1:0])&c[\"Branch\"])) {\n\t       commits[ c[\"parent\"][0] ][\"Branch\"] -= bm;\n\n\t       if (!commits[ c[\"parent\"][1] ])\n        \t  commits[ c[\"parent\"][1] ] = ([ \"Branch\" : bm ]);\n\t       else\n        \t  commits[ c[\"parent\"][1] ][\"Branch\"] += bm;\n\t    }\n\t }\n\t \n         if (!c[\"Branch\"])\n\t    return;\n\t    \n\t mapping bm = ([b0:0])&c[\"Branch\"];\n\t \n\t if (sizeof(bm) && sizeof(([b1:0])&c[\"Branch\"])) {\n\t    if (options[\"verbose\"])\n               werror(\" # Undoing FF @ %.12s %s\\n\", id, squeeze_subject(c[\"\"][1]));\n\t       \n\t    commits[ c[\"parent\"][0] ][\"Branch\"] -= bm;\n\t    \n\t    if (!commits[ c[\"parent\"][1] ])\n               commits[ c[\"parent\"][1] ] = ([ \"Branch\" : bm ]);\n\t    else\n               commits[ c[\"parent\"][1] ][\"Branch\"] += bm;\n\t }\n      }\n      else\n         if (options[\"verbose\"])\n            werror(\" # Unsupported merge subject %O\\n\", c[\"\"][1]);\n   }\n}\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         pp_commit(id);\n\t \n         id = words[1];\n         if (!commits[id])\n            commits[id] = ([]);\n         res += ({id});\n      } else if (h==\"\") {\n         if (commits[id])\n            commits[id][\"\"] += ({line});\n      }\n      else {\n         if (h==\"parent\") {\n            string parent = words[1];\n            if (!commits[parent])\n\t       commits[parent] = ([]);\n\t    if (!commits[id][\"parent\"]) // first parent?\n\t       if (commits[id][\"Branch\"]) {\n\t          if (!commits[parent][\"Branch\"])\n\t\t     commits[parent][\"Branch\"] = ([]);\n        \t  commits[parent][\"Branch\"] += commits[id][\"Branch\"];\n\t       }\n\t }\n         commits[id][h] += words[1..];\n      }\n   }\n   pp_commit(id);\n   return res;\n}\n\nstatic mapping desc = ([]);\n\nstatic mapping branchnames = ([]);    // name : id\nstatic mapping extrabranches = ([]);  // name : id\n\nstatic mapping options = ([]);;\nstatic array option_array = ({\n   ({ \"guessff\", Getopt.MAY_HAVE_ARG, ({\"-g\"}) }),\n   ({ \"verbose\", Getopt.MAY_HAVE_ARG, ({\"-v\", \"--verbose\"}) }),\n});\n\nint main(int argc, array argv) {\n   array oa = Getopt.find_all_options(argv, option_array);\n   foreach (oa, array a)\n      options += ([a[0]:a[1]]);\n   argv = Getopt.get_args(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 id)\n      if (commits[id])\n         commits[id][\"Branch\"] += ([b:id]);\n      else\n         commits[id] = ([ \"Branch\" : ([b:id]) ]);\n   array commit_list = parsecommits(\"^\"+argv[1], @values(branchnames));\n   commit_list += ({argv[1]});\n   commit_list = reverse(commit_list);\n   desc[argv[1]] = 1;\n   foreach (commit_list, string id) {\n      mapping c = commits[id];\n      if (!c)\n         continue;\n      if (commits[id][\"parent\"]) {\n         foreach (commits[id][\"parent\"], string parent)\n            if (desc[parent])\n                desc[id] = 1;\n         if (ismerge(commits[id]))\n            if (!desc[commits[id][\"parent\"][0]])\n\t       printidline(id);\n      }\n      mapping br = commits[id][\"Branch\"];\n      if (br) {\n         mapping reached = br&(branchnames|extrabranches);\n         if (sizeof(reached)>0) {\n\t    array refs = Array.sort_array(indices(reached&branchnames));\n\t    if (sizeof(refs)) {\n\t       printidline(id);\n               flush(\"          \\\\_ Appears in %s [%s]\\n\",\n\t               String.implode_nicely(refs), git_describe(id) );\n\t    }\n            branchnames -= reached;\n            if (sizeof(branchnames)==0)\n               exit(0);\n\t    refs = Array.sort_array(indices(reached&extrabranches));\n\t    if (sizeof(refs)) {\n\t       printidline(id);\n               flush(\"          \\\\_ Appears in %s [%s]\\n\",\n\t               String.implode_nicely(refs), git_describe(id) );\n\t    }\n            extrabranches -= reached;\n         }\n      }\n      m_delete(commits, id);\n   }\n   array refs = Array.sort_array(indices(branchnames));\n   if (options[\"verbose\"] || sizeof(refs)<10)\n      write(\"             Not reachable from %s\\n\", String.implode_nicely(refs));\n   else\n      write(\"             Not reachable from %d refs (use -v option to show them all)\\n\",\n               sizeof(refs));\n}\n\nstatic array outlines = ({});\nstatic mapping shown = ([]);\n\nvoid printidline(string id) {\n  int comtime = commits[id][\"committer\"] && (int)commits[id][\"committer\"][-2];\n  string subj = \" \";\n  if (shown[id])\n     return;\n  shown[id] = 1;\n  if (commits[id][\"\"])\n     subj = commits[id][\"\"][1];\n  outlines += ({\n     sprintf(\"%.12s %-54.54s %.12s\\n\", id,\n         squeeze_subject(subj),\n         comtime?cal->Second(comtime)->format_time_xshort():\"\")\n      });\n}\nvoid flush(string fmt, string ... args) {\n   write(outlines*\"\");\n   write(fmt, @args);\n   outlines = ({});\n}\n\nstring git_describe(string id) {\n   string res = (tryrun(\"git\", \"describe\", id)/\"\\n\")[0];\n   if (res==\"\")\n      res = (tryrun(\"git\", \"describe\", \"--tags\", id)/\"\\n\")[0];\n   return res;\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\")/\"\\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   if (sizeof(tags)) {\n      foreach (run(\"git\", \"show-ref\", \"-d\", @tags)/\"\\n\", string line) {\n         if (line==\"\")\n\t    break;\n         array words = line/\" \";\n         if (line[sizeof(line)-3..]==\"^{}\")\n            res += ([ words[1][..sizeof(words[1])-4] : words[0] ]);\n\t else // Could be a lightweight tag.\n            if (!res[words[1]])\n               res += ([ words[1] : words[0] ]);\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   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       \"master.kernel.org:/pub/scm/linux/kernel/git/\" : \"$KO/\",\n       \"commit '\" : \"C'\"\n   ]),\n});\n\nstatic array mergesub =\n({\n   \"%*[ ]Merge branch '%s' into %s\",\n   \"%*[ ]Merge remote branch '%s' into %s\",\n   \n   \"%*[ ]Merge commit '%s' into %s\",      // Hmm.\n   \"%*[ ]Merge tag '%s' into %s\",         // Hmm^2.\n   \"%*[ ]Merge git://%s into %s\",\n   \"%*[ ]Merge branch %s into %s\",\n});\n\nstatic array mmergesub =\n({\n   \"%*[ ]Merge branch '%s'\",\n   \"%*[ ]Merge commit '%s'\",              // Hmm.\n   // project-specific\n   \"%*[ ]Merge git://git.kernel.org/pub/scm/linux/kernel/git/%s\",\n   // Scary? This is here for mostly historical reasons and really old merges:\n   \"%*[ ]Merge ssh://master.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge master.kernel.org:/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge master.kernel.org:/home/%s\",\n   \"%*[ ]Merge with /pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge with git+ssh://master.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge with ssh://master.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge with http://kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge with rsync://rsync.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge of rsync://rsync.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge rsync://rsync.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge with master.kernel.org:/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge of master.kernel.org:/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge of master.kernel.org:/home/%s\",\n   \"%*[ ]Automatic merge of rsync://rsync.kernel.org/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Automatic merge of master.kernel.org:/home/%s\",\n   \"%*[ ]Automatic merge of master.kernel.org:/pub/scm/linux/kernel/git/%s\",\n   \"%*[ ]Merge HEAD from master.kernel.org:/pub/scm/linux/kernel/git/%s\",\n   \n   // Too generic? Comment them out and run with \"-g -v\" to look\n   // for better candidates.\n   \"%*[ ]Merge git://%[^' ]\",\n   //\"%*[ ]Merge %[^' ]\",\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\nstring tryrun(string ... cmdline) {\n#if __REAL_MAJOR__<7 || __REAL_MAJOR__==7 && __REAL_MINOR__<8\n   return Process.popen(cmdline*\" \" + \" 2>/dev/null\");\n#else\n   mapping r;\n   mixed e = catch { r = Process.run( ({@cmdline}) ); };\n   if (e || r[\"exitcode\"])\n      return \"\";\n   return r[\"stdout\"];\n#endif\n}\n\nstatic object cal = Calendar.ISO.set_timezone(Calendar.Timezone.UTC);\n\n---------------------------------------------------------------------------------\n"}]}