From: Huang Hao Date: Fri, 7 Sep 2012 07:47:18 +0000 (+0800) Subject: Support profile oriented style of config. X-Git-Tag: 0.10~33 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=fa8b8a97e670c738cad3de9b6efda0c1c3bc97b0;p=tools%2Fgbs.git Support profile oriented style of config. - support profile oriented style. It is compatible with subcommand oriented style. - add a class Profile to store url/user/password of repos and api. - add a class BizConfigManager extending from ConfigMgr. It handles profile info and does not care about multi-levels and passwdx issues. It returns current profile instance even config file has subcommand oriented style. - make two functions decode_passwdx, encode_passwd to convert between passwd and passwdx. - move get_repos_conf() from cmd_build.py to conf.py and split it into two parts _parse_build_repos() and _build_profile_by_subcommand() - add test cases in test_profile.py Change-Id: I81b2906456766f1061ccef9e7d51fea794c59409 --- diff --git a/gitbuildsys/cmd_build.py b/gitbuildsys/cmd_build.py index 4df2238..354c483 100644 --- a/gitbuildsys/cmd_build.py +++ b/gitbuildsys/cmd_build.py @@ -25,7 +25,6 @@ import subprocess import tempfile import glob import shutil -import base64 import pwd from gitbuildsys import msger, utils, runner, errors @@ -33,7 +32,6 @@ from gitbuildsys.conf import configmgr from gitbuildsys.safe_url import SafeURL from gitbuildsys.cmd_export import export_sources -from gbp.scripts.buildpackage_rpm import main as gbp_build from gbp.rpm.git import GitRepositoryError, RpmGitRepository import gbp.rpm as rpm from gbp.errors import GbpError @@ -170,57 +168,6 @@ def get_env_proxies(): return proxies -def get_repos_conf(): - """ - Make list of urls using repox.url, repox.user and repox.passwd - configuration file parameters from 'build' section. - Validate configuration parameters. - """ - - repos = {} - # get repo settings form build section - for opt in configmgr.options('build'): - if opt.startswith('repo'): - try: - key, name = opt.split('.') - except ValueError: - raise errors.ConfigError("invalid repo option: %s" % opt) - - if name not in ('url', 'user', 'passwdx'): - raise errors.ConfigError("invalid repo option: %s" % opt) - - if key not in repos: - repos[key] = {} - - if name in repos[key]: - raise errors.ConfigError('Duplicate entry %s' % opt) - - value = configmgr.get(opt, 'build') - if name == 'passwdx': - try: - value = base64.b64decode(value).decode('bz2') - except (TypeError, IOError), err: - raise errors.ConfigError('Error decoding %s: %s' % \ - (opt, err)) - repos[key]['passwd'] = value - else: - repos[key][name] = value - - result = [] - for key, item in repos.iteritems(): - if 'url' not in item: - raise errors.ConfigError("Url is not specified for %s" % key) - - try: - url = SafeURL(item['url'], item.get('user'), item.get('passwd')) - except ValueError, e: - raise errors.ConfigError('%s for %s' % (str(e), key)) - - result.append(url) - - return result - - def do(opts, args): workdir = os.getcwd() @@ -262,7 +209,7 @@ def do(opts, args): build_cmd = configmgr.get('build_cmd', 'build') userid = pwd.getpwuid(os.getuid())[0] - tmpdir = os.path.join(configmgr.get('tmpdir', 'general'), "%s-gbs" % userid) + tmpdir = os.path.join(configmgr.get('tmpdir', 'general'), "%s-gbs" % userid) build_root = os.path.join(tmpdir, 'gbs-buildroot.%s' % buildarch) if opts.buildroot: build_root = opts.buildroot @@ -293,7 +240,7 @@ def do(opts, args): if opts.skip_conf_repos: repos = [] else: - repos = get_repos_conf() + repos = configmgr.get_current_profile().get_repos() if opts.repositories: repos.extend([ SafeURL(i) for i in opts.repositories ]) @@ -316,7 +263,8 @@ def do(opts, args): distconf = opts.dist else: if repoparser.buildconf is None: - msger.warning('failed to get build conf, use default build conf') + msger.warning('failed to get build conf, ' + 'use default build conf') distconf = configmgr.get('distconf', 'build') else: shutil.copy(repoparser.buildconf, tmpdir) diff --git a/gitbuildsys/cmd_remotebuild.py b/gitbuildsys/cmd_remotebuild.py index ee49cdb..e7077f4 100644 --- a/gitbuildsys/cmd_remotebuild.py +++ b/gitbuildsys/cmd_remotebuild.py @@ -24,12 +24,11 @@ import glob from gitbuildsys import msger, errors, utils -from gitbuildsys.conf import configmgr +from gitbuildsys.conf import configmgr, encode_passwd from gitbuildsys.oscapi import OSC, OSCError from gitbuildsys.cmd_export import export_sources import gbp.rpm -from gbp.scripts.buildpackage_rpm import main as gbp_build from gbp.rpm.git import GitRepositoryError, RpmGitRepository from gbp.errors import GbpError @@ -45,9 +44,6 @@ user=%(user)s passx=%(passwdx)s """ -APISERVER = configmgr.get('build_server', 'remotebuild') -USER = configmgr.get('user', 'remotebuild') -PASSWDX = configmgr.get('passwdx', 'remotebuild') def do(opts, args): @@ -69,7 +65,8 @@ def do(opts, args): else: msger.error('Invalid arguments, see gbs remotebuild -h for more info') - if not USER: + apiurl = configmgr.get_current_profile().get_api() + if not apiurl or not apiurl.user: msger.error('empty user is not allowed for remotebuild, '\ 'please add user/passwd to gbs conf, and try again') @@ -109,7 +106,7 @@ def do(opts, args): if opts.target_obsprj is None: target_prj = configmgr.get('target_prj', 'remotebuild') or \ - "home:%s:gbs:%s" % (USER, base_prj) + "home:%s:gbs:%s" % (apiurl.user, base_prj) else: target_prj = opts.target_obsprj @@ -117,9 +114,9 @@ def do(opts, args): oscrc = OSCRC_TEMPLATE % { "http_debug": 1 if msger.get_loglevel() == 'debug' else 0, "debug": 1 if msger.get_loglevel() == 'verbose' else 0, - "apiurl": APISERVER, - "user": USER, - "passwdx": PASSWDX, + "apiurl": apiurl, + "user": apiurl.user, + "passwdx": encode_passwd(apiurl.passwd), } tmpdir = configmgr.get('tmpdir', 'general') @@ -129,7 +126,7 @@ def do(opts, args): tmpf = utils.Temp(dirn=exportdir, prefix='.oscrc', content=oscrc) oscrcpath = tmpf.path - api = OSC(APISERVER, oscrc=oscrcpath) + api = OSC(apiurl, oscrc=oscrcpath) try: if opts.buildlog: @@ -173,9 +170,10 @@ def do(opts, args): # FIXME: How do you know that a certain user does not have # permissions to create any project, anywhewre? if opts.target_obsprj and \ - not target_prj.startswith('home:%s:' % USER): - msger.error('no permission to create project %s, only sub '\ - 'projects of home:%s are allowed ' % (target_prj, USER)) + not target_prj.startswith('home:%s:' % apiurl.user): + msger.error('no permission to create project %s, only sub ' + 'projects of home:%s are ' + 'allowed ' % (target_prj, apiurl.user)) msger.info('copying settings of %s to %s' % (base_prj, target_prj)) api.copy_project(base_prj, target_prj) @@ -216,4 +214,4 @@ def do(opts, args): msger.info('local changes submitted to build server successfully') msger.info('follow the link to monitor the build progress:\n' ' %s/package/show?package=%s&project=%s' \ - % (APISERVER.replace('api', 'build'), package, target_prj)) + % (apiurl.replace('api', 'build'), package, target_prj)) diff --git a/gitbuildsys/conf.py b/gitbuildsys/conf.py index 49b7066..dc4bcfb 100644 --- a/gitbuildsys/conf.py +++ b/gitbuildsys/conf.py @@ -15,6 +15,9 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., 59 # Temple Place - Suite 330, Boston, MA 02111-1307, USA. +''' +Provides classes and functions to read and write gbs.conf. +''' from __future__ import with_statement import os @@ -25,6 +28,29 @@ from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError, \ MissingSectionHeaderError from gitbuildsys import msger, errors +from gitbuildsys.safe_url import SafeURL + + +def decode_passwdx(passwdx): + '''decode passwdx into plain format''' + return base64.b64decode(passwdx).decode('bz2') + + +def encode_passwd(passwd): + '''encode passwd by bz2 and base64''' + return base64.b64encode(passwd.encode('bz2')) + + +def evalute_string(string): + '''safely evaluate string''' + if string.startswith('"') or string.startswith("'"): + return ast.literal_eval(string) + return string + + +def split_and_evaluate_string(string, sep=None, maxsplit=-1): + '''split a string and evaluate each of them''' + return [ evalute_string(i.strip()) for i in string.split(sep, maxsplit) ] class SectionPattern(object): @@ -59,16 +85,9 @@ class SectionPattern(object): if not name: return type_ - name = self.evalute_string(name) + name = evalute_string(name) return type_, name - @staticmethod - def evalute_string(string): - '''safely evaluate string''' - if string.startswith('"') or string.startswith("'"): - return ast.literal_eval(string) - return string - def match(self, string): '''return MatchObject if string match the pattern''' match = self.SECTCRE.match(string) @@ -210,6 +229,9 @@ class BrainConfigParser(SafeConfigParser): fptr.close() class ConfigMgr(object): + '''Support multi-levels of gbs.conf. Use this class to get and set + item value without caring about concrete ini format''' + DEFAULTS = { 'general': { 'tmpdir': '/var/tmp', @@ -273,6 +295,7 @@ distconf = $build__distconf self.reset_from_conf(fpath) def reset_from_conf(self, fpath): + 'reset all config values by files passed in' if fpath: if not os.path.exists(fpath): raise errors.ConfigError('Configuration file %s does not '\ @@ -312,6 +335,7 @@ distconf = $build__distconf return paths def get_default_conf(self, defaults=None): + 'returns ini template string contains default values' from string import Template if not defaults: defaults = self.DEFAULTS @@ -324,6 +348,7 @@ distconf = $build__distconf return Template(self.DEFAULT_CONF_TEMPLATE).safe_substitute(tmpl_keys) def _new_conf(self, fpath=None): + 'generate a new conf file located by fpath' if not fpath: fpath = os.path.expanduser('~/.gbs.conf') @@ -343,11 +368,10 @@ distconf = $build__distconf if defaults['remotebuild']['user']: msger.info('Your password will be encoded before saving ...') defaults['remotebuild']['passwdx'] = \ - base64.b64encode(getpass.getpass().encode('bz2')) + encode_passwd(getpass.getpass()) else: defaults['remotebuild']['passwdx'] = \ - base64.b64encode( - defaults['remotebuild']['passwd'].encode('bz2')) + encode_passwd(defaults['remotebuild']['passwd']) with open(fpath, 'w') as wfile: wfile.write(self.get_default_conf(defaults)) @@ -358,8 +382,15 @@ distconf = $build__distconf return True def _check_passwd(self): + 'convert passwd item to passwdx and then update origin conf files' replaced_keys = False - for sec in self.DEFAULTS.keys(): + + all_sections = set() + for layer in self._cfgparsers: + for sec in layer.sections(): + all_sections.add(sec) + + for sec in all_sections: for key in self.options(sec): if key.endswith('passwd'): for cfgparser in self._cfgparsers: @@ -370,7 +401,7 @@ distconf = $build__distconf continue cfgparser.set(sec, key + 'x', - base64.b64encode(plainpass.encode('bz2')), + encode_passwd(plainpass), key) replaced_keys = True @@ -380,6 +411,7 @@ distconf = $build__distconf self.update() def _get(self, opt, section='general'): + 'get value from multi-levels of config file' sect_found = False for cfgparser in self._cfgparsers: try: @@ -406,6 +438,7 @@ distconf = $build__distconf return False def options(self, section='general'): + 'merge and return options of certain section from multi-levels' sect_found = False options = set() for cfgparser in self._cfgparsers: @@ -425,12 +458,13 @@ distconf = $build__distconf return options def get(self, opt, section='general'): + 'get item value. return plain text of password if item is passwd' if opt == 'passwd': opt = 'passwdx' val = self._get(opt, section) if val: try: - return base64.b64decode(val).decode('bz2') + return decode_passwdx(val) except (TypeError, IOError), err: raise errors.ConfigError('passwdx:%s' % err) else: @@ -440,7 +474,7 @@ distconf = $build__distconf def set(self, opt, val, section='general'): if opt.endswith('passwd'): - val = base64.b64encode(val.encode('bz2')) + val = encode_passwd(val) opt += 'x' for cfgparser in self._cfgparsers: @@ -456,7 +490,153 @@ distconf = $build__distconf raise errors.ConfigError('invalid section %s' % (section)) def update(self): + 'update changed values into files on disk' for cfgparser in self._cfgparsers: cfgparser.update() -configmgr = ConfigMgr() + +class Profile(object): + '''Profile which contains all config values related to same domain''' + + def __init__(self, user, password): + self.common_user = user + self.common_password = password + self.repos = [] + self.api = None + + def make_url(self, url, user, password): + '''make a safe url which contains auth info''' + user = user or self.common_user + password = password or self.common_password + try: + return SafeURL(url, user, password) + except ValueError, err: + raise errors.ConfigError('%s for %s' % (str(err), url)) + + def add_repo(self, url, user, password): + '''add a repo to repo list of the profile''' + self.repos.append(self.make_url(url, user, password)) + + def set_api(self, url, user, password): + '''set OBS api of the profile''' + self.api = self.make_url(url, user, password) + + def get_repos(self): + '''get repo list of the profile''' + return self.repos + + def get_api(self): + '''get OBS api of the profile''' + return self.api + + +class BizConfigManager(ConfigMgr): + '''config manager which handles high level conception, such as profile info + ''' + + def is_profile_oriented(self): + '''return True if config file is profile oriented''' + return self.get_optional_item('general', 'profile') is not None + + def get_current_profile(self): + '''get profile current used''' + if self.is_profile_oriented(): + return self._build_profile_by_name(self.get('profile')) + + msger.warning('subcommand oriented style of config is deprecated, ' + 'please convert to profile oriented style.') + return self._build_profile_by_subcommand() + + def get_optional_item(self, section, option, default=None): + '''return default if section.option does not exist''' + try: + return self.get(option, section) + except errors.ConfigError: + return default + + def _get_url_section(self, section_id): + '''get url/user/passwd from a section''' + url = self.get('url', section_id) + user = self.get_optional_item(section_id, 'user') + password = self.get_optional_item(section_id, 'passwd') + return url, user, password + + def _build_profile_by_name(self, name): + '''return profile object by a given section''' + profile_id = ('profile', name) + user = self.get_optional_item(profile_id, 'user') + password = self.get_optional_item(profile_id, 'passwd') + + profile = Profile(user, password) + + conf_api = self.get_optional_item(profile_id, 'api') + if conf_api: + api = self.get('api', profile_id) + api_id = ('obs', api) + profile.set_api(*self._get_url_section(api_id)) + + conf_repos = self.get_optional_item(profile_id, 'repos') + if conf_repos: + repos = split_and_evaluate_string(conf_repos, ',') + for repo in repos: + repo_id = ('repo', repo) + profile.add_repo(*self._get_url_section(repo_id)) + + return profile + + def _parse_build_repos(self): + """ + Make list of urls using repox.url, repox.user and repox.passwd + configuration file parameters from 'build' section. + Validate configuration parameters. + """ + repos = {} + # get repo settings form build section + for opt in self.options('build'): + if opt.startswith('repo'): + try: + key, name = opt.split('.') + except ValueError: + raise errors.ConfigError("invalid repo option: %s" % opt) + + if name not in ('url', 'user', 'passwdx'): + raise errors.ConfigError("invalid repo option: %s" % opt) + + if key not in repos: + repos[key] = {} + + if name in repos[key]: + raise errors.ConfigError('Duplicate entry %s' % opt) + + value = self.get(opt, 'build') + if name == 'passwdx': + try: + value = decode_passwdx(value) + except (TypeError, IOError), err: + raise errors.ConfigError('Error decoding %s: %s' % \ + (opt, err)) + repos[key]['passwd'] = value + else: + repos[key][name] = value + return sorted(repos.items(), key=lambda i: i[0]) + + def _build_profile_by_subcommand(self): + '''return profile object from subcommand oriented style of config''' + profile = Profile(None, None) + + section_id = 'remotebuild' + url = self.get('build_server', section_id) + user = self.get_optional_item(section_id, 'user') + password = self.get_optional_item(section_id, 'passwd') + profile.set_api(url, user, password) + + repos = self._parse_build_repos() + for key, item in repos: + if 'url' not in item: + raise errors.ConfigError("Url is not specified for %s" % key) + profile.add_repo(item['url'], item.get('user'), item.get('passwd')) + + return profile + + +configmgr = BizConfigManager() diff --git a/gitbuildsys/safe_url.py b/gitbuildsys/safe_url.py index 88e6afe..9e9acfe 100644 --- a/gitbuildsys/safe_url.py +++ b/gitbuildsys/safe_url.py @@ -44,6 +44,9 @@ class SafeURL(str): @property def full(self): '''return the full url with user and password''' + if self.is_local(): + return self + userinfo = self._get_userinfo() hostport = self._get_hostport(self.components) @@ -56,6 +59,10 @@ class SafeURL(str): new_components[1] = login return urlparse.urlunsplit(new_components) + def is_local(self): + 'return True is it is local path' + return self.startswith('/') + def pathjoin(self, *args): '''treat self as path and urljoin''' new = urlparse.urljoin(self.rstrip('/') + '/', *args) diff --git a/gitbuildsys/utils.py b/gitbuildsys/utils.py index 9ac6bc4..613e417 100644 --- a/gitbuildsys/utils.py +++ b/gitbuildsys/utils.py @@ -357,8 +357,11 @@ class RepoParser(object): remotes = [] for repo in repos: - if repo.startswith('/') and os.path.exists(repo): - local_repos.append(repo) + if repo.is_local(): + if os.path.exists(repo): + local_repos.append(repo) + else: + msger.warning('No such repo path:%s' % repo) else: remotes.append(repo) diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 0000000..bdf6f1d --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,114 @@ +#!/usr/bin/python -tt +# vim: ai ts=4 sts=4 et sw=4 +# +# Copyright (c) 2012 Intel, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; version 2 of the License +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 59 +# Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""Functional tests for profile style of config""" +import unittest + +import gitbuildsys.conf +from test_config import Fixture + + +def get_profile(): + '''get current profile to test''' + return gitbuildsys.conf.configmgr.get_current_profile() + + +class ProfileStyleTest(unittest.TestCase): + '''Test for profile oriented config''' + + @Fixture(home='profile.ini') + def test_profile_api(self): + 'test get obs api' + self.assertEquals('https://api.tz/path', get_profile().get_api()) + + @Fixture(home='profile.ini') + def test_api_inherit_auth(self): + 'test api can inherit auto from parent profile section' + self.assertEquals('https://Alice:secret@api.tz/path', + get_profile().get_api().full) + + @Fixture(home='profile_only_has_api.ini') + def test_api_auth_can_be_overwrite(self): + 'test api auth can be overwrite' + self.assertEquals('https://Bob:classified@api.tz/path', + get_profile().get_api().full) + + @Fixture(home='profile.ini') + def test_profile_repos_in_order(self): + 'repos must be in same order as they are write in config' + self.assertEquals(['https://repo/ia32/main', + 'https://repo/ia32/non-oss', + 'https://repo/ia32/base', + '/local/path'], + get_profile().get_repos()) + + @Fixture(home='profile.ini') + def test_repo_inherit_auth(self): + 'test repo can inherit auth from parent section' + self.assertEquals('https://Alice:secret@repo/ia32/main', + get_profile().get_repos()[0].full) + + @Fixture(home='profile.ini') + def test_repo_overwrite_auth(self): + 'test repo auth can be overwrite' + self.assertEquals('https://Bob:classified@repo/ia32/base', + get_profile().get_repos()[2].full) + + @Fixture(home='no_such_profile_section_name.ini') + def test_no_such_profile(self): + 'test get a empty profile when name does not exist' + profile = get_profile() + self.assertEquals(None, profile.get_api()) + self.assertEquals([], profile.get_repos()) + + @Fixture(home='profile.ini') + def test_local_repo_need_not_auth(self): + '''test local path needn't auth info''' + self.assertEquals('/local/path', get_profile().get_repos()[3].full) + + +class SubcommandStyleTest(unittest.TestCase): + '''test for subcommand oriented config''' + + @Fixture(home='subcommand.ini') + def test_api(self): + 'test obs api' + self.assertEquals('https://api/build/server', get_profile().get_api()) + + @Fixture(home='subcommand.ini') + def test_api_auth(self): + 'test api auth' + self.assertEquals('https://Alice:secret@api/build/server', + get_profile().get_api().full) + + @Fixture(home='subcommand.ini') + def test_repos_in_order(self): + 'repos list must be in the same order as they are write in config' + self.assertEquals(['https://repo1/path', + 'https://repo2/path', + '/local/path/repo'], + get_profile().get_repos()) + + @Fixture(home='subcommand.ini') + def test_repo_auth(self): + 'test repo auth' + self.assertEquals('https://Alice:secret@repo1/path', + get_profile().get_repos()[0].full) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_safe_url.py b/tests/test_safe_url.py index c33d6d1..4308835 100644 --- a/tests/test_safe_url.py +++ b/tests/test_safe_url.py @@ -105,4 +105,11 @@ class SafeURLTest(unittest.TestCase): url = SafeURL('/local/path') self.assertEqual('/local/path', url) - self.assertEqual(url, url.full) \ No newline at end of file + self.assertEqual(url, url.full) + + def test_local_path_need_not_auth(self): + '''local path should ignore user and password''' + url = SafeURL('/local/path', 'test', 'password') + + self.assertEqual('/local/path', url) + self.assertEqual(url, url.full) diff --git a/tests/testdata/ini/local_repo.ini b/tests/testdata/ini/local_repo.ini new file mode 100644 index 0000000..ffed290 --- /dev/null +++ b/tests/testdata/ini/local_repo.ini @@ -0,0 +1,8 @@ +[build] +repo1.url = https://repo1/path +repo1.user = Alice +repo1.passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s + +repo2.url = https://repo2/path +repo2.user = Alice +repo2.passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s diff --git a/tests/testdata/ini/no_such_profile_section_name.ini b/tests/testdata/ini/no_such_profile_section_name.ini new file mode 100644 index 0000000..a51818e --- /dev/null +++ b/tests/testdata/ini/no_such_profile_section_name.ini @@ -0,0 +1,2 @@ +[general] +profile = test \ No newline at end of file diff --git a/tests/testdata/ini/profile.ini b/tests/testdata/ini/profile.ini new file mode 100644 index 0000000..54b8284 --- /dev/null +++ b/tests/testdata/ini/profile.ini @@ -0,0 +1,27 @@ +[general] +profile = tz + +[profile "tz"] +user = Alice +#passwd = secret +passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s +repos = ia32_main, ia32_non-oss, ia32_base, local +api = tz + +[obs "tz"] +url = https://api.tz/path + +[repo ia32_main] +url = https://repo/ia32/main + +[repo ia32_non-oss] +url = https://repo/ia32/non-oss + +[repo ia32_base] +url = https://repo/ia32/base +user = Bob +#passwd = classified +passwdx = QlpoOTFBWSZTWRwZil4AAACBgC8kCAAgADEMCCAPKGaQLT4u5IpwoSA4MxS8 + +[repo local] +url = /local/path \ No newline at end of file diff --git a/tests/testdata/ini/profile_only_has_api.ini b/tests/testdata/ini/profile_only_has_api.ini new file mode 100644 index 0000000..89035f0 --- /dev/null +++ b/tests/testdata/ini/profile_only_has_api.ini @@ -0,0 +1,14 @@ +[general] +profile = test + +[profile "test"] +user = Alice +#passwd = secret +passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s +api = test_api + +[obs "test_api"] +url = https://api.tz/path +user = Bob +#passwd = classified +passwdx = QlpoOTFBWSZTWRwZil4AAACBgC8kCAAgADEMCCAPKGaQLT4u5IpwoSA4MxS8 diff --git a/tests/testdata/ini/subcommand.ini b/tests/testdata/ini/subcommand.ini new file mode 100644 index 0000000..974abb3 --- /dev/null +++ b/tests/testdata/ini/subcommand.ini @@ -0,0 +1,16 @@ +[remotebuild] +build_server = https://api/build/server +user = Alice +#passwd = secret +passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s + +[build] +repo1.url = https://repo1/path +repo1.user = Alice +repo1.passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s + +repo2.url = https://repo2/path +repo2.user = Alice +repo2.passwdx = QlpoOTFBWSZTWYfNdxYAAAIBgAoAHAAgADDNAMNEA24u5IpwoSEPmu4s + +repo3.url = /local/path/repo \ No newline at end of file