{"id":110655,"date":"2024-12-18T07:00:00","date_gmt":"2024-12-18T15:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=110655"},"modified":"2024-12-18T10:42:26","modified_gmt":"2024-12-18T18:42:26","slug":"20241218-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20241218-00\/?p=110655","title":{"rendered":"Is there a way to split the git history of a file or combine the histories of two files without a merge commit?"},"content":{"rendered":"<p>Some time ago, I showed how to <a title=\"Mundane git tricks: Combining two files into one while preserving line history\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20190514-00\/?p=102493\"> combine two files in git while preserving their line history<\/a> and how to <a title=\"How do I split a file into two while preserving git line history?\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20190916-00\/?p=102892\"> split a file into two while preserving git line history<\/a>. Both of these techniques rely on merge commits. But what if your team&#8217;s policy is to rebase or squash all commits? Can you accomplish these tasks without merge commits?<\/p>\n<p>Git&#8217;s line attribution algorithm follows file history, so let&#8217;s look at how git tracks file history.<\/p>\n<p>To determine the file history connections for a file between a commit and its parent or parents, git looks for the file in each parent commit at the same path. If it&#8217;s found there, then git considers the file to have been modified in place with respect to that parent. If it&#8217;s not present in the parent commit at the same path, then git looks to see if the file is similar\u00b9 to a file that is present in the child commit but missing in the parent. If it finds one, then it considers the file to have moved from that similar file. Otherwise, the file is considered to have been <del>deleted<\/del> newly-created.<\/p>\n<p>Note that git finds at most one match per parent commit. If it finds the file in a parent commit at the same path, it declares success for that parent commit and doesn&#8217;t keep looking for close matches.<\/p>\n<p>Our tricks with either splitting or merging git line history are trying to create a Y-shaped history. Either two new files whose ancestors are a shared single file, or one new file with two distinct ancestors. But if each commit has only one parent, then your history diagram will just be a straight line. No Y-shaped history is possible given these constraints.<\/p>\n<p>This means that if you do a squash or traditional rebase\u00b2, you lose the ability to create nonlinear history. If you want to do history merging or history splitting, you need to use merge commits.<\/p>\n<p>\u00b9 Git identifies all the files which are present in the parent but which are missing in the child at the same path. These are the deletion candidates. It then looks for a deletion candidate that is identical to the file in the child commit. If there is no perfect match, then it looks for near matches among the deletion candidates according to options you specify like <tt>-M<\/tt> and <tt>-B<\/tt>.<\/p>\n<p>\u00b2 Traditional rebase creates a linear history, but you can use the <tt>--rebase-merges<\/tt> option to (try to) preserve the original merge history.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Studying how git recovers history.<\/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-110655","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-other"],"acf":[],"blog_post_summary":"<p>Studying how git recovers history.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110655","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=110655"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110655\/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=110655"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=110655"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=110655"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}