Git 洗掉了我上個月一直在作業的專案的所有檔案。基本上我嘗試將我的專案推送到 Github 但它沒有用,所以我使用了“Pull (rebase)”并洗掉了所有內容(你真的相信嗎?)。有什么我可以做的嗎?這是完整日志的鏈接,幸好我有。
https://pastebin.com/4W8PAj3M
> git show --textconv :dist/index.js
> git ls-files --stage -- D:\CODE\dist\index.js
> git cat-file -s d6cf00e78b8901535facadfd2f1b02afefe5ec2f
> git show --textconv :src/index.ts
> git ls-files --stage -- D:\CODE\src\index.ts
> git cat-file -s f0ae144245ab480bd437b97be9d58d267399b602
> git push CHANapp main
uj5u.com熱心網友回復:
Git實際上并沒有洗掉所有內容(還沒有!)。(這個git pull命令曾經能夠做到這一點,早在 2005 年左右,當時我對自己做了一兩次。我現在大多避免 git pull,出于多種原因,不僅限于此,揮之不去的傷疤是其中之一原因。??)然而,你處于一個糟糕的角落。要擺脫它,請從git rebase --abort(但請先閱讀)開始。
您可能已經關注了shrey deshwal 的回答,這可能會有所幫助,但您仍然會處于這種極端情況。這里的問題是您啟動了一個 rebase,但從未完成或終止它。您必須選擇這兩個操作之一,“完成”現在可能不可行。請注意,當您使用(來自其他答案)或(來自我的)時,未提交的作業可能會被丟棄。幸運的是,我在您的歷史記錄中看到了一個(嗯,)命令,盡管沒有跡象表明這是否成功。還有一個事實是,您顯示的輸出有 6 次提交,它試圖重新設定基礎(但該 pastebin 日志有一些奇怪的屬性)。git resetgit rebase --abortgit commit> git -c user.useConfigOnly=true commit --quiet --allow-empty-message --file -git rebase
如果一切順利,git rebase --abort將完全按照您開始時的設定進行設定。這會讓您恢復您的檔案(這顯然很好),但不會告訴您您需要知道什么。
龍:在繼續之前你需要知道什么
從本質上講,Git 是關于commits 的。剛接觸 Git 的用戶通常認為這是關于檔案的,但事實并非如此。提交保存檔案,但 Git 是關于提交的。或者,他們認為這是關于分支,但也不是關于分支:分支名稱幫助我們找到提交,但 Git 是關于提交的。
因此,最好將 Git 存盤庫視為一個大型資料庫,或者可能是一對或一組資料庫。這些通常是簡單的鍵值存盤。提交和其他內部Git物件的資料庫由索引散列的ID,這是大的,丑陋的,看上去比較隨意的數字,在表示十六進制,如e9e5ba39a78c8f5057262d49e261b42a8660d5b9為實體。Git需要此哈希 ID 才能提取提交,或 Git 用于使提交作業的任何其他內部物件。幸運的是,Git 不會讓我們記住它們;我們稍后會看到它是如何作業的。
知道了Git是所有關于提交,它理應一個知道什么是提交的。我們已經知道,它的編號,即大丑隨機找哈希ID是一個數字,每次提交獲取一個獨特的一個,具體到一個特定的承諾-但什么是在一個承諾?有兩個部分:
每個提交保存每個檔案的完整快照。提交中的檔案以一種特殊的、只讀的、僅限 Git 的、壓縮和重復資料洗掉的形式保存,只有 Git 可以讀取,實際上什么也不能寫入。
重復資料洗掉處理這樣一個事實,即大多數提交大多與前一次提交具有相同的檔案。檔案內容存盤為內部物件,這些物件也被賦予了數字(盡管這些數字并不是完全唯一的:每次存盤相同的檔案內容時,它都會獲得相同的數字:這就是重復資料洗掉的作用)。檔案的名稱存盤為其他內部物件。如果您的計算機運行 Windows 或 macOS,這允許存盤庫存盤其名稱您的計算機無法“發音”的檔案。1
除了快照,每個提交存盤一些元資料:關于提交本身的資訊。這包括提交作者的姓名和電子郵件地址 - 您的新提交從您的
user.name和user.email設定中獲取。它包括一些日期和時間戳。它包括一個日志訊息,這git log和git show將顯示。對于Git 本身來說至關重要的是,每個提交都存盤了一個先前提交哈希 ID的串列。
大多數提交只存盤一個這樣的哈希 ID,我們稱之為提交的父級。提交則是該父級的子級。孩子持有父母的 ID,所以孩子知道它的父母是誰。但是,父級從不持有其任何子級的 ID。這是因為,為了使哈希 ID 起作用,Git 必須在提交后立即凍結提交。(這適用于所有 Git 的內部物件。)這意味著任何提交的任何部分都不能更改,甚至 Git 本身也不能更改。我們讓孩子今天提交,如果明天它成為一個新孩子的父母,那么,現在太晚了:孩子的 ID 不能放入父母。
所以這些鏈接,孩子指向它的父母,只有一種方式,向后。(這是 Git 中的一個普遍主題:它向后作業。)如果我們繪制一系列單親提交,右側有較新的提交,使用大寫字母代表哈希 ID,我們會得到類似這個:
... <-F <-G <-H
這里H代表最新提交的哈希 ID 。它包含較早提交的哈希 ID G:這是從 出來的箭頭H,針對G。 G然后指向F,它指向更遠的地方,依此類推。
Since each commit holds a full snapshot (plus the de-duplication), Git can pretty easily pull the snapshot out of H's parent G and out of H itself and compare them. Where files are the same, Git can say nothing. Where files in the two commits differ, Git can then compare the contents of the files and produce a recipe: *Do this to the G version of file F, and you get the H version of F. Then Git can step back one hop, and do the same with F-vs-G. Having shown commit G this way, Git can step back yet again and show F, by comparing against its parent (presumably E), and so on.
This backwards-one-hop-at-a-time thing is how git log works: without -p, it shows each commit's author and log message and then moves backwards. With -p, it shows the same thing, adds the diff produced by comparing the parent's snapshot with this commit's snapshot, and then moves backwards. (This is all just for ordinary, single-parent, commits; when we introduce two-parent merge commits, things get complicated for git log.)
1Technically, this is file-system-dependent: a Linux box with an NTFS file system has the same problems as a Windows box, and a macOS system on which you've made a case-sensitive volume can deal with two files, one named README and one named ReadMe, at the same time, just like Linux. But the default setups on Windows and macOS fall short when compared to Linux.
Branch names help us (and Git) find commits
With our simple linear setup with just one branch, we need to answer one question to get Git to work: What's the latest commit? That is, in our drawing, we had H standing in for the actual latest-commit hash ID. But H is really some ugly random-looking thing, ef9b31c... or whatever. We could write this on a whiteboard (and then typo it all day), or save it in a file for cut-and-paste. But why not let Git save it, in a database or something?
This is where the second main database in each Git repository comes in. Each Git repository has its own database of name-to-hash-ID mappings. A branch name, in this database, remembers which commit we want to say is the latest commit that is "on" that branch:
...--F--G--H <-- main
Here, the name main holds the actual hash ID of commit H. So main points to H, just like H points back to G and so on. I get lazy about drawing the arrows from commit-to-commit because of text font limitations on StackOverflow (and because of laziness ??), but they're still there. Since they are part of the commits, they can't change, and always point backwards.
Note that if we have more than one branch name, it's possible that both branches will point to the same commit, like this:
...--F--G--H <-- develop, main
This means all these commits are on both branches. That's another thing that's weird about Git, compared to most other version control systems: commits, in Git, are on as many branches as you like. The branch names just find the last commit.
Since H is the last one on each branch, it doesn't matter which name we pick, but we do need to pick one—or, in your particular case with the rebase, none, but we'll come back to that later. To pick a branch, we use git checkout or git switch. In a new, completely empty repository, we have a problem, because a branch name has to point to some commit, and with no commits yet, we can't have any branch names yet. So GitHub will make a new repository with one initial commit in it, so that it can have a branch name:
A <-- main
When we clone this initial GitHub repository, we get a copy of the one commit A, and no branches at all. Then our Git uses information from their (GitHub's) Git programs to figure out which branch name to create in our repository; with only one name, our Git picks our main, and checks that out:
A <-- main (HEAD)
The HEAD-in-parentheses thing is how Git knows which branch name to use. If we now make some more names:
A <-- dev, main (HEAD), next
our one commit is now on three branches. We can now pick one of these, with git checkout or git switch, and switch to that branch:
A <-- dev (HEAD), main, next
If we now make a new commit ... well, you already know how to do this, so we'll just make the new commit now. The new commit gets a new, unique, random-looking hash ID, which we'll call B. New commit B points back to the commit we were using, commit A:
B <-- dev (HEAD)
/
A <-- main, next
The magic trick Git pulls here is to write B's new hash ID into the current branch name. (Technically Git stores the hash ID into refs/heads/B in the names database.)
Note that our branch names are not on GitHub. Neither is our new commit B. Commit A is in both repositories—we got our A from them—and hence has the same hash ID in both repositories, because it is the same commit. Commit B is only in our repository now.
If we now git switch main, Git will remove, from our work-tree, all the files we committed into commit B. It will then extract, from commit A, all the files that go with commit A, and we will be in this state:
B <-- dev
/
A <-- main (HEAD), next
Any files we left untracked are neither in B nor in A; they just sit around in our working tree as untracked files. Git didn't remove them when we moved away from B, because they are not in B. Git didn't extract them from A, because they are not in A either. So they are still just sitting around in the working tree.
If we switch from main to next, we're now switching from commit A to ... well, commit A again. There's no need to remove and replace any files, so Git doesn't bother: all that happens is that Git moves the name HEAD over to attach it to next:
B <-- dev
/
A <-- main, next (HEAD)
If we make a new commit here now, the new commit points back to A, and next points to the new commit:
B <-- dev
/
A <-- main
\
C <-- next (HEAD)
If we switch back to main and make new commits, they point to A and then to the new commit:
B <-- dev
/
A--D--E <-- main (HEAD)
\
C <-- next
and so on. Eventually we have a whole mess of commits, which so far are still all linear: each child has just one parent. (Parent A has three children though.)
The thing about all of this is that the commits are what matter. The branch names just let us find the most recent. Git says that some commit is "on" a branch if, by starting with that name's "last" commit, we can work our way back to the commit in question. So commit A is on all three branches.
If we delete the name next, commit C is still there:
B <-- dev
/
A--D--E <-- main (HEAD)
\
C ???
Now, though, the only way to get commit C out of Git is to know its hash ID. Eventually Git will realize that C is going unused because it can't be found, and will toss it entirely, but we always have some grace period before this happens.
Note how untracked files—those that aren't in the commit—take no part in any of this. They just sit around occupying space in our working tree. But there can be a problem: if we have an untracked file path/to/F in our working tree, and we ask to switch to some commit that has a file named path/to/F in it, what happens? We'll hold off on this for now, although it plays a part in what you'll need to do.
Merging
You aren't merging, but it's worth a quick look at this before we move to rebasing, because rebasing uses git cherry-pick internally, and git cherry-pick uses a form of merging, internally. So we need to know how merging works.
Suppose we have achieved this graph:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
That is, right now we are using commit J, through the name br1. The parent of J is I and the parent of I is H. Meanwhile the name br2 locates commit L, whose parent is K, whose parent is H again. Commits H and earlier are on both branches, while I-J are only on br1 and K-L are only on br2.
If we now run:
git merge br2
Git will locate commit L. Merging is really about commits, not branches: once again we're just using the name to find the latest commit.
Git will now use the parent links from our current commit J and the other commit L. Working backwards from both branch tip commits, Git will find the best common ancestor: a commit that is on both branches, and is "better than" other commits that are also on both branches. "Better" in this case means roughly "closer to the branch tips", so H is better than G. The commit that Git finds this way is the merge base.
The merge is now ready to proceed, by comparing the merge base commit H snapshot against our current commit snapshot J to see what we changed, then comparing H against their snapshot L to see what they changed. Just as with the parent-to-child diff, this kind of diff shows:
- which files are the same, and which are different;
- for each file that is different, what to do to make it "the same".
By combining these two sets of diffs, git merge can come up with changes that will, applied to the snapshot in H, keep our changes (from H-vs-J) but also add their changes (from H-vs-L). There is a lot that can go wrong here, but if all goes well, Git does apply the combined changes to the merge base snapshot, and then make a new commit M:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
Commit M is special in exactly one way: it has two parents, instead of just one. It still has a snapshot, just like any commit: the snapshot is the one Git made when it applied the combined changes to the merge base snapshot. It still has an author (us) and a log message (the default one is the rather crappy merge branch br2 into br1). But instead of just the usual one parent J, M gets a second parent L too.
Note that when git log hits commit M, it has a problem with showing what changed in M. The usual method is to run a diff against the (single) parent, but M has too many parents. The default answer git log here uses is to silently give up: it just doesn't show a diff at all. Git has another problem too: should it follow the first parent back to J, or the second to L, or what? The default answer git log uses here is to follow both, in a somewhat random-seeming order.
(You actually have a lot of control here, but since this is not about merge and log, we'll just move on now.)
Dealing with multiple Git repositories
You mentioned GitHub. GitHub is a site that hosts Git repositories, among other things. This means that they—GitHub—have Git repositories, which have commits in them, and branch names and other names for finding commits.
I mentioned earlier that:
git clone <url>
copies a repository by copying all of their commits and none of their branches. This is true, but it obviously creates a problem. Suppose they—GitHub—have three commits in their repository, two found by the name main and one by the name develop:
A--B <-- main
\
C <-- develop
When we clone this, we get the commits, but no branches:
A--B ???
\
C ???
We need a way to know that their main means commit B, and their develop means commit C. To achieve that, our Git—our software, working with our repository—gets branch names from their Git and changes those into remote-tracking names:
A--B <-- origin/main
\
C <-- origin/develop
These remote-tracking names remember their branch names, as of the last time we ran git fetch (git clone is basically shorthand for running a bunch of commands, with git fetch being command #4 or #5 or so). Then, having copied their commits and tweaked their branch names, our Git does a specialized git checkout that creates a branch name. We use -b at git clone time to say which of their names we want copied. If we don't use -b—most people don't, most of the time—our Git asks their Git what they recommend; in this case, that's probably main. In all cases, our Git now creates our single branch from the origin/ version of that branch in our repository, which is from the branch of the same name in their Git. Our Git then checks out this commit to fill in our working tree (and staging area):
A--B <-- main (HEAD), origin/main
\
C <-- origin/develop
We now have the ability to add new commits:
D--E--F <-- main (HEAD)
/
A--B <-- origin/main
\
C <-- origin/develop
If they add new commits, say to their main and their develop, and we run git fetch now, we will get:
D--E--F <-- main (HEAD)
/
A--B--G--H <-- origin/main
\
C--I <-- origin/develop
Note how this kind of thing looks exactly the same as when we make our own branches. These remote-tracking names are different from branch names in two important ways:
- we won't update them: they're reserved to remembering the other Git's branches;
- in service of the first point, we can't get "on" them.
In a moment we'll have more about that last part, because it really matters a lot here. For now, just note that to get "on" some branch, we need to have Git attach HEAD to the branch name. Git will do that with our branch names, but not with our remote-tracking names.
Now that we have new commits on our main, we might like to send these new commits to their Git repository. But:
git push origin main
won't work, and the reason is simple enough. We have commit F as our last commit on our main. They now have commit H as their last commit. The git push command figures out which commits we have, that they don't, that they'll need for our git push: that's D-E-F. It then sends over those commits, so that they have, in their repository, this:
D--E--F ???
/
A--B--G--H <-- main
\
C--I <-- develop
Note how there is no name at all for F yet, and the name main—not origin/main, not fred/main, just plain main—finds commit H.
Our git push, having successfully sent commits D-E-F, now asks their Git to please, if it's OK, set their name main to point to commit F. If they did, they would have this:
D--E--F <-- main
/
A--B--G--H ???
\
C--I <-- develop
They have just lost their G-H commits. (The commits still exist, at least for some time, but can't be found: Git finds commits by starting with the names, then working backwards, and there is no name that finds H any more.) So they will say no, I won't do that (non-fast-forward, in Git jargon).
What this means is that we must now somehow combine our work, in D-E-F, with their work, in G-H. One way to do that would be to use git merge: combining work is, after all, what it's for. This is one of your options, and merge is simpler than rebase, because it's just one operation. But you may have a "rebases only" workflow, or a "rebases encouraged" one. If so, you will need to use git rebase.
Rebase is about copying commits
Your exact repository graph topology will vary. Consider using git log --graph or similar to view it, if you don't have a graphical viewer (see Pretty Git branch graphs). Let's draw a simplified view now though:
D--E--F--G--H--I <-- main (HEAD)
/
...--C--J--K <-- origin/main
Given that your rebase output mentioned 1/6, 2/6, and so on, I've drawn in six commits that need to be copied to new-and-supposedly-improved commits.
The way git rebase works is by:
First, listing out the commits to be copied: in this case that's
D-E-F-G-H-I. These commit hash IDs go into a file somewhere.Next, Git does a detached HEAD checkout of the target commit. In this case that's commit
K.Git now enters a loop. One commit at a time, Git tries to
git cherry-pickeach commit that is to be copied.2 Each of these cherry-picks is technically a mini-merge (although the result is an ordinary commit, not a merge commit). Since you have six commits to copy, that gives six chances to have a merge error.The effect of each cherry-pick is to take the diff from the commit's parent, to the commit, and apply that diff to the current working-tree. That is, Git "copies" the commit by turning it into changes. Git then has to apply those changes to whatever commit is checked out now. This means Git has to figure out where the lines went, in case what's checked out now moved the places the changes need to go—and that in turn means Git needs to diff the parent against the current commit. That "make two diffs and combine them" is just what we saw in
git merge, and that's how cherry-picking is merging.If all goes well, the result at this point looks like this:
D--E--F--G--H--I <-- main / ...--C--J--K <-- origin/main \ D'-E'-F'-G'-H'-I' <-- HEADNote how the name
HEADpoints directly to commitI', which is the one copied fromI. This is detached HEAD mode, which we'll come back to in a moment.Because all did go well, Git now forces the name—
main, in this case—that you were using, back when you started all of this, to point "here", whereverHEADpoints now. Git then "re-attaches your HEAD":D--E--F--G--H--I ??? / ...--C--J--K <-- origin/main \ D'-E'-F'-G'-H'-I' <-- main (HEAD)
Since nobody ever looks at raw hash IDs, it seems that Git has somehow overwritten the original commits with these new-and-improved ones. It hasn't, though: D-E-F-G-H-I still exist in your repository. You just can't find them, which means git log doesn't show them either.
Since the new commits now add on to origin/main, if you git push origin main now, you'll have your Git send D'-E'-...-I' to the other Git, and this time, they will add them on.
2Some forms of git rebase don't use git cherry-pick directly. In the most current version of Git, you must specifically ask for these older rebase forms. In old versions of Git, you must ask for the cherry-pick variant with -m or -i or several other flags. In most cases, these all work out the same, so it usually doesn't matter which one you use.
Your rebase is failing, leaving you in the detached HEAD mode
When Git stops in the middle of a rebase, we might have this:
D--E--F--G--H--I <-- main
/
...--C--J--K <-- origin/main
\
D'-E'-F' <-- HEAD
If we look at what we have in our working tree, it's from commit F', perhaps with an attempt to merge the F-to-G changes in to make G'. If we run git log, we see commits F', then E', then D', then K, etc. Our original G-H-I commits seem to be gone. Our original D-E-F commits show up as our copies, but they're not there.
Your git rebase output—starting at line 229 in your pastebin text—says:
Rebasing (1/6) Rebasing (2/6) Rebasing (3/6) error: The following untracked working tree files would be overwritten by merge: node_modules/.bin/acorn node_modules/.bin/acorn.cmd[mass snip]
That is, the cherry-pick or equivalent operation that's trying to copy this third commit (3/6) has run into a problem:
- there exist some untracked working tree files;
- there are committed copies of these files;
- so Git wants to overwrite the working tree files with the committed files.
In this particular case, these files are in node_modules. This particular directory's files should almost never be in any commit, so the commits that are about to be copied are "bad" in a sense (unless you've decided that, for whatever reasons are appropriate to your situation, you should commit them). If they had never been committed, you would not be having this issue.
In "detached HEAD" state, the name HEAD is not attached to any branch name. Using git reset won't fix this. One way to fix it is to run git checkout or git switch with a branch name, but for the rebase-failure case, this is the wrong answer.
What to do about this
To get out of the "detached HEAD" state, run git rebase --abort. When you started the rebase, you had:
D--E--F--G--H--I <-- main (HEAD)
/
...--C--J--K <-- origin/main
All these commits still exist. Running git rebase --abort says throw out what I have now (using git reset internally, in fact) and check out the branch I was on when I started this rebase. So you'll be back in that mode, with your index and working tree matching commit I again. Any untracked files that Git could leave undisturbed, Git will leave undisturbed, but any work you did after the rebase stopped, and have not committed, will be lost. Any work you did and then committed will be saved in a commit, but the commit's hash ID will become difficult to find (you'll need the git reflog command after aborting the rebase).
Now you'll need to decide: should these files be committed? Whatever you decide here ("yes" or "no") means that some existing commits are likely to become "bad" commits.
Next, you'll need to correct the bad commits:
- the files should be committed, so the mistake is that they weren't for a while; or
- the files should never be committed, so the mistake is that they eventually were.
Because the current working tree copies of these various node_modules files are not committed, you should also decide whether you need to save the current working tree copies (however many may be left after the rebase misadventures). If you do decide to save them, you can:
- commit them, or
- move them outside the Git repository.
The first method of course puts them into a new commit. The second method doesn't. So if they should never be committed, use the second method.
If they don't need to be saved—this is the usual case; these files are autogenerated, which is why we don't put them in commits in the first place—then you can either move them, or remove them, at your own discretion. Use whichever you prefer (moving them away lets you move them back, which will be fast but may get you the wrong files if the autogenerated ones would differ; removing them is easier, but may take a long time to autogenerate them again later).
完成此操作后,您將希望進行新的和改進的提交,以糾正舊的(和糟糕的?)提??交的問題。您可以使用git cherry-pick,尤其是git cherry-pick -n其次是手工操作,然后是git commit. 或者,您可以使用 來做到這一點git rebase,盡管這次您需要一種“就地”變基,幾乎可以肯定使用-i和edit。
(這個答案已經足夠了,因為任何后續步驟都取決于您必須做出的決定。)
uj5u.com熱心網友回復:
第一次使用 git reflog
參考日志或“reflogs”記錄了本地存盤庫中分支提示和其他參考的更新時間。參考日志在各種 Git 命令中很有用,用于指定參考的舊值。例如,HEAD@{2} 的意思是“HEAD 曾經是兩個移動前的地方”,master@{one.week.ago} 的意思是“master 曾經指向這個本地存盤庫中一周前的地方”,等等。
然后使用 git reset --hard master@{2}
其中重置回兩次更改前的值
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/349146.html
