I just committed three files to Git (Class1.java, Class2.java, and Config.xml). Then I realized that I made a mistake, and didn't actually want Config.xml in that commit. Now I want to amend my commit and unstage the changes to Config.xml, so that the (amended) commit contains only the changes to Class1.java and Class2.java.
This is easy in Git Gui; select "Amend previous commit", and the "Staged Changes (Will Commit)" list shows me all three files that were changed in the previous commit. I can unstage Config.xml and commit.
But in GitKraken, when I check "Amend", it doesn't show me the three files from the previous commit; it just shows me a blank slate. There's no clear way to unstage one of the files from the commit I'm amending.
I know I can use the command line (or Git Gui). But is there any way, from GitKraken, to amend the previous commit and simply remove one file's changes from it?
Your observations are correct: You can't.
There is no way to remove changes from a commit via amend at this time, as far as I know. You can only add other changes to the commit (which may of course nullify some of the changes you commited before). As you correctly observed, this is because GK does not show you the previous commit when you check Amend, you can only add your unstaged changes.
A pure GK workaround would be a soft reset to the previous commit and a new commit only containing Class1.java and Class2.java. The result would be the same, since an amend would also create a new commit.
I am using Eclipse. And, today, I decided to push all the code I wrote during two weeks. However, I did a mistake in the message of the commit with all my changes, so I clicked on the but "Revert" of the Git perspective of Eclipse. But, all the code I wrote during these two weeks is not anymore in the working tree!
So, I did git log and got this:
commit dd2401c076f4e34f2bdfb642f0ef4ab1dc476458
Date: Sun Jul 16 15:03:40 2017 +0100
Revert "Rewrited the Linear Plan Tree. Renamed the planner/node package to"
This reverts commit 9240fd696fbe827e58a8eb827014125b7b1b5bb1.
commit 9240fd696fbe827e58a8eb827014125b7b1b5bb1
Date: Sun Jul 16 15:02:20 2017 +0100
Rewrited the Linear Plan Tree. Renamed the planner/node package to
So, I executed this command: git reset --soft HEAD~1.
But, in my file explorer, all the files I wrote are still removed and now, git log shows me:
commit 9240fd696fbe827e58a8eb827014125b7b1b5bb1
Author: Lucas Willems <lwillems#clipper.ens.fr>
Date: Sun Jul 16 15:02:20 2017 +0100
Rewrited the Linear Plan Tree. Renamed the planner/node package to
Is my code deleted forever? Is there a way to get it back? Thank you a lot for your help!
Is my code deleted forever?
No. You committed some file content; the committed content is retrievable. Even "deleting a commit" doesn't actually delete it immediately, in Git, due to what Git calls its reflogs.
Note that uncommitted content is much harder to retrieve, which is why people say that you should commit early and often.
Commits, index, and work-tree; or, git reset is a tricky command to explain
Is there a way to get it back?
Yes! But it takes a bit of patience to get there. :-)
The first step is to get a clear visualization of the three "parts" that Git keeps visible—well, somewhat—at all times, in each repository. When you used git reset --soft you told Git to separate some of these parts.
These three parts are the current commit, which is also known as HEAD; the index, which is also called the staging area and the cache; and the work-tree or working tree, which is the only one of the three that you can actually see when you look around at your computer.
Of these three, the one that is easy to see—the work-tree—doesn't actually matter to Git! :-) (We'll "see" the other two indirectly all the time, via git status. Seeing the index directly is a bit tricky: try git ls-files --stage someday. Seeing the HEAD commit directly is also tricky: try git ls-tree -r HEAD, for instance.)
One of these three entites, the commit, is read-only. When you make a commit, it is (mostly) permanent (under some conditions, you can make it go away) and it never changes after that. Its "true name" is one of those big ugly Git hash IDs, 09baf31... or whatever. As long as it exists in your repository, you can name it by this hash ID, e.g.:
git show 09baf31
for instance. But note that git show shows it by comparing it to something—you "show" a commit in a certain context.
When you check out a commit, you ask Git to copy it to those other two places, the index and the work-tree.
The index is how Git builds new commits. When you run git commit, whatever is in the index right now, goes into the new commit. And yet, you cannot easily see what is in the index right now! What you can do is have Git compare the current commit to the index, and tell you what's different. The git status command does that.
To change what's in the index, you will generally work on the third copy of each file, i.e., the one in your work-tree. You do your actual work in your work-tree, where you can see your files, and work on them. Then you run git add <file> to copy from the work-tree to the index.
The git status command compares everything:
HEAD version index version work-tree version
------------ ------------- -----------------
README.md README.md README.md
file.txt file.txt file.txt
Note again that there are three versions of every file. (The HEAD and index versions are kept in special compressed formats, so this does not take that much space.) The HEAD version is read-only, but you can copy any other version back or forward: from HEAD to index, from index to work-tree, and from work-tree to index.
Again, commits are the only permanent part of this whole process. Whatever is in the index and work-tree is temporary; the index is used to make new commits, and the work-tree is just there for you to mess with things, so that you can copy them into the index.
The commit graph
Each commit—the part of Git that stores files permanently—has one of those big ugly hash IDs we noted. It also carries the idea, or more precisely, the hash ID, of a parent commit. The parent of a commit is Git's way of remembering what was committed before that commit.
The very first commit you make in a repository is special because it has no parent. (It can't, it's the first!) We call this a root commit. After that, each new commit remembers its parent. We say that each commit "points back to" its parent:
A <-B <-C
The way Git finds commits is to start from the newest one, which it remembers by a branch name like master. We say that the branch name master points to the tip commit of the branch:
A <-B <-C <-- master
Each commit then points back to some earlier commit, which points back again, and so on. The whole process only stops when we reach the root commit (A in the drawing above), or when we get tired of git log output and stop looking. :-)
Let's say the current commit is C. This means that master points to C. The name HEAD refers to master, because we're on our master branch. The index and work-tree have copies of what's in commit C. All three copies of every file match, so git status just says:
On branch master
nothing to commit, working tree clean
The first part, on branch master, is from the fact that HEAD says master. That's how Git knows that commit C is the current commit: it reads HEAD, which says master; then it reads master, which has the hash ID for commit C; then it looks inside commit C as needed.
The second part says that the index matches the HEAD commit, and the work-tree matches the index. All three copies of every file are all identical.
Making new commits
To add a new commit, we might change one of our files, such as file.txt, so that it has a change in the work-tree. Then we git add file.txt, so that the index copy gets updated. Now git status compares HEAD vs index and sees that file.txt is ready to be committed. It also compares index vs work-tree and says there's nothing else to git add, so we're ready to commit. We run git commit and Git makes a new commit D using the files that are in the index, sets D's parent to C, and—this is the really tricky bit—changes the name master so that master now points to new commit D:
A <-B <-C <-D <-- master
The HEAD file itself doesn't change (it still just says master), but now the HEAD commit is commit D. The HEAD commit matches the index, because of course it does, we just made this commit from the index. The index matches the work-tree, because we didn't change either of those. So now all three match, and everything is normal!
Showing a commit, in context
Since each commit remembers its previous (parent) commit, when you go to look at one, with git show or git log -p, Git compares the content stored under the two commit hashes: the parent, vs the commit itself. It then shows you only what changed between those two.
Each commit has a full, complete copy of everything, but when you use git show to see it, Git shows you "what changed from parent to child", because that's usually more useful to know.
A brief look at git revert
What git revert does is pretty simple, really. This somewhat poorly named command looks at some—any—existing commit to see what it changed, just like git show does. Then, starting from the current commit, Git tries to make a new change that "undoes" whatever the selected commit did. If the commit you're reverting added a line to a file, Git removes that line from that file. If the commit you're reverting removed two lines from a second file, Git puts those two lines into that second file.
If all of this works, git revert makes a new commit with the result. The parent of the new commit is the current commit. This lets you back out any previous commit, not just the last one, as long as Git can figure out how to "undo" what that previous commit did.
Let's draw that in:
A--B--C--D <-- master (HEAD)
I'm going to stop drawing the internal arrows: just remember they're all backwards all the time, as Git works backwards. We add the (HEAD) or some similar notation to keep track of which branch name is in HEAD.
What git reset does
What git reset does is complicated.
The first step is that it moves a branch name. You pick the commit you want, and this is where your HEAD~1 came in. If you had:
A--B--C--D <-- master (HEAD)
and you write HEAD~1, Git counts one commit back from HEAD. Since HEAD locates commit D, this makes Git count back one step to commit C.
Git then makes the actual branch name point to that commit:
D
/
A--B--C <-- master (HEAD)
What happened to commit D? Well, nothing really: it's still there. It's just shoved aside, because we need to show that master now points to C, not to D.
The next step for git reset would be to copy the new HEAD commit's contents to the index. But if you say --soft, you tell it: no, don't touch the index; just do the first step, of making the branch name point somewhere different, and then stop.
If you had used git reset --mixed, Git would have re-set the index, by making it match the new HEAD commit. It would then stop at that point: HEAD and index would match, but the work-tree would be left untouched.
If you had used git reset --hard, Git would keep going: it would also re-set the work-tree, from the index that it just got from the new HEAD commit.
Putting this all together
Now, what you want, in this case, is to have your index and work-tree match what's in commit C. What you should have done originally is git reset --hard HEAD~1, but don't do that now, because now master already points to C!
But: What if, instead of moving the name master, you tell git reset to "move" from commit C to commit C? You would do this with git reset HEAD, i.e., the place to move master is wherever it is now. You can even leave out the HEAD part, since that's the default. You can then also tell git reset to re-set the index and the work-tree:
git reset --hard
This wipes out the current index, replacing it with whatever is in the selected commit (C in the diagram, or whatever it really is in your repository). Then it replaces the work-tree contents with the new index contents too, and now all three are back in sync, just like normal!
But, uh oh, you ran git reset --hard HEAD~1. Enter the reflogs!
Well, let's go back to the diagram:
D
/
C
/
A--B <-- master (HEAD)
What you need now is to get commit C back. It's still in there, shoved up out of the way; it's just hard to find now.
You need to find a way to name commit C: e.g., its hash ID, or some alternative that will work. This is where the reflogs come in.
Commits C and D above don't have anything pointing to them. This is because they don't have a branch name. Fortunately, Git keeps the previous values of each branch around for 30 days by default. If you run:
git reflog master
Git will spill out all the old hash IDs (abbreviated) and these funny master#{1}, master#{2}, and so on names.
The name HEAD itself also has a reflog; git reflog will show those. The HEAD reflog changes fastest, because it's constantly being updated, so I like to use the other ones if possible, but it's up to you.
Once you have the hash ID, you can cut and paste it. Or, if the names aren't changing too fast, you can use the #{1} style names. This is what they probably look like right now:
D master#{2}, HEAD#{2}
/
C master#{1}, HEAD#{1}
/
A--B <-- master (HEAD)
Note that the numbers themselves increment every time you make a change. So if master#{1} names commit C right now, you can now run:
git reset --hard master#{1}
(though you might have to add some quote marks, e.g., git reset --hard "master#{1}" with the quotes, depending on your shell). That will do yet another reset, moving the name master to point to C again.
Or, you can run git reflog, look for the right commit, cut and paste its hash ID, and use git reset --hard <hash-id>.
Eventually—some time after the 30 day expiration—Git will expire some of the old numbers, like master#{912} or whatever, and once that happens, any commits that are all dangly-off-the-edges like that may get garbage collected. So don't wait more than 30 days to get them back.
Conclusion (since this is long)
Commit early and often. The commits are permanent, unless reset, but even then they stick around for a while. Be careful with git reset: although the commits still stick around for 30 days, they can become very hard to find. Also, git reset --hard will overwrite your work-tree, and even git reset --mixed will overwrite your index. Remember, the index is where you build the next commit, and your work-tree is where you work on files.
I think you should have run git reset --hard HEAD~1 intead of "soft". "Soft" means that working vopy and index are not updated, so at filesystem ig still looks like reverted. You shoul run git reset --hard now. It makes sense to run git status and git diff HEAD to check if there are some uncommitted changes which you could want to keep
I would recommend doing the following steps from the Git bash:
git stash
git checkout yourBranch
git revert dd2401c0
git stash apply
The basic strategy here is to stash your current working directory, and then to revert the revert commit. Yes, two reverts cancel each other out.
The problem with doing git reset here is that it moves the HEAD of your branch. This appears to have the side effect that any new files you wrote get erased. In any case, doing a reset would rewrite the history of your branch, which isn't ideal if the branch could be shared by other people.
In the Magit Refs buffer, you can press tab on a branch (the branch I want to merge in) to see what commits would be added if this branch were merged into the currently checked out branch, and you can press enter on the commits to see their changes. Is there a way to see all those changes unified together?
I don't think the magit diff dwim feature is what I want, because it shows all the changes that are on the current branch but not on the branch I want to merge in. I usually don't care about seeing those changes, because I am mostly interested in what the branch I am merging will change, not everything else that changed.
You can see what a branch will add when merged by using merge preview m p. This can be used from the Magit Refs buffer by navigating point to the branch you want to preview a merge of and typing m p.
Yes, "diff dwim" doesn't really do what I mean here either. That should be improved, open a feature request please.
Meanwhile you can use d r and then type the range (at least with completion). Or you could use d d and then flip the revisions (D f) as well as switch the range style (D r). Together this changes feature..master to master...feature.
I did commit with merge, after that I did backout in main branch in order to revert last merge.
After some time I need to merge branch, but mercurial says that abort: merging with a working directory ancestor has no effect
But in develop branch I don't see my changes from other branch.
Merge only picks up things that are new since the last merge. For instance:
default: *------------?
\ /
develop: o--o--o--o
Each o represents some commit. We started with the default branch (named default) with just one commit, the one marked * instead of o here. Then we made a develop branch starting with that same first commit, and did some work.
Once the work was ready, we went back to the default branch and used hg merge. This proposed making a new commit, ?. It did so by looking back to *—the commit that we had in common between default and develop—and looking at what we did on the two branches, and combining them.
We did nothing on default, so the combining was easy. Mercurial was able to just take everything we did on develop and put it all into default. Let's fill in the merge as a real commit now:
default: o------------*
\ /
develop: o--o--o--o
Notice that I've moved the *. It's now the new merge. This is the most recent commit that links the two branches.
Meanwhile, hg backout makes a new commit that undoes a previous commit. Specifically you're asking to back out the merge. Let's draw that:
default: o------------*--u
\ /
develop: o--o--o--o
This new commit is sort of like the antimatter version of the merge, in terms of changes made anyway. Everything in default goes back to the way it was before the merge. So I used the letter u (for "undo") instead of the usual round o dot for a more typical commit.
Where's the latest common commit on the two branches, though? The answer should be obvious: it's still *.
Now you ask Mercurial to merge again. It finds the changes in develop since the merge—and there aren't any. There is no new work to merge!
Suppose we check out develop again and make some new commits:
default: o------------*--u
\ /
develop: o--o--o--o------o--o
Now we can hg merge develop into default again, to pick up the changes from the two new commits. But the changes you undid, with commit u (the backout commit), are still un-done. We'll only pick up the changes from the new (since the merge) commits on develop, and we keep the changes made (since the merge) on default, which is to say, the u undo changes:
default: o------------o--u------*
\ / /
develop: o--o--o--o------o--o
If you want all those changes back, you can simple back out the backout. That is, any time before or after adding the new commits on develop, and even before or after merging those new commits, you can undo the undo. Let's turn the u upside down into a "redo" and see how that looks, if we insert it before the next merge:
default: o------------o--u--n-----*
\ / /
develop: o--o--o--o--------o--o
In other words, commit u (the hg backout) un-did the changes from the merge. It does not, and cannot, actually make the merge not happen. So to re-obtain the changes from the merge, you have to undo the undo, which is our n here.
I did an accidental commit which really makes no sense to have anywhere in history. How can I remove this commit from existence (especially I don't want it appear remotely).
In the magit-status, it shows:
Unpushed commits:
fe73b07 updated gitignore
974e70d test
ab333e6 trying to go with a flat structure
What can I do?
Bonus: actually, I just want to keep the "updated gitignore" from this commit.
Point at test, press E to start a rebase. M-n two swap commits. C-c C-c to finalize.
Resolve merge conflicts if any and done.
This is more a git question than an emacs or magit question really. If I understand you correctly, you want to get rid of older commits. One way to go about this is to re-order your commits and than get rid of the last two commits.
The first amounts to picking the right commits during rebase, the latter amounts to using the right incantation of git reset. I would suggest you take a look at this link on reordering commits. In addition, I would urge you to take a (ton of) look(s) at this useful fixup section of the step-by-step adventure through git.
Like #abo-abo said, initiate an interactive rebase by putting point on the first commit you want to remove and then pressing E (on Magit's next version ee.
A new log-like view appears, with every line prefixed with an action, initially pick. Then press n to move to the first commit you want to remove, and k, which will strike out that line meaning to drop that commit. (That's what happens in the ui, when this information is later feed to Git, the line is actually removed). This also moves to the next line, which in this case is the other commit you want to remove. So press k again.
Now that all commits you want to remove are marked as such, press C-c C-c to tell Git to make it happen.