提交机器特定的配置文件


82

在我进行开发时,常见的情况是该代码库将包含几个需要计算机特定设置的配置文件。这些文件将被检入Git,其他开发人员将总是不小心将它们重新检入并破坏其他人的配置。

一个简单的解决方案是不将其签入Git,甚至不为它们添加.gitignore条目。但是,我发现在文件中包含一些合理的默认值(这很明智),开发人员可以修改这些默认值以满足他的需要。

有没有一种优雅的方法可以使Git很好地处理此类文件?我希望能够修改特定于计算机的配置文件,然后能够在不检入该文件的情况下运行“ git commit -a”。


1
听起来您的设计和同事的大脑都有问题。告诉他们确保他们知道要提交到源代码控制系统中的内容,否则他们将检查您是否不需要这些废话。另外:为什么不只是分割文件,每个系统一个文件?
波德

11
我敢肯定这是一个相当普遍的情况吗?您如何跟踪机器的特定配置?为每个系统分割文件似乎很混乱,这有点破坏了分布式版本控制的目的:如果在新计算机上检出该文件,则
不必

1
您可能至少可以使用更新前的钩子来阻止任何破坏性的提交,这些钩子都是您推送到任何共享存储库的。它可以查找修改某些开发人员制作的配置文件的提交,也可以查找触摸该文件的提交,而消息中未提及特殊关键字。
菲勒·米勒

2
+1,这一个普遍的问题。@Pod:在回购中添加“ Joe.conf”是不切实际的,但是您仍然希望能够时时更新内容...有时由于代码的更改,配置必须进行更改。
塔纳托斯

Answers:


59

让您的程序读取一对配置文件以获取其设置。首先,它应该读取config.defaults将包含在存储库中的文件。然后,它应该读取一个config.local文件,该文件应列在.gitignore

通过这种安排,新设置将显示在默认文件中,并在更新后立即生效。如果它们被覆盖,它们只会在特定系统上有所不同。

作为对此的一种变体,您可以config在版本控制中仅提供一个常规文件,并使其进行一些操作,例如include config.local引入机器特定的值。这在您的代码中引入了更通用的机制(相对于策略),因此启用了更复杂的配置(如果您的应用程序需要这样做)。在许多大型开源软件中都可以看到,从这里流行的扩展是to include conf.d,它从目录中的所有文件读取配置。

请参阅我对类似问题的回答。


我要给这个答案。这种方法达到了预期的效果,唯一的缺点是,在应用程序部分需要额外的逻辑。
ghempton

18

你可以试试看git update-index --skip-worktree filename。这将告诉git假装对文件名的本地更改不存在,因此git commit -a将其忽略。它还具有抵抗的额外优势git reset --hard,因此您不会意外丢失本地更改。另外,如果文件在上游更改,自动合并将自动失败(除非工作目录副本与索引副本匹配,在这种情况下它将自动更新)。缺点是该命令必须在所有涉及的计算机上运行,​​并且很难自动执行此操作。另请参阅有关git update-index --assume-unchanged此想法的一个微妙不同的版本。有关这两者的详细信息,请参见git help update-index


您可以在“假设不变”和“跳过工作树”之间的差异问题中找到有关这些命令的更多信息。从最高答案开始--skip-worktree在这种情况下看起来像您想要的。
有意义的2014年

10

另一种方法是在另一个专用分支中维护对公共配置文件的本地更改。对于某些需要进行一些本地更改的项目,我会这样做。这项技术可能不适用于所有情况,但在某些情况下对我有用。

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

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关键字的提交,颠倒顺序,最后将每个提交(现在是最早的提交)放入master

最后,在将主服务器中的更改推送到上游之后,我切换回work并重新设置基准:

git checkout work
git rebase master

Git将重新应用work分支中的每个提交,从而有效地跳过master通过摘樱桃已应用的提交。您应该只剩下NOCOMMIT本地提交。

这种技术使推送过程更加耗时,但是它为我解决了一个问题,因此我想与大家分享。


2
您知道您正在要求遗忘的“非问题询问者”这样做吗?谁只是运行的人git commit -a没有在世界上的照顾?
菲勒·米勒

按照相同的策略,您可以标记提交,在其中设置本地配置文件,并使用git rebase --ontogit fetch进行组合
Danilo SouzaMorães18年

8

一种可能是将实际文件包含在.gitignore中,但请使用其他扩展名签入默认配置。Rails应用程序的一个典型示例是config / database.yml文件。我们将检入config / database.yml.sample,每个开发人员都会创建自己的已经被.gitignored的config / database.yml。


是的,这是一个渐进的改进,但是它并不是最佳的,因为如果有意更改签入的版本,则它不会反映在开发人员配置文件中。何时添加新属性等是一个有用的例子
。– ghempton

这可能是由于未提交属性而提交的良好提交说明和描述性错误消息所致。与您的团队交流有关变更的电子邮件也有帮助。
Brian Kelly

有关此解决方案的更多信息和一个出色的示例,请参见此答案
有意义的2014年

1

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

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

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

编写安装脚本比您想像的要容易,并且给您很大的灵活性。


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如下所示:

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中使用了ENV vars,例如,您也可以为其添加默认值。在Docker的上下文中,我可以将ENV变量保存在docker-compose.yml文件或版本控制中忽略的其他文件中。

# settings.py
import os
DEBUG = os.getenv('DJANGO_DEBUG') == 'True'
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST', 'localhost')

0

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

但是,拥有.defaultand .localconfig文件最终是最好的解决方案,因为这允许具有计算机特定配置的任何人更改默认值,而不必破坏自己的配置。


这是行不通的-如果跟踪文件并将其添加到.gitignore以后,则仍会跟踪更改。
Zeemee

0

我喜欢这里推荐的默认和本地配置文件。为了管理项目中的本地配置文件.gitignore,我制作了一个git repo ~/settings。在这里,我可以管理所有项目中的所有本地设置。例如,您在其中创建一个文件夹project1~/settings并将该项目的所有本地配置内容放入其中。之后,您可以将该文件/文件夹符号链接到project1

使用这种方法,您可以跟踪本地配置文件,而不必将其放入普通的源代码存储库中。


0

以@Greg Hewgill的答案为基础,您可以添加具有本地更改的特定提交,并将其标记为localchange:

git checkout -b feature master
vim config.local
git add -A && git commit -m "local commit" && git tag localchange

然后继续添加功能的提交。完成工作之后,您可以执行以下操作将此分支合并回master,而无需localchange提交:

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

这些命令将:

1)将您的功能分支重新设置为master,而忽略localchange提交。2)快速转发母版,无需离开功能分支3)将localchange提交添加回功能分支的顶部,以便您可以继续进行操作。您可以对要继续工作的任何其他分支执行此操作。4)将localchange标签重置为此精心挑选的提交,以便我们可以使用rebase --onto以相同的方式再次。

这并不是要取代已接受的答案作为最佳的通用解决方案,而是要以一种开阔的方式思考问题。您基本上可以避免仅将fromlocalchange迁移到masterfeature并快速转发master ,从而避免将本地更改意外合并到master。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.