【问题标题】:Deploy a project using Git push使用 Git 推送部署项目
【发布时间】:2010-09-21 16:56:10
【问题描述】:

是否可以使用git push 部署网站?我有一种预感,这与使用git hooks 在服务器端执行git reset --hard 有关,但我将如何实现呢?

【问题讨论】:

  • 我猜这只适用于只有一个生产服务器的情况,对吧?
  • @Rijk 好吧,您可以使用 Git 同时推送到多台服务器,但是一旦达到该级别,您可能需要一个实际的解决方案,而不是像这样的 hack。
  • 我在我的项目中成功使用了capistrano,虽然它最初是为 Ruby on Rails 应用程序部署而设计的,但也适用于 PHP 和其他项目。
  • 在 ru.so 上将答案翻译成俄语:ru.stackoverflow.com/questions/428483/…

标签: git deployment webserver githooks


【解决方案1】:

我这样做的方式是在我的部署服务器上有一个裸 Git 存储库,用于推送更改。然后我登录到部署服务器,切换到实际的 Web 服务器 docs 目录,然后执行 git pull。我不使用任何钩子来尝试自动执行此操作,这似乎比它的价值更麻烦。

【讨论】:

  • 如果新代码中出现错误,您是在每次提交时重置还是在整个拉取时重置? (或者只有 1 种可能?)
  • @Rudie:如果您需要回滚部署服务器上的更改,那么您可以使用 git reset最新 更改之间移动(所有提交,而不仅仅是整个拉)。如果您需要回滚不是最新提交的特定内容,那么您可以使用git revert,但这可能只应在紧急情况下使用(git revert 创建一个新提交,以撤消先前提交的效果)。跨度>
  • 只是出于好奇:为什么你认为 hooks 会比它的价值更麻烦?
  • @Rijk:当为此使用钩子时,实际的 Web 服务器文档目录由自动后台进程更改。登录让我可以更好地控制何时将更改应用于 docs 目录。此外,当出现问题时更容易修复。如果提交者没有足够的访问权限来登录 Web 服务器,那么 Hooks 可能更合适。
  • 所以你实际的 webapp 文件夹也是一个 .git 存储库? .git 文件夹呢,对外界可见吗?
【解决方案2】:

听起来您的服务器上应该有两个副本。一个裸副本,您可以从中推送/拉取,完成后您将推送您的更改,然后您将其克隆到您的 Web 目录并设置一个 cronjob 以每天从您的 Web 目录更新 git pull 或所以。

【讨论】:

    【解决方案3】:

    您可以设置一个 git 钩子,当提交说“稳定”分支时,它会拉取更改并将它们应用到 PHP 站点。最大的缺点是,如果出现问题,您将无法进行太多控制,并且会增加测试时间 - 但您可以了解当您将主干分支合并到稳定分支时将涉及多少工作以了解您可能遇到多少冲突。密切关注任何特定于站点的文件(例如配置文件)非常重要,除非您只打算只运行一个站点。

    您是否考虑过将更改推送到网站?

    有关 git 挂钩的信息,请参阅 githooks 文档。

    【讨论】:

      【解决方案4】:

      我在this site 上找到了this script,它似乎工作得很好。

      1. 将 .git 目录复制到 Web 服务器
      2. 在您的本地副本上,修改您的 .git/config 文件并将您的 Web 服务器添加为远程:

        [remote "production"]
            url = username@webserver:/path/to/htdocs/.git
        
      3. 在服务器上,将 .git/hooks/post-update 替换为 this file(在下面的答案中)

      4. 添加对文件的执行权限(同样,在服务器上):

        chmod +x .git/hooks/post-update
        
      5. 现在,只需在本地推送到您的网络服务器,它就会自动更新工作副本:

        git push production
        

      【讨论】:

      • 确保你有一个 .htaccess 策略来保护 .git 目录不被读取。如果可以访问整个源代码,那么喜欢 URL 潜水的人可能会在现场度过一天。
      • 或者只是将公共目录设为 git repo 的子目录。然后,您可以拥有可以确保不会公开的私人文件。
      • 此链接已失效。还有更新后文件的链接吗?
      • 也许我遗漏了一些东西,但您不希望您的生产服务器从主 git 存储库生产分支 pull。我猜OP只有一台服务器?我通常让我的持续集成服务器来部署我的站点(在部署之前运行一些测试)。
      • 从已经有一系列提交的存储库中执行这些步骤;起初你不能推送,因为主分支已经签出。然后,如果您在远程签出替代分支,则仅将不同的文件签出到工作目录中。我希望钩子能重置 -- 对我来说很难
      【解决方案5】:

      本质上,您需要做的只是以下几点:

      server = $1
      branch = $2
      git push $server $branch
      ssh <username>@$server "cd /path/to/www; git pull"
      

      我的应用程序中有这些行作为一个名为deploy 的可执行文件。

      所以当我想进行部署时,我输入./deploy myserver mybranch

      【讨论】:

      • 如果您需要不同的 ssh 私钥或用户名,请参阅我的回答如何解决问题
      • 这个解决方案在部署到多台服务器时比我自己的要快!只需推送到主仓库并从中并行提取。如果您不想或不能将密钥部署到每个实例,请使用密钥代理! ssh -A ...
      • 如果您包含有关设置 SSH 密钥的指南会更容易,此答案依赖于“无缝”工作
      • 在自动部署中应避免使用git pull,因为如果有任何冲突,它的合并部分可能需要手动清理。
      【解决方案6】:

      使用下面的更新后文件:

      1. 将 .git 目录复制到 Web 服务器
      2. 在您的本地副本上,修改您的 .git/config 文件并将您的 Web 服务器添加为远程:

        [remote "production"]
            url = username@webserver:/path/to/htdocs/.git
        
      3. 在服务器上,将 .git/hooks/post-update 替换为以下文件

      4. 添加对文件的执行权限(同样,在服务器上):

        chmod +x .git/hooks/post-update
        
      5. 现在,只需在本地推送到您的网络服务器,它就会自动更新工作副本:

        git push production
        
      #!/bin/sh
      #
      # This hook does two things:
      #
      #  1. update the "info" files that allow the list of references to be
      #     queries over dumb transports such as http
      #
      #  2. if this repository looks like it is a non-bare repository, and
      #     the checked-out branch is pushed to, then update the working copy.
      #     This makes "push" function somewhat similarly to darcs and bzr.
      #
      # To enable this hook, make this file executable by "chmod +x post-update". 
      git-update-server-info 
      is_bare=$(git-config --get --bool core.bare) 
      if [ -z "$is_bare" ]
      then
            # for compatibility's sake, guess
            git_dir_full=$(cd $GIT_DIR; pwd)
            case $git_dir_full in */.git) is_bare=false;; *) is_bare=true;; esac
      fi 
      update_wc() {
            ref=$1
            echo "Push to checked out branch $ref" >&2
            if [ ! -f $GIT_DIR/logs/HEAD ]
            then
                   echo "E:push to non-bare repository requires a HEAD reflog" >&2
                   exit 1
            fi
            if (cd $GIT_WORK_TREE; git-diff-files -q --exit-code >/dev/null)
            then
                   wc_dirty=0
            else
                   echo "W:unstaged changes found in working copy" >&2
                   wc_dirty=1
                   desc="working copy"
            fi
            if git diff-index --cached HEAD@{1} >/dev/null
            then
                   index_dirty=0
            else
                   echo "W:uncommitted, staged changes found" >&2
                   index_dirty=1
                   if [ -n "$desc" ]
                   then
                         desc="$desc and index"
                   else
                         desc="index"
                   fi
            fi
            if [ "$wc_dirty" -ne 0 -o "$index_dirty" -ne 0 ]
            then
                   new=$(git rev-parse HEAD)
                   echo "W:stashing dirty $desc - see git-stash(1)" >&2
                   ( trap 'echo trapped $$; git symbolic-ref HEAD "'"$ref"'"' 2 3 13 15 ERR EXIT
                   git-update-ref --no-deref HEAD HEAD@{1}
                   cd $GIT_WORK_TREE
                   git stash save "dirty $desc before update to $new";
                   git-symbolic-ref HEAD "$ref"
                   )
            fi 
            # eye candy - show the WC updates :)
            echo "Updating working copy" >&2
            (cd $GIT_WORK_TREE
            git-diff-index -R --name-status HEAD >&2
            git-reset --hard HEAD)
      } 
      if [ "$is_bare" = "false" ]
      then
            active_branch=`git-symbolic-ref HEAD`
            export GIT_DIR=$(cd $GIT_DIR; pwd)
            GIT_WORK_TREE=${GIT_WORK_TREE-..}
            for ref
            do
                   if [ "$ref" = "$active_branch" ]
                   then
                         update_wc $ref
                   fi
            done
      fi
      

      【讨论】:

      • 天啊...只需使用您用于开发的语言编写此脚本,无论是 php、python、groovy 还是其他语言!我从来不理解这种对 shell 脚本的喜爱,这些脚本(主观上)有很奇怪的语法和很少的功能特性。
      • @dVaffection 在任何情况下,如果您使用 git,您将编写 shell 命令。因此,不要用另一种语言编写脚本并不断在该语言和外壳之间折衷。你不觉得把它全部写在 shell 中似乎是合乎逻辑的吗?
      • 我还必须在服务器上执行“git config receive.denyCurrentBranch updateInstead”,以便它接受推送。我认为是因为分支已签出?
      【解决方案7】:

      在许多错误的开始和死胡同之后,感谢this article,我终于能够使用“git push remote”来部署网站代码。

      作者的更新后脚本只有一行长,他的解决方案不需要 .htaccess 配置来像其他人那样隐藏 Git 存储库。

      如果您将其部署在 Amazon EC2 实例上,会遇到一些障碍;

      1) 如果您使用 sudo 创建裸目标存储库,则必须将存储库的所有者更改为 ec2-user 否则推送将失败。 (尝试“chown ec2-user:ec2-user repo。”)

      2) 如果您未在 /etc/ssh/ssh_config 作为 IdentityFile 参数或在~/.ssh/config 使用“[主机] - 主机名 - 身份文件 - 用户”布局描述 here...

      ...但是,如果 Host 在 ~/.ssh/config 中配置并且与 HostName 不同,则 Git 推送将失败。 (这可能是一个 Git 错误)

      【讨论】:

      • 我按照你提到的文章中的步骤进行操作,一切都很顺利。我只是想知道在安全性或稳定性方面是否存在一些缺点。对此有何建议?
      • xl-t:假设您通过 SSH 使用 Git,我想说危险在于使用 Git 时出错。你可以问文章的作者;他以“欢迎提出问题和建议”结束。我目前的(脑死)复制策略是使用 Panic Software 的 Transmit。
      • 链接的文章在使用钩子时有一个重要要求。如果 .git 恰好与工作目录位于相同的命名方案中,则挂钩将失败。即 /foo/bar (工作目录)和 /foo/bar.git (准系统 git 存储库)。因此,请确保将 /foo/bar 重命名为其他名称,例如 /foo/bar.live 或 /foo/blah 好吧,如果您想知道,如果您的工作目录与准系统存储库是“远程:致命:无法跳转回原始 cwd:没有这样的文件或目录”
      • 我不明白为什么你需要一个部署后挂钩来运行。将代码更改推送到远程仓库意味着远程仓库是最新的。我错过了什么?
      • @CharlieS 您缺少的是 git 不会让您将分支推送到已签出该分支的存储库。在这种情况下,(恕我直言,非常好的)答案是拥有两个存储库:您推送到的一个裸存储库和第二个存储库,其工作目录在裸存储库被推送到时通过钩子更新。
      【解决方案8】:

      不要在服务器上安装 git 或复制 .git 文件夹。要从 git clone 更新服务器,您可以使用以下命令:

      git ls-files -z | rsync --files-from - --copy-links -av0 . user@server.com:/var/www/project
      

      您可能必须删除从项目中删除的文件。

      这会复制所有签入的文件。 rsync 使用的是安装在服务器上的 ssh。

      您在服务器上安装的软件越少,他就越安全,并且越容易管理它的配置和记录它。也不需要在服务器上保留完整的 git clone。这只会使正确保护一切变得更加复杂。

      【讨论】:

      • 一个警告:它将同步您在工作目录中的文件。我认为可以避免使用存储当前更改、清理所有内容、部署然后恢复存储的脚本。
      • 服务器是男性?
      【解决方案9】:

      考虑到您有多个开发人员访问同一个存储库的环境,以下指南可能会有所帮助。

      确保您拥有一个所有开发人员都属于的 unix 组,并将 .git 存储库的所有权授予该组。

      1. 在服务器存储库的 .git/config 中设置 sharedrepository = true。 (这告诉 git 允许提交和部署所需的多个用户。

      2. 将每个用户的 bashrc 文件中的 umask 设置为相同 - 002 是一个好的开始

      【讨论】:

        【解决方案10】:

        Giddyup 是与语言无关的 just-add-water git 挂钩,可通过 git push 自动部署。它还允许您使用自定义启动/停止挂钩来重新启动 Web 服务器、预热缓存等。

        https://github.com/mpalmer/giddyup

        查看examples

        【讨论】:

          【解决方案11】:

          更新:我现在使用 Lloyd Moore 解决方案和关键代理 ssh -A ...。推送到主存储库,然后从所有机器并行提取它会更快一些,并且需要在这些机器上进行更少的设置。


          这里没有看到这个解决方案。如果服务器上安装了 git,只需通过 ssh 推送即可。

          您需要在本地 .git/config 中添加以下条目

          [remote "amazon"]
              url = amazon:/path/to/project.git
              fetch = +refs/heads/*:refs/remotes/amazon/*
          

          但是,嘿,amazon: 是什么?在您的本地 ~/.ssh/config 中,您需要添加以下条目:

          Host amazon
              Hostname <YOUR_IP>
              User <USER>
              IdentityFile ~/.ssh/amazon-private-key
          

          现在你可以打电话了

          git push amazon master
          ssh <USER>@<YOUR_IP> 'cd /path/to/project && git pull'
          

          (顺便说一句:/path/to/project.git 与实际工作目录/path/to/project 不同)

          【讨论】:

            【解决方案12】:

            我最终创建了自己的基本部署工具,它会自动从 repo 中提取新更新 - https://github.com/jesalg/SlimJim - 基本上它会监听 github post-receive-hook 并使用代理来触发更新脚本。

            【讨论】:

              【解决方案13】:

              我们使用capistrano 来管理部署。 我们构建 capistrano 以部署在临时服务器上,然后与我们所有的服务器运行 rsync。

              cap deploy
              cap deploy:start_rsync (when the staging is ok)
              

              使用 capistrano,我们可以在出现错误时轻松回滚

              cap deploy:rollback
              cap deploy:start_rsync
              

              【讨论】:

              • 您是否将通过 rsync 进行的实时部署集成到 capistrano 中?
              【解决方案14】:

              我对@9​​87654321@ 解决方案的看法。

              git archive --prefix=deploy/  master | tar -x -C $TMPDIR | rsync $TMPDIR/deploy/ --copy-links -av username@server.com:/home/user/my_app && rm -rf $TMPDIR/deploy
              
              • 将 master 分支归档到 tar 中
              • 将 tar 存档解压缩到系统临时文件夹中的部署目录中。
              • rsync 更改到服务器
              • 从临时文件夹中删除部署目录。

              【讨论】:

                【解决方案15】:

                我正在使用toroid.org 的以下解决方案,它有一个更简单的钩子脚本。

                在服务器上:

                $ mkdir website.git && cd website.git
                $ git init --bare
                Initialized empty Git repository in /home/ams/website.git/
                

                并在服务器上安装钩子:

                $ mkdir /var/www/www.example.org
                $ cat > hooks/post-receive
                #!/bin/sh
                GIT_WORK_TREE=/var/www/www.example.org git checkout -f
                GIT_WORK_TREE=/var/www/www git clean -f -d # clean directory from removed files
                
                $ chmod +x hooks/post-receive
                

                在您的客户端上:

                $ mkdir website && cd website
                $ git init
                Initialized empty Git repository in /home/ams/website/.git/
                $ echo 'Hello, world!' > index.html
                $ git add index.html
                $ git commit -q -m "The humble beginnings of my web site."
                
                $ git remote add web ssh://server.example.org/home/ams/website.git
                $ git push web +master:refs/heads/master
                

                然后发布,只需键入

                $ git push web
                

                网站上有完整说明:http://toroid.org/ams/git-website-howto

                【讨论】:

                • 这种方式不会删除存储库中的现有文件。
                • 为什么是git push web +master:refs/heads/master 而不仅仅是git push web master
                【解决方案16】:

                git config --local receive.denyCurrentBranch updateInstead

                在 Git 2.3 中添加,这可能是一个很好的可能性:https://github.com/git/git/blob/v2.3.0/Documentation/config.txt#L2155

                您在服务器存储库中设置它,如果它是干净的,它还会更新工作树。

                在 2.4 中使用push-to-checkout hook and handling of unborn branches 进行了进一步改进。

                示例用法:

                git init server
                cd server
                touch a
                git add .
                git commit -m 0
                git config --local receive.denyCurrentBranch updateInstead
                
                cd ..
                git clone server local
                cd local
                touch b
                git add .
                git commit -m 1
                git push origin master:master
                
                cd ../server
                ls
                

                输出:

                a
                b
                

                这确实有on the GitHub announcement提到的以下缺点:

                • 您的服务器将包含一个 .git 目录,其中包含您项目的整个历史记录。您可能需要额外确保它不能提供给用户!
                • 在部署过程中,用户可能会瞬间遇到站点状态不一致的情况,有些文件是旧版本,有些文件是新版本,甚至是半写的文件。如果这对您的项目来说是个问题,那么推送部署可能不适合您。
                • 如果您的项目需要“构建”步骤,那么您必须明确设置,可能通过 githooks。

                但所有这些点都超出了 Git 的范围,必须由外部代码处理。所以从这个意义上说,这与 Git 挂钩是最终的解决方案。

                【讨论】:

                • 要设置它,在终端运行这个命令:'git config receive.denyCurrentBranch updateInstead'
                • 恕我直言,这应该是最受好评的答案。如果这个 oneliner 可以通过设置一个 git 选项来解决它,那么用长长的 shell 脚本/钩子来挖掘所有这些答案总是很疯狂。
                【解决方案17】:

                作为补充答案,我想提供一个替代方案。我正在使用 git-ftp,它工作正常。

                https://github.com/git-ftp/git-ftp

                简单易用,只需键入:

                git ftp push
                

                git会自动上传项目文件。

                问候

                【讨论】:

                  【解决方案18】:

                  部署场景

                  在我们的场景中,我们将代码存储在 github/bitbucket 上并希望部署到实时服务器。 在这种情况下,以下组合适用于我们(这是此处高度赞成的答案的混合)

                  1. 将您的.git 目录复制到您的网络服务器
                  2. 在您的本地副本上git remote add live ssh://user@host:port/folder
                  3. 远程:git config receive.denyCurrentBranch ignore
                  4. 在远程:nano .git/hooks/post-receive 并添加此内容:

                    #!/bin/sh GIT_WORK_TREE=/var/www/vhosts/example.org git checkout -f

                  5. 远程:chmod +x .git/hooks/post-receive

                  6. 现在您可以使用git push live 推送到那里

                  注意事项

                  • 此解决方案适用于较旧的 git 版本(使用 1.7 和 1.9 测试)
                  • 您需要确保先推送到 github/bitbucket,这样您才能在实时获得一致的 repo
                  • 如果您的 .git 文件夹位于文档根目录中,请确保通过添加到 .htaccess (source) 将其从外部隐藏:

                    RedirectMatch 404 /\..*$

                  【讨论】:

                    【解决方案19】:

                    我对 post-receive hook 使用了两种解决方案:

                    部署解决方案 1

                    #!/bin/bash 
                    #  /git-repo/hooks/post-receive - file content on server (chmod as 755 to be executed)
                    # DEPLOY SOLUTION 1 
                    
                        export GIT_DIR=/git/repo-bare.git
                        export GIT_BRANCH1=master
                        export GIT_TARGET1=/var/www/html
                        export GIT_BRANCH2=dev
                        export GIT_TARGET2=/var/www/dev
                        echo "GIT DIR:  $GIT_DIR/"
                        echo "GIT TARGET1:  $GIT_TARGET1/"
                        echo "GIT BRANCH1:  $GIT_BRANCH1/"
                        echo "GIT TARGET2:  $GIT_TARGET2/"
                        echo "GIT BRANCH2:  $GIT_BRANCH2/"
                        echo ""
                    
                        cd $GIT_DIR/
                    
                    while read oldrev newrev refname
                    do
                        branch=$(git rev-parse --abbrev-ref $refname)
                        BRANCH_REGEX='^${GIT_BRANCH1}.*$'
                        if [[ $branch =~ $BRANCH_REGEX ]] ; then
                            export GIT_WORK_TREE=$GIT_TARGET1/.
                            echo "Checking out branch: $branch";
                            echo "Checking out to workdir: $GIT_WORK_TREE"; 
                    
                            git checkout -f $branch
                        fi
                    
                        BRANCH_REGEX='^${GIT_BRANCH2}.*$'
                        if [[ $branch =~ $BRANCH_REGEX ]] ; then
                            export GIT_WORK_TREE=$GIT_TARGET2/.
                            echo "Checking out branch: $branch";
                            echo "Checking out to workdir: $GIT_WORK_TREE"; 
                    
                            git checkout -f $branch
                        fi
                    done
                    

                    部署解决方案 2

                    #!/bin/bash 
                    #  /git-repo/hooks/post-receive - file content on server (chmod as 755 to be executed)
                    # DEPLOY SOLUTION 2
                    
                        export GIT_DIR=/git/repo-bare.git
                        export GIT_BRANCH1=master
                        export GIT_TARGET1=/var/www/html
                        export GIT_BRANCH2=dev
                        export GIT_TARGET2=/var/www/dev
                        export GIT_TEMP_DIR1=/tmp/deploy1
                        export GIT_TEMP_DIR2=/tmp/deploy2
                        echo "GIT DIR:  $GIT_DIR/"
                        echo "GIT TARGET1:  $GIT_TARGET1/"
                        echo "GIT BRANCH1:  $GIT_BRANCH1/"
                        echo "GIT TARGET2:  $GIT_TARGET2/"
                        echo "GIT BRANCH2:  $GIT_BRANCH2/"
                        echo "GIT TEMP DIR1:  $GIT_TEMP_DIR1/"
                        echo "GIT TEMP DIR2:  $GIT_TEMP_DIR2/"
                        echo ""
                    
                        cd $GIT_DIR/
                    
                    while read oldrev newrev refname
                    do
                        branch=$(git rev-parse --abbrev-ref $refname)
                        BRANCH_REGEX='^${GIT_BRANCH1}.*$'
                        if [[ $branch =~ $BRANCH_REGEX ]] ; then
                            export GIT_WORK_TREE=$GIT_TARGET1/.
                            echo "Checking out branch: $branch";
                            echo "Checking out to workdir: $GIT_WORK_TREE"; 
                    
                            # DEPLOY SOLUTION 2: 
                            cd $GIT_DIR/; mkdir -p $GIT_TEMP_DIR1; 
                            export GIT_WORK_TREE=$GIT_TEMP_DIR1/.
                            git checkout -f $branch
                            export GIT_WORK_TREE=$GIT_TARGET1/.
                            rsync $GIT_TEMP_DIR1/. -v -q --delete --delete-after -av $GIT_TARGET1/.
                            rm -rf $GIT_TEMP_DIR1
                        fi
                    
                        BRANCH_REGEX='^${GIT_BRANCH2}.*$'
                        if [[ $branch =~ $BRANCH_REGEX ]] ; then
                            export GIT_WORK_TREE=$GIT_TARGET2/.
                            echo "Checking out branch: $branch";
                            echo "Checking out to workdir: $GIT_WORK_TREE"; 
                    
                            # DEPLOY SOLUTION 2: 
                            cd $GIT_DIR/; mkdir -p $GIT_TEMP_DIR2; 
                            export GIT_WORK_TREE=$GIT_TEMP_DIR2/.
                            git checkout -f $branch
                            export GIT_WORK_TREE=$GIT_TARGET2/.
                            rsync $GIT_TEMP_DIR2/. -v -q --delete --delete-after -av $GIT_TARGET2/.
                            rm -rf $GIT_TEMP_DIR2
                        fi
                    done
                    

                    这两个解决方案都基于此线程中可用的早期解决方案。

                    请注意, BRANCH_REGEX='^${GIT_BRANCH1}.$' 过滤与“master”或“dev*”字符串匹配的分支名称,如果推送的分支匹配,则部署工作树。 这使得将开发版本和主版本部署到不同的地方成为可能。

                    DEPLOY SOLUTION 1 仅删除作为 repo 一部分并通过提交删除的文件。它比部署解决方案 2 更快。

                    DEPLOY SOLUTION 2 的优点是,它会从生产目录中删除任何新文件,这些文件是在服务器端添加的,无论它是否添加到 repo 中。它将永远是回购的干净骗局。它比部署解决方案 1 慢。

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2017-04-27
                      • 1970-01-01
                      • 2022-12-12
                      • 2013-02-28
                      相关资源
                      最近更新 更多