提交以 zip 格式下载的 Github 更改
Posted
技术标签:
【中文标题】提交以 zip 格式下载的 Github 更改【英文标题】:commit change of Github downloaded as zip 【发布时间】:2021-12-03 02:10:54 【问题描述】:我正在和朋友一起做一个项目,但是我没有克隆 repo,而是将它下载为 zip 并进行了一些更改。与此同时,遥控器得到了我朋友的更新。现在,我的代码已经过时,我无法提交我的更改,因为 github 无法识别差异。 我尝试克隆到不同的位置,而不是将修改后的文件复制到克隆的位置,但是这样我可以提交我的更改,但使用过时的代码(不是我朋友所做的新更改)。 任何建议将不胜感激?
【问题讨论】:
嗨,哈利勒。您能否包括您收到的错误消息的输出?另外,您能否在您的问题中详细说明 .zip 是否包含.git
目录?
【参考方案1】:
zip 文件不是 Git 存储库,不能作为一个存储库使用。
从本质上讲,Git 存储库实际上是一个很大的提交集合。 每次提交就像源文件的完整 zip 文件。 因此,存储库实际上是一组不断扩展的 许多 zip 文件。
您可以做什么来“正确地”处理这个问题——这可能会有点痛苦,取决于你对原始 zip 文件所做的事情和/或你的整体编程能力——是这个,从你所做的尝试开始:
-
将您现在拥有的文件保存在某处,不碍事。
使用
git clone
为自己创建自己的 Git 存储库。此克隆将填充您正在复制的存储库中的所有提交。
不知何故找到原始提交,您让 Git 从中制作了一个 zip 文件。为您自己创建一个选择此提交的新分支。
我们稍后会回到第 3 步,但首先我们应该更多地讨论提交。上面简短的一句话描述(“就像一个 zip 文件”)没有错误,但没有抓住提交的真正本质。 (如果您不耐烦并且已经知道所有这些,请滚动到最后。)
什么是 Git 提交
每个 Git 提交是:
编号。任何人在任何地方所做的每一次提交都会获得一个唯一编号。为了使这项工作发挥作用,这些数字是巨大的,并且看似随机(尽管它们实际上只是密码散列函数的输出)。它们对人类来说毫无用处,但提交编号是 Git 找到提交的方式,因此 Git 需要它们。 Git 将这些 hash IDs 称为object IDs。
基本上由两部分组成。提交的两个部分是:
所有文件,就像在一个 zip 存档中一样(但存储方式完全不同);和 一些元数据,或有关提交本身的信息:提交者、时间、原因(他们的日志消息)等等。在元数据中,Git 为自己保留了一条至关重要的信息:每次提交都会记住某个先前提交的原始哈希 ID。实际上,它是一组提交的复数 ID,但 大多数 提交在这里只有一个。 Git 调用记住哈希 ID 的 parent 提交,并且提交本身是该父级的 child。
因为散列 ID 是每个提交的完整内容的加密散列,所以在提交后不可能更改任何关于任何提交的内容。而且,由于哈希 ID 是不可预测的——它们包括诸如提交的时间之类的东西——不可能将 future 提交的哈希 ID 包含在任何我们做出的承诺。因此,提交必须只记住他们的父母,而不是他们的孩子。
所有这些的结果通常是一个简单的线性提交链:
... <-F <-G <-H
H
代表我们最新提交的实际哈希 ID,不管它是什么。提交H
在其元数据中包含早期(父)提交G
的原始哈希ID,因此我们说H
指向 G
。同时,G
也是一个提交,所以它也有元数据,其中包含 its 父 F
的原始哈希 ID:G
指向 F
。 F
反过来指向一些更早的提交,依此类推。
这是存储库中的历史记录,只不过是存储库中的提交。所有(全部?)我们所要做的就是以某种方式找到最新的:H
在上图中。但请注意,历史可能分歧:
I--J [commits you might make starting from H]
/
...--G--H
\
K--L [commits your friend makes, also starting with H]
哪些提交是“最新的”?答案确实是两者:J
是你的最新的,L
是他们的最新的。
这是分支的一种形式。现在,您确实将在 your 克隆中进行提交,而您的朋友将在 他们的 克隆中进行提交,但在某些时候,必须有人协调所有这些克隆。有很多工具可以解决这个问题,但我们甚至不会在这里开始;这实际上只是为了指出branch这个词的许多含义。这个词在 Git 中被严重过度使用,但在某种程度上,我们被它所困。
更好地了解 Git 存储库
我已经说过,存储库的核心是提交的集合(数据库),这是真的,但同样没有说明我们将如何使用它。实际上,存储库更像是 几个 数据库,其中一个是提交和其他内部 Git 对象,加上另一个大的和许多较小的。 p>
由于人类不善于记住哈希 ID,Git 为我们提供了一个简单的方法:它提供了一个 names 数据库。这些名称包括但不限于 Git 所称的 branch 名称。一个名称,在 Git 中——一个分支名称,或者一个标签名称,或者 Git 称之为 remote-tracking branch names 我称之为 remote-tracking names 的东西(因为它们'实际上根本不是 branch 名称)——Git 中的每个名称都用于存储 一个 哈希 ID。
这就是我们所需要的!一个哈希 ID 就足够了。当名称 是 一个 branch 名称时,根据定义,该哈希 ID 是 latest 提交“在”该分支上:
I--J <-- my-feature
/
...--G--H <-- main
\
K--L <-- bob-feature
这里,提交H
是最新 提交main
。这不是最新的提交:I
、J
、K
和 L
都是后来的。但这是最新的main
,这就是 Git 分支的定义方式。提交J
是最新的my-feature
。
“在”某个分支上的实际提交集是我们可以通过从末尾开始并向后工作来找到的所有提交。所以通过H
的提交都在所有三个分支上。如果您习惯于其他版本控制系统,那么同时提交在多个分支上的想法可能会非常奇怪。但这就是 Git 的工作原理。
关于分支名称的另一件事是它们移动。如果提交 I-J
看起来正确,我们现在可以通过将名称 main
沿 I-J
行向前移动来使它们on main
:
I--J <-- main, my-feature
/
...--G--H
\
K--L <-- bob-feature
现在到J
的所有提交都在两个分支上,而K-L
的提交仅在bob-feature
上。或者,如果这是一个错误,我们可以强制名称 main
再次后退两步到 H
。
所以这告诉我们如何在 Git 存储库中使用分支名称:它们帮助我们——以及 Git——find 提交,通过找到我们想要声明的提交是 最新的 em> 为那个分支。提交本身不会也不能移动:它们都是一成不变的。 (我们可以更改绘制它们的方式:例如,我们没有理由必须将my-feature
放在顶行,或者我们可以垂直绘制更高或更低的新提交,或者随心所欲。但提交本身实际上是不可变的。)
你的工作树和索引
如果提交包含一个快照并且是不可变的——它确实如此——我们如何完成任何实际工作?事实上,提交中的文件不仅会一直被冻结和压缩(就像它们在 zip 存档中一样),而且还会在整个存储库内容中去重复,并且在只有 Git 本身可以读取的表单。因此,就像任何存档一样,我们必须先让 Git extract 文件 from 提交,然后才能使用它们。
因此,一个普通的存储库提供了一个工作区——Git 称之为 working tree 或 work-tree——您可以在其中进行工作。当我们签出一个提交时,Git 会从保存在快照中的文件中填充这个工作树。
由于我们对 Git 所做的大部分工作都涉及到提交 new 提交(向存储库添加更多历史记录),因此您现在通常会修改其中一些文件,并且可能会创建新文件和/或删除现有文件文件,然后你会希望 Git 从更新的文件中进行新的提交。在大多数版本控制系统中,这很简单:您只需运行它们的“提交”动词。 Git 在这里并不简单。
出于各种原因,您可能有一天会同意或不同意,Git 现在将 Git 称为的东西强加给您,例如 index 或 暂存区 ,或者(现在很少)缓存。为什么这个东西有三个名字有点神秘:我想是因为原来的名字index很差,而cache这个名字更差。 暂存区这个名字至少反映了你在大多数情况下是如何使用它的。但这并不是描述性的。我自己对 Git 索引的单行描述是 该索引包含您提出的 next 提交。
在其他系统中,当您使用他们的提交动词时,他们会查看您的工作树以查看您所做的工作。相反,Git 查看它的索引。无论 Git 索引中的文件是什么,那些 都是进入提交的文件。这意味着索引有效地保存了将进入提交的文件的副本。
Git 的内部格式会删除重复文件。这对于提高 Git 效率非常重要:没有它,因为每次提交都包含每个文件的完整副本,您的存储库将迅速变得肥胖。但大多数提交大多重复使用以前提交的文件。通过仅存储一个副本、只读和压缩(最终超级压缩),Git 保持了合理的存储需求。
同时,在Git 的索引,也就是暂存区,采用这种压缩和去重的格式。文件的索引副本和提交副本之间的区别在于,您可以让 Git 替换 索引副本(删除它,然后放入不同的压缩和去重副本)。提交不能更改,但索引可以。
因此,当您第一次签出某个提交,使其成为当前提交时,Git 会填充您的工作树——但也会填充 它的索引,从中犯罪。现在你提议的下一个提交匹配 当前提交。
当您修改工作树副本(Git 不使用)时,它们会逐渐与索引副本不同。索引副本与当前或HEAD
提交副本匹配。不过,在某个时候,您已准备好提交一些文件或文件。此时您必须在文件上运行git add
。1
git 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
的方式更新其索引。然后,一旦成功,提交继续其正常操作。不过这里有很多棘手的东西,与预提交挂钩和其他问题有关。作为初学者最好避免使用git commit -a
,一旦您是高级 Git 用户,您通常仍然出于其他原因想要避免使用git commit -a
。
git status
、未跟踪的文件和 .gitignore
在我们实际模拟git commit
之前,有必要简要了解一下git status
命令以及Git 所说的未跟踪文件。未跟踪的文件可以“忽略”,这有点用词不当。 跟踪的文件——那些未被跟踪的文件——不能像这样被忽略。
因为你的工作树是你的,并且因为它只是你计算机上的一个普通目录(或文件夹,如果你更喜欢这个词的话)保存普通文件,你可以在这里做任何你喜欢的事情,而无需Git 知道你在做什么。
那,再加上你必须在文件上运行git add
才能让 Git 费心看到你已经做了什么,这让使用 Git 变得很痛苦。为了降低疼痛程度,我们有git status
。一旦你了解了git diff
的作用,git status
的作用就很容易描述。 (git diff
所做的事情……如果我们要涵盖所有细节,那就不那么简单了,但现在我假设你知道。)
git status
的部分作用是为您运行两个 git diff --name-status
命令。第一个比较 HEAD
(当前提交)与 Git 的索引。它没有显示实际的差异,但是对于任何相同的文件,它什么都不说,而对于任何不同的文件,它都是这样说的。
这意味着您可以立即知道您在 Git 的索引中更改了哪些文件。这些文件在您提议的下一次提交中是不同的。如果您现在提交它们,它们将在您的新提交中有所不同。
此处未提及的文件在当前和提议的下一个提交中必须相同 ...或者,它们可能不在HEAD
和 Git 的索引。也许它们是所有新的文件。如果它们在 Git 的索引中 ,它们将在此处显示为“新文件”。这部分git status
输出将这些列为要提交的文件,每个文件要么是新的、修改的或删除的:它是索引中的新文件,或者HEAD
和索引都有文件它们是不同的,或者它在HEAD
但不在在索引中。
已经为您收集了该列表,git status
现在继续运行第二个差异。这次它将 Git 索引中的文件与工作树中的文件进行比较。再一次,我们可以:
not staged for commit
。
工作树中丢失的文件:它们在索引中,但对您不可见。索引内容不能直接观察到,但是您可以使用任何普通的文件列表和查看命令查看您的工作树内容,因为这些是普通文件。这个特定的文件已经不存在了,所以 Git 说这个文件被删除了(但仍然是 not staged for commit
)。
全新的文件:它们不在索引中,但在在您的工作树中。
最后一组文件具有特殊状态。这些是您的未跟踪文件。 工作树中的任何文件现在中的任何文件都是未跟踪的文件。git status
命令分离出最后一个未跟踪的列表文件,来自前两个:
$ 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
部分中。
这里的未跟踪文件被分离出来主要是出于一个原因:我们使用很多东西,Git 会创建一个 很多 未跟踪文件。我们需要一种机制来告诉 Git 两件事:
不要抱怨这个文件,并且 如果我使用整体git add .
或类似的方法来添加所有内容,也不要添加此文件。
(我们还没有真正涵盖整体“添加所有内容”操作,但它们非常方便,一旦git status
显示正确的内容。我们可以添加所有内容,或者一些特定的子目录,或者其他什么。Git 不会添加我们也告诉它关闭的任何 untracked 文件。)
要吸收很多细节,所以让我们在这里停下来继续下一部分。
从 Git 的索引进行新的提交
一旦你按照你想要的方式安排了 Git 的索引,git status
打印你想要它打印的东西——你打算显示的文件,确实出现在to be committed
部分,它们有你希望它们拥有的东西,而不是出现在not staged for commit
部分——你可以简单地运行:
git commit
Git 现在将从您那里收集所有需要进入新提交的 元数据,它需要从您那里获取 。特别是,Git 将:
阅读您的user.name
和 user.email
设置以决定在这部分中添加什么;
使用您的计算机时钟来确定时间和日期部分的时间和日期;
从您那里收集提交日志消息,通常是在.git/COMMIT_EDITMSG
上打开一个编辑器(您可以使用-m
来快捷方式);和
使用当前提交的哈希ID作为新提交的父。
Git 还会将索引中的所有文件转换为一个新的永久冻结快照以放入新的提交中,然后将所有这些写出作为新提交,获得一个新的唯一哈希 ID。
现在,让我们假设,在这一点上,我们有这种情况:
...--G--H <-- main, my-feature (HEAD)
也就是说,我们有两个现有的分支名称,main
和 my-feature
,它们都选择提交 H
。我们使用名称my-feature
。这意味着当前提交是提交H
。 Git 将从提交H
中的任何内容填充我们的工作树及其索引。从那时起,我们更新了 Git 的索引。
git commit
命令现在已经获取了索引内容,冻结了它们,添加了必要的元数据,并写出了一个新的提交,它得到了一些新的哈希唯一哈希 ID,但我们称之为“提交 @987654428 @"在这里:
...--G--H <-- main
\
I <-- my-feature (HEAD)
git commit
的最后一步是将I
的实际哈希 ID(无论是什么)写入当前分支名称。由于HEAD
附加到名称my-feature
,这就是要更新的分支名称。所以现在名称my-feature
指向提交I
。根据定义,我们的新提交 I
现在是分支 my-feature
上的最新提交。
您的分支名称是您的; 远程跟踪名称记住 他们的
我们现在来到另一个地方,与许多其他版本控制系统相比,Git 有点奇怪。在许多系统上,分支名称是一个非常可靠的东西,它会永远持续下去,并且 每个人 克隆存储库的人在任何地方都使用相同的分支名称。在 Git 中并非如此!相反,Git 存储库中的分支名称特定于该存储库。
(它们必须是,因为我们的新提交的哈希 ID 直接进入分支名称的技巧。我们只能更新 我们的 存储库,而不是其他任何人的,此时。)
因此,当您运行 git clone
将某个存储库复制到您自己的笔记本电脑或克隆的任何位置时,您的 Git 会复制所有提交,但不会复制它们的分支名称。您的 Git 不会使用它们的分支名称来创建您的分支名称,而是使用它们的分支名称并重命名它们。从技术上讲,它们完全不是分支名称:而是 远程跟踪名称。
如果你调用另一个 Git origin
——这是“另一个 Git”的标准名称,当你 git clone
-d 来自另一个 Git 时——你的 Git 将采用 他们的 em> main
并把它变成你的origin/main
。你的 Git 将把他们的feature
变成你的origin/feature
。你的 Git 会将他们的分支变成你的 origin/*
远程跟踪名称。
(正如我之前提到的,Git 称这些 远程跟踪分支名称。但它们根本不是 分支 名称。它们只是你的 Git 的方式记住其他人的分支名称。在另一个存储库中,他们前面没有origin/
。这就是为什么我只称他们为 remote-tracking名称: 你的 Git 正在记住一些其他存储库的分支名称,但不是 as 分支名称。)
复制了他们所有的提交,并将他们所有的分支名称转换为远程跟踪名称,你的 Git 现在有一个问题:你有 没有分支名称。您的 Git 将使用什么名称将 HEAD
附加到?没有名字!
解决这个难题的常用方法是 Git 现在在您的存储库中创建 一个 分支名称。哪一个?好吧,这就是git clone -b
的用途:根据分支名称之一告诉 Git 要创建哪个名称。如果您不使用-b
(大多数人不使用),那么您的 Git 会询问他们的 Git 他们推荐的名称。这往往是master
或main
(取决于谁在托管您正在克隆的 Git 存储库)。所以他们推荐了他们的main
,例如,你的Git现在从你的origin/main
创建你自己的main
,它记得他们的main
(哇!):
...--G--H <-- main (HEAD), origin/main
您的 Git 现在检查这个分支名称,一切正常:您的 当前分支名称 是 main
,而您的 当前提交 是 main
选择的任何提交. (在这种情况下,我像往常一样将其绘制为哈希 H
。)
如果他们有其他分支,您的 Git 存储库可能看起来更像这样:
I--J <-- origin/feature1
/
...--G--H <-- main (HEAD), origin/main
\
K <-- origin/feature2
您的每个远程跟踪名称都存在以查找 他们的最新 提交,就像他们的每个 分支 名称在其存储库中存在以查找适合他们的内容最新的提交。
稍后,您可以运行git fetch
。当你这样做时,你的 Git 按名称查找他们的(origin
:只涉及另一个 Git 存储库,因此只有一个标准名称),调用名称 origin
下列出的 URL,并询问他们的分支名称和最新的提交哈希 ID 是什么。如果这些最新提交与 your 存储库中的远程跟踪名称匹配,则无需执行任何操作。如果没有,您的 Git 可以使用哈希 ID 从它们那里获取任何 new 提交。现在,您的存储库拥有了他们所有的提交,以及您自己尚未提供给他们的任何提交。您的 Git 现在会更新您的远程跟踪名称以记住他们最新的提交。
我们终于准备好解决您的问题了
让我们画出你做了什么:
获取一个 zip 文件,将其下载到您的笔记本电脑上。这是由其存储库中的某些提交创建的存档:
...--G--H <-- main
您的 zip 文件因此代表提交H
。它缺少元数据,但它包含快照中的所有文件。
解压压缩文件。您现在拥有来自提交 H
的所有文件,但没有 Git 存储库。
处理文件。
发现错误,并将存储库本身克隆到您的笔记本电脑。
您现在在笔记本电脑的某处有一个存储库,以及一个文件夹,其中包含来自提交 H
的文件,但在笔记本电脑的其他地方已修改。您现在拥有的存储库 可能看起来更像这样:
...--G--H--I--J--K <-- main (HEAD), origin/main
出于清洁目的,您想要做的是找到哪个提交是提交H
。
你可以运行git log
,它会一一溢出提交。如果他们有分支和合并,这会变得很复杂,你应该通读Pretty Git branch graphs,但如果没有,你可以只按日期或其他内容搜索,以找到提交H
。实际的哈希 ID 会很大、丑陋且看起来很随机,因此它们无济于事。 (要使用它们,您可能想用鼠标剪切和粘贴:尝试输入一个真的很容易出错!)
有一条可能的捷径。 如果您仍有原始 zip 文件,请查看其元数据。有一个文件注释包含实际的哈希 ID。抓住它(用鼠标或其他方式),你就是金子!如果没有,你如何找到正确的哈希——我在这里打电话给H
——取决于你。您可以使用的另一个技巧是:git diff
可以将 any 提交与 any 文件树 进行比较,甚至是 Git 存储库之外的提交。使用git log
,您可以获得哈希 ID;你可以运行:
git diff <hash> /path/to/unzipped/files
并获得差异列表。如果您看到的唯一更改是您的更改,那么这里的hash
可能是H
。您可以使用git log
的日期来获取此类git diff
的候选候选名单,然后使用试错法找到最接近的提交。
假设您已经找到了哈希 ID H
,那么您现在要做的就是创建一个直接指向该哈希 ID 的新分支名称。为此,请使用git branch
命令:
git branch branch-xyzzy <hash>
(选择一个更好的分支名称,然后再次使用鼠标剪切和粘贴哈希 ID)。现在您在 您的 存储库中拥有:
...--G--H <-- branch-xyzzy
\
I--J--K <-- main (HEAD), origin/main
你现在可以运行git checkout branch-xyzzy
:
...--G--H <-- branch-xyzzy (HEAD)
\
I--J--K <-- main, origin/main
in 你的工作树的文件现在是那些来自 提交H
的文件。从您在 zip 存档中工作的地方复制文件,使用 git diff
和/或 git status
找出要做什么 git add
或只是 git add .
并运行 git status
,然后您就可以提交了!您的新提交将获得一个新的唯一哈希 ID,名称 branch-xyzzy
将指向它:
...--G--H--L <-- branch-xyzzy (HEAD)
\
I--J--K <-- main, origin/main
或者,等效地:
...--G--H--I--J--K <-- main, origin/main
\
L <-- branch-xyzzy (HEAD)
请注意我们如何在不实际更改任何提交的情况下重新绘制绘图。习惯图形绘图变化很大的事实:您使用的任何绘图软件,例如 GitKraken 内置的东西——你用gitkraken标记你的问题——都会有自己的偏好.它们可能与您的匹配,也可能不匹配。重要的是从提交到早期提交的箭头,以及指向特定提交的各种名称。从 commit 到 commit can't 的箭头会改变,因为 any commit 的任何部分都不能改变,但名称中的箭头 可以。 p>
我们经常使用最后一点。例如,现在您已经:
...--G--H--I--J--K <-- main, origin/main
\
L <-- branch-xyzzy (HEAD)
您可能想使用git rebase
。此副本 提交到新的和改进的。提交L
可能没问题,但如果它基于提交K
可能会更好。你实际上不能这样做,但你可以做出一个新的和改进的提交——我们称之为L'
——确实这样做:
L' <-- improved-branch-xyzzy
/
...--G--H--I--J--K <-- main, origin/main
\
L <-- old-branch-xyzzy
如果我们现在删除旧的名称,使得提交L
很难找到,并再次使用名称指向L'
而不是@987654503 @:
L' <-- branch-xyzzy (HEAD)
/
...--G--H--I--J--K <-- main, origin/main
\
L ???
然后使用任何提交查看器查看提交,看起来好像我们更改了提交L
。新副本L'
具有不同的哈希ID,并向后指向K
,但与H
-vs-@987654509 进行相同的更改 @会显示。它具有与L
相同的提交消息。因此,如果我们不记得哈希 ID(而且从来没有人记得),我们可能甚至都不知道这件事发生了!
【讨论】:
以上是关于提交以 zip 格式下载的 Github 更改的主要内容,如果未能解决你的问题,请参考以下文章
在提交到master之前,我可以查看对GitHub托管页面的更改吗?