如何备份本地Git仓库?

171

我在一个相对较小的项目中使用git,并发现将.git目录的内容压缩成zip文件是一种不错的备份方式。但这有点奇怪,因为在还原时,我需要做的第一件事情是git reset --hard

这种备份git仓库的方式是否存在问题?此外,是否有更好的方法来进行备份(例如便携式git格式或类似的东西)?


为什么没有人给出使用 git bundle 的显而易见的答案呢? - gatopeich
@gatopeich 他们做了。向下滚动。 - Dan Rosenstark
2
所有被点赞的答案都包含关于自定义脚本的大段文字,即使是那个提到“git bundle”的答案。 - gatopeich
8个回答

159

另一种官方的方法是使用git bundle

这将创建一个支持git fetchgit pull更新您的第二个仓库的文件。
对于增量备份和恢复非常有用。

但如果您需要备份所有内容(因为您没有已经存在某些旧内容的第二个仓库),备份就需要更加复杂,如我在Kent Fredric的评论后提到的。

$ git bundle create /tmp/foo master
$ git bundle create /tmp/foo-all --all
$ git bundle list-heads /tmp/foo
$ git bundle list-heads /tmp/foo-all

(这是一个原子操作,与从.git文件夹创建归档不同,如评论所述by fantabolous)


警告:我不建议使用Pat Notzsolution,它是克隆存储库。
备份许多文件总是比备份或更新...只有一个更棘手。

如果您查看OP Yar answerhistory of edits,您会发现Yar首先使用了clone --mirror,...与编辑:

在Dropbox中使用这个会一团糟
您将遇到同步错误,并且您无法在Dropbox中回滚目录。
如果要备份到Dropbox,请使用git bundle

Yar的current solution使用了git bundle

我休息我的案子。


@ShadowCreeper 我同意。增量捆绑包是可能的,但管理起来很棘手。 - VonC
嘿@VonC,希望你不介意,我已经更改了接受的答案以促进SO上的新输入。希望你一切都好! - Dan Rosenstark
@Yar,我完全不介意。很棒的新输入。很好的选择。祝你新年快乐 :) - VonC
3
回复一条旧评论,但压缩文件夹和捆绑的另一个区别是,捆绑是原子性的,所以如果在操作过程中有人碰巧更新了你的代码库,它就不会被搞乱。 - fantabolous
3
@fantabolous 的观点很好。为了更加突出它,我已经将其包含在答案中。 - VonC
显示剩余8条评论

68
我这样做的方法是创建一个远程(裸)仓库(在独立驱动器、USB密钥、备份服务器甚至Github上),然后使用push --mirror将该远程仓库与我的本地仓库完全对应(除了远程仓库是仓库)。
这将推送所有引用(分支和标记),包括非快进更新。我使用它来创建本地仓库的备份。 手册页面是这样描述它的:
“不是为每个要推送的引用命名,而是指定将$GIT_DIR/refs/下的所有引用(其中包括但不限于refs/heads/refs/remotes/refs/tags/)映射到远程仓库。新创建的本地引用将被推送到远程端,本地更新的引用将被强制更新到远程端,并删除的引用将从远程端移除。如果配置选项remote.<remote>.mirror设置了,则这是默认设置。”
我创建了一个别名来执行推送:
git config --add alias.bak "push --mirror github"

那么,每当我想要备份时,我只需运行git bak


+1。同意。git bundle很好,可以将备份移动到一个文件中。但是使用可以插在任何地方的驱动器,裸仓库也可以。 - VonC
+1 很棒,我会研究一下。谢谢你提供的例子。 - Dan Rosenstark
@Pat Notz,最终我决定采用你的方法,并在下面放了一个答案(得分永久保持为零 :))。 - Dan Rosenstark
请注意,--mirror 实际上不会对其获取的对象进行任何验证。您应该在某个时候运行 git fsck 以防止损坏。 - docwhat

35

[只是为了自己参考,把这里留下了。]

我的捆绑脚本叫做git-backup,看起来像这样。

#!/usr/bin/env ruby
if __FILE__ == $0
        bundle_name = ARGV[0] if (ARGV[0])
        bundle_name = `pwd`.split('/').last.chomp if bundle_name.nil? 
        bundle_name += ".git.bundle"
        puts "Backing up to bundle #{bundle_name}"
        `git bundle create /data/Dropbox/backup/git-repos/#{bundle_name} --all`
end
有时候我使用 git backup,有时候我使用git backup different-name,这样我就可以获得我所需的大部分可能性。

2
+1 因为您没有使用 --global 选项,所以此别名仅在您的项目中可见(它在您的 .git/config 文件中定义)-- 这可能是您想要的。感谢您提供更详细和格式良好的答案。 - Pat Notz
1
@yar:你知道如何在不使用命令行的情况下,只使用TortoiseGit来完成这些任务吗?我正在寻找解决方案,以便我的非命令行Windows用户也能够使用。 - macf00bar
@Yar:我不太确定你的意思。你是说如果我删除了一个由Dropbox支持的目录,那么其中包含的所有文件以前的修订都会消失吗?更多关于SpiderOak版本控制策略的信息请参见此处:https://spideroak.com/engineering_matters#efficient_versioning 。老实说,我并没有经常使用SpiderOak,并不完全确定它的限制。但似乎他们应该为这样的问题提供了解决方案,因为他们非常强调技术能力。另外,Dropbox的免费账户是否仍然有30天回滚限制呢? - intuited
@intuited,如果您删除了一个由Dropbox支持的目录,则可以逐个恢复其文件。不过,我已经有一段时间没有检查过了。不确定免费帐户上Packrat的限制是什么,但这并不重要:游戏已经赢了,Dropbox正在击败其他所有竞争对手。他们足够技术先进。 - Dan Rosenstark
@Yar:我很确定SpiderOak在恢复文件方面的工作方式与Dropbox相同。我考虑过Dropbox,但基于其条款和条件声称他们会因为涉嫌版权侵犯而没有预警地删除用户账户,所以最终决定不用。而SpiderOak 使用客户端加密技术,因此无法有任何类似的过失。 - intuited
显示剩余2条评论

24

我开始在Yar的脚本上进行一些修改,结果已经上传到Github上,包括手册和安装脚本:

https://github.com/najamelan/git-backup

安装:

git clone "https://github.com/najamelan/git-backup.git"
cd git-backup
sudo ./install.sh

欢迎在 GitHub 上提出建议和拉取请求。
#!/usr/bin/env ruby
#
# For documentation please sea man git-backup(1)
#
# TODO:
# - make it a class rather than a function
# - check the standard format of git warnings to be conform
# - do better checking for git repo than calling git status
# - if multiple entries found in config file, specify which file
# - make it work with submodules
# - propose to make backup directory if it does not exists
# - depth feature in git config (eg. only keep 3 backups for a repo - like rotate...)
# - TESTING



# allow calling from other scripts
def git_backup


# constants:
git_dir_name    = '.git'          # just to avoid magic "strings"
filename_suffix = ".git.bundle"   # will be added to the filename of the created backup


# Test if we are inside a git repo
`git status 2>&1`

if $?.exitstatus != 0

   puts 'fatal: Not a git repository: .git or at least cannot get zero exit status from "git status"'
   exit 2


else # git status success

   until        File::directory?( Dir.pwd + '/' + git_dir_name )             \
            or  File::directory?( Dir.pwd                      ) == '/'


         Dir.chdir( '..' )
   end


   unless File::directory?( Dir.pwd + '/.git' )

      raise( 'fatal: Directory still not a git repo: ' + Dir.pwd )

   end

end


# git-config --get of version 1.7.10 does:
#
# if the key does not exist git config exits with 1
# if the key exists twice in the same file   with 2
# if the key exists exactly once             with 0
#
# if the key does not exist       , an empty string is send to stdin
# if the key exists multiple times, the last value  is send to stdin
# if exaclty one key is found once, it's value      is send to stdin
#


# get the setting for the backup directory
# ----------------------------------------

directory = `git config --get backup.directory`


# git config adds a newline, so remove it
directory.chomp!


# check exit status of git config
case $?.exitstatus

   when 1 : directory = Dir.pwd[ /(.+)\/[^\/]+/, 1]

            puts 'Warning: Could not find backup.directory in your git config file. Please set it. See "man git config" for more details on git configuration files. Defaulting to the same directroy your git repo is in: ' + directory

   when 2 : puts 'Warning: Multiple entries of backup.directory found in your git config file. Will use the last one: ' + directory

   else     unless $?.exitstatus == 0 then raise( 'fatal: unknown exit status from git-config: ' + $?.exitstatus ) end

end


# verify directory exists
unless File::directory?( directory )

   raise( 'fatal: backup directory does not exists: ' + directory )

end


# The date and time prefix
# ------------------------

prefix           = ''
prefix_date      = Time.now.strftime( '%F'       ) + ' - ' # %F = YYYY-MM-DD
prefix_time      = Time.now.strftime( '%H:%M:%S' ) + ' - '
add_date_default = true
add_time_default = false

prefix += prefix_date if git_config_bool( 'backup.prefix-date', add_date_default )
prefix += prefix_time if git_config_bool( 'backup.prefix-time', add_time_default )



# default bundle name is the name of the repo
bundle_name = Dir.pwd.split('/').last

# set the name of the file to the first command line argument if given
bundle_name = ARGV[0] if( ARGV[0] )


bundle_name = File::join( directory, prefix + bundle_name + filename_suffix )


puts "Backing up to bundle #{bundle_name.inspect}"


# git bundle will print it's own error messages if it fails
`git bundle create #{bundle_name.inspect} --all --remotes`


end # def git_backup



# helper function to call git config to retrieve a boolean setting
def git_config_bool( option, default_value )

   # get the setting for the prefix-time from git config
   config_value = `git config --get #{option.inspect}`

   # check exit status of git config
   case $?.exitstatus

      # when not set take default
      when 1 : return default_value

      when 0 : return true unless config_value =~ /(false|no|0)/i

      when 2 : puts 'Warning: Multiple entries of #{option.inspect} found in your git config file. Will use the last one: ' + config_value
               return true unless config_value =~ /(false|no|0)/i

      else     raise( 'fatal: unknown exit status from git-config: ' + $?.exitstatus )

   end
end

# function needs to be called if we are not included in another script
git_backup if __FILE__ == $0

1
@Yar 很棒的捆绑脚本,基于我在下面回答中提倡的 git 捆绑。+1。 - VonC
1
我已经在本地裸仓库中安装了你的应用程序...一旦安装完成,如何使用它...文档中没有相关信息,你应该包含一个示例部分,说明如何进行备份。 - JAF
抱歉,您无法使其正常工作。通常情况下,您需要运行“sudo install.sh”,然后配置它(它使用git config系统)以设置目标目录(请参阅github上的自述文件)。接下来,在您的存储库中运行“git backup”。顺便说一句,这是一个使用git bundle的实验,并回答了这个问题,但是git bundle从未创建过绝对精确的副本(例如,如果我正确地记得,特别是关于git remotes),所以我个人实际上使用tar备份.git目录。 - user1115652

9

这个问题的两个答案都是正确的,但我仍然缺少一个完整而简短的解决方案,将Github存储库备份到本地文件中。 此处提供了gist,随意fork或根据您的需要进行修改。

backup.sh:

#!/bin/bash
# Backup the repositories indicated in the command line
# Example:
# bin/backup user1/repo1 user1/repo2
set -e
for i in $@; do
  FILENAME=$(echo $i | sed 's/\//-/g')
  echo "== Backing up $i to $FILENAME.bak"
  git clone git@github.com:$i $FILENAME.git --mirror
  cd "$FILENAME.git"
  git bundle create ../$FILENAME.bak --all
  cd ..
  rm -rf $i.git
  echo "== Repository saved as $FILENAME.bak"
done

restore.sh:

#!/bin/bash
# Restore the repository indicated in the command line
# Example:
# bin/restore filename.bak
set -e

FOLDER_NAME=$(echo $1 | sed 's/.bak//')
git clone --bare $1 $FOLDER_NAME.git

1
有趣。比我的答案更精确。+1 - VonC
谢谢,这对Github很有用。被接受的答案是针对当前问题的。 - Dan Rosenstark

9

在浏览了上面的大段文字之后,我发现了一个简单的官方方法,让你觉得好像没有这样的方法一样。

创建一个完整的捆绑包,具体步骤如下:

$ git bundle create <filename> --all

使用以下命令进行还原:

$ git clone <filename> <folder>

据我所知,此操作是原子操作。有关详细信息,请查看官方文档

关于“zip”:Git Bundles 压缩后的大小与 .git 文件夹相比惊人地小。


这并没有回答有关zip的整个问题,还假设我们已经阅读了其他答案。请修正它,使其具有原子性并处理整个问题,我很高兴将其作为被接受的答案(10年后)。谢谢。 - Dan Rosenstark

6
你可以使用git-copy备份git仓库。 git-copy会将新项目保存为裸仓库,这意味着最少的存储成本。
git copy /path/to/project /backup/project.backup

然后你可以使用git clone恢复你的项目

git clone /backup/project.backup project

2
啊!这个答案让我误以为“git copy”是一个官方的git命令。 - gatopeich

0

通过谷歌搜索来到这个问题。

以下是我以最简单的方式所做的。

git checkout branch_to_clone

然后从这个分支创建一个新的git分支

git checkout -b new_cloned_branch
Switched to branch 'new_cloned_branch'

回到原始分支并继续:

git checkout branch_to_clone

假设您搞砸了,需要从备份分支中恢复某些内容:

git checkout new_cloned_branch -- <filepath>  #notice the space before and after "--"

最好的部分是,如果出了什么问题,你可以删除源分支并返回备份分支!


1
我喜欢这个方法,但我不确定它是否是最佳实践?我经常制作“备份”git分支,最终会有很多备份分支。我不确定这样做是否可以(从不同日期创建大约20个备份分支)。我想我总是可以删除旧备份 - 但如果我想保留所有备份 - 是否可以这样做呢?到目前为止,一切都很好 - 但知道这是否是好或坏的实践会很好。 - Kyle Vassella
这并不是所谓的“最佳实践”,我认为它更多地与个人处理事情的习惯有关。我通常只在一个分支中编写代码,直到工作完成,并保留另一个分支用于“临时”请求。两者都有备份,一旦完成,删除主分支! :) - NoobEditor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接