本文介绍了Git-在线存储库中有未跟踪的文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用git(bitbucket)来控制我的Linux配置文件.所有文件都在目录~/.cfg/中.然后,我在~/.cfg/local/中还有一些本地配置文件,这些文件在计算机之间可能是不同的.

我想将本地文件的副本作为一种示例本地配置保留在我的在线存储库中,但不希望不跟踪这些文件.我并不在乎它们是否被git clone克隆,无论哪种方式都可以.

我尝试遵循此答案,但这会从在线存储库中删除文件.

我还尝试了此博客文章中概述的解决方案效果更好,但不幸的是有两个缺点:1)必须在每台计算机上重复; 2)实际上并没有取消关注文件.因此,如果我不小心从某台计算机上上传了本地配置(忘记从帖子中运行命令),则在其他任何计算机上的下一个git pull都将覆盖该计算机的本地配置.


总而言之,我想要一个执行以下操作的解决方案:

  1. 它将整个~/.cfg/(包括~/.cfg/local/)的初始上传保存在在线存储库中.
  2. 每当我执行标准git add -A; git commit -m "asdf"; git push
  3. 时,它推送~/.cfg/的内容,但不推送~/.cfg/local/的内容
  4. 当我git pull时,它提取~/.cfg/的内容,但不提取~/.cfg/local/的内容.
解决方案

对不起,但是答案是:不,Git无法做到这一点.您可以关闭 ,但这并不有趣:它需要部分工作每个运行git clone的人,从那时开始,都会反复遇到可能导致灼伤的事件.这就是为什么标准方法是此答案

了解为什么为什么 Git无法做到这一点.让我们更具体地看一下那个"是什么:

可以这样做.但是措辞很奇怪,因为Git不存储文件. Git存储 commits ,其中包含文件.这似乎只是一种语义,但是关于热水"是否适合淋浴(40˚C/104˚F:很热,但不会烫伤),还是再次给您带来了纯粹的语义",灼伤(95˚C/203˚F:接近沸腾,在标准压力下).

因此,您可以提交包含包含cfg/foo和cfg/local/bar的文件的提交.到目前为止,还没有真正的问题-主要问题是您的提交不能包含空目录cfg/local/,因为Git在每次提交中仅存储文件本身,而不在包含目录中存储文件:它假定任何使用以后,只要有文件要存储,存储库将根据需要自动创建目录,该文件的名称会迫使Future/other Git调用os.mkdir或创建包含该文件的目录. >

这是第一个问题,其中那些单纯"的语义至少有点烫手:Git不会推送文件. Git推动 commits .

您在这里有三个命令.第一个git add -A告诉Git:通过用工作树中的新版本替换索引中记录的所有文件的索引副本.第二个git commit告诉Git:使用存储在索引中的文件进行新提交.第三个​​git push告诉Git:发送一些提交到其他Git ,然后要求其他Git将其一个或多个引用(例如refs/heads/master(其master分支)设置为某个哈希ID).

这带来了新的术语索引,这就是麻烦的开始.

如果您的cfg/local/bar文件在索引中,那么它将在您的提交中.如果索引中的 not 不在您的提交中,则 not 将不在您的提交中.这很简单,但是其含义令人讨厌:

  • 您可以从索引中删除文件,而无需触摸工作树版本(git rm --cached cfg/local/bar),但这会引起将来的问题.

  • 或者,您可以在索引为 的文件副本上设置--assume-unchanged或--skip-worktree位.这几乎足够好,但还不够. (顺便说一句,两者大致相同,但跳过工作树"是用于这种用途的-除非其真正意图确实是用于稀疏结帐.我将在下面写下跳过工作树",但这确实意味着一个.)

设置该位要求您在git clone之后手动运行命令.索引是存储库的您的副本的专用索引,因此,每个运行git clone的人也必须至少在git clone之后运行此git update-index命令. (尽管您当然可以编写脚本来执行并分发该脚本,但Git不允许您通过Git本身使之自动化.)

您可能已经看到,只有几乎有效.

Git会再次在这里烧死你.问题在于git pull并不是真正的东西:它意味着运行git fetch,然后运行第二个Git命令,而 second Git命令正在运行造成麻烦.

第二个Git命令通常是git merge,我们现在假设它是.另一个选项git rebase对您来说是 worse ,因为变基实质上是重复git cherry-pick且每个Cherry-pick操作本身都是合并,导致 multiple 合并

合并(例如提交)发生在索引中或通过索引发生. Git将来自三个 all 个文件加载到索引中,通过两个单独的步骤(基本vs我们的"以及基本vs他们的)将文件配对,然后将配对.因此,这将合并索引中的每个文件,或者,如果先前在提交中的 索引中的文件现在不在索引中,则删除或重命名文件.

这意味着,如果文件cfg/local/bar存在于合并基础提交和其"提交中,并且如果希望初始git clone用cfg/local/bar填充cfg/local,则文件必须存在于此文件中. —那么它也必须存在于我们的"提交中,否则Git将坚持删除以保留我们的更改.反过来,这意味着,如果他们在他们的提交中更改了他们的副本,Git也会在您的提交中将其更改也应用于您的副本.

如果您使用git update-index来标记--skip-worktree标志,那么您一直都在重新提交cfg/local/bar的原始版本.该标志只是告诉Git:嘿,不要看我自己的文件版本,只需假设索引中的副本仍然正确即可.这会影响git add -A步骤:而不是更新索引中列出的所有文件,它实际上会执行:更新所有未特别标记的文件.您可以更改所有cfg/local/bar您喜欢的内容,并将跳过更新:它不会将您的工作树cfg/local/bar复制回索引中,而是在您第一次运行git clone时将其复制副本保留在索引中git checkout为您.

因此,所有您的提交都具有cfg/local/bar,但是这些提交的 contents 在每次提交中都将存储在 ,即使您已更改工作树副本,它们的内容也与您运行git clone时获得的内容相同.您的skip-worktree位告诉您的Git仅保留cfg/local/bar的索引副本,这已经完成了.

但是现在是合并时间,并且他们出于任何原因更改了 cfg/local/bar-原因无关紧要,重要的是他们做了更改了它— 现在您的Git面临着将您的更改(无)与他们的更改(某些)相结合的工作.它通过采取唯一的更改(当然是它们)和 now 来做到这一点,您的Git会坚持将更新的cfg/local/bar复制到您的工作树中.这会覆盖您的cfg/local/bar ,这就是痛苦所在:这就是这种方法会烧伤您的地方.

如果他们 从不(从来没有,不是一次)更改其cfg/local/bar,则此方法-设置skip-worktree-将会实际上工作.但这取决于陌生人的好意,或者至少取决于以下想法,即 every commit 中cfg/local/bar中的本地配置完全相同...在这种情况下,重点是什么?完全承诺吗?

但是,如果他们曾经做过更改,那么在将他们的更改与缺少更改合并时,您会被烫伤(轻微或其他),因为Git会希望用更新后的内容覆盖您的cfg/local/bar./p>

另一种选择是,您更早地从索引中删除 :现在,每次提交 you 推送都没有文件. Git将其视为命令:从具有文件的提交转到没有文件的提交时,请删除该文件.因此,如果采用这种方法,您是更改文件的人!您告诉了其他所有人:删除此文件!

唯一的100%保证的正确处理方法是:切勿首先提交文件.如果 >在存储库中的提交没有没有cfg/local/bar,该文件将从不放入索引.如果该名称也列在.gitignore中,则不会自动将添加所有文件"添加到索引中,因此不会在将来提交中.这意味着无论您何时开始或完成时,它都不会在那儿. Git永远不会合并它,也不会覆盖它的副本.它将始终是一个未跟踪且被忽略的文件,存在于您的工作树中,但不存在于您的任何提交中.

当然,这意味着会有一些初期的痛苦:每次运行git clone <url>时,您还必须这样做:cp -r .cfg/local-committed/ .cfg/local.但是,如果要使用--skip-worktree,则每次运行git clone <url>时,都必须立即使用git update-index --skip-worktree .cfg/local/bar进行操作.因此,它与坏选择的痛苦程度完全相同,而没有任何坏处.

此外,如果您控制软件,则可以设置软件,以便在首次运行程序时如果.cfg/local/不存在,则程序通过从.cfg/local-committed/复制来创建 .cfg/local/.然后,首次安装"的痛苦也消失了! 这就是为什么将默认配置提交到一个单独的文件中,该解决方案是正确的解决方案,该解决方案是用户手动或自动复制到本地配置文件,该文件永远是未跟踪的文件. >

I'm using git (bitbucket) to source control my linux configuration files. All the files are in the directory ~/.cfg/. Then I additionally have some local configuration files in ~/.cfg/local/ which are supposed to be different from machine to machine.

I would like to keep a copy of the local files in my online repository as a kind of sample local config but would like to otherwise not track the files. I don't really care whether they get cloned with git clone though, either way is fine.

I tried following this answer but that removes the files from the online repository.

I also tried the solution outlined in this blog post, which worked better, but unfortunately has 2 drawbacks: 1) it has to be repeated on each machine and 2) it does not actually unfollow the files. So if I ever accidentally upload a local config from some machine (forgetting to run the command from the post), the next git pull on any other machine will override that machine's local configuration.


To summarize, I would like a solution that does the following:

  1. It keeps the initial upload of the entire ~/.cfg/ (including ~/.cfg/local/) in the online repository.
  2. It pushes the contents of ~/.cfg/ but not the contents of ~/.cfg/local/ whenever I do the standard git add -A; git commit -m "asdf"; git push
  3. It pulls the contents of ~/.cfg/ but not the contents of ~/.cfg/local/ when I git pull.
解决方案

Sorry, but the answer is: No, Git can't do that. You can get close, but it's not fun: it requires work on the part of everyone who runs git clone, and from then on there are repeated encounters that can cause burns. That's why the standard method is the one recommended in this answer to Can I 'git commit' a file and ignore its content changes?

It may help to understand why Git can't do that. Let's look more specifically at what "that" is:

This, you can do. But the phrasing is odd, because Git does not store files. Git stores commits, which contain files. That might seem like mere semantics, but then again, it's "mere semantics" as to whether "hot" water is nice for a shower (40˚C / 104˚F: hot, but not scalding), or will give you second-degree burns (95˚C / 203˚F: near boiling, at standard pressure).

So, you can have a commit that contains files including cfg/foo and cfg/local/bar. So far, no real problem—the main problem is that you cannot have a commit that contains an empty directory cfg/local/, as Git stores only the file themselves in each commit, not the containing directory: it assumes that anyone using the repository later will create directories automatically as needed, whenever there's a file to be stored, whose name forces that future / other Git to call os.mkdir or whatever it is that creates a directory to contain that file.

Here's the first problem, where those "mere" semantics are at least a little scalding: Git doesn't push files. Git pushes commits.

You have three commands here. The first one, git add -A, tells Git: Update the index copy of all files that are recorded in the index, by replacing it with a fresh version from my work-tree. The second one, git commit, tells Git: Make a new commit using the files that are stored in the index. The third, git push, tells Git: Send some commit(s) to some other Git, then ask that other Git to set one or more of its references, such as its refs/heads/master—its master branch—to some hash-ID.

This brings in this new term, the index, and that's where the trouble starts.

If your cfg/local/bar file is in your index, it will be in your commits. If it is not in your index, it will not be in your commits. That's as simple as it gets, but its implications are nasty:

  • You can remove the file from your index without touching the work-tree version (git rm --cached cfg/local/bar), but this is going to cause a future problem.

  • Or, you can set the --assume-unchanged or --skip-worktree bits on the copy of the file that's in your index. This is almost good enough, but not quite. (Incidentally, the two are more or less equivalent, but "skip worktree" is the one that's intended for this kind of use—except that its true intent is really for use in sparse checkout. I'll write "skip worktree" below but this really means either one.)

Setting the bit requires that you run a command manually after git clone. The index is private to your copy of the repository, so everyone who runs git clone must run this git update-index command too, at least once, right after git clone. (Git will not let you automate this through Git itself, though of course you can write a script to do it and distribute the script.)

As you've probably already seen, this only almost works.

Once again, Git will burn you here. The problem is that git pull is not really a thing of its own: it means run git fetch, then run a second Git command and the second Git command is going to cause trouble.

The second Git command is normally git merge, and we'll assume for now that it is. The other option, git rebase, is worse for you, as rebase is essentially repeated git cherry-pick with each cherry-pick operation itself being a merge, resulting in multiple merges.

Merges, like commits, happen in or through the index. Git loads all the files from three commits into the index, pairing up files in two separate steps (base vs "ours", and base vs theirs), and then combining the pairings. So this merges each file that's in the index, or, if a file that was in the index in an earlier commit isn't in the index now, removes or renames files.

This means that if a file cfg/local/bar exists in the merge base commit and in "their" commit—and it will need to be there, if you want an initial git clone to populate cfg/local with cfg/local/bar—then it needs to exist in the "ours" commit as well, otherwise Git will insist on removing it to keep our change. That, in turn, means that if they have changed their copy in their commit, Git will want to apply their change to your copy in your commit too.

If you've used git update-index to fuss with the --skip-worktree flag, you've been re-committing the original version of cfg/local/bar all along. The flag just tells Git: Hey, don't look at my own version of this file, just assume that the copy in the index is still correct. This affects the git add -A step: instead of Update all files that are listed in the index, it actually does: Update all files that aren't specially marked. You can change cfg/local/bar all you like, and git add -A will skip over the update: it won't copy your work-tree cfg/local/bar back into the index, instead keeping the copy it put into the index back when you first had git clone run git checkout for you.

So all of your commits have a cfg/local/bar, but the contents these commits store in that cfg/local/bar, in each commit, are the same contents you got when you ran git clone, even if you've changed the work-tree copy. Your skip-worktree bit told your Git to just leave the index copy of cfg/local/bar alone, which it has done.

But now that it's merge time, and they have changed their cfg/local/bar for whatever reason—the reason doesn't matter, what matters is that they did change it—now your Git is faced with the job of combining your changes (none) with their changes (some). It does so by taking the only changes—theirs, of course—and now your Git will insist on copying the updated cfg/local/bar out into your work-tree. This will overwrite your cfg/local/bar, and that's the pain point: that's where this approach burns you.

If they never (not ever, not once) change their cfg/local/bar, this approach—setting skip-worktree—will actually work. But that depends on the kindness of strangers, or at least, on the idea that the local config in cfg/local/bar in every commit ever be exactly the same ... in which case, what was the point of committing it at all?

But if they ever do change it, you'll get burned (mild or otherwise) when you merge their change with your lack-of-change, because Git will want to overwrite your cfg/local/bar with their updated one.

The alternative, in which you remove your cfg/local/bar from your index early on, is worse: now every commit you push doesn't have the file at all. Git views this as a command: When going from a commit that does have the file, to one that doesn't have the file, remove the file. So if you take this approach, you're the one who changed the file! You told everyone else: Remove this file!

The only truly, 100% guaranteed, correct way to deal with this is: Never commit the file in the first place. If every commit in the repository doesn't have cfg/local/bar, that file will never be put into the index. If that name is listed in a .gitignore as well, no automatic "add all files" will add it to the index, so it won't be in future commits. That means it won't be in there when you start, nor when you finish. Git will never want to merge it, nor overwrite your copy of it. It will always be an untracked-and-ignored file, existing in your work-tree, but not in any of your commits.

Of course, this means there's a little bit of initial pain: every time you run git clone <url> you must also do: cp -r .cfg/local-committed/ .cfg/local. But if you were going to use --skip-worktree, then every time you run git clone <url> you must follow that immediately with git update-index --skip-worktree .cfg/local/bar. So it's exactly the same amount of pain as the bad alternative, without any of its badness.

Moreover, if you're in control of the software, you can set up the software so that, if .cfg/local/ does not exist when you first run the program, the program creates .cfg/local/ by copying from .cfg/local-committed/. Then that pain of "first setup" goes away too! That's why committing the default configuration into a separate file, that the user either manually or automatically copies to the local configuration file, which remains an untracked file forever, is the correct solution.

这篇关于Git-在线存储库中有未跟踪的文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-05 23:20