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


Reply via email to