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

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

问题描述

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

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

此工作流程一切正常.

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

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

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

  1. 从存储库中删除文件,然后将package文件夹添加回gitignore.提交.
  2. 推送到遥控器(显然会推送删除的文件)
  3. 按下后,在服务器上快速
  4. 运行 composer更新,以重新安装已删除的软件包.

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

有什么方法可以从跟踪中删除软件包文件夹,而在推动提交时不会导致从远程删除软件包吗??

我已经在这里阅读了有关假定不变 skip-worktree 的信息(),但我不确定该使用哪种效果这些命令中的哪些(如果有的话)专门在遥控器上?

有什么方法可以从跟踪中删除软件包文件夹,而在推动提交时不会导致从远程删除软件包吗??

否.

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

不幸的是,您在这里所做的任何操作都会有些丑陋和痛苦.

我已经阅读了有关假定不变和跳过工作树的信息……但是我不确定该使用哪种命令,以及这两个命令(如果有的话)专门在远程上会产生什么作用?

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


要了解所有这些,您需要一个Git实际功能的正确模型.

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

提交存储的内容分为两个部分:数据,其中包含所有文件的快照;加上元数据,Git在其中存储信息,例如谁进行了提交,何时(日期和时间戳)以及为什么(日志消息).作为元数据的重要部分,每个提交还存储一组上一个提交哈希ID,作为文本中的原始哈希ID.这样,Git可以从任何给定的提交向后转到以前的某个提交.

任何Git提交的实际哈希ID只是其所有数据的校验和.(从技术上讲,这只是元数据的校验和,因为快照本身存储为单独的Git对象,其哈希ID进入提交对象.但是,此单独对象的哈希ID也是校验和,因此通过 Merkle树,都可以解决.)这是为什么commit完全是只读的,并且一直冻结.如果您尝试更改提交中的任何内容,则实际上并不会更改该提交.相反,您将获得一个 new 提交,其中包含一个新的且不同的哈希ID.旧的提交仍然存在,其哈希ID保持不变.

因此:Git完全是关于提交的,而Git通过其哈希ID查找提交.但是我们人类无法处理哈希ID(很快,是08337还是03887?).我们希望拥有名称,例如 master .同时,Git希望以一种快速的方法在某个结束于某个时间点的提交链中找到 last 提交.因此,Git为我们提供了名称,方法是让我们创建分支名称.

分支名称只是在某些链中保存了 last 提交的哈希ID.该提交作为其 parent 的成员,保留了链中上一个提交的哈希ID.父提交作为父级(我们的最后一个提交的祖父母)持有该提交的哈希ID,再往后移,依此类推:

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

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

提交 new 的行为包括:

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

实际上已经进行了提交(并在此过程中找到了其哈希ID),Git只是将新的哈希ID I 写入分支名称 master 中:

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

我们有一个新的提交.

要查看在诸如 I 之类的提交中发生了什么,Git将提交及其所有文件提取到一个临时区域,然后提取先前的提交H 的文件到一个临时区域,并进行比较.对于那些相同的东西,Git什么也没说.对于那些不同的东西,Git显示出不同之处.对于那些新的,Git说它们是添加的",对于以前的提交但不是在此提交中的那些,git表示它们是删除的".

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

但是,新提交的新快照来自何处?在某些版本控制系统中,新快照来自工作树中的文件.这不是Git中的情况.相反,Git从Git的 index 中进行新的提交.您至少不能轻易地查看这些文件,但是当Git第一次提取某些提交时,它会有效地将所有已保存提交的冻结文件复制到Git的索引中.只有当它们进入索引后,Git才会将它们复制(并解冻/补水)到您的工作树中,以便您可以使用它们.

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

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

这意味着每个文件始终具有三个实时"副本.一个冻结在当前提交中.一种是半冷冻的,在 index 中,Git也将其称为临时区域.最后一个是您在工作树中的副本,您可以使用它进行任何操作:这是一个普通文件,而不是特殊的仅Git格式.

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

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

  • 然后, git status 将索引中的所有文件与工作树中的所有文件进行比较.对于每个相同的文件,Git什么也没说.对于每个不同的文件,Git表示该文件不是准备提交的文件.如果工作树中的文件是新文件-不在索引中-Git会抱怨该文件是 untracked .如果文件是从工作树中删除 ,则Git表示已将其删除.

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

.gitignore 文件中列出未跟踪的文件会使Git关闭: git status 不再发牢骚.如果文件尚不存在,它也会使 git add 不将文件 add 添加到索引中,但对文件中 的文件没有影响.指数.如果文件在索引中,则按照定义对其进行跟踪,并且 git add 会很高兴地添加它.

最后,这是-假定不变的-skip-worktree 出现的地方.可以对索引中的文件进行设置.设置任何一个标志都会告诉Git:嘿,当您要考虑该文件的工作树副本时……您可以立即跳过它.也就是说, git add 查看索引和工作树,并检查 .gitignore 文件,以查看跟踪的内容,未跟踪的内容,工作树中的更新内容以及在建议的下一次提交中需要更新的内容,以及很快.如果某些文件被未跟踪并列在 .gitignore 中,则 git add 将跳过该文件.如果是跟踪,如果工作树副本不同,则Git会添加它.除非设置了跳过标志.如果设置了-假定未更改标志,则Git将假定未更改,也不会添加它.如果设置了-skip-worktree 标志,那么即使文件实际上已更改,Git也会明确地确定不应添加它 .

所以-skip-worktree 表示我们想要的内容:即使文件已更改,也不要 git add 这个文件.-assume-unchanged 标志也可以正常工作,因为Git假定它未更改,因此也不会 git add 进行更改.今天的实际操作没有什么区别,但是跳过工作树"表示正确的意图.

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

但是文件的副本是从哪里来的?答案在前面的 git checkout 中: git checkout 将所有文件从我们选择的提交复制到索引中.它通过第一个 git checkout 进入索引,然后进入工作树.如果从那时起我们一直对工作树副本感到困惑,那么,设置的 flag 意味着 git add 永远不会将工作树副本复制回索引副本中,因此它仍然与旧提交相同.我们已经使用文件的副本(保存在索引中)进行了新的提交,可能已经花费了几天甚至数月之久.

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

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

但是,如果您 git rm 这些文件,那么 else 从具有该文件的提交移动到没有该文件的提交会发生什么?例如,也许您正在推入的遥控器现在已将该文件签出,然后他们打算 git checkout 一个 new 提交,使您没有这些文件.当然,他们的Git会忠实地从他们的 Git的索引以及他们的 Git用户的工作树中删除这些文件.那就是您不想要的,因此现在您不得不将文件的副本保留在您的 Git索引中,以便将其放入您的新提交中./p>

这就是这个复杂的舞蹈的全部内容.每个提交都是一个快照,在新提交中,您希望快照具有某些特定文件的副本.因此,您必须将他们的副本复制到您的 Git索引中.您可以从一些提交中获取该信息,然后将其复制到索引中.然后,即使您不在自己的工作树中使用它,也可以将它放在您的 Git的索引/暂存区域中.在使用这三个副本时,您将 right 副本(不是您的工作树之一)保留在自己的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天全站免登陆