UNPKG

epubjs

Version:

Render ePub documents in the browser, across many devices

236 lines (161 loc) 17.4 kB
<?xml version='1.0' encoding='utf-8'?> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Pro Git - professional version control</title> <meta content="http://www.w3.org/1999/xhtml; charset=utf-8" http-equiv="Content-Type"/> <link href="stylesheet.css" type="text/css" rel="stylesheet"/> <style type="text/css"> @page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }</style> </head> <body class="calibre"> <h2 class="calibre4" id="calibre_pb_52">Rewriting History</h2> <p class="calibre3">Many times, when working with Git, you may want to revise your commit history for some reason. One of the great things about Git is that it allows you to make decisions at the last possible moment. You can decide what files go into which commits right before you commit with the staging area, you can decide that you didn't mean to be working on something yet with the stash command, and you can rewrite commits that already happened so they look like they happened in a different way. This can involve changing the order of the commits, changing messages or modifying files in a commit, squashing together or splitting apart commits, or removing commits entirely - all before you share your work with others.</p> <p class="calibre3">In this section, you'll cover how to accomplish these very useful tasks so that you can make your commit history look the way you want before you share it with others.</p> <h3 class="calibre5">Changing the Last Commit</h3> <p class="calibre3">Changing your last commit is probably the most common rewriting of history that you'll do. You'll often want to do two basic things to your last commit: change the commit message, or change the snapshot you just recorded by adding, changing and removing files.</p> <p class="calibre3">If you only want to modify your last commit message, it's very simple:</p> <pre class="calibre9"><code class="calibre10">$ git commit --amend </code></pre> <p class="calibre3">That drops you into your text editor, which has your last commit message in it, ready for you to modify the message. When you save and close the editor, the editor writes a new commit containing that message and makes it your new last commit.</p> <p class="calibre3">If you've committed and then you want to change the snapshot you committed by adding or changing files, possibly because you forgot to add a newly created file when you originally committed, the process works basically the same way. You stage the changes you want by editing a file and running <code class="calibre10">git add</code> on it or <code class="calibre10">git rm</code> to a tracked file, and the subsequent <code class="calibre10">git commit --amend</code> takes your current staging area and makes it the snapshot for the new commit.</p> <p class="calibre3">You need to be careful with this technique because amending changes the SHA-1 of the commit. It's like a very small rebase - don't amend your last commit if you've already pushed it.</p> <h3 class="calibre5">Changing Multiple Commit Messages</h3> <p class="calibre3">To modify a commit that is farther back in your history, you must move to more complex tools. Git doesn't have a modify-history tool, but you can use the rebase tool to rebase a series of commits onto the HEAD they were originally based on instead of moving them to another one. With the interactive rebase tool, you can then stop after each commit you want to modify and change the message, add files, or do whatever you wish. You can run rebase interactively by adding the <code class="calibre10">-i</code> option to <code class="calibre10">git rebase</code>. You must indicate how far back you want to rewrite commits by telling the command which commit to rebase onto.</p> <p class="calibre3">For example, if you want to change the last three commit messages, or any of the commit messages in that group, you supply as an argument to <code class="calibre10">git rebase -i</code> the parent of the last commit you want to edit, which is <code class="calibre10">HEAD~2^</code> or <code class="calibre10">HEAD~3</code>. It may be easier to remember the <code class="calibre10">~3</code> because you're trying to edit the last three commits; but keep in mind that you're actually designating four commits ago, the parent of the last commit you want to edit:</p> <pre class="calibre9"><code class="calibre10">$ git rebase -i HEAD~3 </code></pre> <p class="calibre3">Remember again that this is a rebasing command - every commit included in the range <code class="calibre10">HEAD~3..HEAD</code> will be rewritten, whether you change the message or not. Don't include any commit you've already pushed to a central server - doing so will confuse other developers by providing an alternate version of the same change.</p> <p class="calibre3">Running this command gives you a list of commits in your text editor that looks something like this:</p> <pre class="calibre9"><code class="calibre10">pick f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file # Rebase 710f0f8..a5f4a0d onto 710f0f8 # # Commands: # p, pick = use commit # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. # </code></pre> <p class="calibre3">It's important to note that these commits are listed in the opposite order than you normally see them using the <code class="calibre10">log</code> command. If you run a <code class="calibre10">log</code>, you see something like this:</p> <pre class="calibre9"><code class="calibre10">$ git log --pretty=format:"%h %s" HEAD~3..HEAD a5f4a0d added cat-file 310154e updated README formatting and added blame f7f3f6d changed my name a bit </code></pre> <p class="calibre3">Notice the reverse order. The interactive rebase gives you a script that it's going to run. It will start at the commit you specify on the command line (<code class="calibre10">HEAD~3</code>) and replay the changes introduced in each of these commits from top to bottom. It lists the oldest at the top, rather than the newest, because that's the first one it will replay.</p> <p class="calibre3">You need to edit the script so that it stops at the commit you want to edit. To do so, change the word pick to the word edit for each of the commits you want the script to stop after. For example, to modify only the third commit message, you change the file to look like this:</p> <pre class="calibre9"><code class="calibre10">edit f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file </code></pre> <p class="calibre3">When you save and exit the editor, Git rewinds you back to the last commit in that list and drops you on the command line with the following message:</p> <pre class="calibre9"><code class="calibre10">$ git rebase -i HEAD~3 Stopped at 7482e0d... updated the gemspec to hopefully work better You can amend the commit now, with git commit --amend Once you're satisfied with your changes, run git rebase --continue </code></pre> <p class="calibre3">These instructions tell you exactly what to do. Type</p> <pre class="calibre9"><code class="calibre10">$ git commit --amend </code></pre> <p class="calibre3">Change the commit message, and exit the editor. Then, run</p> <pre class="calibre9"><code class="calibre10">$ git rebase --continue </code></pre> <p class="calibre3">This command will apply the other two commits automatically, and then you're done. If you change pick to edit on more lines, you can repeat these steps for each commit you change to edit. Each time, Git will stop, let you amend the commit, and continue when you're finished.</p> <h3 class="calibre5">Reordering Commits</h3> <p class="calibre3">You can also use interactive rebases to reorder or remove commits entirely. If you want to remove the "added cat-file" commit and change the order in which the other two commits are introduced, you can change the rebase script from this</p> <pre class="calibre9"><code class="calibre10">pick f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file </code></pre> <p class="calibre3">to this:</p> <pre class="calibre9"><code class="calibre10">pick 310154e updated README formatting and added blame pick f7f3f6d changed my name a bit </code></pre> <p class="calibre3">When you save and exit the editor, Git rewinds your branch to the parent of these commits, applies <code class="calibre10">310154e</code> and then <code class="calibre10">f7f3f6d</code>, and then stops. You effectively change the order of those commits and remove the "added cat-file" commit completely.</p> <h3 class="calibre5">Squashing a Commit</h3> <p class="calibre3">It's also possible to take a series of commits and squash them down into a single commit with the interactive rebasing tool. The script puts helpful instructions in the rebase message:</p> <pre class="calibre9"><code class="calibre10"># # Commands: # p, pick = use commit # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. # </code></pre> <p class="calibre3">If, instead of "pick" or "edit", you specify "squash", Git applies both that change and the change directly before it and makes you merge the commit messages together. So, if you want to make a single commit from these three commits, you make the script look like this:</p> <pre class="calibre9"><code class="calibre10">pick f7f3f6d changed my name a bit squash 310154e updated README formatting and added blame squash a5f4a0d added cat-file </code></pre> <p class="calibre3">When you save and exit the editor, Git applies all three changes and then puts you back into the editor to merge the three commit messages:</p> <pre class="calibre9"><code class="calibre10"># This is a combination of 3 commits. # The first commit's message is: changed my name a bit # This is the 2nd commit message: updated README formatting and added blame # This is the 3rd commit message: added cat-file </code></pre> <p class="calibre3">When you save that, you have a single commit that introduces the changes of all three previous commits.</p> <h3 class="calibre5">Splitting a Commit</h3> <p class="calibre3">Splitting a commit undoes a commit and then partially stages and commits as many times as commits you want to end up with. For example, suppose you want to split the middle commit of your three commits. Instead of "updated README formatting and added blame", you want to split it into two commits: "updated README formatting" for the first, and "added blame" for the second. You can do that in the <code class="calibre10">rebase -i</code> script by changing the instruction on the commit you want to split to "edit":</p> <pre class="calibre9"><code class="calibre10">pick f7f3f6d changed my name a bit edit 310154e updated README formatting and added blame pick a5f4a0d added cat-file </code></pre> <p class="calibre3">Then, when the script drops you to the command line, you reset that commit, take the changes that have been reset, and create multiple commits out of them. When you save and exit the editor, Git rewinds to the parent of the first commit in your list, applies the first commit (<code class="calibre10">f7f3f6d</code>), applies the second (<code class="calibre10">310154e</code>), and drops you to the console. There, you can do a mixed reset of that commit with <code class="calibre10">git reset HEAD^</code>, which effectively undoes that commit and leaves the modified files unstaged. Now you can stage and commit files until you have several commits, and run <code class="calibre10">git rebase --continue</code> when you're done:</p> <pre class="calibre9"><code class="calibre10">$ git reset HEAD^ $ git add README $ git commit -m 'updated README formatting' $ git add lib/simplegit.rb $ git commit -m 'added blame' $ git rebase --continue </code></pre> <p class="calibre3">Git applies the last commit (<code class="calibre10">a5f4a0d</code>) in the script, and your history looks like this:</p> <pre class="calibre9"><code class="calibre10">$ git log -4 --pretty=format:"%h %s" 1c002dd added cat-file 9b29157 added blame 35cfb2b updated README formatting f3cc40e changed my name a bit </code></pre> <p class="calibre3">Once again, this changes the SHAs of all the commits in your list, so make sure no commit shows up in that list that you've already pushed to a shared repository.</p> <h3 class="calibre5">The Nuclear Option: filter-branch</h3> <p class="calibre3">There is another history-rewriting option that you can use if you need to rewrite a larger number of commits in some scriptable way - for instance, changing your e-mail address globally or removing a file from every commit. The command is <code class="calibre10">filter-branch</code>, and it can rewrite huge swaths of your history, so you probably shouldn't use it unless your project isn't yet public and other people haven't based work off the commits you're about to rewrite. However, it can be very useful. You'll learn a few of the common uses so you can get an idea of some of the things it's capable of.</p> <h4 class="calibre14">Removing a File from Every Commit</h4> <p class="calibre3">This occurs fairly commonly. Someone accidentally commits a huge binary file with a thoughtless <code class="calibre10">git add .</code>, and you want to remove it everywhere. Perhaps you accidentally committed a file that contained a password, and you want to make your project open source. <code class="calibre10">filter-branch</code> is the tool you probably want to use to scrub your entire history. To remove a file named passwords.txt from your entire history, you can use the <code class="calibre10">--tree-filter</code> option to <code class="calibre10">filter-branch</code>:</p> <pre class="calibre9"><code class="calibre10">$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21) Ref 'refs/heads/master' was rewritten </code></pre> <p class="calibre3">The <code class="calibre10">--tree-filter</code> option runs the specified command after each checkout of the project and then recommits the results. In this case, you remove a file called passwords.txt from every snapshot, whether it exists or not. If you want to remove all accidentally committed editor backup files, you can run something like <code class="calibre10">git filter-branch --tree-filter 'rm -f *~' HEAD</code>.</p> <p class="calibre3">You'll be able to watch Git rewriting trees and commits and then move the branch pointer at the end. It's generally a good idea to do this in a testing branch and then hard-reset your master branch after you've determined the outcome is what you really want. To run <code class="calibre10">filter-branch</code> on all your branches, you can pass <code class="calibre10">--all</code> to the command.</p> <h4 class="calibre14">Making a Subdirectory the New Root</h4> <p class="calibre3">Suppose you've done an import from another source control system and have subdirectories that make no sense (trunk, tags, and so on). If you want to make the <code class="calibre10">trunk</code> subdirectory be the new project root for every commit, <code class="calibre10">filter-branch</code> can help you do that, too:</p> <pre class="calibre9"><code class="calibre10">$ git filter-branch --subdirectory-filter trunk HEAD Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12) Ref 'refs/heads/master' was rewritten </code></pre> <p class="calibre3">Now your new project root is what was in the <code class="calibre10">trunk</code> subdirectory each time. Git will also automatically remove commits that did not affect the subdirectory. </p> <h4 class="calibre14">Changing E-Mail Addresses Globally</h4> <p class="calibre3">Another common case is that you forgot to run <code class="calibre10">git config</code> to set your name and e-mail address before you started working, or perhaps you want to open-source a project at work and change all your work e-mail addresses to your personal address. In any case, you can change e-mail addresses in multiple commits in a batch with <code class="calibre10">filter-branch</code> as well. You need to be careful to change only the e-mail addresses that are yours, so you use <code class="calibre10">--commit-filter</code>:</p> <pre class="calibre9"><code class="calibre10">$ git filter-branch --commit-filter ' if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ]; then GIT_AUTHOR_NAME="Scott Chacon"; GIT_AUTHOR_EMAIL="schacon@example.com"; git commit-tree "$@"; else git commit-tree "$@"; fi' HEAD </code></pre> <p class="calibre3">This goes through and rewrites every commit to have your new address. Because commits contain the SHA-1 values of their parents, this command changes every commit SHA in your history, not just those that have the matching e-mail address.</p> </body> </html>