Reputation: 1911
The Problem:
I know I could do git revert -n "my specific commit"..HEAD the problem with that is that it stops at the first merge. passing -m 1 would fix that, but then it stops at non merges.
git reset --hard and force pushing is NOT an option. I can't force push on this git host.
The Situation: (why would I need this)
I know there are lot's of issues around this on here, but I haven't found one with this exact problem and a reasonable solution without force pushing.
Upvotes: 3
Views: 946
Reputation: 12270
You can use git checkout -p <sha1>
to revert the working tree to a given sha1 without changing where your current branch is.
git checkout -p "my specific commit"
Answer "a" to each question - I did not find how to force it into a non-interactive variant, although you could use Linux command yes
- it just types "y" forever:
yes | git checkout -p "my specific commit"
Then commit and push
git commit -m'Reverting to my specific commit'
git push
The result is a single commit that undoes the bad history in one fell swoop.
EDIT: Warning: If files were removed from my specific commit
to the current HEAD, this command will not restore them. The solutions proposed by @torek and @Marcus are therefore preferable to this one.
Upvotes: 0
Reputation: 725
git reset --hard good_commit_hash
git reset --soft current_commit_hash
git commit
will do what you want.
(You can simply use "@{u}" as "current_commit_hash" if the upstream (remote) branch is the place where you want to commit on.)
Short: if you want to do any kind of cleanup in your repository, use git reset. It's kind of swiss army knife.
Upvotes: 6
Reputation: 489748
Use git read-tree -u <hash>
, followed by making a new commit.
Remember that a Git repository is essentially a collection of commits. Each commit holds a full and complete snapshot of all files—well, all the files that are in that commit, but expressed this way it sounds tautological—plus some metadata: the name and email address of the person who made the commit, a timestamp of when they made it, a log message where they explain why they made it, and so on. (One of the crucial pieces of metadata is this commit's parent commit's hash ID, but for once we don't have to concern ourselves with this part at all!)
The true name of any given commit is its hash ID. You can temporarily stick this commit into your work-tree by doing:
git checkout <hash-id>
but of course that just gets you a "detached HEAD", and when you re-attach your HEAD by doing git checkout master
, you're back to the broken source code snapshot.
So, each commit represents a snapshot of all files. You had a snapshot, at some point, that you like, and you want to go back to it—but without actually going directly to it with git checkout <hash>
. This means that what you want is to make a new commit whose snapshot is the same as an existing commit. There's some good commit with some existing hash ID, and git checkout <hash>
is ... OK, but you want a new commit with a new hash ID, that otherwise matches the good one, at the (new) tip of the current branch. This new commit should have, as its parent, the current commit, just like any new commit has the current commit as its parent.
The method outlined in joanis' answer will work: it compares what's in the selected commit to what's in the current work-tree, and asks you if you'd like to adjust the work-tree to match the selected commit, for each diff-hunk in each file. Answering "a" says "take all for this file", but that leaves you with the job of putting in a lot of answers, one for each different file.
It's simpler—but flawed—to use a different mode of git checkout
:
git checkout <good-commit-hash> -- .
This mode tells git checkout
: Don't switch to another commit. Don't change HEAD
itself in any way! But do go ferret through the Git database to find the file(s) that match the path-specifier .
that are in the good commit. For each matching file, take it out of that commit, copy it to my current index, and copy it to my current work-tree.
If you do this in the top level of your work-tree, all files in the good commit will match the path-specifier .
. So if you have files README
and main.py
in commit <hash>
, those will replace the README
and main.py
in your index, and the README
and main.py
in your work-tree. You'll be ready to commit.
The flaw here is this: What if you now have a third file, say, bug.txt
, that isn't in the good commit you're trying to switch back to? To fix that, you must explicitly remove any file that is in your current index and work-tree that is not in the good commit.
You could manually remove any such files. Or there may not be any such files, in which case the problem is merely theoretical. But there is a guaranteed cure, which is harmless if there's no problem. You can just start with:
git rm -r .
to remove everything. That removes bug.txt
and main.py
and README
. A subsequent git checkout <good-commit-hash> -- .
puts back the good main.py
and README
, and bug.txt
remains removed, so that you are ready to git commit
the result.
There's a lower-level Git command that does this for you, though, and that's git read-tree
. This command isn't really for everyday use,1 but this is not an everyday problem either. In this particular case, using:
git read-tree -u <good-commit-hash>
tells the plumbing command: Wipe out my current index. Get the files from the given hash ID and put those into my index instead. Wherever you removed a file entirely, remove if from my work-tree too. Wherever you replaced a file, replace it in my work-tree too. That is exactly the same result, in terms of files and their contents, that you get with git rm -r .; git checkout <good-commit-hash> -- .
, except that it is more efficient.
In either case, you are now ready to make a new commit, using the current index contents, which now match the current work-tree contents (except for untracked files, which remain untracked as usual).
1The git read-tree
command is really a plumbing command, meant for making new fancy front-end commands that do something, well, fancy. For instance, someone might, someday, write a git revert-to
command, which just consists of a few status checks followed by git read-tree -u <hash>
and git commit
.
Upvotes: 3