使用GIT部署代码-签出vs重置-难吗? [英] Deploy code using GIT - checkout vs reset --hard?

查看:73
本文介绍了使用GIT部署代码-签出vs重置-难吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要确保服务器上我的python部署位置之一始终与远程分支同步,并删除服务器上的所有更改.选项1和选项2有什么区别?首选哪一个?我打算及时运行此命令,以确保服务器代码与远程服务器保持同步.

选项1:

git clean -f -x
git fetch --all
git reset --hard origin/master

选项2:

git clean -f -x
git fetch --all
git checkout --force origin/master

解决方案

要正确理解它们之间的区别,让我们从以下定义开始:

  • 一个Git存储库包含 commits ,这是一些目录树的快照( tree ),具有相关联的元数据,通常包括一个父提交(但允许任何数量的父提交ID).这些提交由较大的丑陋哈希ID,deadc0defeedbeefac0ffee...等标识.因为这些大的丑陋哈希ID大,丑陋且看似随机,所以我们使用名称(主要是分支名和标记名)来跟踪它们.

    存储在存储库中的文件具有仅对Git本身有用的格式.普通命令无法读取它们. (只有Git才能写入这些存储的对象,并且它们具有一次写入的形式:一旦写入,就永远不能更改它们.这对所有四种Git对象都是如此,但是不是分支名称,应该会更改:每次向分支添加新的提交时,存储在分支名称下的哈希ID都会更改.)

  • 每个存储库也都有一个 index .索引(请注意,有一个特殊的,独特的"the"索引)是您构建要执行的 next 提交的地方.它从匹配某些现有提交开始.索引具有多个作用,包括使Git快速运行(其 cache 角色),处理合并和安排下一次提交(其 staging area 角色).但这首先是匹配一些现有的提交.

    索引本质上是一棵扁平的树,因此它就像以特定方式提交的内容一样.与内部Git对象一样,它的形式仅对Git有用.

  • 大多数存储库还具有(单个,不同的)工作树,您可以在其中对文件进行工作.像索引一样,树开始匹配某些提交.由于这些文件是普通文件,计算机的其余部分都可以处理,因此它们既可以写入也可以读取,也可以添加新文件或删除文件.

    如果在工作树中更改文件,并且想要提交新版本,则必须将文件从工作树复制到索引中.这就是git add的作用:它将文件复制到索引中,或者替换现有文件(如果文件的路径名以前在其中)或存储一个全新的文件(如果路径名是新文件). /p>

    同样,要从索引中删除文件,必须运行git rm.默认情况下,这会将文件从 索引工作树中删除(但是您可以告诉它不要留下工作树版本).

  • 存储库始终 1 具有一个当前提交.这称为HEAD提交. .git/HEAD(这是.git目录中的普通文件)的真正工作方式是,它通常仅包含当前分支名称.实际的提交ID存储在分支名称下.但是,Git具有所谓的分离式HEAD"模式,其中HEAD包含哈希ID.对于git checkoutgit reset来说,这一切都很快.

因此,存在一个当前提交,通常来自当前分支名称,此外,分支名称实际上只是查找名称的一种方式-到ID映射以查找提交.我们称之为HEAD,它的缩写是:嘿,Git,去读.git/HEAD,找到当前的分支名称,并使用它来查找当前的提交.或者,如果我处于分离的HEAD模式,请阅读.git/HEAD和以前一样,但是看到它有一个哈希ID,然后 是当前提交."

现在,您在这里看到的两个不同的命令是git resetgit checkout.这些有非常不同的目标:

  • git reset(主要是 2 )是关于以某种方式更改当前分支名称的名称到ID映射,并且还可以选择更改索引,甚至更改所有三个:当前提交,索引,工作树.

  • git checkout(主要是)通过将新的分支名称写入HEAD或通过分离" HEAD来更改哪个分支是当前分支:写入提交哈希ID进入HEAD.在此过程中,git checkout 更改索引和工作树.

在这里事情变得有些复杂. :-)由于您使用的是--hard,因此git reset 更新索引和工作树.因此,现在听起来更像是git checkout,并且在某些方面还是如此.但是有几个关键的区别:

  1. 使用git reset 更改提交当前分支指向.使用git checkout更改提交当前分支.

    记住,我们在上面说过,像masterdevelop这样的名称是让Git记住大的丑陋哈希ID的方式.结果是这些名称的行为就像标签一样,粘贴或指向特定的提交. git reset命令使您可以移动标签:更改其指向的位置;将其从一个特定提交上剥离并将其粘贴到另一个特定提交上.相比之下,git checkout 不会移动标签.而是更改哪个分支名称存储在.git/HEAD中.您git checkout master切换到分支master,然后git checkout develop切换到分支develop.标签保留在原处,但是Git更改了存储在.git/HEAD中的名称.

    当您在分支上"时,所有这些都适用于 normal 情况.如果您处于分离的HEAD"情况下,git reset仍会将分离的HEAD从一个提交移至另一个提交,但是由于不涉及分支 name ,因此不会更改任何现有的分支名称.同样,如果您git checkout不是分支名称,则git checkout通过将原始提交ID写入.git/HEAD来分离您的HEAD".

    请注意,git reset永远不会分离HEAD,也永远不会重新连接HEAD.这就是git checkout所做的事情.同时,git checkout从未更改任何分支标签,但git reset却没有.

  2. git checkout命令尝试为非破坏性(无论如何,在此模式下,请再次参见脚注2). git reset命令愉快地销毁未保存的工作.

    实际上,这意味着如果您对索引和/或工作树进行了更改,则git checkout不会覆盖它们.这变得特别复杂,因为它会切换分支或提交(如果可以的话).这样做是通过将未保存的工作保留在索引和/或工作树中的位置(如果可以的话)来实现的.如果不能,则只会出错.

    相反,git reset --hard会丢弃这些未保存的更改:--hard意味着覆盖索引和工作树.

在评论中,您添加了:

我想忽略任何人可能在服务器上所做的所有提交(即使那不太可能)

但是忽略"与丢弃"不同,并且这些都不能解决对索引和/或工作树所做的未提交的更改.

通常,使用这样的集中式服务器的正确做法是设置一个没有工作树的em,即--bare存储库.没有工作树的存储库无法在其中完成任何工作.首先,没有什么可以保存,也许忽略或也许丢弃的.如果使用此方法,所有这些区别将消失.那是你最好的选择.

即使您不能或不会使用--bare存储库,也让我们看看其余的项目.除了上述所有考虑因素之外,请记住,一个集中式存储库(某人首先将git push es new 提交至该集中存储库)仍具有当前(HEAD)提交,如果没有,则为当前提交分离-命名当前分支.它还有一个索引,如果不是空的话,还有一个工作树. Git通常会拒绝推送到工作树的当前分支:请参见receive.denyCurrentBranch. html"rel =" nofollow noreferrer> git config文档.由于裸存储库没有工作树,因此对于裸存储库,这个特殊的Git问题也消失了.

(不过,您的描述听起来像是将您设置为不是作为其他人直接推送到的中央服务器,而是作为另一台服务器的客户端,在此客户端上使用git fetch.的情况下,以下几项就不成问题了.)

因此,假设您有一个带有工作树的非裸仓库.为了使推送成功,您可能需要使用分离的HEAD,否则Git将拒绝推送到当前分支. (较新的Git可以接受它们,但是您必须非常小心地使用它;有关详细信息,请参见链接的文档.)分离的HEAD将为您提供当前提交而没有当前分支,并且将生成git resetgit checkout表亲比你在树枝上更亲密.

(它还破坏了客户端克隆中央存储库时看到的功能:客户端将签出服务器上当前存在的任何分支,即,客户端询问服务器您的HEAD分支是什么?"并自动将其签出. .此功能的价值有些可疑,因此您可能不会错过它,但是值得注意.)

如果您正在使用此分离的HEAD,则git reset --hard origin/mastergit checkout origin/master之间的主要区别是未提交的索引和/或工作树修改发生了什么.使用git reset --hard,它们将被清除,而不会发出警告.任何在服务器上积极工作的人将是 SOL .索引和工作树将被重新设置以匹配新提交.

如果不是不是使用分离的HEAD,则git reset --hard origin/master会将origin/master的提交ID写入当前分支名称.和以前一样,git reset也会更新索引和工作树.通过从旧的提交上剥离分支标签并将其粘贴到新的提交上,这不仅清除了活动的工作,还丢弃了在当前分支上进行的提交.

对于git checkout,这也有点复杂.由于origin/master是所谓的远程跟踪分支,因此检出此名称将为您提供一个独立的HEAD,并将提交哈希直接存储在.git/HEAD中.不管是否已连接或分离HEAD,这都是正确的.所以HEAD现在 是分离的,前提是检出成功.如果索引和工作树中没有未提交的更改,则检出应该 3 成功.如果存在 尚未提交的更改,则签出操作将在可能的情况下将其带到新的分离的HEAD上,否则,如果在不清除未提交的工作的情况下无法切换到该提交,则结帐失败.

因此,如果您选择沿此路径前进,那么这些就是您的选择.由于Git是一个大型工具集,而不是一个单一的固定解决方案,因此,如果更适合,您可以选择一些 other 路径.

(请注意,git clean -f -x将删除未跟踪和忽略的文件,即索引中未包含的内容.但是,如果没有-d,它将不会删除未跟踪的目录.)


1 始终是当前提交"规则有一个例外,该规则主要用于完全没有提交的新的空存储库.除非您在空的存储库中工作,否则将不会遇到此异常,或者使用git checkout --orphan来创建一个新的但未出生的分支(如Git所说).

2 git resetgit checkout都有其他用法,它们的主要功能可能有所不同.我只在这里描述它们的主要功能.

3 git checkout仍然可能由于例如磁盘空间不足而失败.

I need to make sure that one of my python deploy location on the server is always in sync with a remote branch and remove any changes on the server. What is the difference between Option 1 and Option 2? Which one is preferred? I intend to run this on a timely manner to ensure server code is in sync with remote.

Option 1:

git clean -f -x
git fetch --all
git reset --hard origin/master

Option 2:

git clean -f -x
git fetch --all
git checkout --force origin/master

解决方案

To understand the difference properly, let's start with these definitions:

  • A Git repository contains commits, which are snapshots of some tree-of-files-in-directories (a tree) with associated metadata, typically including one parent commit (but any number of parent commit IDs are allowed). These commits are identified by big ugly hash IDs, deadc0defeedbeefac0ffee... and so on. Because these big ugly hash IDs are big, ugly, and seemingly-random, we use names—branch and tag names, mainly—to keep track of them.

    Files stored in the repository have a form useful only to Git itself. Normal commands cannot read them. (Nothing but Git can write these stored objects, and they have a write-once form: once written, they can never be changed. This is true of all four kinds of Git objects, but not of branch names, which are supposed to change: the hash ID stored under a branch name changes every time you add a new commit to the branch.)

  • Every repository also has an index. The index—note that there is one special, distinguished "the" index—is where you build the next commit you intend to make. It starts out matching some existing commit. The index has several roles, including making Git go fast (its cache role), and handling merges and arranging for the next commit (its staging area role). But it starts out matching some existing commit.

    The index is, essentially, a flattened tree, so it is like the contents of a commit in that particular way. As with internal Git objects, it is in a form useful only to Git.

  • Most repositories also have a (single, distinguished) work tree, which is where you do your work on your files. Like the index, the tree starts out matching some commit. Because these are ordinary files the rest of the computer can deal with, though, they can be written-to as well as read, or you can add new files or remove files.

    If you change a file in the work-tree, and want to commit the new version, you must copy the file from the work-tree into the index. This is what git add does: it copies a file into the index, either replacing the existing one—if the file's path-name was in there before—or storing an entirely new one (if the path-name is new).

    Likewise, to remove a file from the index, you must run git rm. By default this removes the file from both the index and the work-tree (but you can tell it to leave the work-tree version alone).

  • A repository always1 has one current commit. This is called the HEAD commit. The way .git/HEAD (it's an ordinary file, in the .git directory) really works is that it normally just contains the current branch name. The actual commit ID is stored under the branch name. However, Git has what is called "detached HEAD" mode, where HEAD contains a hash ID instead. This all matters in a moment for both git checkout and git reset.

So, there is a current commit, usually from a current branch name, plus the fact that a branch name is really just a way to look up the name-to-ID mapping to find the commit. We call that HEAD, which is short for: "Hey, Git, go read .git/HEAD, find the current branch name, and use that to find the current commit. Or, if I'm in detached HEAD mode, read .git/HEAD as before, but see that it has a hash ID instead, and then that's the current commit."

Now, the two different commands you are looking at here are git reset and git checkout. These have very different goals:

  • git reset is (mostly2) about changing the current branch name's name-to-ID mapping in some way, with the option of also changing the index too, or even changing all three: the current commit, the index, and the work-tree.

  • git checkout is (mostly) about changing which branch is the current branch, by writing a new branch name into HEAD, or by "detaching" HEAD: writing a commit hash ID into HEAD. In the process, git checkout will change both the index and the work-tree.

Here is where things get a bit complicated. :-) Since you are using --hard, git reset will update the index and work-tree. So now it sounds like it is a lot more like git checkout, and in some ways it is. But there are a couple of critical differences:

  1. Using git reset changes which commit the current branch points-to. Using git checkout changes which commit is current.

    Remember, we said above that names like master and develop are how we get Git to remember big ugly hash IDs. The effect is that these names act like labels, pasted onto, or pointing to, specific commits. The git reset command lets you move the label: to change where it points; to peel it off one specific commit and paste it onto another. By contrast, git checkout does not move labels. Instead, it changes which branch name is stored in .git/HEAD. You git checkout master to switch to branch master, and then git checkout develop to switch to branch develop. The labels stay where they are, but Git changes the name stored in .git/HEAD.

    All of this applies in the normal case, when you are "on a branch". If you are in the "detached HEAD" case, git reset still moves your detached HEAD from one commit to another—but since there's no branch name involved, this does not change any existing branch names. Likewise, if you git checkout something that is not a branch name, git checkout "detaches your HEAD" by writing the raw commit ID into .git/HEAD.

    Note that git reset never detaches HEAD and never re-attaches HEAD. That's something that git checkout does. Meanwhile, git checkout never changes any branch labels, but git reset does.

  2. The git checkout command tries to be non-destructive (in this mode anyway; see footnote 2 again). The git reset command happily destroys unsaved work.

    In practice, what this means is that if you have made changes to the index and/or work-tree, git checkout won't overwrite them. This gets particularly complicated, because it will switch branches or commits, if it can. It does so by leaving the unsaved work in place, in the index and/or work-tree, if it can. If it can't, it simply errors out.

    By contrast, git reset --hard will throw away these unsaved changes: --hard means overwrite both index and work-tree.

In a comment, you added:

I want to ignore any commits that anyone might have made on the server (even though that is unlikely)

but "ignore" is different from "discard", and none of this addresses uncommitted changes made to the index and/or work-tree.

Normally, the right thing to do with a centralized server like this is to set one up that has no work-tree, i.e., is a --bare repository. A repository that has no work-tree can have no work being done in it. There is nothing to maybe-save, maybe-ignore, or maybe-discard in the first place. If you use this method, all these distinctions fall away. That's your best bet.

Even if you cannot or will not use a --bare repository, let's look at the remaining items. Besides all of the above considerations, remember that a centralized repository—one to which someone git pushes new commits in the first place—still has a current (HEAD) commit, which—if it is not detached—names a current branch. It also still has an index, and if not bare, a work-tree. Git will normally refuse a push to the current branch of a work-tree: see receive.denyCurrentBranch in the git config documentation. Since a bare repository has no work-tree, this particular Git problem also vanishes for bare repositories.

(Your description, though, sounds like you have this set up not as a central server to which others push directly, but rather as a client of yet another server, where you use git fetch on the client. If that's the case, several items below become non-issues.)

So, let's say you have a non-bare repository, with a work-tree. To allow pushes to succeed, you will probably need to use a detached HEAD, otherwise Git will refuse pushes to the current branch. (Newer Gits have the ability to accept them, but you must use it very carefully; see the linked documentation for details.) A detached HEAD will give you a current commit without a current branch, and will make git reset and git checkout closer cousins than if you were on a branch.

(It also defeats a feature that clients see when they clone the central repository: clients will check out whichever branch is current on the server, i.e., the client asks the server "what's your HEAD branch?" and checks that out automatically. This feature has somewhat dubious value, so you may not miss it, but it's worth noting.)

If you are using this detached HEAD, then, the primary difference between git reset --hard origin/master and git checkout origin/master is what happens to uncommitted index and/or work-tree modifications. With git reset --hard, they will be wiped out, with no warning. Anyone actively working on the server will be SOL. The index and work-tree will be re-set to match the new commit.

If you are not using a detached HEAD, git reset --hard origin/master will write the commit ID for origin/master into the current branch name. As before, git reset will update the index and work-tree as well. This not only wipes out active work, it also throws away commits made on the current branch, by peeling the branch label off the old commit and pasting it onto the new one.

With git checkout, well, this is also a bit complicated. Since origin/master is a so-called remote-tracking branch, checking out this name gives you a detached HEAD, with the commit hash stored directly in .git/HEAD. This is true regardless of whether HEAD was attached or detached before. So HEAD now is detached, provided that the checkout succeeds. If there are no uncommitted changes in the index and work-tree, the checkout should3 succeed. If there are uncommitted changes, the checkout will carry them to the new detached HEAD if possible, or fail if switching to that commit cannot be done without wiping out the uncommitted work.

Hence, those are your options, if you choose to proceed along this path. Since Git is a large tool-set, rather than a single canned solution, you can pick some other path(s) if those are more suitable.

(Note that git clean -f -x will remove untracked and ignored files, i.e., things that are not in the index. Without -d, however, it will not remove untracked directories.)


1There is an exception to this "always a current commit" rule, used mainly for the case of a new, empty repository, when there are no commits at all. You won't encounter this exception unless you are working in an empty repository, or use git checkout --orphan to create a new but unborn branch, as Git calls it.

2Both git reset and git checkout have additional usages that deviate, anywhere from a little bit to a lot, from their main function. I am describing only their main function here.

3A git checkout can still fail due to, e.g., running out of disk space.

这篇关于使用GIT部署代码-签出vs重置-难吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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