{"id":104302,"date":"2020-09-28T07:00:00","date_gmt":"2020-09-28T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=104302"},"modified":"2020-09-27T22:01:28","modified_gmt":"2020-09-28T05:01:28","slug":"20200928-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20200928-00\/?p=104302","title":{"rendered":"git commit-tree parlor tricks, Part 9: How can I bulk-revert an entire repo to an earlier commit?"},"content":{"rendered":"<p>Suppose you&#8217;ve made a bunch of commits to a branch, and then you decide that you want to roll back the entire repo to an earlier commit. Just pretend the last dozen commits never happened. The branch policy prevents force-pushes, so you will have to make a new commit that effectively reverts a large number of commits.<\/p>\n<div id=\"p20200928_head\" style=\"display: none;\">\u00a0<\/div>\n<div id=\"p20200928_defs\" style=\"height: 0;\">\u00a0<\/div>\n<table style=\"text-align: center;\" title=\"Described in text.\" border=\"0\" cellspacing=\"1\">\n<tbody>\n<tr>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">A<\/td>\n<td id=\"p20200928_larr\">\u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M1<\/td>\n<td id=\"p20200928_larr\">\u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M2<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We start with some commit A, and there have been some commits M1 and M2 on top of it. What&#8217;s the easiest way to do a bulk revert back to A?<\/p>\n<p>You might think you could merge commit A with the <code>-s theirs<\/code> option, but that doesn&#8217;t work because commit A is already in the history of the branch, so the merge does nothing.<\/p>\n<p>Another thing you could try is to <code>git checkout A -- .<\/code> from the root of the repo, saying that you want to take every single file from commit <code>t<\/code> and put it into the current tree. This mostly works, except that any files added after commit A will not be deleted. The <code>git checkout A -- .<\/code> will update all the files that were present in A, but any files added in M1 and M2 will not be deleted.<\/p>\n<p>My old standby is <code>git commit-tree<\/code>. In this case, we want to create a commit on top of <code>HEAD<\/code> with the contents of an earlier commit.<\/p>\n<pre>git commit-tree A^{tree} -p HEAD -m \"Bulk revert back to A\"\r\n<\/pre>\n<p><b>Note<\/b>: If using the Windows <code>cmd<\/code> command prompt, you need to type<\/p>\n<pre>git commit-tree A^^{tree} -p HEAD -m \"Bulk revert back to A\"\r\n<\/pre>\n<p>for reasons <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20060517-00\/?p=31173\"> discussed earlier<\/a>.<\/p>\n<p>The <code>git commit-tree<\/code> command will spit out a hash, which you can fast-forward to.<\/p>\n<p>If you want to express this as a merge, you could say<\/p>\n<pre>git commit-tree A^{tree} -p HEAD -p A -m \"Bulk revert back to A\"\r\n<\/pre>\n<p>This generates a merge commit that would not normally be found in nature: Merging a commit that you already have.<\/p>\n<table style=\"text-align: center;\" title=\"Described in text.\" border=\"0\" cellspacing=\"1\">\n<tbody>\n<tr>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">A<\/td>\n<td id=\"p20200928_larr\">\u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M1<\/td>\n<td id=\"p20200928_larr\">\u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M2<\/td>\n<td id=\"p20200928_larr\">\u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M3<\/td>\n<\/tr>\n<tr>\n<td>&nbsp;<\/td>\n<td id=\"p20200928_longarc\">\u2196\ufe0e<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td id=\"p20200928_blank\">\u2199\ufe0e<\/td>\n<\/tr>\n<tr>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td id=\"p20200928_blank\">\u2190 \u2190<\/td>\n<td id=\"p20200928_blank\">\u2190<\/td>\n<td id=\"p20200928_blank\">\u2190 \u2190<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We have manufactured a commit M3 which represents a merge of its own ancestor commit A.\u00b9<\/p>\n<p>Now, there are plenty of other ways to accomplish the same thing. I like <code>commit-tree<\/code> because it directly creates exactly the commit I want, and it does so without affecting the index or working tree.<\/p>\n<p>On the other hand, it doesn&#8217;t give you a chance to inspect and possibly alter the result before committing. Then again, maybe that&#8217;s not a problem. After you fast-forward to the manually-created commit, you can make whatever additional changes you like and either make a new commit on top, or amend the previous commit.<\/p>\n<p>But maybe you prefer to have the commit staged. You can do that by reading the desired target into the index and asking for the working tree to be updated to match. Make sure your index and working tree are clean by doing a <code>git status<\/code> and verifying that it says &#8220;Nothing to commit, working tree clean.&#8221; And then do this:<\/p>\n<pre>git read-tree -mu A\r\n<\/pre>\n<p>This makes the index and working tree match the tree from commit A. (If your working tree and index are not clean before doing this, the results will be merged in, which will probably be a mess.) You can now inspect the results, make additional changes, whatever, before you commit.<\/p>\n<p>If you want to commit this as a &#8220;not found in nature&#8221; merge, you&#8217;ll still have to do some <code>git commit-tree<\/code> magic:<\/p>\n<pre>git write-tree\r\n<\/pre>\n<p>This will print a tree hash. Feed that hash to the next line:<\/p>\n<pre>git commit-tree \u2329hash\u232a -p HEAD -p A -m \"Bulk revert back to A\"\r\n<\/pre>\n<p>This will print a commit hash. Fast-forward to that commit to complete the artificial merge.<\/p>\n<p>Okay, this is all great, but what if you didn&#8217;t want to revert the entire repo, but just a part of it? We&#8217;ll look at that next week.<\/p>\n<p><b>Bonus chatter<\/b>: If you wanted to express this as a pure revert rather than a merge, then omit the <code>-p A<\/code> from the command lines. But using the merge has the nice side effect of assigning <code>git blame<\/code> to the commits that led to A. The commits M1 and M2 will never be assigned blame, which makes sense, since all their changes were reverted.<\/p>\n<p>That nice side effect on <code>git blame<\/code> means that this technique is useful if you are reverting the last change, since it takes the change and its revert out of the <code>git blame<\/code>.<\/p>\n<p>\u00b9 Note that the topologically equivalent diagram does occur in nature:<\/p>\n<table style=\"text-align: center;\" title=\"Described in text.\" border=\"0\" cellspacing=\"1\">\n<tbody>\n<tr>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">A<\/td>\n<td id=\"p20200928_larr5\" colspan=\"5\">\u2190 \u2190 \u2190 \u2190 \u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M3<\/td>\n<\/tr>\n<tr>\n<td>&nbsp;<\/td>\n<td id=\"p20200928_nwarr\">\u2196\ufe0e<\/td>\n<td colspan=\"3\">\u00a0<\/td>\n<td id=\"p20200928_swarr\">\u2199\ufe0e<\/td>\n<\/tr>\n<tr>\n<td colspan=\"2\">\u00a0<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M1<\/td>\n<td id=\"p20200928_larr\">\u2190<\/td>\n<td style=\"border: solid 1px black; width: 50px; font-size: 120%;\">M2<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>This represents the situation where a no-fast-forward merge is taken from a topic branch.<\/p>\n<p>\n<script>\nwindow.addEventListener(\"load\", function() {\n  var fullFF = getComputedStyle(document.body).fontFamily;\n  var simpleFF = fullFF.replace(\/ Emoji\/g, \"\");\n  \/\/ break up \"style\" to prevent wordpress from injecting random junk\n  document.getElementById(\"p20200928_head\").innerHTML =\n`<s` + `tyle>\nbody { font-family: ${simpleFF}; }\n.emoji { font-family: ${fullFF}; }\n.entry-content th { padding: 1px; } \/* stylesheet workaround *\/\n.entry-content td { padding: 1px; } \/* stylesheet workaround *\/\n<\/s` + `tyle>`;\n}); \/\/ wacky comment to prevent wordpress from injecting random junk\n(function() {\n  var svg = {\n    defs: `<svg width=\"0\" height=\"0\">\n <defs>\n  <marker id=\"arrowhead\" markerWidth=\"5\" markerHeight=\"5\" refX=\"-2\" refY=\"0\"\n    viewBox=\"-6 -6 12 12\" orient=\"auto\">\n    <polygon points=\"-2,0 -5,5 5,0 -5,-5\" fill=\"black\" stroke=\"black\"\n      stroke-dasharray=\"1 0\" \/>\n  <\/marker>\n  <path id=\"larr\" d=\"M20,10 L3,10\" stroke=\"black\"\n   marker-end=\"url(#arrowhead)\" \/>\n  <path id=\"larr5\" d=\"M176,10 L3,10\" stroke=\"black\"\n   marker-end=\"url(#arrowhead)\" \/>\n  <path id=\"nwarr\" d=\"M20,20 L3,3\" stroke=\"black\"\n   marker-end=\"url(#arrowhead)\" \/>\n  <path id=\"swarr\" d=\"M20,2 L3,17\" stroke=\"black\"\n   marker-end=\"url(#arrowhead)\" \/>\n  <path id=\"swarrd\" d=\"M20,0 L3,17\" stroke=\"black\" stroke-dasharray=\"4 2\"\n   marker-end=\"url(#arrowhead)\" \/>\n <\/defs>\n<\/svg>`,\n    larr: `<svg width=\"20\" height=\"20\"><use href=\"#larr\"\/><\/svg>`,\n    larr5: `<svg width=\"176\" height=\"20\"><use href=\"#larr5\"\/><\/svg>`,\n    nwarr: `<svg width=\"20\" height=\"20\"><use href=\"#nwarr\"\/><\/svg>`,\n    swarr: `<svg width=\"20\" height=\"20\"><use href=\"#swarr\"\/><\/svg>`,\n    blank: ``,\n    longarc: `<svg width=\"20\" height=\"20\" style=\"overflow: visible\"><path d=\"M180,5 C 130,30 45,30 -5,5\" stroke=\"black\" fill=\"transparent\" marker-end=\"url(#arrowhead)\"\/><\/svg>`,\n  };\n  Object.keys(svg).forEach(function (key) {\n    Array.prototype.forEach.call(document.querySelectorAll(\"#p20200928_\" + key),\n      function (e) {\n        e.innerHTML = svg[key];\n      });\n  });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Take me back to a simpler time.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[26],"class_list":["post-104302","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-other"],"acf":[],"blog_post_summary":"<p>Take me back to a simpler time.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/104302","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=104302"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/104302\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=104302"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=104302"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=104302"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}