Hello community, here is the log from the commit of package osc for openSUSE:Factory checked in at 2019-10-28 16:59:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/osc (Old) and /work/SRC/openSUSE:Factory/.osc.new.2990 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "osc" Mon Oct 28 16:59:23 2019 rev:138 rq:743494 version:0.166.0 Changes: -------- --- /work/SRC/openSUSE:Factory/osc/osc.changes 2019-08-05 13:40:44.524393220 +0200 +++ /work/SRC/openSUSE:Factory/.osc.new.2990/osc.changes 2019-10-28 17:00:22.237758170 +0100 @@ -1,0 +2,25 @@ +Thu Oct 24 10:05:06 UTC 2019 - Marco Strigl <[email protected]> + +- 0.166.0 (boo#1154972) + * New password handling backend. Supported password stores: + - Plaintext password + - Obfuscated password + - python-keyring (kwallet, secret store) + - gnome-keyring + - no store at all (ask for the password every time + * Refactor initial setup of osc (to select password store) + * fix decoding on osc lbl (boo#1137477) + * fix breakage of submitting complete branches back as an + submit request that contain packages without a change. + * fix error with plugins and osc -h + * various decoding improvements + * Transfer the name of the input file to vc instead of the content + (obs-build/vc will do the rest and open the file). + * support appimage builds + * new command browse. (opens a browser opening the project or package) + * new option --incoming for osc rq and osc review to only show only + incoming reqeusts for a project. +- new Recommends for xdg-utils. osc-browse uses xdg-open to open + the url to the project/package in an internet browser + +------------------------------------------------------------------- Old: ---- osc-0.165.4.tar.gz New: ---- osc-0.166.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ osc.spec ++++++ --- /var/tmp/diff_new_pack.ClEM4F/_old 2019-10-28 17:00:22.777758819 +0100 +++ /var/tmp/diff_new_pack.ClEM4F/_new 2019-10-28 17:00:22.785758829 +0100 @@ -27,12 +27,12 @@ %define use_python python %endif -%define version_unconverted 0.165.4 +%define version_unconverted 0.166.0 %define osc_plugin_dir %{_prefix}/lib/osc-plugins %define macros_file macros.osc Name: osc -Version: 0.165.4 +Version: 0.166.0 Release: 0 Summary: Open Build Service Commander License: GPL-2.0-or-later @@ -82,6 +82,7 @@ Recommends: obs-service-download_files Recommends: obs-service-format_spec_file Recommends: obs-service-source_validator +Recommends: xdg-utils %endif %endif # needed for storing credentials in kwallet/gnome-keyring ++++++ PKGBUILD ++++++ --- /var/tmp/diff_new_pack.ClEM4F/_old 2019-10-28 17:00:22.833758886 +0100 +++ /var/tmp/diff_new_pack.ClEM4F/_new 2019-10-28 17:00:22.833758886 +0100 @@ -1,5 +1,5 @@ pkgname=osc -pkgver=0.165.4 +pkgver=0.166.0 pkgrel=0 pkgdesc="Open Build Service client" arch=('i686' 'x86_64') ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ClEM4F/_old 2019-10-28 17:00:22.853758910 +0100 +++ /var/tmp/diff_new_pack.ClEM4F/_new 2019-10-28 17:00:22.853758910 +0100 @@ -1,7 +1,7 @@ <services> <service name="tar_scm" mode="disabled"> - <param name="version">0.165.4</param> - <param name="revision">0.165.4</param> + <param name="version">0.166.0</param> + <param name="revision">0.166.0</param> <param name="url">git://github.com/openSUSE/osc.git</param> <param name="scm">git</param> </service> ++++++ appimage.yml ++++++ --- /var/tmp/diff_new_pack.ClEM4F/_old 2019-10-28 17:00:22.877758939 +0100 +++ /var/tmp/diff_new_pack.ClEM4F/_new 2019-10-28 17:00:22.877758939 +0100 @@ -9,10 +9,10 @@ - build - osc - python-yaml -# - obs-service-obs_scm -# - obs-service-tar_scm -# - obs-service-set_version -# - obs-service-recompress + - obs-service-obs_scm + - obs-service-tar_scm + - obs-service-set_version + - obs-service-recompress - openSUSE-release - openSUSE-release-ftp - rsync @@ -21,12 +21,13 @@ - mkdir -p $BUILD_APPDIR/usr/share/pixmaps - cp /usr/share/pixmaps/appimage.png $BUILD_APPDIR/usr/share/pixmaps - mkdir -p $BUILD_APPDIR/usr/share/applications - - echo "[Desktop Entry]" > $BUILD_APPDIR/usr/share/applications/osc.desktop - - echo "Name=osc" >> $BUILD_APPDIR/usr/share/applications/osc.desktop - - echo "Exec=osc" >> $BUILD_APPDIR/usr/share/applications/osc.desktop - - echo "Icon=appimage" >> $BUILD_APPDIR/usr/share/applications/osc.desktop - - echo "Type=Application" >> $BUILD_APPDIR/usr/share/applications/osc.desktop - - sed -i -e 's,^#!/usr/bin/python,#!/usr/bin/env python,' $BUILD_APPDIR/usr/bin/osc + - echo "[Desktop Entry]" > $BUILD_APPDIR/usr/share/applications/osc.desktop + - echo "Name=osc" >> $BUILD_APPDIR/usr/share/applications/osc.desktop + - echo "Exec=osc" >> $BUILD_APPDIR/usr/share/applications/osc.desktop + - echo "Icon=appimage" >> $BUILD_APPDIR/usr/share/applications/osc.desktop + - echo "Categories=Development" >> $BUILD_APPDIR/usr/share/applications/osc.desktop + - echo "Type=Application" >> $BUILD_APPDIR/usr/share/applications/osc.desktop +# - sed -i -e 's,^#!/usr/bin/python,#!/usr/bin/env python,' $BUILD_APPDIR/usr/bin/osc - linuxdeployqt $BUILD_APPDIR/usr/share/applications/*.desktop -bundle-non-qt-libs -verbose=2 ++++++ debian.changelog ++++++ --- /var/tmp/diff_new_pack.ClEM4F/_old 2019-10-28 17:00:22.909758978 +0100 +++ /var/tmp/diff_new_pack.ClEM4F/_new 2019-10-28 17:00:22.909758978 +0100 @@ -1,4 +1,4 @@ -osc (0.165.4) unstable; urgency=low +osc (0.166.0) unstable; urgency=low - Update to 0.161.1 -- Marco Strigl <[email protected]> Thu, 26 Oct 2017 14:42:00 +0200 ++++++ osc-0.165.4.tar.gz -> osc-0.166.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/NEWS new/osc-0.166.0/NEWS --- old/osc-0.165.4/NEWS 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/NEWS 2019-10-24 11:48:35.000000000 +0200 @@ -1,3 +1,23 @@ +0.166.0 + - New password handling backend. Supported password stores: + * Plaintext password + * Obfuscated password + * python-keyring (kwallet, secret store) + * gnome-keyring + * no store at all (ask for the password every time + - Refactor initial setup of osc (to select password store) + - fix decoding on osc lbl (boo#1137477) + - fix breakage of submitting complete branches back as an + submit request that contain packages without a change. + - fix error with plugins and osc -h + - various decoding improvements + - Transfer the name of the input file to vc instead of the content + (obs-build/vc will do the rest and open the file). + - support appimage builds + - new command browse. (opens a browser opening the project or package) + - new option --incoming for osc rq and osc review to only show only + incoming reqeusts for a project. + 0.165.4 - allow optional fork when creating a maintenance request - fix RPMError fallback diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/OscConfigParser.py new/osc-0.166.0/osc/OscConfigParser.py --- old/osc-0.165.4/osc/OscConfigParser.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/OscConfigParser.py 2019-10-24 11:48:35.000000000 +0200 @@ -355,4 +355,10 @@ ret.append(str(line)) return '\n'.join(ret) + def _validate_value_types(self, section="", option="", value=""): + if not isinstance(section, str): + raise TypeError("section names must be strings") + if not isinstance(option, str): + raise TypeError("option keys must be strings") + # vim: sw=4 et diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/build.py new/osc-0.166.0/osc/build.py --- old/osc-0.165.4/osc/build.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/build.py 2019-10-24 11:48:35.000000000 +0200 @@ -646,8 +646,7 @@ pac = store_read_package(os.curdir) if opts.multibuild_package: buildargs.append('--buildflavor=%s' % opts.multibuild_package) - if pac != '_repository': - pac = pac + ":" + opts.multibuild_package + pac = pac + ":" + opts.multibuild_package if opts.shell: buildargs.append("--shell") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/commandline.py new/osc-0.166.0/osc/commandline.py --- old/osc-0.165.4/osc/commandline.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/commandline.py 2019-10-24 11:48:35.000000000 +0200 @@ -146,27 +146,22 @@ except oscerr.NoConfigfile as e: print(e.msg, file=sys.stderr) print('Creating osc configuration file %s ...' % e.file, file=sys.stderr) - import getpass - config = {} - config['user'] = raw_input('Username: ') - config['pass'] = getpass.getpass() - if self.options.no_keyring: - config['use_keyring'] = '0' - if self.options.no_gnome_keyring: - config['gnome_keyring'] = '0' + apiurl = conf.DEFAULTS['apiurl'] if self.options.apiurl: - config['apiurl'] = self.options.apiurl - - conf.write_initial_config(e.file, config) + apiurl = self.options.apiurl + conf.interactive_config_setup(e.file, apiurl) print('done', file=sys.stderr) if try_again: self.postoptparse(try_again = False) except oscerr.ConfigMissingApiurl as e: print(e.msg, file=sys.stderr) - import getpass - user = raw_input('Username: ') - passwd = getpass.getpass() - conf.add_section(e.file, e.url, user, passwd) + conf.interactive_config_setup(e.file, e.url, initial=False) + if try_again: + self.postoptparse(try_again = False) + except oscerr.ConfigMissingCredentialsError as e: + print(e.msg) + print('Please enter new credentials.') + conf.interactive_config_setup(e.file, e.url, initial=False) if try_again: self.postoptparse(try_again = False) @@ -1250,73 +1245,64 @@ project = store_read_project(os.curdir) sr_ids = [] - # for single request - actionxml = "" - options_block = "<options>" - if src_update: - options_block += """<sourceupdate>%s</sourceupdate>""" % (src_update) - if opts.update_link: - options_block + """<updatelink>true</updatelink></options> """ - options_block += "</options>" - # loop via all packages for checking their state - for p in meta_get_packagelist(apiurl, project): - # get _link info from server, that knows about the local state ... - u = makeurl(apiurl, ['source', project, p]) - f = http_GET(u) - root = ET.parse(f).getroot() - target_project = None - if len(args) == 1: - target_project = args[0] - linkinfo = root.find('linkinfo') - if linkinfo == None: - if len(args) < 1: - print("Package ", p, " is not a source link and no target specified.") - sys.exit("This is currently not supported.") - else: - if linkinfo.get('error'): - print("Package ", p, " is a broken source link.") - sys.exit("Please fix this first") - t = linkinfo.get('project') - if t: - if target_project == None: - target_project = t - if len(root.findall('entry')) > 1: # This is not really correct, but should work mostly - # Real fix is to ask the api if sources are modificated - # but there is no such call yet. - print("Submitting package ", p) - else: - print(" Skipping not modified package ", p) - continue + target_project = None + if len(args) == 1: + target_project = args[0] + if opts.separate_requests or opts.seperate_requests: + for p in meta_get_packagelist(apiurl, project): + # get _link info from server, that knows about the local state ... + u = makeurl(apiurl, ['source', project, p]) + f = http_GET(u) + root = ET.parse(f).getroot() + _check_service(root) + linkinfo = root.find('linkinfo') + if linkinfo == None: + if len(args) < 1: + print("Package ", p, " is not a source link and no target specified.") + sys.exit("This is currently not supported.") else: - print("Skipping package ", p, " since it is a source link pointing inside the project.") - continue - - # check for failed source service - _check_service(root) - - # submitting this package - if opts.separate_requests or opts.seperate_requests: - # create a single request - result = create_submit_request(apiurl, project, p, src_update=src_update) + if linkinfo.get('error'): + print("Package ", p, " is a broken source link.") + sys.exit("Please fix this first") + t = linkinfo.get('project') + if t is None: + print("Skipping package ", p, " since it is a source link pointing inside the project.") + continue + print("Submitting package ", p) + try: + result = create_submit_request(apiurl, project, p, target_project, src_update=src_update) + except HTTPError as e: + if e.hdrs.get('X-Opensuse-Errorcode') == 'missing_action': + print("Package ", p, " no changes. Skipping...") + continue + raise if not result: sys.exit("submit request creation failed") sr_ids.append(result) - else: - s = """<action type="submit"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> %s </action>""" % \ - (project, p, target_project, p, options_block) - actionxml += s - - if actionxml != "": + else: + actionxml = "" + options_block = "<options>" + if src_update: + options_block += """<sourceupdate>%s</sourceupdate>""" % (src_update) + if opts.update_link: + options_block + """<updatelink>true</updatelink></options> """ + options_block += "</options>" + target_prj_block = "" + if target_project is not None: + target_prj_block = """<target project="%s"/>""" % target_project + s = """<action type="submit"> <source project="%s" /> %s %s </action>""" % \ + (project, target_prj_block, options_block) + actionxml += s xml = """<request> %s <state name="new"/> <description>%s</description> </request> """ % \ - (actionxml, cgi.escape(opts.message or "")) + (actionxml, cgi.escape(opts.message or "")) u = makeurl(apiurl, ['request'], query='cmd=create&addrevision=1') f = http_POST(u, data=xml) root = ET.parse(f).getroot() sr_ids.append(root.get('id')) - print("Request created: ", end=' ') + print("Request(s) created: ", end=' ') for i in sr_ids: print(i, end=' ') @@ -1345,6 +1331,10 @@ p = findpacs(os.curdir)[0] src_project = p.prjname src_package = p.name + if self.options.apiurl and self.options.apiurl != p.apiurl: + print('The apiurl for the working copy of this package is %s' % p.apiurl) + print('You cannot use this command with the -A %s option.' % self.options.apiurl) + sys.exit(1) apiurl = p.apiurl if len(args) == 0 and p.islink(): dst_project = p.linkinfo.project @@ -2132,6 +2122,8 @@ help='non-interactive review of request') @cmdln.option('--exclude-target-project', action='append', help='exclude target project from request list') + @cmdln.option('--incoming', action='store_true', + help='Show only requests where the project is target') @cmdln.option('--involved-projects', action='store_true', help='show all requests for project/packages where USER is involved') @cmdln.option('--target-package-filter', metavar='TARGET_PACKAGE_FILTER', @@ -2243,6 +2235,9 @@ if opts.state == '' and subcmd != 'review': opts.state = 'declined,new,review' + if opts.incoming: + conf.config['include_request_from_project'] = False + if args[0] == 'help': return self.do_help(['help', 'request']) @@ -2506,7 +2501,7 @@ print('Buildstatus for \'%s/%s\':' % (action.src_project, action.src_package)) print('\n'.join(get_results(apiurl, action.src_project, action.src_package))) if opts.diff: - diff = '' + diff = b'' try: # works since OBS 2.1 diff = request_diff(apiurl, reqid) @@ -2520,7 +2515,7 @@ action.tgt_project.encode(), action.tgt_package.encode()) diff += submit_action_diff(apiurl, action) diff += b'\n\n' - run_pager(decode_it(diff), tmp_suffix='') + run_pager(diff, tmp_suffix='') # checkout elif cmd == 'checkout' or cmd == 'co': @@ -3346,6 +3341,10 @@ home:USERNAME:branches:ATTRIBUTE:PACKAGE if nothing else specified. + If osc maintained or sm is issued only the relevant instances of a + package will be shown. No branch will be created. This is similar + to osc mbranch --dryrun. + usage: osc sm [SOURCEPACKAGE] [-a ATTRIBUTE] osc mbranch [ SOURCEPACKAGE [ TARGETPROJECT ] ] @@ -4341,6 +4340,44 @@ print(url_tmpl % (project.replace(':', ':/'), repo, project)) + def do_browse(self, subcmd, opts, *args): + """${cmd_name}: opens browser + + usage: + osc browse [PROJECT [PACKAGE]] + + ${cmd_option_list} + """ + + apiurl = self.get_api_url() + + package = None + if len(args) == 1: + project = args[0] + elif len(args) == 2: + project = args[0] + package = args[1] + elif len(args) == 0: + project = store_read_project('.') + if is_package_dir('.'): + package = store_read_package('.') + else: + raise oscerr.WrongArgs('Wrong number of arguments') + + root = ET.fromstring(b''.join(show_configuration(apiurl))) + node = root.find('obs_url') + if node is None or not node.text: + raise oscerr.APIError('obs_url configuration element expected') + obs_url = node.text + + if package is None: + url = "{}/project/show/{}".format(obs_url, project) + else: + url = "{}/package/show/{}/{}".format(obs_url, project, package) + + run_external('xdg-open', url) + + @cmdln.option('-r', '--revision', metavar='rev', help='checkout the specified revision. ' 'NOTE: if you checkout the complete project ' @@ -4736,6 +4773,11 @@ try: self._commit(subcmd, opts, args) except oscerr.ExtRuntimeError as e: + pattern = re.compile("No such file") + if "No such file" in e.msg: + editor = os.getenv('EDITOR', default=get_default_editor()) + print("Editor %s not found" % editor) + return 1 print("ERROR: service run failed", e, file=sys.stderr) return 1 except oscerr.PackageNotInstalled as e: @@ -5470,23 +5512,29 @@ print_buildlog(apiurl, project, package, repository, arch, offset, strip_time, opts.last) - def print_repos(self, repos_only=False, exc_class=oscerr.WrongArgs, exc_msg='Missing arguments'): + def print_repos(self, repos_only=False, exc_class=oscerr.WrongArgs, exc_msg='Missing arguments', project=None): wd = os.curdir doprint = False if is_package_dir(wd): - msg = "package" + msg = 'Valid arguments for this package are:' doprint = True elif is_project_dir(wd): - msg = "project" + msg = 'Valid arguments for this project are:' doprint = True + args = [] + if project is not None: + args.append(project) + msg = 'Valid arguments are:' + doprint=True + if doprint: - print('Valid arguments for this %s are:' % msg) + print(msg) print() if repos_only: - self.do_repositories("repos_only", None) + self.do_repositories("repos_only", None, *args) else: - self.do_repositories(None, None) + self.do_repositories(None, None, *args) raise exc_class(exc_msg) @cmdln.alias('rbl') @@ -5627,7 +5675,7 @@ while len(data): if opts.strip_time or conf.config['buildlog_strip_time']: data = buildlog_strip_time(data) - sys.stdout.write(data) + sys.stdout.write(decode_it(data)) data = f.read(BUFSIZE) f.close() @@ -5823,9 +5871,10 @@ raise oscerr.WrongArgs('Incorrect number of arguments (Note: \'.\' is no package wc)') if opts.alternative_project: project = opts.alternative_project + package = '_repository' else: project = store_read_project('.') - package = store_read_package('.') + package = store_read_package('.') repository, arch, build_descr = self.parse_repoarchdescr(args, alternative_project=opts.alternative_project, ignore_descr=True, multibuild_package=opts.multibuild_package) elif len(args) == 4 or len(args) == 5: project = args[0] @@ -6059,7 +6108,7 @@ for subarch in osc.build.can_also_build.get(mainarch): all_archs.append(subarch) for arg in args: - if arg.endswith('.spec') or arg.endswith('.dsc') or arg.endswith('.kiwi') or arg.endswith('.livebuild') or arg == 'PKGBUILD' or arg == 'build.collax' or arg == 'Dockerfile' or arg == 'fissile.yml': + if arg.endswith('.spec') or arg.endswith('.dsc') or arg.endswith('.kiwi') or arg.endswith('.livebuild') or arg == 'PKGBUILD' or arg == 'build.collax' or arg == 'Dockerfile' or arg == 'fissile.yml' or arg == 'appimage.yml': arg_descr = arg else: if (arg == osc.build.hostarch or arg in all_archs) and arg_arch is None: @@ -6121,7 +6170,8 @@ # reduce(lambda x, y: x + y, (glob.glob(x) for x in ('*.spec', '*.dsc', '*.kiwi'))) # but be a bit more readable :) descr = glob.glob('*.spec') + glob.glob('*.dsc') + glob.glob('*.kiwi') + glob.glob('*.livebuild') \ - + glob.glob('PKGBUILD') + glob.glob('build.collax') + glob.glob('Dockerfile') + glob.glob('fissile.yml') + + glob.glob('PKGBUILD') + glob.glob('build.collax') + glob.glob('Dockerfile') + glob.glob('fissile.yml') \ + + glob.glob('appimage.yml') # FIXME: # * request repos from server and select by build type. @@ -6180,7 +6230,7 @@ if not arg_descr: msg = 'Multiple build description files found: %s' % ', '.join(cands) elif not ignore_descr: - msg = 'Missing argument: build description (spec, dsc, kiwi or livebuild file)' + msg = 'Missing argument: build description (for example a spec, dsc or kiwi file)' try: p = Package('.') if p.islink() and not p.isexpanded(): @@ -7285,7 +7335,7 @@ repos = list(get_repos_of_project(apiurl, project)) if not [i for i in repos if repository == i.name]: - self.print_repos(exc_msg='Invalid repository \'%s\'' % repository) + self.print_repos(exc_msg='Invalid repository \'%s\'' % repository, project=project) arches = [architecture] if architecture is None: @@ -8430,7 +8480,7 @@ if isinstance(data, str): sys.stdout.write(data) else: - sys.stdout.write(decode_it(data)) + sys.stdout.buffer.write(data) # helper function to download a file from a specific revision @@ -8924,10 +8974,14 @@ cmd_list.append("-m") cmd_list.append(opts.message) if opts.file: - if not os.path.isfile(opts.file): + if len(args) > 1: + raise oscerr.WrongOptions('--file and file_with_comment are mutually exclusive') + elif not os.path.isfile(opts.file): raise oscerr.WrongOptions('\'%s\': is no file' % opts.file) - cmd_list.append("-m") - cmd_list.append(open(opts.file).read().strip()) + args = list(args) + if not args: + cmd_list.append('') + cmd_list.append(opts.file) if opts.just_edit: cmd_list.append("-e") @@ -8975,6 +9029,10 @@ help='indicates that the config value should be read from stdin') @cmdln.option('-p', '--prompt', action='store_true', help='prompt for a value') + @cmdln.option('--change-password', action='store_true', + help='Change password') + @cmdln.option('--select-password-store', action='store_true', + help='Change the password store') @cmdln.option('--no-echo', action='store_true', help='prompt for a value but do not echo entered characters') @cmdln.option('--dump', action='store_true', @@ -8988,12 +9046,22 @@ osc config section option (get current value) osc config section option value (set to value) osc config section option --delete (delete option/reset to the default) + osc config section --change-password (changes the password in section "section") (section is either an apiurl or an alias or 'general') osc config --dump (dump the complete configuration) ${cmd_usage} ${cmd_option_list} """ + prompt_value = 'Value: ' + if opts.change_password: + opts.no_echo = True + opts.prompt = True + opts.select_password_store = True + prompt_value = 'Password: ' + if len(args) != 1: + raise oscerr.WrongArgs('--change-password only needs the apiurl') + args = [args[0], 'pass'] if len(args) < 2 and not (opts.dump or opts.dump_full): raise oscerr.WrongArgs('Too few arguments') elif opts.dump or opts.dump_full: @@ -9027,24 +9095,27 @@ elif opts.no_echo or opts.prompt: if opts.no_echo: import getpass - inp = getpass.getpass('Value: ').strip() + inp = getpass.getpass(prompt_value).strip() else: - inp = raw_input('Value: ').strip() + inp = raw_input(prompt_value).strip() if not inp: raise oscerr.WrongArgs('error: no value was entered') val = [inp] - opt, newval = conf.config_set_option(section, opt, ' '.join(val), delete=opts.delete, update=True) + creds_mgr_descr = None + if opt == 'pass' and opts.select_password_store: + creds_mgr_descr = conf.select_credentials_manager_descr() + orig_opt = opt + opt, newval = conf.config_set_option(section, opt, ' '.join(val), delete=opts.delete, update=True, creds_mgr_descr=creds_mgr_descr) if newval is None and opts.delete: print('\'%s\': \'%s\' got removed' % (section, opt)) elif newval is None: print('\'%s\': \'%s\' is not set' % (section, opt)) else: - if opts.no_echo: + if orig_opt == 'pass': + print('Password has been changed.') + elif opts.no_echo: # supress value print('\'%s\': set \'%s\'' % (section, opt)) - elif opt == 'pass' and not conf.config['plaintext_passwd'] and newval == 'your_password': - opt, newval = conf.config_set_option(section, 'passx') - print('\'%s\': \'pass\' was rewritten to \'passx\': \'%s\'' % (section, newval)) else: print('\'%s\': \'%s\' is set to \'%s\'' % (section, opt, newval)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/conf.py new/osc-0.166.0/osc/conf.py --- old/osc-0.165.4/osc/conf.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/conf.py 2019-10-24 11:48:35.000000000 +0200 @@ -43,6 +43,7 @@ import sys import ssl import warnings +import getpass try: from http.cookiejar import LWPCookieJar, CookieJar @@ -63,7 +64,9 @@ from . import OscConfigParser from osc import oscerr +from osc.util.helper import raw_input from .oscsslexcp import NoSecureSSLError +from osc import credentials GENERIC_KEYRING = False GNOME_KEYRING = False @@ -76,12 +79,7 @@ import gobject gobject.set_application_name('osc') import gnomekeyring - if os.environ['GNOME_DESKTOP_SESSION_ID']: - # otherwise gnome keyring bindings spit out errors, when you have - # it installed, but you are not under gnome - # (even though hundreds of gnome-keyring daemons got started in parallel) - # another option would be to support kwallet here - GNOME_KEYRING = gnomekeyring.is_available() + GNOME_KEYRING = gnomekeyring.is_available() except: pass @@ -97,9 +95,9 @@ return 1 DEFAULTS = {'apiurl': 'https://api.opensuse.org', - 'user': 'your_username', - 'pass': 'your_password', - 'passx': '', + 'user': None, + 'pass': None, + 'passx': None, 'packagecachedir': '/var/tmp/osbuild-packagecache', 'su-wrapper': 'sudo', @@ -151,8 +149,8 @@ 'checkout_rooted': '0', # local files to ignore with status, addremove, .... 'exclude_glob': '.osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.vctmp.*', - # whether to keep passwords in plaintext. - 'plaintext_passwd': '1', + # whether to keep passwords in plaintext (deprecated (see creds manager)). + 'plaintext_passwd': '0', # limit the age of requests shown with 'osc req list'. # this is a default only, can be overridden by 'osc req list -D NNN' # Use 0 for unlimted. @@ -298,12 +296,6 @@ # local files to ignore with status, addremove, .... #exclude_glob = %(exclude_glob)s -# keep passwords in plaintext. -# Set to 0 to obfuscate passwords. It's no real security, just -# prevents most people from remembering your password if they watch -# you editing this file. -#plaintext_passwd = %(plaintext_passwd)s - # limit the age of requests shown with 'osc req list'. # this is a default only, can be overridden by 'osc req list -D NNN' # Use 0 for unlimted. @@ -327,9 +319,6 @@ # print call traces in case of errors #traceback = 1 -# use KDE/Gnome/MacOS/Windows keyring for credentials if available -#use_keyring = 1 - # check for unversioned/removed files before commit #check_filelist = 1 @@ -358,8 +347,6 @@ #review_inherit_group = 1 [%(apiurl)s] -user = %(user)s -pass = %(pass)s # set aliases for this apiurl # aliases = foo, bar # real name used in .changes, unless the one from osc meta prj <user> will be used @@ -371,8 +358,6 @@ # User: mumblegack # Plain text password #pass = -# Force using of keyring for this API -#keyring = 1 """ @@ -672,7 +657,7 @@ raise -def config_set_option(section, opt, val=None, delete=False, update=True, **kwargs): +def config_set_option(section, opt, val=None, delete=False, update=True, creds_mgr_descr=None, **kwargs): """ Sets a config option. If val is not specified the current/default value is returned. If val is specified, opt is set to val and the new value is returned. @@ -708,11 +693,37 @@ raise oscerr.ConfigError('unknown config option \'%s\'' % opt, config['conffile']) run = False if val: - cp.set(section, opt, val) - write_config(config['conffile'], cp) + if opt == 'pass': + creds_mgr = _get_credentials_manager(section, cp) + user = _extract_user_compat(cp, section, creds_mgr) + old_pw = creds_mgr.get_password(section, user, defer=False) + try: + creds_mgr.delete_password(section, user) + if creds_mgr_descr: + creds_mgr_new = creds_mgr_descr.create(cp) + else: + creds_mgr_new = creds_mgr + creds_mgr_new.set_password(section, user, val) + write_config(config['conffile'], cp) + opt = credentials.AbstractCredentialsManager.config_entry + old_pw = None + finally: + if old_pw is not None: + creds_mgr.set_password(section, user, old_pw) + # not nice, but needed if the Credentials Manager will change + # something in cp + write_config(config['conffile'], cp) + else: + cp.set(section, opt, val) + write_config(config['conffile'], cp) run = True - elif delete and cp.has_option(section, opt): - cp.remove_option(section, opt) + elif delete and (cp.has_option(section, opt) or opt == 'pass'): + if opt == 'pass': + creds_mgr = _get_credentials_manager(section, cp) + user = _extract_user_compar(cp, section, creds_mgr) + creds_mgr.delete_password(section, user) + else: + cp.remove_option(section, opt) write_config(config['conffile'], cp) run = True if run and update: @@ -725,15 +736,17 @@ return (opt, cp.get(section, opt, raw=True)) return (opt, None) -def passx_decode(passx): - """decode the obfuscated password back to plain text password""" - return bz2.decompress(base64.b64decode(passx.encode("ascii"))).decode("ascii") - -def passx_encode(passwd): - """encode plain text password to obfuscated form""" - return base64.b64encode(bz2.compress(passwd.encode('ascii'))).decode("ascii") +def _extract_user_compat(cp, section, creds_mgr): + """ + This extracts the user either from the ConfigParser or + the creds_mgr. Only needed for deprecated Gnome Keyring + """ + user = cp.get(section, 'user') + if user is None and hasattr(creds_mgr, 'get_user'): + user = creds_mgr.get_user(section) + return user -def write_initial_config(conffile, entries, custom_template=''): +def write_initial_config(conffile, entries, custom_template='', creds_mgr_descriptor=None): """ write osc's intial configuration file. entries is a dict which contains values for the config file (e.g. { 'user' : 'username', 'pass' : 'password' } ). @@ -742,37 +755,19 @@ conf_template = custom_template or new_conf_template config = DEFAULTS.copy() config.update(entries) - # at this point use_keyring and gnome_keyring are str objects - if config['use_keyring'] == '1' and GENERIC_KEYRING: - protocol, host, path = \ - parse_apisrv_url(None, config['apiurl']) - keyring.set_password(host, config['user'], config['pass']) - config['pass'] = '' - config['passx'] = '' - elif config['gnome_keyring'] == '1' and GNOME_KEYRING: - protocol, host, path = \ - parse_apisrv_url(None, config['apiurl']) - gnomekeyring.set_network_password_sync( - user=config['user'], - password=config['pass'], - protocol=protocol, - server=host, - object=path) - config['user'] = '' - config['pass'] = '' - config['passx'] = '' - if not config['plaintext_passwd']: - config['pass'] = '' - else: - config['passx'] = passx_encode(config['pass']) - sio = StringIO(conf_template.strip() % config) cp = OscConfigParser.OscConfigParser(DEFAULTS) cp.readfp(sio) + cp.set(config['apiurl'], 'user', config['user']) + if creds_mgr_descriptor: + creds_mgr = creds_mgr_descriptor.create(cp) + else: + creds_mgr = _get_credentials_manager(config['apiurl'], cp) + creds_mgr.set_password(config['apiurl'], config['user'], config['pass']) write_config(conffile, cp) -def add_section(filename, url, user, passwd): +def add_section(filename, url, user, passwd, creds_mgr_descriptor=None): """ Add a section to config file for new api url. """ @@ -783,33 +778,39 @@ except OscConfigParser.configparser.DuplicateSectionError: # Section might have existed, but was empty pass + cp.set(url, 'user', user) + if creds_mgr_descriptor: + creds_mgr = creds_mgr_descriptor.create(cp) + else: + creds_mgr = _get_credentials_manager(url, cp) + creds_mgr.set_password(url, user, passwd) + write_config(filename, cp) + + +def _get_credentials_manager(url, cp): + if cp.has_option(url, credentials.AbstractCredentialsManager.config_entry): + creds_mgr = credentials.create_credentials_manager(url, cp) + if creds_mgr is None: + msg = 'Unable to instantiate creds mgr (section: %s)' % url + conffile = get_configParser.conffile + raise oscerr.ConfigMissingCredentialsError(msg, conffile, url) + return creds_mgr if config['use_keyring'] and GENERIC_KEYRING: - protocol, host, path = parse_apisrv_url(None, url) - keyring.set_password(host, user, passwd) - cp.set(url, 'keyring', '1') - cp.set(url, 'user', user) - cp.remove_option(url, 'pass') - cp.remove_option(url, 'passx') + return credentials.get_keyring_credentials_manager(cp) elif config['gnome_keyring'] and GNOME_KEYRING: protocol, host, path = parse_apisrv_url(None, url) - gnomekeyring.set_network_password_sync( - user=user, - password=passwd, - protocol=protocol, - server=host, - object=path) - cp.set(url, 'keyring', '1') - cp.remove_option(url, 'pass') - cp.remove_option(url, 'passx') - else: - cp.set(url, 'user', user) - if not config['plaintext_passwd']: - cp.remove_option(url, 'pass') - cp.set(url, 'passx', passx_encode(passwd)) - else: - cp.remove_option(url, 'passx') - cp.set(url, 'pass', passwd) - write_config(filename, cp) + return credentials.GnomeKeyringCredentialsManager(cp, None) + elif cp.get(url, 'passx') is not None: + return credentials.ObfuscatedConfigFileCredentialsManager(cp, None) + return credentials.PlaintextConfigFileCredentialsManager(cp, None) + + +class APIHostOptionsEntry(dict): + def __getitem__(self, key, *args, **kwargs): + value = super(self.__class__, self).__getitem__(key, *args, **kwargs) + if key == 'pass' and callable(value): + value = value() + return value def get_config(override_conffile=None, @@ -888,74 +889,16 @@ # backward compatiblity scheme, host, path = parse_apisrv_url(config.get('scheme', 'https'), url) apiurl = urljoin(scheme, host, path) - user = None - password = None - if config['use_keyring'] and GENERIC_KEYRING: - try: - # Read from keyring lib if available - user = cp.get(url, 'user', raw=True) - password = str(keyring.get_password(host, user)) - except: - # Fallback to file based auth. - pass - elif config['gnome_keyring'] and GNOME_KEYRING: - # Read from gnome keyring if available - try: - gk_data = gnomekeyring.find_network_password_sync(protocol=scheme, server=host, object=path) - if not 'user' in gk_data[0]: - raise oscerr.ConfigError('no user found in keyring', conffile) - user = gk_data[0]['user'] - if 'password' in gk_data[0]: - password = str(gk_data[0]['password']) - else: - # this is most likely an error - print('warning: no password found in keyring', file=sys.stderr) - except gnomekeyring.NoMatchError: - # Fallback to file based auth. - pass - - if not user is None and len(user) == 0: - user = None - print('Warning: blank user in the keyring for the ' \ - 'apiurl %s.\nPlease fix your keyring entry.', file=sys.stderr) - - if user is not None and password is None: - err = ('no password defined for "%s".\nPlease fix your keyring ' - 'entry or gnome-keyring setup.\nAssuming an empty password.' - % url) - print(err, file=sys.stderr) - password = '' - - # Read credentials from config + creds_mgr = _get_credentials_manager(url, cp) + # if the deprecated gnomekeyring is used we should use the apiurl instead of url + # (that's what the old code did), but this makes things more complex + # (also, it is very unlikely that url and apiurl differ) + user = _extract_user_compat(cp, url, creds_mgr) if user is None: - #FIXME: this could actually be the ideal spot to take defaults - #from the general section. - user = cp.get(url, 'user', raw=True) # need to set raw to prevent '%' expansion - password = cp.get(url, 'pass', raw=True) # especially on password! - try: - passwordx = passx_decode(cp.get(url, 'passx', raw=True)) # especially on password! - except: - passwordx = '' - - if password == None or password == 'your_password': - password = '' - - if user is None or user == '': - raise oscerr.ConfigError('user is blank for %s, please delete or complete the "user=" entry in %s.' % (apiurl, config['conffile']), config['conffile']) - - if config['plaintext_passwd'] and passwordx or not config['plaintext_passwd'] and password: - if config['plaintext_passwd']: - if password != passwordx: - print('%s: rewriting from encoded pass to plain pass' % url, file=sys.stderr) - add_section(conffile, url, user, passwordx) - password = passwordx - else: - if password != passwordx: - print('%s: rewriting from plain pass to encoded pass' % url, file=sys.stderr) - add_section(conffile, url, user, password) - - if not config['plaintext_passwd']: - password = passwordx + raise oscerr.ConfigMissingCredentialsError('No user found in section %s' % url, conffile, url) + password = creds_mgr.get_password(url, user) + if password is None: + raise oscerr.ConfigMissingCredentialsError('No password found in section %s' % url, conffile, url) if cp.has_option(url, 'http_headers'): http_headers = cp.get(url, 'http_headers') @@ -972,9 +915,10 @@ raise oscerr.ConfigError(msg, conffile) aliases[key] = url - api_host_options[apiurl] = {'user': user, - 'pass': password, - 'http_headers': http_headers} + entry = {'user': user, + 'pass': password, + 'http_headers': http_headers} + api_host_options[apiurl] = APIHostOptionsEntry(entry) optional = ('realname', 'email', 'sslcertck', 'cafile', 'capath') for key in optional: @@ -1015,6 +959,8 @@ if 'build_platform' in config: print('Warning: Use of \'build_platform\' config option is deprecated! (use \'build_repository\' instead)', file=sys.stderr) config['build_repository'] = config['build_platform'] + if config['plaintext_passwd']: + print('The \'plaintext_passwd\' option is deprecated and will be ignored', file=sys.stderr) config['verbose'] = int(config['verbose']) # override values which we were called with @@ -1061,4 +1007,30 @@ return conffile +def interactive_config_setup(conffile, apiurl, initial=True): + user = raw_input('Username: ') + passwd = getpass.getpass() + creds_mgr_descr = select_credentials_manager_descr() + if initial: + config = {'user': user, 'pass': passwd} + if apiurl: + config['apiurl'] = apiurl + write_initial_config(conffile, config, creds_mgr_descriptor=creds_mgr_descr) + else: + add_section(conffile, apiurl, user, passwd, creds_mgr_descriptor=creds_mgr_descr) + +def select_credentials_manager_descr(): + if not credentials.has_keyring_support(): + print('To use keyrings please install python-keyring.') + creds_mgr_descriptors = credentials.get_credentials_manager_descriptors() + for i, creds_mgr_descr in enumerate(creds_mgr_descriptors, 1): + print('%d) %s (%s)' % (i, creds_mgr_descr.name(), creds_mgr_descr.description()))# + i = raw_input('Select credentials manager: ') + if not i.isdigit(): + sys.exit('Invalid selection') + i = int(i) - 1 + if i < 0 or i >= len(creds_mgr_descriptors): + sys.exit('Invalid selection') + return creds_mgr_descriptors[i] + # vim: sw=4 et diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/core.py new/osc-0.166.0/osc/core.py --- old/osc-0.165.4/osc/core.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/core.py 2019-10-24 11:48:35.000000000 +0200 @@ -5,7 +5,7 @@ from __future__ import print_function -__version__ = '0.165.4' +__version__ = '0.166.0' # __store_version__ is to be incremented when the format of the working copy # "store" changes in an incompatible way. Please add any needed migration @@ -55,7 +55,7 @@ except ImportError: from .util.helper import cmp_to_key -from osc.util.helper import decode_list, decode_it +from osc.util.helper import decode_list, decode_it, raw_input try: # python 2.6 and python 2.7 @@ -1683,8 +1683,8 @@ # diff3 OPTIONS... MINE OLDER YOURS ret = -1 with open(filename, 'w') as f: - ret = run_external('diff3', '-m', '-E', myfilename, - storefilename, upfilename, stdout=f) + args = ('-m', '-E', myfilename, storefilename, upfilename) + ret = run_external('diff3', *args, stdout=f) # "An exit status of 0 means `diff3' was successful, 1 means some # conflicts were found, and 2 means trouble." @@ -1703,6 +1703,7 @@ self.write_conflictlist() return 'C' else: + merge_cmd = 'diff3 ' + ' '.join(args) raise oscerr.ExtRuntimeError('diff3 failed with exit code: %s' % ret, merge_cmd) def update_local_filesmeta(self, revision=None): @@ -2860,7 +2861,7 @@ now = datetime.datetime.utcnow() now = now + datetime.timedelta(hours=hours) - self.accept_at = now.isoformat() + self.accept_at = now.isoformat() + '+00:00' @staticmethod def format_review(review, show_srcupdate=False): @@ -3553,7 +3554,7 @@ f = http_GET(url) return f.readlines() except HTTPError as e: - e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac) + e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (unquote(prj), pac) raise @@ -4676,7 +4677,7 @@ targetfilename = targetfilename or filename query = {} if meta: - query['rev'] = 1 + query['meta'] = 1 if revision: query['rev'] = revision u = makeurl(apiurl, ['source', prj, package, pathname2url(filename.encode(locale.getpreferredencoding(), 'replace'))], query=query) @@ -5145,9 +5146,10 @@ if root.get('project') != dst_project: # The source comes from a different project via a project link, we need to create this instance meta_change = True - except: + except HTTPError as e: + if e.code != 404: + raise meta_change = True - if meta_change: if missing_target: dst_meta = '<package name="%s"><title/><description/></package>' % dst_package @@ -5239,7 +5241,9 @@ if root.get('project') != dst_project: # The source comes from a different project via a project link, we need to create this instance meta_change = True - except: + except HTTPError as e: + if e.code != 404: + raise meta_change = True if meta_change: @@ -7479,7 +7483,7 @@ elif repl == 'm': if tmpfile is not None: tmpfile.seek(0) - comment = edit_message(footer=tmpfile.read()) + comment = edit_message(footer=decode_it(tmpfile.read())) else: comment = edit_text() create_comment(apiurl, 'request', comment, request.reqid) @@ -7683,21 +7687,6 @@ filter_role(res, user, role) return res -def raw_input(*args): - try: - import builtins - func = builtins.input - except ImportError: - #python 2.7 - import __builtin__ - func = __builtin__.raw_input - - try: - return func(*args) - except EOFError: - # interpret ctrl-d as user abort - raise oscerr.UserAbort() - def run_external(filename, *args, **kwargs): """Executes the program filename via subprocess.call. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/credentials.py new/osc-0.166.0/osc/credentials.py --- old/osc-0.165.4/osc/credentials.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-0.166.0/osc/credentials.py 2019-10-24 11:48:35.000000000 +0200 @@ -0,0 +1,315 @@ +import importlib +import bz2 +import base64 +import getpass +try: + from urllib.parse import urlsplit +except ImportError: + from urlparse import urlsplit +try: + import keyring +except ImportError: + keyring = None +try: + import gnomekeyring +except ImportError: + gnomekeyring = None + + +class AbstractCredentialsManagerDescriptor(object): + def name(self): + raise NotImplementedError() + + def description(self): + raise NotImplementedError() + + def create(self, cp): + raise NotImplementedError() + + def __lt__(self, other): + return self.name() < other.name() + + +class AbstractCredentialsManager(object): + config_entry = 'credentials_mgr_class' + + def __init__(self, cp, options): + super(AbstractCredentialsManager, self).__init__() + self._cp = cp + self._process_options(options) + + @classmethod + def create(cls, cp, options): + return cls(cp, options) + + def get_password(self, url, user, defer=True): + # If defer is True a callable can be returned + # and the password is retrieved if the callable + # is called. Implementations are free to ignore + # defer parameter and can directly return the password. + # If defer is False the password is directly returned. + raise NotImplementedError() + + def set_password(self, url, user, password): + raise NotImplementedError() + + def delete_password(self, url, user): + raise NotImplementedError() + + def _qualified_name(self): + return qualified_name(self) + + def _process_options(self, options): + pass + + +class PlaintextConfigFileCredentialsManager(AbstractCredentialsManager): + def get_password(self, url, user, defer=True): + return self._cp.get(url, 'pass', raw=True) + + def set_password(self, url, user, password): + self._cp.set(url, 'pass', password) + self._cp.set(url, self.config_entry, self._qualified_name()) + + def delete_password(self, url, user): + self._cp.remove_option(url, 'pass') + + def _process_options(self, options): + if options is not None: + raise RuntimeError('options must be None') + + +class PlaintextConfigFileDescriptor(AbstractCredentialsManagerDescriptor): + def name(self): + return 'Config file credentials manager' + + def description(self): + return 'Store the credentials in the config file (plain text)' + + def create(self, cp): + return PlaintextConfigFileCredentialsManager(cp, None) + + +class ObfuscatedConfigFileCredentialsManager( + PlaintextConfigFileCredentialsManager): + def get_password(self, url, user, defer=True): + passwd = super(self.__class__, self).get_password(url, user) + return self.decode_password(passwd) + + def set_password(self, url, user, password): + compressed_pw = bz2.compress(password.encode('ascii')) + password = base64.b64encode(compressed_pw).decode("ascii") + super(self.__class__, self).set_password(url, user, password) + + @classmethod + def decode_password(cls, password): + compressed_pw = base64.b64decode(password.encode("ascii")) + return bz2.decompress(compressed_pw).decode("ascii") + + +class ObfuscatedConfigFileDescriptor(AbstractCredentialsManagerDescriptor): + def name(self): + return 'Obfuscated Config file credentials manager' + + def description(self): + return 'Store the credentials in the config file (obfuscated)' + + def create(self, cp): + return ObfuscatedConfigFileCredentialsManager(cp, None) + + +class TransientCredentialsManager(AbstractCredentialsManager): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self._password = None + + def _process_options(self, options): + if options is not None: + raise RuntimeError('options must be None') + + def get_password(self, url, user, defer=True): + if defer: + return self + return self() + + def set_password(self, url, user, password): + self._password = password + self._cp.set(url, self.config_entry, self._qualified_name()) + + def delete_password(self, url, user): + self._password = None + + def __call__(self): + if self._password is None: + self._password = getpass.getpass('Password: ') + return self._password + + +class TransientDescriptor(AbstractCredentialsManagerDescriptor): + def name(self): + return 'Transient password store' + + def description(self): + return 'Do not store the password and always ask for the password' + + def create(self, cp): + return TransientCredentialsManager(cp, None) + + +class KeyringCredentialsManager(AbstractCredentialsManager): + def __init__(self, cp, options, appname='osc'): + super(self.__class__, self).__init__(cp, options) + self._appname = appname + + def _process_options(self, options): + if options is None: + raise RuntimeError('options may not be None') + self._backend_cls_name = options + + def _load_backend(self): + keyring_backend = keyring.core.load_keyring(self._backend_cls_name) + keyring.set_keyring(keyring_backend) + + @classmethod + def create(cls, cp, options): + if not has_keyring_support(): + return None + return super(cls, cls).create(cp, options) + + def get_password(self, url, user, defer=True): + self._load_backend() + return keyring.get_password(self._appname, user) + + def set_password(self, url, user, password): + self._load_backend() + keyring.set_password(self._appname, user, password) + config_value = self._qualified_name() + ':' + self._backend_cls_name + self._cp.set(url, self.config_entry, config_value) + + def delete_password(self, url, user): + self._load_backend() + keyring.delete_password(self._appname, user) + + +class KeyringCredentialsDescriptor(AbstractCredentialsManagerDescriptor): + def __init__(self, keyring_backend): + self._keyring_backend = keyring_backend + + def name(self): + return self._keyring_backend.name + + def description(self): + return 'Backend provided by python-keyring' + + def create(self, cp): + qualified_backend_name = qualified_name(self._keyring_backend) + return KeyringCredentialsManager(cp, qualified_backend_name) + + +class GnomeKeyringCredentialsManager(AbstractCredentialsManager): + @classmethod + def create(cls, cp, options): + if gnomekeyring is None: + return None + return super(cls, cls).create(cp, options) + + def get_password(self, url, user, defer=True): + gk_data = self._keyring_data(url, user) + if gk_data is None: + return None + return gk_data['password'] + + def set_password(self, url, user, password): + scheme, host, path = self._urlsplit(url) + gnomekeyring.set_network_password_sync( + user=user, + password=password, + protocol=scheme, + server=host, + object=path) + self._cp.set(url, self.config_entry, self._qualified_name()) + + def delete_password(self, url, user): + gk_data = self._keyring_data(url, user) + if gk_data is None: + return + gnomekeyring.item_delete_sync(gk_data['keyring'], gk_data['item_id']) + + def get_user(self, url): + gk_data = self._keyring_data(url, None) + if gk_data is None: + return None + return gk_data['user'] + + def _keyring_data(self, url, user): + scheme, host, path = self._urlsplit(url) + try: + entries = gnomekeyring.find_network_password_sync(protocol=scheme, + server=host, + object=path) + except gnomekeyring.NoMatchError: + return None + + for entry in entries: + if 'user' not in entry or 'password' not in entry: + continue + if user is None or entry['user'] == user: + return entry + return None + + def _urlsplit(self, url): + splitted_url = urlsplit(url) + return splitted_url.scheme, splitted_url.netloc, splitted_url.path + + +class GnomeKeyringCredentialsDescriptor(AbstractCredentialsManagerDescriptor): + def name(self): + return 'GNOME Keyring Manager (deprecated)' + + def description(self): + return 'Deprecated GNOME Keyring Manager. If you use \ + this we will send you a Dial-In modem' + + def create(self, cp): + return GnomeKeyringCredentialsManager(cp, None) + + +def get_credentials_manager_descriptors(): + if has_keyring_support(): + backend_list = keyring.backend.get_all_keyring() + else: + backend_list = [] + descriptors = [] + for backend in backend_list: + descriptors.append(KeyringCredentialsDescriptor(backend)) + descriptors.sort() + if gnomekeyring: + descriptors.append(GnomeKeyringCredentialsDescriptor()) + descriptors.append(PlaintextConfigFileDescriptor()) + descriptors.append(ObfuscatedConfigFileDescriptor()) + descriptors.append(TransientDescriptor()) + return descriptors + + +def get_keyring_credentials_manager(cp): + keyring_backend = keyring.get_keyring() + return KeyringCredentialsManager(cp, qualified_name(keyring_backend)) + + +def create_credentials_manager(url, cp): + config_entry = cp.get(url, AbstractCredentialsManager.config_entry) + if ':' in config_entry: + creds_mgr_cls, options = config_entry.split(':', 1) + else: + creds_mgr_cls = config_entry + options = None + mod, cls = creds_mgr_cls.rsplit('.', 1) + return getattr(importlib.import_module(mod), cls).create(cp, options) + + +def qualified_name(obj): + return obj.__module__ + '.' + obj.__class__.__name__ + + +def has_keyring_support(): + return keyring is not None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/oscerr.py new/osc-0.166.0/osc/oscerr.py --- old/osc-0.165.4/osc/oscerr.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/oscerr.py 2019-10-24 11:48:35.000000000 +0200 @@ -28,6 +28,11 @@ ConfigError.__init__(self, msg, fname) self.url = url +class ConfigMissingCredentialsError(ConfigError): + def __init__(self, msg, fname, url): + ConfigError.__init__(self, msg, fname) + self.url = url + class APIError(OscBaseError): """Exception raised when there is an error in the output from the API""" def __init__(self, msg): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/osc/util/helper.py new/osc-0.166.0/osc/util/helper.py --- old/osc-0.165.4/osc/util/helper.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/osc/util/helper.py 2019-10-24 11:48:35.000000000 +0200 @@ -66,4 +66,19 @@ return obj.decode(locale.getlocale()[1]) except: return obj.decode('latin-1') - + + +def raw_input(*args): + try: + import builtins + func = builtins.input + except ImportError: + #python 2.7 + import __builtin__ + func = __builtin__.raw_input + + try: + return func(*args) + except EOFError: + # interpret ctrl-d as user abort + raise oscerr.UserAbort() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/tests/suite.py new/osc-0.166.0/tests/suite.py --- old/osc-0.165.4/tests/suite.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/tests/suite.py 2019-10-24 11:48:35.000000000 +0200 @@ -22,7 +22,6 @@ import test_request import test_setlinkrev import test_prdiff -import test_conf import test_results import test_helpers @@ -41,7 +40,6 @@ suite.addTests(test_request.suite()) suite.addTests(test_setlinkrev.suite()) suite.addTests(test_prdiff.suite()) -suite.addTests(test_conf.suite()) suite.addTests(test_results.suite()) suite.addTests(test_helpers.suite()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/tests/test_conf.py new/osc-0.166.0/tests/test_conf.py --- old/osc-0.165.4/tests/test_conf.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/tests/test_conf.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,32 +0,0 @@ -from osc.conf import passx_encode, passx_decode -from common import OscTestCase - -import os - -FIXTURES_DIR = os.path.join(os.getcwd(), 'conf_fixtures') - -def suite(): - import unittest - return unittest.makeSuite(TestConf) - -class TestConf(OscTestCase): - def _get_fixtures_dir(self): - return FIXTURES_DIR - - def setUp(self): - return super(TestConf, self).setUp(copytree=False) - - def testPassxEncodeDecode(self): - - passwd = "J0e'sPassword!@#" - passx = passx_encode(passwd) - #base64.b64encode(passwd.encode('bz2')) - passx27 = "QlpoOTFBWSZTWaDg4dQAAAKfgCiAQABAEEAAJgCYgCAAMQAACEyYmTyei67AsYSDSaLuSKcKEhQcHDqA" - - self.assertEqual(passwd, passx_decode(passx)) - self.assertEqual(passwd, passx_decode(passx27)) - self.assertEqual(passx, passx27) - -if __name__ == '__main__': - import unittest - unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-0.165.4/tests/test_update.py new/osc-0.166.0/tests/test_update.py --- old/osc-0.165.4/tests/test_update.py 2019-08-05 08:45:32.000000000 +0200 +++ new/osc-0.166.0/tests/test_update.py 2019-10-24 11:48:35.000000000 +0200 @@ -222,7 +222,7 @@ self._check_digests('testUpdateServiceFilesAddDelete_files', '_service:foo', '_service:bar') @GET('http://localhost/source/osctest/metamode?meta=1&rev=latest', file='testUpdateMetaMode_filesremote') - @GET('http://localhost/source/osctest/metamode/_meta?rev=1', file='testUpdateMetaMode__meta') + @GET('http://localhost/source/osctest/metamode/_meta?meta=1&rev=1', file='testUpdateMetaMode__meta') def testUpdateMetaMode(self): """update package with metamode enabled""" self._change_to_pkg('metamode') ++++++ osc.dsc ++++++ --- /var/tmp/diff_new_pack.ClEM4F/_old 2019-10-28 17:00:23.209759338 +0100 +++ /var/tmp/diff_new_pack.ClEM4F/_new 2019-10-28 17:00:23.209759338 +0100 @@ -1,6 +1,6 @@ Format: 1.0 Source: osc -Version: 0.165.4 +Version: 0.166.0 Binary: osc Maintainer: Adrian Schroeter <[email protected]> Architecture: any
