$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: worktree.h
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Makefile
deleted: zlib.c
Untracked files:
(use "git add <file>..." to include in what will be committed)
newfile.x
在这里,我修改了worktree.h并在其上运行了git add,因此HEAD和索引副本是“不同的”,我们在Changes to be committed部分中看到了这一点。 我修改了Makefile,但 * 没有 * git add这个,* 删除了 * zlib.c,* 没有 * git add删除,并创建了一个全新的文件newfile.x,* 没有 * git add这个文件,所以在标题为Changes not staged for commit的部分,Git将Makefile列为modified,而zlib.c是deleted,但是它没有在这里列出newfile.x,而是在Untracked files部分。
...--G--H--I--J--K <-- main, origin/main
\
L <-- branch-xyzzy (HEAD)
注意我们是如何在不改变提交的情况下重新绘制图形的,要习惯图形绘制经常变化的事实:无论你使用什么绘图软件,比如GitKraken内置的软件--你用gitkraken标记了你的问题--都有自己的偏好。它们可能与你的偏好匹配,也可能不匹配。重要的是从提交到先前提交的箭头,以及指向特定提交的各种名称。从提交到提交的箭头不能改变。因为 any commit的任何部分都不能改变,但是来自name的箭头可以改变。 我们经常用到最后一点。例如,现在您有:
...--G--H--I--J--K <-- main, origin/main
\
L <-- branch-xyzzy (HEAD)
2条答案
按热度按时间uhry853o1#
zip文件不是Git存储库,不能用作Git存储库。
一个Git仓库,在它的核心,实际上是一个巨大的 commits 的集合。**每个commits就像一个完整的源文件zip文件。**所以一个仓库,实际上,是一个不断扩展的 * 许多 * zip文件的集合。
要“正确地”处理这个问题--这可能会有点痛苦,甚至非常痛苦,这取决于您对原始zip文件所做的操作和/或您的总体编程能力--您可以做的是,从您所做的尝试开始:
1.把你现在有的文件放在某个地方,不要碍事。
1.使用
git clone
为你自己创建一个Git仓库,这个克隆将被你正在复制的仓库中的所有提交所填充。1.找到你让Git生成zip文件的原始提交,然后创建一个新的分支来选择这个提交,供你自己使用。
我们稍后会回到步骤3,但首先我们应该讨论更多关于提交的内容。上面简短的一句话描述(“像zip文件”)并没有 * 错 *,但没有抓住提交的真正本质。(如果你不耐烦,并且已经知道了所有这些,滚动到最后。)
什么是Git提交
每个Git提交都是:
1.编号。任何人在任何地方的每一次提交都会得到一个 unique 编号。为了实现这一点,这些编号都是 * 巨大的 *,而且看起来是随机的(尽管它们实际上只是加密哈希函数的输出)。它们对人类来说毫无用处,但提交编号是Git * 找到 * 一次提交的方式,所以Git * 需要 * 它们。Git称这些为 *hash ID *,或者更正式地说,*object ID *。
在元数据内部,Git为自己保留了一条至关重要的信息:每个提交都会记住之前某个提交的哈希ID。实际上,它是一组提交的复数ID,但 * 大多数 * 提交在这里只有一个。Git将记住的哈希ID称为 parent commit,而提交本身就是父提交的 child。
因为哈希ID是每个提交的完整内容的加密哈希,所以不可能在提交后 * 修改任何提交 *。而且,因为哈希ID是不可预测的--它们包括提交的时间 *-所以不可能在我们提交的任何提交中包含未来提交的哈希ID。所以提交必须只记住它们的父提交。而不是他们的孩子
所有这些的结果通常是一个简单的线性提交链:
其中
H
代表我们最近一次提交的实际哈希ID,不管它是什么。提交H
在其元数据中包含之前提交的原始哈希ID(父)提交G
,所以我们说H
* 指向 *G
,同时G
也是一个提交,所以它也有元数据,其包含 * 其 * 父F
的原始散列ID:G
指向F
。F
又向后指向更早的某个提交,以此类推。这是一个仓库中的历史记录,它只不过是仓库中的提交,我们所要做的就是找到最新的提交:上图中的
H
。但请注意,历史可以 * 偏离:*这些提交中哪一个是“最新的”?答案实际上是 * 两者都是:*
J
是 * 您的 * 最新提交,而L
是 * 他们的 * 最新提交。这是分支的一种形式。现在,你确实会在你的克隆中提交,你的朋友也会在他们的克隆中提交,但是在某个时候,必须有人协调所有这些克隆。有很多工具可以处理这个问题,但是我们不会在这里开始讨论。这只是想指出单词 * 分支 * 的很多含义,这个单词在Git中被过度使用,但在某种程度上,我们还是坚持使用它。
更好地概述Git存储库
我已经说过,仓库的核心是一个提交的集合--一个数据库,这是真的,但同样没有说明我们将如何“使用”它。事实上,仓库更像是“几个”数据库,其中一个是提交和其他内部Git对象,“加上”另一个大的和许多小的。
由于人类不善于记住哈希ID,Git给了我们一个简单的解决方法:它提供了一个 names 数据库。这些名称包括但不限于Git所称的 * 分支 * names。一个名称,在Git中--一个分支名称,或者一个标记名称,或者Git所称的 *remote-tracking分支名称 *,我称之为 * remote-tracking分支名称 *(因为它们实际上根本不是 branch 名称)--Git中的每个名称用于存储 * 一个 * 哈希ID。
这就是我们所需要的!一个哈希ID就足够了。当名称 * 是 * 一个 * 分支 * 名称时,根据定义,这个哈希ID就是该分支上的 * 最新 * 提交:
在这里,提交
H
是main
* 上的 * 最新 * 提交 *,而不是最新提交:I
,J
,K
和L
都是后来的,但它是main
* 上最新的 *,这就是Git分支的定义,提交J
是my-feature
* 上最新的 *。某个分支上的实际提交集是 * 我们从最后开始向后查找所能找到的所有提交 *。所以直到
H
的提交都在 * 所有三个分支上 *。如果你习惯了其他版本控制系统,那么同时在多个分支上提交的想法可能会非常奇怪。但这就是Git的工作原理。关于分支名称的另一个问题是它们会移动,如果
I-J
看起来是正确的,我们可以通过将名称main
沿着I-J
行向前移动,使它们在main
上移动:现在所有到
J
的提交都在这两个分支上,而K-L
的提交只在bob-feature
上。或者,如果这是个错误,我们可以强制名称main
后退两步,再次回到H
。这告诉我们如何在Git仓库中使用分支名称:它们帮助我们--Git--通过查找我们想要声明的分支的 * 最新 * 提交来 * 查找 * 提交。提交本身不会,也不能移动:它们都是一成不变的。(我们可以改变我们“画”它们的方式:例如,我们没有理由把
my-feature
放在第一行,或者我们可以把新的提交垂直地画得更高,更低,或者我们喜欢的任何地方,但是提交本身实际上是不可变的。您的工作树和索引
如果一个提交保存了一个快照,并且是不可变的--它确实是不可变的--那么我们如何才能完成任何实际的工作呢?事实上,一个提交中的文件不仅仅是永久冻结和压缩的(就像它们在zip压缩包中一样),而且在整个仓库内容中也是“去重复”的,并且只有Git自己可以读取。我们必须让Git从提交中提取文件,然后才能使用它们。
一个普通的仓库会提供一个工作区--Git称之为 working tree 或 work-tree--你可以在这里做你的工作。当我们 checkout 一个提交时,Git会从快照中保存的文件填充这个工作树。
由于我们使用Git所做的大部分工作都涉及到提交新的文件--向仓库添加更多的历史记录--你现在通常会修改其中的一些文件,可能会创建新的文件和/或删除现有的文件,然后你会希望Git从更新后的文件中提交新的文件。你只需要运行他们的“commit”动词。Git在这里并不简单。
出于各种各样的原因,你可能会同意或不同意,Git现在强加给你这个东西,Git称之为 index,或 staging area,或(现在很少)cache。为什么这个东西有三个名字有点神秘:我认为这是因为原来的名称 index 很糟糕,而名称 cache 更糟糕。名称 staging area 至少反映了大多数情况下你是如何使用它的。但它并不是那么具有描述性。我自己对Git索引的一行描述是:索引保存了你建议的 next commit。
在其他系统中,当你使用他们的提交动词时,他们会查看你的工作树来查看你做了什么。Git会查看 * 它的索引 。无论Git的索引中有什么文件, 那些 * 都是要提交的文件。这意味着索引有效地保存了要提交的文件的 * 副本 *。
Git的内部格式可以删除重复文件,这是提高Git效率的关键:如果没有它,由于每个提交都保存了每个文件的完整副本,你的仓库很快就会变得臃肿。但大多数提交大多会重用之前提交的文件。通过只存储一个副本,只读和压缩--最终是超级压缩--Git保持了合理的存储需求。
与此同时,Git的索引区,也就是暂存区,是以压缩和去重的格式存在的。文件的索引副本和提交副本之间的区别在于,你可以让Git替换索引副本(删除它,并放入另一个压缩和去重的副本)。提交不能改变,但索引可以。
因此,当你第一次 * checkout * 某个提交,使其成为当前提交时,Git会从该提交中填充工作树,同时填充其索引。现在,你提议的下一个提交与当前提交匹配。
当你修改工作树副本时--Git * 不使用 *--它们会逐渐变得与索引副本不同。索引副本与当前的提交副本
HEAD
匹配。不过在某个时候,你已经准备好提交一些文件了。此时你必须对这些文件运行git add
。1git add
的作用很简单,只要您了解索引即可。如果文件 * 是 * 重复的,Git会丢弃刚压缩过的副本,重新使用旧的;如果不是 * 重复的,Git会安排文件在提交完成后永久保存,并使用新的内部对象更新索引。无论哪种方式,索引副本现在都与工作树副本匹配,只是索引副本已经准备好提交了。或者,换句话说
git add
更新您建议的下次提交。1如果你愿意,你可以使用
git commit -a
作为一个快捷方式。如果你是Git新手,这是很诱人的。* 不要这样做!* 这是一个陷阱!它让你 * 避免思考Git的索引 *,但最终Git会用索引的一些令人惊讶的方面扇你一巴掌。你需要记住Git的索引,即使它只是一种背景存在。不过,值得一提的是,
git commit -a
实际上是把git commit
变成了git add -u && git commit
,也就是说,首先Git尝试像git add -u
那样更新索引,然后,一旦成功,commit就继续正常的操作,不过这里有很多棘手的东西,这与预提交钩子和其他问题有关。初学者最好避免使用git commit -a
,而一旦你是一个高级Git用户,你往往会出于其他原因而 * 仍然 * 希望避免使用git commit -a
。git status
、未跟踪文件和.gitignore
在我们实际模拟
git commit
之前,有必要先简单地看一下git status
命令和Git所称的 untracked files。untracked files可以被“ignored”,这是一种用词不当。因为你的工作树是你的,而且它只是你电脑上的一个普通目录(或者文件夹,如果你更喜欢这个词的话),里面存放着普通的文件,所以你可以在这里做任何你想做的事情,而Git不会知道你在做什么。
这一点,再加上你必须在文件上运行
git add
之后,Git才会费心去 * 看 * 你做了什么,这使得使用Git非常痛苦。我们有git status
。一旦你理解了git diff
的功能,git status
的功能就很容易描述了。(git diff
所做的事情......如果我们要讨论所有细节的话,就不那么简单了,但现在我假设您已经知道了。)git status
所做的部分工作是为你运行两个git diff --name-status
命令,第一个命令比较HEAD
--当前commit-vs Git的索引,它没有显示实际的差异,但是对于任何 * 相同 * 的文件,它什么也没说,对于任何 * 不同 * 的文件,它会这么说。这意味着你可以立即知道你在Git的索引中 * 修改了 * 哪些文件。这些文件在你 * 建议的下一次提交 * 中是不同的。如果你现在提交它们,它们在你的新提交中也会不同。
这里没有提到的文件必须在当前提交和建议的下一次提交中是相同的......或者,也许它们根本不在
HEAD
和Git的索引中。也许它们都是新文件。如果它们在Git的索引中,它们将在这里显示为“新文件”。git status
输出的这一节将这些文件列为要提交的文件。并且每一个都是新的、修改的或删除的:它在索引中是新的,或者HEAD
和index都有该文件,但它们不同,或者它在HEAD
中,但 * 不在 * 索引中。为你收集了这个列表之后,
git status
现在继续运行第二次diff,这次它比较Git索引中的文件和你工作树中的文件。not staged for commit
。not staged for commit
)。这最后一组文件有一个特殊的状态,它们是你的“未跟踪文件”。任何在你的工作树中的文件,但现在不在Git的索引中,都是未跟踪文件。
git status
命令将最后一组未跟踪文件与前两组文件分开:在这里,我修改了
worktree.h
并在其上运行了git add
,因此HEAD
和索引副本是“不同的”,我们在Changes to be committed
部分中看到了这一点。我修改了
Makefile
,但 * 没有 *git add
这个,* 删除了 *zlib.c
,* 没有 *git add
删除,并创建了一个全新的文件newfile.x
,* 没有 *git add
这个文件,所以在标题为Changes not staged for commit
的部分,Git将Makefile
列为modified
,而zlib.c
是deleted
,但是它没有在这里列出newfile.x
,而是在Untracked files
部分。这里的未跟踪文件被分离出来主要是出于一个原因:很多我们使用Git的东西会产生很多未被跟踪的文件。2我们需要一种机制来告诉Git两件事:
git add .
或类似命令来添加 * 所有内容 *,也不要 * 添加 * 这个文件。(We我还没有真正涉及到“添加所有内容”的操作,但是一旦
git status
显示了正确的内容,它们就非常方便了。我们可以添加 * 所有内容 *,或者某个特定子目录中的所有内容,或者其他任何内容。Git * 不会 * 添加任何我们也告诉它不要再提的 * 未跟踪 * 文件。有很多细节需要理解,所以让我们到此为止,继续下一节。
从Git的索引创建新的提交
一旦你按照你想要的方式排列了Git的索引,
git status
* 打印 * 你想要它 * 打印 * 的内容--你想要显示的文件确实会显示在to be committed
部分,并且它们包含了你想要它们包含的内容,但不会显示在not staged for commit
部分--你只需运行:Git现在会从你那里收集所有需要进入新提交的 * 元数据 *,这些元数据是Git需要从你那里获取的。
user.name
和user.email
设置,以决定将什么放入此部分;.git/COMMIT_EDITMSG
上打开一个编辑器(您可以使用-m
简化此操作);以及Git还会将索引中的所有文件转换成一个新的永久冻结快照,放入新的提交中,然后将所有这些文件作为新的提交写入,新的提交将获得一个新的唯一哈希ID。
现在,让我们假设,在这一点上,我们有这样的情况:
也就是说,我们有两个分支名称
main
和my-feature
,它们都 * 选择了提交H
*。我们 * 使用了 * 名称my-feature
。这意味着 * 当前提交 * 是提交H
。Git将从提交H
中的任何内容填充工作树及其索引。git commit
命令现在已经获取了索引内容,冻结了它们,添加了必要的元数据,并写出了一个新的提交,它获得了一些新的哈希唯一哈希ID,但我们在这里只称之为“commitI
“:git commit
的 * 最后一步 * 是将I
的实际哈希ID(不管是什么)写入 * 当前分支名称 。由于HEAD
附加到名称my-feature
,因此, 得到更新的分支名称 *。因此,现在名称my-feature
指向提交I
。根据定义,我们的新提交I
现在是分支my-feature
上的 * 最新 * 提交。您的分支名称是 * 您的;* 远程跟踪的名字记住 * 他们的 *
现在我们来看看另一个地方,与其他版本控制系统相比,Git有点奇怪。在许多系统中,分支名称是非常可靠的,可以永久保存,并且 * 每个 * 克隆仓库的人都在任何地方使用相同的分支名称。但在Git中却不是这样!相反,Git仓库中的 * 分支名称 * 是 * 特定于该仓库的 *。
(They必须如此,因为我们的新提交的哈希ID会直接进入分支名称。此时,我们只能更新我们的仓库,而不能更新任何人的仓库。
因此,当你运行
git clone
将仓库复制到你自己的笔记本电脑上时,或者复制到任何地方时,你的Git会复制它们所有的提交,但不会复制它们的分支名称。你的Git不会将它们的分支名称复制到你的分支名称中,而是将它们的分支名称重新命名 *。而是 * 远程跟踪名称 *。如果你把另一个Git叫做
origin
--这是“另一个Git”的标准名称,当你git clone
-d来自另一个Git时,你的Git会把他们的main
变成你的origin/main
,你的Git会把他们的feature
变成你的origin/feature
。你的Git会把它们的分支变成你的origin/*
远程跟踪名称。(Git调用这些 *remote-tracking分支名称 *,正如我之前提到的,但它们根本不是 * 分支名称 ,它们只是你的Git记住 * 别人的 * 分支名称的方式,在其他仓库中, 他们前面没有
origin/
*,这就是为什么我只调用它们 remote-tracking名称: 你的Git记住了其他仓库的分支名称,但不是 as 分支名称。复制了所有提交,并将所有分支名称转换为远程跟踪名称后,Git现在遇到了一个问题:您没有分支名称。您的Git将使用什么名称来附加
HEAD
?没有任何名称!通常解决这个难题的方法是Git现在在仓库中创建 * 一个 * 分支名称。哪一个?这就是
git clone -b
的作用:你告诉你的Git要创建哪个名字,基于他们的分支名称之一。如果你不使用-b
--大多数人不这样做--你的Git会问他们的Git他们 * 推荐 * 什么名字。(取决于你正在克隆的Git仓库的宿主)。所以他们推荐他们的main
,例如,现在你的Git会从你的origin/main
生成你自己的main
,它会记住你的main
(哇!):你的Git现在检查出这个分支名称,一切正常:你的 * 当前分支名 * 是
main
,你的 * 当前提交 * 是main
选择的任何提交(在本例中,我像往常一样把它画成散列H
)。如果它们有其他分支,你的Git仓库可能会更像这样:
您的每个远程跟踪名称都是为了查找 * 他们最近的 * 提交而存在的,就像他们的每个 * 分支 * 名称在他们的存储库中是为了查找对他们来说最新的提交一样。
稍后,你可以运行
git fetch
。当你运行git fetch
时,你的Git会根据名字查找他们的名字(origin
:只涉及一个Git仓库,所以它只有一个标准名称),调用名称origin
下列出的URL,并询问它们的分支名称和最新提交哈希ID是什么。如果这些最新提交与 * your * 仓库中的远程跟踪名称匹配,则什么也不做。如果不匹配,则您的Git可以从它们那里获取任何 * new * 提交,现在你的仓库拥有了他们所有的提交,加上你自己的任何你还没有给他们的提交。你的Git现在更新你的远程跟踪名称来记住他们最新的提交。我们终于准备好解决您的问题
让我们画出你做了什么:
因此,您的zip文件 * 代表 * commit
H
,它缺少元数据,但包含快照中的所有 * 文件 *。H
的所有 * files *,但是没有Git仓库。现在你在笔记本电脑上有了一个仓库,还有一个文件夹,里面装满了提交
H
的文件,但在笔记本电脑的其他地方已经修改过了。出于清洁的目的,您 * 想要 * 做的是 * 找到哪个提交是提交
H
*。你可以运行
git log
,它会一个接一个地溢出提交。如果它们有 * branches and merges *,这会变得很复杂,你应该通读Pretty Git branch graphs,但如果没有,你可以通过日期或其他方式搜索,找到提交H
。实际的哈希ID会很大,很难看,看起来很随机,所以它们不会有任何帮助。(要使用它们,您可能需要使用鼠标进行剪切和粘贴:尝试键入一个字符确实很容易出错!)有一个可能的捷径。**如果你仍然有原始的zip文件,看看它的元数据。有一个文件注解保存了实际的哈希ID。抓住它(用鼠标或任何东西),你就成功了!**如果没有,你如何找到正确的哈希--我在这里称之为
H
--取决于你。你可以使用的另一个技巧是:git diff
可以将 * 任意 * 提交与 * 任意文件树 * 进行比较,即使文件树在Git仓库之外。您可以运行:并得到一个比较列表。如果你看到的修改都是你的修改,那么这里的
hash
很可能就是H
。你可以用git log
的日期来得到一个候选的git diff
的短列表,然后用试错法来找到最接近的提交。假设您 * 已经 * 找到了哈希ID
H
,那么您现在要做的就是 * 创建一个直接指向此哈希ID的新分支名称 *。(pick一个更好的分支名称,并再次使用鼠标来剪切和粘贴哈希ID)。现在,在 * your * repository中,您有了:
现在可以运行
git checkout branch-xyzzy
:你的工作树中的文件现在是那些来自提交
H
的文件。从你处理压缩包的地方复制文件,使用git diff
和/或git status
来找出git add
的文件,或者只使用git add .
,然后运行git status
,你就可以提交了!你的新提交会得到一个新的,唯一的哈希ID,名称branch-xyzzy
将指向它:或者,等同于:
注意我们是如何在不改变提交的情况下重新绘制图形的,要习惯图形绘制经常变化的事实:无论你使用什么绘图软件,比如GitKraken内置的软件--你用gitkraken标记了你的问题--都有自己的偏好。它们可能与你的偏好匹配,也可能不匹配。重要的是从提交到先前提交的箭头,以及指向特定提交的各种名称。从提交到提交的箭头不能改变。因为 any commit的任何部分都不能改变,但是来自name的箭头可以改变。
我们经常用到最后一点。例如,现在您有:
你可能想使用
git rebase
。这会将提交 * 复制 * 到新的和改进的提交中。提交L
可能没问题,但如果它构建在提交K
上,可能会 * 更好 *。你实际上不能这样做,但你可以创建一个 * 新的和改进的 * 提交--我们称之为L'
--它 * 可以 * 做到以下几点:如果我们现在 * 删除 * 旧的 name,使提交
L
难以找到,并再次使用名称指向L'
而不是L
:然后使用任何提交查看器来 * 查看提交 ,看起来好像我们更改了提交
L
。新的副本L'
具有不同的 * 哈希ID,并向后指向K
,但是做了H
-vs-L
所显示的相同的“更改”,它有L
所拥有的相同的“提交消息”,所以如果我们不记得哈希ID--没有人记得--我们可能都不知道发生了什么!xe55xuns2#
不需要太复杂。任何文件夹都可以是git repo,只要它包含.git文件夹。我做了以下操作:
mv .git/ ~/new-location/.git/
)将.git文件夹复制到下载的zip文件夹现在创建一个新分支,提交所有内容,合并到main/master并修复所有冲突,然后再次提交,就完成了。