提交机器特定的配置文件

88

在我进行开发时,一个常见的情况是代码库将有几个配置文件需要机器特定的设置。这些文件将被提交到Git中,其他开发人员会不小心再次将它们提交,从而破坏其他人的配置。

一个简单的解决方案是不要将它们提交到Git中,甚至为它们添加 .gitignore 条目。但是,我发现更加优雅的方式是在文件中设置一些合理的默认值,开发人员可以修改以适应自己的需求。

是否有一种优雅的方法使Git与这些文件友好相处?我想能够修改特定于机器的配置文件,然后运行 "git commit -a" 而无需将该文件检入。


1
这听起来像是你的设计和同事的头脑出了问题。告诉他们确保他们知道自己提交到源代码控制系统中的内容,否则他们会提交一些你不想要的垃圾。另外:为什么不将文件拆分为每个系统一个文件呢? - Pod
11
我很确定这是一个相当普遍的情况吧?你如何跟踪机器特定的配置?为每个系统拆分文件似乎非常混乱,并且有点违背了分布式版本控制的目的:如果在新机器上检出它,就不需要检入新文件。 - ghempton
1
你可能至少可以通过在你们所有推送到的共享仓库上使用一个预更新钩子来防止引入破坏性提交。它可以查找由特定开发人员修改的配置文件的提交,或者可以查找触及该文件但消息中没有提及特定关键字的提交。 - Phil Miller
3
+1,这确实是一个常见的问题。@Pod:将“Joe.conf”放在版本库中不切实际,但有时您仍希望能够更新配置文件……有时由于代码更改,配置文件必须进行更改。 - Thanatos
10个回答

61
让你的程序读取一对配置文件以获取其设置。首先,它应该读取一个名为config.defaults的文件,该文件应该包含在代码库中。然后,它应该读取一个名为config.local的文件,该文件应该列在.gitignore中。
通过这种方式,当默认文件更新时,新的设置会立即生效。只有在需要覆盖特定系统时,它们才会变化。
作为这种方式的变化,您可以只使用通用的config文件,将其放在版本控制中,并使用include config.local的方法引入特定机器的值。这引入了更通用的机制(而不是策略)到你的代码中,因此可以实现更复杂的配置(如果你的应用需要)。从这个方面来看,许多大型开源软件都采用了一种流行的扩展,即include conf.d,该扩展可以从目录中的所有文件中读取配置。
此外,还可以查看我回答类似问题的答案

我来给出答案。这种方法可以实现所需的效果,唯一的缺点是需要应用程序额外的逻辑处理。 - ghempton

17
您可以尝试使用命令git update-index --skip-worktree 文件名。这会告诉git假装本地对文件名的更改不存在,因此git commit -a会忽略它。它还有一个额外的好处,即可以抵御git reset --hard,因此您不会意外丢失本地更改。如果文件在上游发生更改,自动合并也会优雅地失败(除非工作目录副本与索引副本匹配,在这种情况下,它将被自动更新)。缺点是必须在涉及的所有计算机上运行该命令,而且难以自动化执行。有关此想法的微妙不同版本,请参阅git update-index --assume-unchanged。可以在git help update-index中找到这两个命令的详细信息。

您可以在问题“assume-unchanged”和“skip-worktree”的区别中找到有关这些命令的更多信息。从最佳答案来看,在这种情况下,您需要使用--skip-worktree - Senseful

10

另一种方法是在另一个私有分支中维护对共同配置文件的本地更改。我在需要多个本地更改的一些项目中使用这种技术。这种技术可能不适用于所有情况,但在某些情况下对我很有效。

首先,我基于主分支创建一个新的分支(在这种特殊情况下,我正在使用git-svn,因此需要从主分支提交,但这里并不是非常重要):

git checkout -b work master

现在根据需要修改配置文件并提交。我通常在提交信息中加入一些独特的标识,比如“NOCOMMIT”或“PRIVATE”(这将在以后很有用)。此时,您可以使用自己的配置文件在私有分支上工作。

当您想要将工作推回上游时,请从您的work分支中挑选每个更改并合并到主分支中。我有一个脚本来帮助完成这项工作,大致如下:

#!/bin/sh

BRANCH=`git branch | grep ^\\* | cut -d' ' -f2`
if [ $BRANCH != "master" ]; then
  echo "$0: Current branch is not master"
  exit 1
fi

git log --pretty=oneline work...master | grep -v NOCOMMIT: | cut -d' ' -f1 | tac | xargs -l git cherry-pick

首先,这个代码检查我是否在master分支上(健全性检查)。 然后,它列出了work中的每个提交,过滤掉提到NOCOMMIT关键字的提交,倒序排列,最后将每个提交(现在从最老的开始)cherry-pick到master中。

最后,在将更改推送到上游的master后,我切换回work并进行变基:

git checkout work
git rebase master

Git会重新应用在work分支中的每个提交,有效地跳过那些已经通过cherry-picking应用在master中的提交。你最终只剩下NOCOMMIT本地提交。

这种技术使得推送过程变得有点耗时,但它解决了我的问题,所以我想分享一下。


2
你意识到你在要求那些毫不在意的人来完成这个任务吗?那些只是毫不犹豫地运行 git commit -a 的人? - Phil Miller
1
按照相同的策略,您可以标记设置本地配置文件的提交,并使用 git rebase --ontogit fetch 的组合来完成相同的操作。 - Danilo Souza Morães

8

一种可能的方法是将实际文件放在.gitignore中,但使用不同的扩展名检查默认配置。Rails应用程序的典型示例是config/database.yml文件。我们会检查config/database.yml.sample,并且每个开发人员都创建自己的config/database.yml,该文件已经.gitignored。


是的,这是一个渐进式的改进,但仍然不是最优的,因为如果签入的版本被有意更改,则不会反映在开发人员配置文件中。当添加新属性时等情况下,这将非常有用。 - ghempton
可以通过良好的提交注释和描述性错误信息来解决这个问题,当属性未设置时抱怨。此外,向团队发送电子邮件来沟通更改也有帮助。 - Brian Kelly
关于此解决方案的更多信息和一个很好的示例,请参见这个答案 - Senseful

1
我同意最佳答案,但也想补充一些内容。我使用ANT脚本从GIT存储库中剥离和修改文件,以确保不会覆盖任何生产文件。ANT中有一个很好的选项可以修改Java属性文件。这意味着将您的本地测试变量放入Java样式的属性文件中,并添加一些代码来处理它,但这为您提供了在将站点上传到FTP之前自动构建站点的机会。通常,您会将生产信息放在site.default.properties文件中,并让ANT管理设置。您的本地设置将在site.local.properties文件中。
    <?php
/**
 * This class will read one or two files with JAVA style property files. For instance site.local.properties & site.default.properties
 * This will enable developers to make config files for their personal development environment, while maintaining a config file for 
 * the production site. 
 * Hint: use ANT to build the site and use the ANT <propertyfile> command to change some parameters while building.
 * @author martin
 *
 */
class javaPropertyFileReader {

    private $_properties;
    private $_validFile;

    /**
     * Constructor
     * @return javaPropertyFileReader
     */
    public function   __construct(){
        $this->_validFile = false;
        return $this;
    }//__construct

    /**
     * Reads one or both Java style property files
     * @param String $filenameDefaults
     * @param String $filenameLocal
     * @throws Exception
     * @return javaPropertyFileReader
     */
    public function readFile($filenameDefaults, $filenameLocal = ""){

        $this->handleFile($filenameDefaults);
        if ($filenameLocal != "") $this->handleFile($filenameLocal);
    }//readFile

    /**
     * This private function will do all the work of reading the file and  setting up the properties
     * @param String $filename
     * @throws Exception
     * @return javaPropertyFileReader
     */
    private function handleFile($filename){

    $file = @file_get_contents($filename);

    if ($file === false) {
         throw (New Exception("Cannot open property file: " . $filename, "01"));
    }
    else {
        # indicate a valid file was opened
        $this->_validFile = true;

        // if file is Windows style, remove the carriage returns
        $file = str_replace("\r", "", $file);

        // split file into array : one line for each record
        $lines = explode("\n", $file);

        // cycle lines from file
        foreach ($lines as $line){
            $line = trim($line);

            if (substr($line, 0,1) == "#" || $line == "") {
                #skip comment line
            }
            else{
                // create a property via an associative array
                $parts   = explode("=", $line);
                $varName = trim($parts[0]);
                $value   = trim($parts[1]);

                // assign property
                $this->_properties[$varName] = $value;
            }
        }// for each line in a file
    }
    return $this;
    }//readFile

    /**
     * This function will retrieve the value of a property from the property list.
     * @param String $propertyName
     * @throws Exception
     * @return NULL or value of requested property
     */
    function getProperty($propertyName){
        if (!$this->_validFile) throw (new Exception("No file opened", "03"));

        if (key_exists($propertyName, $this->_properties)){
            return $this->_properties[$propertyName];
        }
        else{
          return NULL;
        }
    }//getProperty

    /**
     * This function will retreive an array of properties beginning with a certain prefix.
     * @param String $propertyPrefix
     * @param Boolean $caseSensitive
     * @throws Exception
     * @return Array
     */
    function getPropertyArray($propertyPrefix, $caseSensitive = true){
        if (!$this->_validFile) throw (new Exception("No file opened", "03"));

        $res = array();

        if (! $caseSensitive) $propertyPrefix= strtolower($propertyPrefix);

        foreach ($this->_properties as $key => $prop){
            $l = strlen($propertyPrefix);

            if (! $caseSensitive) $key = strtolower($key);

            if (substr($key, 0, $l ) == $propertyPrefix) $res[$key] = $prop;
        }//for each proprty

        return $res;
    }//getPropertyArray

    function createDefineFromProperty($propertyName){
        $propValue = $this->getProperty($propertyName);
        define($propertyName, $propValue);
    }//createDefineFromProperty


    /**
     * This will create a number of 'constants' (DEFINE) from an array of properties that have a certain prefix.
     * An exception is thrown if 
     * @param  String $propertyPrefix
     * @throws Exception
     * @return Array The array of found properties is returned.
     */
    function createDefinesFromProperties($propertyPrefix){
        // find properties
        $props = $this->getPropertyArray($propertyPrefix);

        // cycle all properties 
        foreach($props as $key => $prop){

            // check for a valid define name
            if (preg_match("'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'", $key)) {
                define($key, $prop);
            }   
            else{
                throw (new Exception("Invalid entry in property file: cannot create define for {" . $key . "}", "04"));
            }   
        }// for each property found

        return $props;
    }//createDefineFromProperty

}//class javaPropertyFileReader

然后使用它:
  $props = new javaPropertyFileReader();
  $props->readFile($_SERVER["DOCUMENT_ROOT"] . "/lib/site.default.properties",$_SERVER["DOCUMENT_ROOT"] . "/lib/site.local.properties");

  #create one DEFINE
  $props->createDefineFromProperty("picture-path");

  # create a number of DEFINEs for enabled modules
  $modules = $props->createDefinesFromProperties("mod_enabled_");

你的 site.default.properties 文件应该长这样:

Your site.default.properties would look like:

release-date=x
environment=PROD
picture-path=/images/

SITE_VERSION_PRODUCTION=PROD
SITE_VERSION_TEST=TEST
SITE_VERSION_DEVELOP=DEV

# Available Modules
mod_enabled_x=false
mod_enabled_y=true
mod_enabled_z=true

您的 site.local.properties 文件应该是这样的(注意环境和启用的模块的区别):

release-date=x
environment=TEST
picture-path=/images/

SITE_VERSION_PRODUCTION=PROD
SITE_VERSION_TEST=TEST
SITE_VERSION_DEVELOP=DEV

# Available Modules
mod_enabled_x=true
mod_enabled_y=true
mod_enabled_z=true

以下是您的ANT指令:($d{deploy} 是您的部署目标目录)

<propertyfile
    file="${deploy}/lib/site.properties"
    comment="Site properties">
    <entry  key="environment" value="PROD"/>
    <entry  key="release-date" type="date" value="now" pattern="yyyyMMddHHmm"/>
</propertyfile>

1
现在(2019年),我在Python/Django中使用环境变量,你也可以为它们添加默认值。在Docker的上下文中,我可以将环境变量保存在docker-compose.yml文件中或一个被忽略版本控制的额外文件中。
# settings.py
import os
DEBUG = os.getenv('DJANGO_DEBUG') == 'True'
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST', 'localhost')

1

使用不同的扩展名(例如 .default)将默认配置检入,并使用符号链接将默认配置符号链接到正确位置,将正确位置添加到 .gitignore 中,并将与配置相关的所有其他内容添加到 .gitignore 中(因此唯一要检查的是 config.default)。

另外,编写一个快速安装脚本,为您的应用程序范围设置符号链接。

我们在以前的公司中使用了类似的方法。该安装脚本可以自动检测您运行的环境(沙箱、开发、QA、生产),并自动执行正确的操作。如果您有一个 config.sandbox 文件,并且正在从沙箱运行,则会链接该文件(否则它只会链接 .defaults 文件)。常见的过程是复制 .defaults 文件并根据需要更改设置。

编写安装脚本比你想象的要容易,而且还给予您很大的灵活性。


0

最简单的解决方案是编辑文件到默认值,提交它,然后将其添加到您的.gitignore。这样,开发人员在执行git commit -a时不会意外提交它,但在您想要使用git add --force更改默认值的(假定很少出现的)情况下,他们仍然可以提交它。

然而,拥有一个.default.local配置文件最终是最好的解决方案,因为这允许具有特定于机器的配置的任何人更改默认值,而无需破坏自己的配置。


这个不起作用 - 如果文件被跟踪并稍后添加到.gitignore中,更改仍将被跟踪。 - Zeemee

0
在@Greg Hewgill的答案基础上,您可以使用本地更改添加一个特定的提交并将其标记为localchange:
git checkout -b feature master
vim config.local
git add -A && git commit -m "local commit" && git tag localchange

然后继续添加您的功能提交。完成工作后,您可以通过执行以下操作将此分支合并回主分支而不包括本地更改提交:

git rebase --onto master localchange feature
git fetch . feature:master
git cherry-pick localchange
git tag localchange -f

这些命令将会:

1)将你的特性分支rebase到master,忽略localchange提交。 2)在不离开特性分支的情况下快进master。 3)将localchange提交添加回特性分支的顶部,以便您可以继续在其上工作。您可以对任何其他要继续工作的分支执行此操作。 4)将localchange标签重置为此cherry-picked提交,以便我们可以以相同的方式再次使用rebase --onto

这并不意味着要取代最佳通用解决方案的被接受答案,而是一种思考问题的新方法。你基本上通过只从localchangefeature进行rebase和快进master来避免意外地将本地更改合并到master。


0

我按照这里推荐的方式使用默认和本地配置文件。 为了管理我的本地配置文件,它们在项目的.gitignore中,我创建了一个git仓库~/settings。在那里,我管理所有项目的本地设置。例如,您可以在~/settings中创建一个名为project1的文件夹,并将该项目的所有本地配置文件放入其中。之后,您可以将这些文件/文件夹的符号链接到您的project1中。

通过这种方法,您可以跟踪您的本地配置文件,并且不需要将它们放入正常的源代码存储库中。


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