configs: apply dynamicconfig during clone

Summary:
During clone the hgrc.dynamic file doesn't exist and doesn't even have
a place for us to generate it to. Let's instead generate and apply the config in
memory.

In the future, if generate fetches data from the network, this will mean clone
would depend on the network, since if generate fails the clone would fail. In
some situations this is desirable, since users shouldn't be cloning without our
approved configs, but if it causes problems we could probably tweak generate to
support an offline mode.

Reviewed By: quark-zju

Differential Revision: D21643086

fbshipit-source-id: d9a758207738d5983213d95725061517e0aa17db
This commit is contained in:
Durham Goode 2020-05-19 19:49:43 -07:00 committed by Facebook GitHub Bot
parent 428cfc7b6b
commit f0d7044aff
8 changed files with 163 additions and 12 deletions

View File

@ -165,6 +165,12 @@ coreconfigitem("commit", "description-size-limit", default=None)
coreconfigitem("commit", "extras-size-limit", default=None)
coreconfigitem("committemplate", ".*", default=None, generic=True)
coreconfigitem("connectionpool", "lifetime", default=None)
coreconfigitem("configs", "autogeneratedynamicconfig", default=False)
coreconfigitem("configs", "generationtime", default=-1)
coreconfigitem("configs", "loaddynamicconfig", default=False)
coreconfigitem("configs", "mismatchsampling", default=10000)
coreconfigitem("configs", "mismatchwarn", default=False)
coreconfigitem("configs", "validatedynamicconfig", default=False)
coreconfigitem("convert", "git.committeractions", default=lambda: ["messagedifferent"])
coreconfigitem("convert", "git.extrakeys", default=list)
coreconfigitem("convert", "git.findcopiesharder", default=False)

View File

@ -439,14 +439,21 @@ class localrepository(object):
# Callback are in the form: func(repo, roots) --> processed root.
# This list it to be filled by extension during repo setup
self._phasedefaults = []
try:
reponame = self.ui.config("remotefilelog", "reponame", "")
# If the repo already exists, load the existing configs
if self.localvfs.isdir():
uiconfig.loaddynamicconfig(self.ui, self.path)
# Load the primary config after the dynamic one, so it overwrites it
self.ui.readconfig(self.localvfs.join("hgrc"), self.root)
uiconfig.validatedynamicconfig(self.ui)
self._loadextensions()
except IOError:
pass
else:
# If the repo does not already exists, load the dynamic configs in
# memory only. They will be written to disk later once the localvfs
# is created.
uiconfig.applydynamicconfig(self.ui, reponame)
self._loadextensions()
cacheaudited = self.ui.configbool("unsafe", "wvfsauditorcache")
self.wvfs.audit._cached = cacheaudited
@ -463,8 +470,10 @@ class localrepository(object):
if engine.revlogheader():
self.supported.add("exp-compression-%s" % name)
created = False
if not self.localvfs.isdir():
if create:
created = True
self.requirements = newreporequirements(self)
if "store" in self.requirements:
self.storerequirements = newrepostorerequirements(self)
@ -523,6 +532,12 @@ class localrepository(object):
raise
self.sharedvfs = self.localvfs
# If this is a new repo, generate the dynamic configs. We must do this
# after the sharedvfs is set up so we can generate the dynamic config in
# the shared vfs.
if created:
uiconfig.generatedynamicconfig(self.ui, reponame, self.sharedpath)
self.store = store.store(
self.requirements,
self.sharedpath,

View File

@ -17,7 +17,7 @@ import subprocess
import time
from typing import List, Optional, Tuple
from bindings import configparser
from bindings import configparser, dynamicconfig
from ..hgext.extutil import runbgcommand
from . import configitems, error, pycompat, util
@ -554,7 +554,7 @@ def parselist(value):
def loaddynamicconfig(ui, path):
if ui.configbool("configs", "loaddynamicconfig", False):
if ui.configbool("configs", "loaddynamicconfig"):
sharedpathfile = os.path.join(path, "sharedpath")
if os.path.exists(sharedpathfile):
with open(sharedpathfile, "r") as f:
@ -562,7 +562,7 @@ def loaddynamicconfig(ui, path):
hgrcdyn = os.path.join(path, "hgrc.dynamic")
if ui.configbool("configs", "autogeneratedynamicconfig", False):
if ui.configbool("configs", "autogeneratedynamicconfig"):
if not os.path.exists(hgrcdyn):
try:
ui.debug("synchronously generating dynamic config\n")
@ -574,7 +574,7 @@ def loaddynamicconfig(ui, path):
ui.readconfig(hgrcdyn, path)
generationtime = ui.configint("configs", "generationtime", -1)
generationtime = ui.configint("configs", "generationtime")
if generationtime != -1:
mtimelimit = time.time() - generationtime
if not os.path.exists(hgrcdyn) or os.lstat(hgrcdyn).st_mtime < mtimelimit:
@ -586,8 +586,8 @@ def loaddynamicconfig(ui, path):
def validatedynamicconfig(ui):
if not (
ui.configbool("configs", "loaddynamicconfig", False)
and ui.configbool("configs", "validatedynamicconfig", False)
ui.configbool("configs", "loaddynamicconfig")
and ui.configbool("configs", "validatedynamicconfig")
):
return
@ -634,10 +634,10 @@ def validatedynamicconfig(ui):
dynamic_value,
file_value,
)
if ui.configbool("configs", "mismatchwarn", False):
if ui.configbool("configs", "mismatchwarn"):
ui.warn(msg)
samplerate = ui.configint("configs", "mismatchsampling", 10000)
samplerate = ui.configint("configs", "mismatchsampling")
if random.randint(1, samplerate) == 1:
ui.log(
"config_mismatch",
@ -646,3 +646,13 @@ def validatedynamicconfig(ui):
expected=file_value,
actual=dynamic_value,
)
def applydynamicconfig(ui, reponame):
if ui.configbool("configs", "loaddynamicconfig"):
dynamicconfig.applydynamicconfig(ui._uiconfig._rcfg._rcfg, reponame)
validatedynamicconfig(ui)
def generatedynamicconfig(ui, reponame, sharedpath):
if ui.configbool("configs", "loaddynamicconfig"):
dynamicconfig.generatedynamicconfig(reponame, sharedpath)

View File

@ -26,6 +26,7 @@ pyconfigparser = { path = "modules/pyconfigparser" }
pydag = { path = "modules/pydag" }
pydiffhelpers = { path = "modules/pydiffhelpers" }
pydirs = { path = "modules/pydirs" }
pydynamicconfig = { path = "modules/pydynamicconfig" }
pyedenapi = { path = "modules/pyedenapi" }
pyerror = { path = "modules/pyerror" }
pyfs = { path = "modules/pyfs" }

View File

@ -0,0 +1,16 @@
[package]
name = "pydynamicconfig"
version = "0.1.0"
edition = "2018"
[dependencies]
anyhow = "1.0"
cpython = { version = "0.5", default-features = false }
cpython-ext = { path = "../../../../lib/cpython-ext", default-features = false }
dynamicconfig = { path = "../../../../lib/dynamicconfig" }
configparser = { path = "../../../../lib/configparser" }
pyconfigparser = { path = "../pyconfigparser" }
[features]
python2 = ["cpython/python27-sys", "cpython-ext/python2"]
python3 = ["cpython/python3-sys", "cpython-ext/python3"]

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
#![allow(non_camel_case_types)]
use std::fs;
use anyhow::{format_err, Error};
use cpython::*;
use cpython_ext::{error::ResultPyErrExt, PyNone, PyPathBuf};
use dynamicconfig::Generator;
use pyconfigparser::config;
pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
let name = [package, "dynamicconfig"].join(".");
let m = PyModule::new(py, &name)?;
m.add(
py,
"applydynamicconfig",
py_fn!(py, applydynamicconfig(config: config, repo_name: String)),
)?;
m.add(
py,
"generatedynamicconfig",
py_fn!(
py,
generatedynamicconfig(repo_name: String, shared_path: PyPathBuf)
),
)?;
Ok(m)
}
fn applydynamicconfig(py: Python, config: config, repo_name: String) -> PyResult<PyNone> {
let dyn_cfg = Generator::new(repo_name)
.map_pyerr(py)?
.execute()
.map_pyerr(py)?;
for section in dyn_cfg.sections() {
for key in dyn_cfg.keys(section.clone()).iter_mut() {
if let Some(value) = dyn_cfg.get(section.clone(), key.clone()) {
config.set(
py,
section.to_string(),
key.to_string(),
Some(value.to_string()),
"hgrc.dynamic".into(),
)?;
}
}
}
Ok(PyNone)
}
fn generatedynamicconfig(
py: Python,
repo_name: String,
shared_path: PyPathBuf,
) -> PyResult<PyNone> {
let config = Generator::new(repo_name)
.map_pyerr(py)?
.execute()
.map_pyerr(py)?;
let config_str = config.to_string();
let config_str = format!(
"# Generated by `hg debugdynamicconfig` - DO NOT MODIFY\n{}",
config_str
);
fs::write(shared_path.as_path().join("hgrc.dynamic"), config_str).map_pyerr(py)?;
Ok(PyNone)
}

View File

@ -26,6 +26,11 @@ pub fn populate_module(py: Python<'_>, module: &PyModule) -> PyResult<PyNone> {
m.add(py, "dag", pydag::init_module(py, &name)?)?;
m.add(py, "diffhelpers", pydiffhelpers::init_module(py, &name)?)?;
m.add(py, "dirs", pydirs::init_module(py, &name)?)?;
m.add(
py,
"dynamicconfig",
pydynamicconfig::init_module(py, &name)?,
)?;
m.add(py, "edenapi", pyedenapi::init_module(py, &name)?)?;
m.add(py, "error", pyerror::init_module(py, &name)?)?;
m.add(py, "fs", pyfs::init_module(py, &name)?)?;

View File

@ -92,3 +92,24 @@ Verify we generate and load from a shared repo
$ hg config section.key
value
Verify we load dynamicconfigs during clone
$ newserver server
$ cd $TESTTMP
$ export HG_TEST_DYNAMICCONFIG="$TESTTMP/test_hgrc"
$ cat > test_hgrc <<EOF
> [hooks]
> pretxnclose = echo "Hook ran!"
> EOF
$ hg clone ssh://user@dummy/server client2
no changes found
Hook ran!
updating to branch default
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
Hook ran!
Hook ran!
$ cat client2/.hg/hgrc.dynamic
# Generated by `hg debugdynamicconfig` - DO NOT MODIFY
[hooks]
pretxnclose=echo "Hook ran!"