git 存储库中的 Composer 包冲突;如何在推送到远程时取消跟踪文件但避免删除文件 [英] Composer package conflict in git repository; how to untrack files but avoid deletion of files when pushing to remote

查看:17
本文介绍了git 存储库中的 Composer 包冲突;如何在推送到远程时取消跟踪文件但避免删除文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我通过 Composer 在我的 Web 应用程序上安装了一个包.并将包文件夹添加到 .gitignore,同时提交 composer.jsoncomposer.lock

为了部署到我们的服务器,我们推送到服务器上的裸 Git 远程,然后将修改后的文件推送到服务器上的相关位置.

这个工作流程一切正常.

稍后,在存储库上工作的其他人将包文件添加到存储库并从 gitignore 中删除了包.

我们希望包版本完全由 composer 管理,而不是像以前那样由 git 存储库管理.

到目前为止,我唯一的想法是执行以下操作:

  1. 从 repo 中删除文件并将包文件夹添加回 gitignore.提交这个.
  2. 推送到远程(显然会推送已删除的文件)
  3. 推送后在服务器上快速运行composer update,重新安装被删除的包.

但这里的问题是,这从服务器上删除包几秒钟,我们希望尽可能避免这种情况,因为它是网站上的核心插件.我们不想造成破坏.

有什么方法可以让我从跟踪中删除包文件夹,同时在提交提交时不会导致包从远程删除?

我已在此处阅读了有关 assume-unchangedskip-worktree 的信息(Git - 'assume-unchanged' 和 'skip-worktree' 之间的区别),但我不确定使用哪个以及什么效果这些命令中的哪些将(如果有)专门在遥控器上?

解决方案

有什么方法可以让我从跟踪中删除包文件夹,同时在提交提交时不会导致包从远程删除?

没有.

幸运的是,您可能不需要.

不幸的是,你在这里做的任何事情都会有些难看和使用起来很痛苦.

<块引用>

我已经阅读了有关假设不变和跳过工作树的信息...但我不确定使用哪个命令以及这些命令中的任何一个会对遥控器产生什么影响(如果有的话)?

两者都可以,但 --skip-worktree 是您应该在这里使用的.两者都不会对任何其他 Git 存储库产生任何影响.

<小时>

要理解所有这些,您需要一个 Git 实际工作的正确模型.

首先请记住,Git 中的基本存储单元是 commit.每个提交都有一个唯一的、又大又丑的哈希 ID,例如 083378cc35c4dbcc607e4cdd24a5fca440163d17.该哈希 ID 是提交的真实名称".各地的每个 Git 存储库都同意 that 哈希 ID 是为 that 提交保留的,即使所讨论的 Git 存储库还没有 那个提交.(这就是 Git 中所有真正魔力的来源:这些看似随机但实际上完全非随机的哈希 ID 的唯一性.)

提交存储的内容分为两部分:数据,由所有文件的快照组成;加上 元数据,Git 存储诸如谁提交、何时(日期和时间戳)以及为什么(日志消息)等信息.作为元数据的关键部分,每个提交还存储一些 previous 提交哈希 ID,作为文本中的原始哈希 ID.这让 Git 从任何给定的提交 backwards 转到某个先前的提交.

任何 Git 提交的实际哈希 ID 只是其所有数据的校验和.(从技术上讲,它只是元数据的校验和,因为快照本身存储为单独的 Git 对象,其哈希 ID 进入提交对象.但是,这个单独对象的哈希 ID 也是校验和,所以通过 Merkle 树,一切顺利.)这就是为什么提交是完全只读的,一直被冻结.如果您尝试更改提交中的任何内容,您实际上并没有更改该提交.相反,您会得到一个 new 提交,它带有一个新的和不同的哈希 ID.旧的提交仍然存在,其哈希 ID 未更改.

所以:Git 是关于提交的,Git 通过它们的哈希 ID 查找提交.但是我们人类无法处理哈希 ID(快速,是 08337-something 还是 03887-something?).我们希望有 names,例如 master.同时,Git 想要一种快速的方法来找到在某个点结束的某个提交链中的 last 提交.所以 Git 通过让我们创建分支名称来为我们提供名称.

分支名称只是保存某个链中last 提交的哈希 ID.该提交作为它的 parent 持有链中 previous 提交的哈希 ID.父提交作为它的父提交——我们最后一次提交的祖父提交——持有更远一步提交的哈希 ID,依此类推:

... <-F <-G <-H <-- 主控

如果提交哈希 ID 是像 H 这样的单个字母,这可能是一个准确的绘图:名称 master 将包含哈希 ID H,提交 H 将持有哈希 ID G 作为其父级,提交 G 将持有哈希 ID F 作为其父级,等等.

new 提交的行为包括:

  • 写出所有文件的快照;和
  • 添加适当的元数据:您作为作者和提交者,现在"作为日期和时间戳,等等.这个新提交的 parent 应该是 current 提交的任何内容,如当前分支名称中记录的那样.如果 master 指向 H,那么新提交的父级——我们称之为 I——将是 H,以便我指向H`.

在实际进行了这个提交(并在过程中找到了它的哈希 ID)之后,Git 只需将新的哈希 ID I 写入分支名称 master:

... <-F <-G <-H <-I <-- 主

我们有一个新的提交.

要查看在诸如 I 之类的提交中发生了什么,Git 将提交(其所有文件)提取到一个临时区域,然后提取上一个提交 H 的文件到一个临时区域,并比较它们.对于那些相同的,Git 什么也没说.对于那些不同的,Git 显示了差异.对于那些新的,Git 说它们被添加"了,而对于那些在之前的提交中但没有在这次提交中的,git 说它们被删除"了.

现在,对某个特定的提交进行 git checkout 意味着将该提交的 content(即数据)以您可以使用的形式输出.提交中文件的永久冻结副本采用仅 Git 格式,这对于存档来说很好,但对于完成新工作却无用.因此,Git 必须将提交提取到一个工作区,您可以在其中查看和处理您的文件.Git 将此工作区称为您的 work-treeworking tree(或这些名称的一些变体).除了在您询问时将文件写入其中之外,Git 基本上不干涉这个工作区域:那是 您的 游乐场,而不是 Git.

但是新提交中的新快照从何而来?在某些版本控制系统中,新快照来自工作树中的文件.这在 Git 中不是.相反,Git 从 Git 的 index 中的任何内容进行新的提交.您无法看到这些文件——至少,不容易——但是当 Git 第一次提取某个提交时,它会有效地将所有该提交的已保存、冻结的文件复制到 Git 的索引中.只有当它们在索引中时,Git 才会将它们复制(并解冻/再水化)到您的工作树中,以便您可以使用它们.

提交中的冻结副本与索引中的软冻结"副本之间的关键区别在于您可以覆盖索引副本.1您无法覆盖 已提交 副本,但这没关系:提交无法更改,但您可以进行新的和更好的提交,这就是版本控制的意义所在.

每当您运行 git commit 时,Git 在第一步(制作快照)中所做的就是将每个文件的所有预冻结 index 副本打包文件.因此我们可以将索引视为提议的下一次提交.这也是为什么你必须一直 git add 文件,即使它们已经在之前的提交中.git add 所做的是将工作树文件复制覆盖在该文件索引中的任何内容之上(尽管技术细节再次参见脚注 1).p>

这意味着每个文件始终存在三个实时"副本.一个在当前提交中被冻结.一个是半冻结的,在 index 中,Git 也将其称为 staging area.最后一个是你的副本,在你的工作树中,你可以做任何你想做的事情:它是一个普通的文件,不是一个特殊的 Git-only 格式.

当您运行 git status 时,Git 会运行 两个单独的比较:

  • 首先,git status 将当前(HEAD)提交中的所有文件与索引中的所有文件进行比较.对于每个相同的文件,Git 什么也没说.对于每个不同的文件,Git 表示该文件已为提交暂存.如果索引中的文件是新文件——不在 HEAD 中——Git 称它为新文件;如果某个文件从索引中消失,Git 会说它已删除.

  • 然后,git status 将索引中的所有文件与工作树中的所有文件进行比较.对于每个相同的文件,Git 什么也没说.对于每个不同的文件,Git 表示该文件不会为提交暂存.如果工作树中的文件是新文件(不在索引中),Git 会抱怨该文件未跟踪.如果文件从工作树中消失,Git 会说它已被删除.

最后一种情况是 未跟踪 文件的来源.它还为我们提供了未跟踪的定义:如果工作树中存在的文件也不存在于索引中,则它是未跟踪的.由于我们无法看到索引,我们只能在 git status 抱怨这些未跟踪的文件时看到这种情况.

.gitignore 文件中列出未跟踪的文件会使 Git 闭嘴:git status 不会再抱怨了.如果文件不存在,它也会使 git addadd 文件到索引中,但它对 中的文件没有影响指数.如果文件在索引中,根据定义,它是被跟踪的,git add 会很高兴地添加它.

这终于是 --assume-unchanged--skip-worktree 的用武之地了.可以在索引中 的文件上设置.设置任何一个标志都会告诉 Git:嘿,当你要考虑这个文件的工作树副本时......你现在可以跳过它. 也就是说,git add 浏览索引和工作树,并检查 .gitignore 文件,以查看跟踪的内容、未跟踪的内容、工作树中更新的内容以及在建议的下一次提交中需要更新的内容,以及很快.如果某个文件未跟踪并列在 .gitignore 中,git add 将跳过它.如果它被跟踪,如果工作树副本不同,Git 将添加它...除非设置了跳过标志.如果设置了 --assume-unchanged 标志,Git 将 assume 它没有更改,并且不添加它.如果设置了 --skip-worktree 标志,Git 知道它绝对不应该添加它,即使文件实际上已更改.

所以 --skip-worktree 的意思是我们这里想要的:不要git add这个文件,即使它被改变了.--assume-unchanged 标志也可以工作,因为 Git 假定它没有改变,因此也不 git add 它.今天实际操作并没有什么不同,但是skip worktree"表达了正确的intent.

请注意,由于这些标志设置在文件的 index(也称为暂存区域)副本上,因此它们仅适用于 已跟踪 文件.跟踪的文件是索引/暂存区域中的文件.该文件必须在索引中,然后才能设置标志.而且,如果该文件在索引中,则该文件的 该副本(即现在在索引中的那个)就是将在 next 提交中的那个制作.

但是这个文件的副本是从哪里来的呢?答案在我们之前的 git checkout 中:git checkout 将我们选择的提交中的所有文件复制到索引中.它通过我们的第一个 git checkout 进入索引,然后进入我们的工作树.如果从那时起我们一直对工作树副本大惊小怪,那么,我们设置的 flag 意味着 git add 从未将工作树副本复制回索引副本,所以它仍然与旧提交相同.我们一直在使用保存在索引中的文件的副本进行新的提交,可能是几天或几个月或任何时间.

让这件事头疼的是,如果我们 git checkout 一些 other 提交,而另一个提交有一个 不同 副本在其中的文件中,Git 将希望用我们尝试切换到的提交中的副本替换我们的索引副本.将其复制到索引不会删除我们设置的标志,但它 覆盖 工作树副本.如果我们更改了工作树副本,Git 会在不询问的情况下覆盖它(这可能很糟糕)或者说:我无法检查那个提交,它会覆盖你的(假设/跳过,但我不会提及)该文件的工作树副本.实际上,Git 采用后一种方法.

要解决这个问题,每次您 git checkout 提交 覆盖您的标记文件时,您都必须移动或复制您的工作树副本顺便说一句,let git checkout 覆盖索引和工作树副本,然后将您的工作树副本移动或复制回原位.显然最好一开始就不要陷入这种情况.

但是,如果你 git rm 这些文件,那么 else 从有文件的提交转移到没有文件的提交会发生什么?例如,也许您正在推送的遥控器现在已签出该文件,然后他们将向 git checkout 一个 new 提交您所做的 没有这些文件.当然,他们的 Git 会尽职尽责地从 他们的 Git 的索引以及 他们的 Git 用户的工作树中删除这些文件.那是你不想要的,所以现在你只能在你的 Git 的索引中保留他们的该文件的副本,以便它进入你的新提交.

这就是这个复杂的舞蹈的全部意义所在.每个提交都是一个快照,在您的新提交中,您希望您的快照拥有它们的某些特定文件的副本.因此,您必须将 他们的 副本复制到 您的 Git 的索引中.你从一些提交中得到它,将它复制到你的索引中.然后将其保留在 您的 Git 的索引/暂存区域中,即使您没有在自己的工作树中使用它.在处理这三个副本时,您将 正确 副本(不是您的工作树副本)保存在您自己的 Git 索引中.

<小时>

1从技术上讲,索引中的内容是对冻结副本的引用.更新索引副本包括制作一个 new 冻结副本,准备好提交,并将新引用写入索引.如果您开始使用 git update-index 直接放入新文件,或者使用 git ls-files --stage 查看索引,这些细节很重要:您会看到Git 的内部 blob 对象哈希 ID 在这里.但是您可以将索引视为以内部冻结格式保存每个文件的完整副本:该心智模型对于您通常使用 Git 的级别来说已经足够好了.

I installed a package on my web application via composer. And added the package folder to .gitignore, whilst committing composer.json and composer.lock

To deploy to our server, we Push to a bare Git remote on the server which in turn pushes the modified files to the relevant location on the server.

This workflow was all working fine.

At a later date, someone else working on the repository added the package files to the repository and removed the package from gitignore.

We want the package version to be managed purely by composer and not by the git repository, as it was before.

My only idea so far is to do the following:

  1. Remove the files from the repo and add the package folder back to gitignore. Commit this.
  2. Push to the remote (which will obviously push the removed files)
  3. run composer update quickly on the server once pushed, to reinstall the removed package.

BUT the problem here is that this will remove the package for a few seconds from the server, and we want to avoid that if possible as it is a core plugin on the site. We don't want to cause something to break.

Is there any way I can remove the package folder from being tracked, whilst NOT causing the package to be deleted from the remote when the commit is pushed?

I have read about assume-unchanged and skip-worktree here (Git - Difference Between 'assume-unchanged' and 'skip-worktree'), but I am unsure which to use and what effect either of these commands will have (if any) specifically on the remote?

解决方案

Is there any way I can remove the package folder from being tracked, whilst NOT causing the package to be deleted from the remote when the commit is pushed?

No.

Fortunately, you may not need to.

Unfortunately, whatever you do here will be somewhat ugly and painful to use.

I have read about assume-unchanged and skip-worktree ... but I am unsure which to use and what effect either of these commands will have (if any) specifically on the remote?

Either will work but --skip-worktree is the one that you are supposed to use here. Neither will have any effect on any other Git repository.


To understand all of this, you need a correct model of what Git actually does.

Remember first that the basic unit of storage in Git is the commit. Each commit has a unique, big ugly hash ID, such as 083378cc35c4dbcc607e4cdd24a5fca440163d17. That hash ID is the "true name" of the commit. Every Git repository everywhere agrees that that hash ID is reserved for that commit, even if the Git repository in question does not have that commit yet. (This is where all the real magic in Git comes from: the uniqueness of these seemingly-random, but actually totally-not-random, hash IDs.)

What a commit stores comes in two parts: the data, which consist of a snapshot of all of your files; plus the metadata, where Git stores information such as who made the commit, when (date-and-time stamps), and why (log message). As a crucial piece of metadata, each commit also stores some set of previous commit hash IDs, as raw hash IDs in text. This lets Git go from any given commit, backwards, to some previous commit.

The actual hash ID for any Git commit is simply a checksum of all of its data. (Technically it's just a checksum of the metadata, because the snapshot itself is stored as a separate Git object whose hash ID goes into the commit object. However, this separate object's hash ID is a checksum as well, so through the mathematics of Merkle trees, it all works out.) This is why everything in a commit is totally read-only, frozen for all time. If you try to change anything inside a commit, you don't actually change the commit. Instead, you get a new commit, with a new and different hash ID. The old commit still exists, with its unchanged hash ID.

So: Git is all about commits, and Git finds commits by their hash IDs. But we humans can't deal with hash IDs (quick, was that 08337-something or 03887-something?). We would like to have names, like master. Meanwhile, Git would like a quick way to find the last commit in some chain of commits that ends at some point. So Git offers us names, by letting us create branch names.

A branch name simply holds the hash ID of the last commit in some chain. That commit holds, as its parent, the hash ID of the previous commit in the chain. The parent commit holds, as its parent—our last commit's grandparent—the hash ID of the commit one step further back, and so on:

... <-F <-G <-H   <-- master

If commit hash IDs were single letters like H, this might be an accurate drawing: the name master would hold hash ID H, commit H would hold hash ID G as its parent, commit G would hold hash ID F as its parent, and so on.

The act of making a new commit consists of:

  • writing out a snapshot of all files; and
  • adding the appropriate metadata: you as author and committer, "now" as the date-and-time-stamps, and so on. The parent of this new commit should be whatever the current commit is, as recorded in the current branch name. If master points to H then the parent of the new commit—which we'll call I—will be H, so that I points back toH`.

Having actually made this commit (and found its hash ID in the process), Git simply writes the new hash ID I into the branch name master:

... <-F <-G <-H <-I   <-- master

and we have a new commit.

To see what happened in a commit such as I, Git extracts the commit—all its files—to a temporary area, then extracts the previous commit H's files to a temporary area, and compares them. For those that are the same, Git says nothing. For those that are different, Git shows the difference. For those that are new, Git says they are "added", and for those that are in the previous commit but not in this commit, git says that they are "deleted".

Now, doing a git checkout of some particular commit means writing that commit's content—i.e., data—out in a form you can use. The frozen-for-all-time copies of files inside the commit are in a Git-only format, which is fine for archival, but useless for getting new work done. So Git has to extract the commit to a work area, where you can see and work with your files. Git calls this work area your work-tree or working tree (or some variant of these names). Aside from writing files into it when you ask, Git is mostly hands-off of this work area: that's your playground, not Git's.

But where does the new snapshot, in a new commit, come from? In some version control systems, the new snapshot comes from the files in your work-tree. This is not the case in Git. Instead, Git makes new commits from whatever is in Git's index. You can't see these files—at least, not easily—but when Git first extracts some commit, it effectively copies all of that commit's saved, frozen files into Git's index. Only once they're in the index does Git copy (and defrost / rehydrate) them into your work-tree so that you can work with them.

The crucial difference between the frozen copies in a commit, and the "soft-frozen" copies in the index, is that you can overwrite the index copy.1 You can't overwrite the committed copy, but that's OK: commits cannot be changed, but you can make new and better commits, and that's what version control is about anyway.

Whenever you run git commit, what Git does in that first step—making the snapshot—is that it simply packages up all the pre-frozen index copies of each file. So we can think of the index as the proposed next commit. This is also why you have to git add files all the time, even if they're already in the previous commit. What git add is doing is copying the work-tree file over top of whatever was in the index for that file (though see footnote 1 again for technical details).

What this means is there are three "live" copies of each file at all times. One is frozen in the current commit. One is semi-frozen, in the index, which Git also calls the staging area. The last one is your copy, in your work-tree, which you can do whatever you want with: it's a normal file, not in a special Git-only format.

When you run git status, Git runs two separate comparisons:

  • First, git status compares all the files in the current (HEAD) commit to all the files in the index. For every file that is the same, Git says nothing. For every file that is different, Git says that this file is staged for commit. If a file in the index is new—isn't in HEAD—Git calls it new; and if a file is gone from the index, Git says it's deleted.

  • Then, git status compares all the files in the index to all the files in the work-tree. For every file that is the same, Git says nothing. For every file that is different, Git says that this file is not staged for commit. If a file in the work-tree is new—isn't in the index—Git complains that the file is untracked. If a file is gone from the work-tree, Git says it's deleted.

This last case is where untracked files come from. It also gives us the very definition of untracked: a file that exists in the work-tree is untracked if it does not also exist in the index. Since we can't see the index, we only see this is the case when git status whines about these untracked files.

Listing an untracked file in a .gitignore file makes Git shut up: git status won't whine any more. It also makes git add not add the file to the index if it's not already there, but it has no effect on files that are in the index. If the file is in the index, it is, by definition, tracked, and git add will happily add it.

This, at last, is where --assume-unchanged and --skip-worktree come in. These are flags that you can set on files that are in the index. Setting either flag tells Git: Hey, when you're about to consider the work-tree copy of this file ... you can maybe just skip it now. That is, git add looks through the index and work-tree, and checks .gitignore files, to see what's tracked, what's untracked, what's newer in the work-tree and needs updating in the proposed next commit, and so on. If some file is untracked and listed in .gitignore, git add will skip it. If it's tracked, Git will add it if the work-tree copy is different ... unless the skipping flags are set. If the --assume-unchanged flag is set, Git will assume it's not changed, and not add it. If the --skip-worktree flag is set, Git knows it definitely should not add it, even if the file is actually changed.

So --skip-worktree means what we want here: don't git add this file, even if it's changed. The --assume-unchanged flag works as well, because Git assumes it's not changed and hence doesn't git add it either. There's no difference in actual operation today, but "skip worktree" expresses the right intent.

Note that because these flags are set on an index (aka staging-area) copy of the file, they only work on tracked files. Tracked files are those in the index / staging-area. The file has to be in the index before you can set the flags. And, if the file is in the index, that copy of the file—the one that's in the index right now—is the one that will be in the next commit you make.

But where did this copy of the file come from? The answer is in our git checkout earlier: git checkout copied all the files from the commit we chose, to the index. It got into the index, and then into our work-tree, by our first git checkout. If we've fussed with the work-tree copy since then, well, the flag we set means that git add never copied the work-tree copy back into the index copy, so it's still the same as the old commit. We've been making new commits, perhaps for days or months or whatever, using the old copy of the file, as saved in the index.

What makes this a pain in the butt is that if we git checkout some other commit, and the other commit has a different copy of the file in it, Git is going to want to replace our index copy with the one from the commit we are trying to switch to. Copying that to the index won't remove the flag we set, but it will overwrite the work-tree copy. If we've changed the work-tree copy, Git will either overwrite it without asking (this is probably bad) or say: I can't check out that commit, it will overwrite your (assumed/skipped, but I won't mention that) work-tree copy of that file. In practice, Git takes the latter approach.

To work around it, every time you git checkout a commit that would overwrite your flagged file, you'll have to move or copy your work-tree copy out of the way, let git checkout overwrite the index and work-tree copies, then move or copy your work-tree copy back into place. It's clearly better never to get into this situation in the first place.

But, if you git rm these files, what happens to someone else who moves from a commit that has the files, to a commit that doesn't? For instance, perhaps the remote you're pushing-to has that file checked out right now, and they're going to then git checkout a new commit you make that doesn't have those files. Of course their Git will dutifully remove those files from their Git's index, and from their Git's user's work-tree. That's what you don't want, so now you're stuck with keeping their copy of that file in your Git's index, so that it goes into your new commits.

That's what this complicated dance is all about. Every commit is a snapshot and in your new commits, you want your snapshots to have their copy of some particular file(s). So you have to get their copy into your Git's index. You get that from some commit, copying it into your index. Then you keep it in place, in your Git's index / staging-area, even though you don't use it in your own work-tree. While working with the three copies, you keep the right copy—which is not your work-tree one—in your own Git's index.


1Technically, what's in the index is a reference to the frozen copy. Updating the index copy consists of making a new frozen copy, ready for commit, and writing the new reference into the index. These details matter if you start using git update-index directly to put new files in, or use git ls-files --stage to view the index: you'll see Git's internal blob object hash IDs here. But you can just think of the index as holding a full copy of each file, in the internal, frozen format: that mental model works well enough for the level at which you normally work with Git.

这篇关于git 存储库中的 Composer 包冲突;如何在推送到远程时取消跟踪文件但避免删除文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆