fixed url of the plugin
[opensuse:osc.git] / osc / commandline.py
1 # Copyright (C) 2006 Novell Inc.  All rights reserved.
2 # This program is free software; it may be used, copied, modified
3 # and distributed under the terms of the GNU General Public Licence,
4 # either version 2, or version 3 (at your option).
5
6
7 from core import *
8 import cmdln
9 import conf
10 import oscerr
11 import sys
12 from util import safewriter
13 from optparse import SUPPRESS_HELP
14
15 MAN_HEADER = r""".TH %(ucname)s "1" "%(date)s" "%(name)s %(version)s" "User Commands"
16 .SH NAME
17 %(name)s \- openSUSE build service command-line tool.
18 .SH SYNOPSIS
19 .B %(name)s
20 [\fIGLOBALOPTS\fR] \fISUBCOMMAND \fR[\fIOPTS\fR] [\fIARGS\fR...]
21 .br
22 .B %(name)s
23 \fIhelp SUBCOMMAND\fR
24 .SH DESCRIPTION
25 openSUSE build service command-line tool.
26 """
27 MAN_FOOTER = r"""
28 .SH "SEE ALSO"
29 Type 'osc help <subcommand>' for more detailed help on a specific subcommand.
30 .PP
31 For additional information, see
32  * http://en.opensuse.org/openSUSE:Build_Service_Tutorial
33  * http://en.opensuse.org/openSUSE:OSC
34 .PP
35 You can modify osc commands, or roll you own, via the plugin API:
36  * http://en.opensuse.org/openSUSE:OSC_plugins
37 .SH AUTHOR
38 osc was written by several authors. This man page is automatically generated.
39 """
40
41 class Osc(cmdln.Cmdln):
42     """Usage: osc [GLOBALOPTS] SUBCOMMAND [OPTS] [ARGS...]
43     or: osc help SUBCOMMAND
44
45     openSUSE build service command-line tool.
46     Type 'osc help <subcommand>' for help on a specific subcommand.
47
48     ${command_list}
49     ${help_list}
50     global ${option_list}
51     For additional information, see
52     * http://en.opensuse.org/openSUSE:Build_Service_Tutorial
53     * http://en.opensuse.org/openSUSE:OSC
54
55     You can modify osc commands, or roll you own, via the plugin API:
56     * http://en.opensuse.org/openSUSE:OSC_plugins
57     """
58     name = 'osc'
59     conf = None
60
61     man_header = MAN_HEADER
62     man_footer = MAN_FOOTER
63
64     def __init__(self, *args, **kwargs):
65         cmdln.Cmdln.__init__(self, *args, **kwargs)
66         cmdln.Cmdln.do_help.aliases.append('h')
67         sys.stderr = safewriter.SafeWriter(sys.stderr)
68         sys.stdout = safewriter.SafeWriter(sys.stdout)
69
70     def get_version(self):
71         return get_osc_version()
72
73     def get_optparser(self):
74         """this is the parser for "global" options (not specific to subcommand)"""
75
76         optparser = cmdln.CmdlnOptionParser(self, version=get_osc_version())
77         optparser.add_option('--debugger', action='store_true',
78                       help='jump into the debugger before executing anything')
79         optparser.add_option('--post-mortem', action='store_true',
80                       help='jump into the debugger in case of errors')
81         optparser.add_option('-t', '--traceback', action='store_true',
82                       help='print call trace in case of errors')
83         optparser.add_option('-H', '--http-debug', action='store_true',
84                       help='debug HTTP traffic (filters some headers)')
85         optparser.add_option('--http-full-debug', action='store_true',
86                       help='debug HTTP traffic (filters no headers)'),
87         optparser.add_option('-d', '--debug', action='store_true',
88                       help='print info useful for debugging')
89         optparser.add_option('-A', '--apiurl', dest='apiurl',
90                       metavar='URL/alias',
91                       help='specify URL to access API server at or an alias')
92         optparser.add_option('-c', '--config', dest='conffile',
93                       metavar='FILE',
94                       help='specify alternate configuration file')
95         optparser.add_option('--no-keyring', action='store_true',
96                       help='disable usage of desktop keyring system')
97         optparser.add_option('--no-gnome-keyring', action='store_true',
98                       help='disable usage of GNOME Keyring')
99         optparser.add_option('-v', '--verbose', dest='verbose', action='count', default=0,
100                       help='increase verbosity')
101         optparser.add_option('-q', '--quiet',   dest='verbose', action='store_const', const=-1,
102                       help='be quiet, not verbose')
103         return optparser
104
105
106     def postoptparse(self, try_again = True):
107         """merge commandline options into the config"""
108         try:
109             conf.get_config(override_conffile = self.options.conffile,
110                             override_apiurl = self.options.apiurl,
111                             override_debug = self.options.debug,
112                             override_http_debug = self.options.http_debug,
113                             override_http_full_debug = self.options.http_full_debug,
114                             override_traceback = self.options.traceback,
115                             override_post_mortem = self.options.post_mortem,
116                             override_no_keyring = self.options.no_keyring,
117                             override_no_gnome_keyring = self.options.no_gnome_keyring,
118                             override_verbose = self.options.verbose)
119         except oscerr.NoConfigfile, e:
120             print >>sys.stderr, e.msg
121             print >>sys.stderr, 'Creating osc configuration file %s ...' % e.file
122             import getpass
123             config = {}
124             config['user'] = raw_input('Username: ')
125             config['pass'] = getpass.getpass()
126             if self.options.no_keyring:
127                 config['use_keyring'] = '0'
128             if self.options.no_gnome_keyring:
129                 config['gnome_keyring'] = '0'
130             if self.options.apiurl:
131                 config['apiurl'] = self.options.apiurl
132
133             conf.write_initial_config(e.file, config)
134             print >>sys.stderr, 'done'
135             if try_again: self.postoptparse(try_again = False)
136         except oscerr.ConfigMissingApiurl, e:
137             print >>sys.stderr, e.msg
138             import getpass
139             user = raw_input('Username: ')
140             passwd = getpass.getpass()
141             conf.add_section(e.file, e.url, user, passwd)
142             if try_again: self.postoptparse(try_again = False)
143
144         self.options.verbose = conf.config['verbose']
145         self.download_progress = None
146         if conf.config.get('show_download_progress', False):
147             from meter import TextMeter
148             self.download_progress = TextMeter(hide_finished=True)
149
150
151     def get_cmd_help(self, cmdname):
152         doc = self._get_cmd_handler(cmdname).__doc__
153         doc = self._help_reindent(doc)
154         doc = self._help_preprocess(doc, cmdname)
155         doc = doc.rstrip() + '\n' # trim down trailing space
156         return self._str(doc)
157
158     def get_api_url(self):
159         localdir = os.getcwd()
160         if (is_package_dir(localdir) or is_project_dir(localdir)) and not self.options.apiurl:
161            return store_read_apiurl(os.curdir)
162         else:
163            return conf.config['apiurl']
164
165     # overridden from class Cmdln() to use config variables in help texts
166     def _help_preprocess(self, help, cmdname):
167         help_msg = cmdln.Cmdln._help_preprocess(self, help, cmdname)
168         return help_msg % conf.config
169
170
171     def do_init(self, subcmd, opts, project, package=None):
172         """${cmd_name}: Initialize a directory as working copy
173
174         Initialize an existing directory to be a working copy of an
175         (already existing) buildservice project/package.
176
177         (This is the same as checking out a package and then copying sources
178         into the directory. It does NOT create a new package. To create a
179         package, use 'osc meta pkg ... ...')
180
181         You wouldn't normally use this command.
182
183         To get a working copy of a package (e.g. for building it or working on
184         it, you would normally use the checkout command. Use "osc help
185         checkout" to get help for it.
186
187         usage:
188             osc init PRJ
189             osc init PRJ PAC
190         ${cmd_option_list}
191         """
192
193         apiurl = self.get_api_url()
194
195         if not package:
196             Project.init_project(apiurl, os.curdir, project, conf.config['do_package_tracking'])
197             print 'Initializing %s (Project: %s)' % (os.curdir, project)
198         else:
199             Package.init_package(apiurl, project, package, os.curdir)
200             store_write_string(os.curdir, '_files', show_files_meta(apiurl, project, package) + '\n')
201             print 'Initializing %s (Project: %s, Package: %s)' % (os.curdir, project, package)
202
203     @cmdln.alias('ls')
204     @cmdln.alias('ll')
205     @cmdln.alias('lL')
206     @cmdln.alias('LL')
207     @cmdln.option('-a', '--arch', metavar='ARCH',
208                         help='specify architecture (only for binaries)')
209     @cmdln.option('-r', '--repo', metavar='REPO',
210                         help='specify repository (only for binaries)')
211     @cmdln.option('-b', '--binaries', action='store_true',
212                         help='list built binaries instead of sources')
213     @cmdln.option('-R', '--revision', metavar='REVISION',
214                         help='specify revision (only for sources)')
215     @cmdln.option('-e', '--expand', action='store_true',
216                         help='expand linked package (only for sources)')
217     @cmdln.option('-u', '--unexpand', action='store_true',
218                         help='always work with unexpanded (source) packages')
219     @cmdln.option('-v', '--verbose', action='store_true',
220                         help='print extra information')
221     @cmdln.option('-l', '--long', action='store_true', dest='verbose',
222                         help='print extra information')
223     @cmdln.option('-D', '--deleted', action='store_true',
224                         help='show only the former deleted projects or packages')
225     def do_list(self, subcmd, opts, *args):
226         """${cmd_name}: List sources or binaries on the server
227
228         Examples for listing sources:
229            ls                          # list all projects (deprecated)
230            ls /                        # list all projects
231            ls PROJECT                  # list packages in a project
232            ls PROJECT PACKAGE          # list source files of package of a project
233            ls PROJECT PACKAGE <file>   # list <file> if this file exists
234            ls -v PROJECT PACKAGE       # verbosely list source files of package
235            ls -l PROJECT PACKAGE       # verbosely list source files of package
236            ll PROJECT PACKAGE          # verbosely list source files of package
237            LL PROJECT PACKAGE          # verbosely list source files of expanded link
238
239         With --verbose, the following fields will be shown for each item:
240            MD5 hash of file
241            Revision number of the last commit
242            Size (in bytes)
243            Date and time of the last commit
244
245         Examples for listing binaries:
246            ls -b PROJECT               # list all binaries of a project
247            ls -b PROJECT -a ARCH       # list ARCH binaries of a project
248            ls -b PROJECT -r REPO       # list binaries in REPO
249            ls -b PROJECT PACKAGE REPO ARCH
250
251         Usage:
252            ${cmd_name} [PROJECT [PACKAGE]]
253            ${cmd_name} -b [PROJECT [PACKAGE [REPO [ARCH]]]]
254         ${cmd_option_list}
255         """
256
257         args = slash_split(args)
258         if subcmd == 'll':
259             opts.verbose = True
260         if subcmd == 'lL' or subcmd == 'LL':
261             opts.verbose = True
262             opts.expand = True
263
264         project = None
265         package = None
266         fname = None
267         if len(args) == 0:
268             # For consistency with *all* other commands
269             # this list what the server has in the current wd.
270             # CAUTION: 'osc ls -b' already works like this.
271             pass
272         if len(args) > 0:
273             project = args[0]
274             if project == '/': project = None
275         if len(args) > 1:
276             package = args[1]
277             if opts.deleted:
278                 raise oscerr.WrongArgs("Too many arguments when listing deleted packages")
279         if len(args) > 2:
280             if opts.deleted:
281                 raise oscerr.WrongArgs("Too many arguments when listing deleted packages")
282             if opts.binaries:
283                 if opts.repo:
284                     if opts.repo != args[2]:
285                         raise oscerr.WrongArgs("conflicting repos specified ('%s' vs '%s')"%(opts.repo, args[2]))
286                 else:
287                     opts.repo = args[2]
288             else:
289                 fname = args[2]
290
291         if len(args) > 3:
292             if not opts.binaries:
293                 raise oscerr.WrongArgs('Too many arguments')
294             if opts.arch:
295                 if opts.arch != args[3]:
296                     raise oscerr.WrongArgs("conflicting archs specified ('%s' vs '%s')"%(opts.arch, args[3]))
297             else:
298                 opts.arch = args[3]
299
300
301         if opts.binaries and opts.expand:
302             raise oscerr.WrongOptions('Sorry, --binaries and --expand are mutual exclusive.')
303
304         apiurl = self.get_api_url() 
305
306         # list binaries
307         if opts.binaries:
308             # ls -b toplevel doesn't make sense, so use info from
309             # current dir if available
310             if len(args) == 0:
311                 cwd = os.getcwd()
312                 if is_project_dir(cwd):
313                     project = store_read_project(cwd)
314                 elif is_package_dir(cwd):
315                     project = store_read_project(cwd)
316                     package = store_read_package(cwd)
317
318             if not project:
319                 raise oscerr.WrongArgs('There are no binaries to list above project level.')
320             if opts.revision:
321                 raise oscerr.WrongOptions('Sorry, the --revision option is not supported for binaries.')
322
323             repos = []
324
325             if opts.repo and opts.arch:
326                 repos.append(Repo(opts.repo, opts.arch))
327             elif opts.repo and not opts.arch:
328                 repos = [repo for repo in get_repos_of_project(apiurl, project) if repo.name == opts.repo]
329             elif opts.arch and not opts.repo:
330                 repos = [repo for repo in get_repos_of_project(apiurl, project) if repo.arch == opts.arch]
331             else:
332                 repos = get_repos_of_project(apiurl, project)
333
334             results = []
335             for repo in repos:
336                 results.append((repo, get_binarylist(apiurl, project, repo.name, repo.arch, package=package, verbose=opts.verbose)))
337
338             for result in results:
339                 indent = ''
340                 if len(results) > 1:
341                     print '%s/%s' % (result[0].name, result[0].arch)
342                     indent = ' '
343
344                 if opts.verbose:
345                     for f in result[1]:
346                         print "%9d %s %-40s" % (f.size, shorttime(f.mtime), f.name)
347                 else:
348                     for f in result[1]:
349                         print indent+f
350
351         # list sources
352         elif not opts.binaries:
353             if not args:
354                 for prj in meta_get_project_list(apiurl, opts.deleted):
355                     print prj
356
357             elif len(args) == 1:
358                 if opts.verbose:
359                     if self.options.verbose:
360                         print >>sys.stderr, 'Sorry, the --verbose option is not implemented for projects.'
361                 if opts.expand:
362                     raise oscerr.WrongOptions('Sorry, the --expand option is not implemented for projects.')
363                 for pkg in meta_get_packagelist(apiurl, project, opts.deleted):
364                     print pkg
365
366             elif len(args) == 2 or len(args) == 3:
367                 link_seen = False
368                 print_not_found = True
369                 rev = opts.revision
370                 for i in [ 1, 2 ]:
371                     l = meta_get_filelist(apiurl,
372                                       project,
373                                       package,
374                                       verbose=opts.verbose,
375                                       expand=opts.expand,
376                                       revision=rev)
377                     link_seen = '_link' in l
378                     if opts.verbose:
379                         out = [ '%s %7s %9d %s %s' % (i.md5, i.rev, i.size, shorttime(i.mtime), i.name) \
380                             for i in l if not fname or fname == i.name ]
381                         if len(out) > 0:
382                             print_not_found = False
383                             print '\n'.join(out)
384                     elif fname:
385                         if fname in l:
386                             print fname
387                             print_not_found = False
388                     else:
389                         print '\n'.join(l)
390                     if opts.expand or opts.unexpand or not link_seen: break
391                     m = show_files_meta(apiurl, project, package)
392                     li = Linkinfo()
393                     li.read(ET.fromstring(''.join(m)).find('linkinfo'))
394                     if li.haserror():
395                         raise oscerr.LinkExpandError(project, package, li.error)
396                     project, package, rev = li.project, li.package, li.rev
397                     if rev:
398                         print '# -> %s %s (%s)' % (project, package, rev)
399                     else:
400                         print '# -> %s %s (latest)' % (project, package)
401                     opts.expand = True
402                 if fname and print_not_found:
403                     print 'file \'%s\' does not exist' % fname
404
405
406     @cmdln.option('-f', '--force', action='store_true',
407                         help='force generation of new patchinfo file')
408     @cmdln.option('-n', '--new', action='store_true',
409                         help='Use new, OBS 2.3 style patchinfo format. Will become default on release of OBS 2.3.')
410     @cmdln.option('--force-update', action='store_true',
411                         help='drops away collected packages from an already built patch and let it collect again')
412     def do_patchinfo(self, subcmd, opts, *args):
413         """${cmd_name}: Generate and edit a patchinfo file.
414
415         A patchinfo file describes the packages for an update and the kind of
416         problem it solves.
417
418         Examples:
419             osc patchinfo
420             osc patchinfo PATCH_NAME
421         ${cmd_option_list}
422         """
423
424         project_dir = localdir = os.getcwd()
425         if is_project_dir(localdir):
426             project = store_read_project(localdir)
427             apiurl = self.get_api_url()
428         else:
429             sys.exit('This command must be called in a checked out project.')
430         patchinfo = None
431         for p in meta_get_packagelist(apiurl, project):
432             if p.startswith("_patchinfo") or p.startswith("patchinfo"):
433                 patchinfo = p
434
435         if opts.force or not patchinfo:
436             print "Creating initial patchinfo..."
437             query='cmd=createpatchinfo'
438             if opts.new:
439                 query+='&new_format=1'
440             if args and args[0]:
441                 query += "&name=" + args[0]
442             url = makeurl(apiurl, ['source', project], query=query)
443             f = http_POST(url)
444             for p in meta_get_packagelist(apiurl, project):
445                 if p.startswith("_patchinfo") or p.startswith("patchinfo"):
446                     patchinfo = p
447
448         # CAUTION:
449         #  Both conf.config['checkout_no_colon'] and conf.config['checkout_rooted'] 
450         #  fool this test:
451         if not os.path.exists(project_dir + "/" + patchinfo):
452             checkout_package(apiurl, project, patchinfo, prj_dir=project_dir)
453
454         filename = project_dir + "/" + patchinfo + "/_patchinfo"
455         run_editor(filename)
456
457     @cmdln.alias('bsdevelproject')
458     @cmdln.option('-r', '--raw', action='store_true',
459                         help='print raw xml snippet')
460     def do_develproject(self, subcmd, opts, *args):
461         """${cmd_name}: print the bsdevelproject of a package
462
463         Examples:
464             osc develproject PRJ PKG
465             osc develproject
466         ${cmd_option_list}
467         """
468         args = slash_split(args)
469         apiurl = self.get_api_url()
470
471         if len(args) == 0:
472             project = store_read_project(os.curdir)
473             package = store_read_package(os.curdir)
474         elif len(args) == 2:
475             project = args[0]
476             package = args[1]
477         else:
478             raise oscerr.WrongArgs('need Project and Package')
479
480         devel = show_develproject(apiurl, project, package, opts.raw)
481         if devel is None:
482             print '\'%s/%s\' has no devel project' % (project, package)
483         elif opts.raw:
484             ET.dump(devel)
485         else:
486             print devel
487
488
489     @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
490                         help='affect only a given attribute')
491     @cmdln.option('--attribute-defaults', action='store_true',
492                         help='include defined attribute defaults')
493     @cmdln.option('--attribute-project', action='store_true',
494                         help='include project values, if missing in packages ')
495     @cmdln.option('-f', '--force', action='store_true',
496                         help='force the save operation, allows to ignores some errors like depending repositories. For prj meta only.')
497     @cmdln.option('-F', '--file', metavar='FILE',
498                         help='read metadata from FILE, instead of opening an editor. '
499                         '\'-\' denotes standard input. ')
500     @cmdln.option('-e', '--edit', action='store_true',
501                         help='edit metadata')
502     @cmdln.option('-c', '--create', action='store_true',
503                         help='create attribute without values')
504     @cmdln.option('-s', '--set', metavar='ATTRIBUTE_VALUES',
505                         help='set attribute values')
506     @cmdln.option('--delete', action='store_true',
507                         help='delete a pattern or attribute')
508     def do_meta(self, subcmd, opts, *args):
509         """${cmd_name}: Show meta information, or edit it
510
511         Show or edit build service metadata of type <prj|pkg|prjconf|user|pattern>.
512
513         This command displays metadata on buildservice objects like projects,
514         packages, or users. The type of metadata is specified by the word after
515         "meta", like e.g. "meta prj".
516
517         prj denotes metadata of a buildservice project.
518         prjconf denotes the (build) configuration of a project.
519         pkg denotes metadata of a buildservice package.
520         user denotes the metadata of a user.
521         pattern denotes installation patterns defined for a project.
522
523         To list patterns, use 'osc meta pattern PRJ'. An additional argument
524         will be the pattern file to view or edit.
525
526         With the --edit switch, the metadata can be edited. Per default, osc
527         opens the program specified by the environmental variable EDITOR with a
528         temporary file. Alternatively, content to be saved can be supplied via
529         the --file switch. If the argument is '-', input is taken from stdin:
530         osc meta prjconf home:user | sed ... | osc meta prjconf home:user -F -
531
532         When trying to edit a non-existing resource, it is created implicitly.
533
534
535         Examples:
536             osc meta prj PRJ
537             osc meta pkg PRJ PKG
538             osc meta pkg PRJ PKG -e
539             osc meta attribute PRJ [PKG [SUBPACKAGE]] [--attribute ATTRIBUTE] [--create|--delete|--set [value_list]]
540
541         Usage:
542             osc meta <prj|pkg|prjconf|user|pattern|attribute> ARGS...
543             osc meta <prj|pkg|prjconf|user|pattern|attribute> -e|--edit ARGS...
544             osc meta <prj|pkg|prjconf|user|pattern|attribute> -F|--file ARGS...
545             osc meta pattern --delete PRJ PATTERN
546         ${cmd_option_list}
547         """
548
549         args = slash_split(args)
550
551         if not args or args[0] not in metatypes.keys():
552             raise oscerr.WrongArgs('Unknown meta type. Choose one of %s.' \
553                                                % ', '.join(metatypes))
554
555         cmd = args[0]
556         del args[0]
557
558         apiurl = self.get_api_url()
559
560         if cmd in ['pkg']:
561             min_args, max_args = 0, 2
562         elif cmd in ['pattern']:
563             min_args, max_args = 1, 2
564         elif cmd in ['attribute']:
565             min_args, max_args = 1, 3
566         elif cmd in ['prj', 'prjconf']:
567             min_args, max_args = 0, 1
568         else:
569             min_args, max_args = 1, 1
570
571         if len(args) < min_args:
572             raise oscerr.WrongArgs('Too few arguments.')
573         if len(args) > max_args:
574             raise oscerr.WrongArgs('Too many arguments.')
575
576         # specific arguments
577         attributepath = []
578         if cmd in ['pkg', 'prj', 'prjconf' ]:
579             if len(args) == 0:
580                 project = store_read_project(os.curdir)
581             else:
582                 project = args[0]
583
584             if cmd == 'pkg':
585                 if len(args) < 2:
586                     package = store_read_package(os.curdir)
587                 else:
588                     package = args[1]
589
590         elif cmd == 'attribute':
591             project = args[0]
592             if len(args) > 1:
593                 package = args[1]
594             else:
595                 package = None
596                 if opts.attribute_project:
597                     raise oscerr.WrongOptions('--attribute-project works only when also a package is given')
598             if len(args) > 2:
599                 subpackage = args[2]
600             else:
601                 subpackage = None
602             attributepath.append('source')
603             attributepath.append(project)
604             if package:
605                 attributepath.append(package)
606             if subpackage:
607                 attributepath.append(subpackage)
608             attributepath.append('_attribute')
609         elif cmd == 'user':
610             user = args[0]
611         elif cmd == 'pattern':
612             project = args[0]
613             if len(args) > 1:
614                 pattern = args[1]
615             else:
616                 pattern = None
617                 # enforce pattern argument if needed
618                 if opts.edit or opts.file:
619                     raise oscerr.WrongArgs('A pattern file argument is required.')
620
621         # show
622         if not opts.edit and not opts.file and not opts.delete and not opts.create and not opts.set:
623             if cmd == 'prj':
624                 sys.stdout.write(''.join(show_project_meta(apiurl, project)))
625             elif cmd == 'pkg':
626                 sys.stdout.write(''.join(show_package_meta(apiurl, project, package)))
627             elif cmd == 'attribute':
628                 sys.stdout.write(''.join(show_attribute_meta(apiurl, project, package, subpackage, opts.attribute, opts.attribute_defaults, opts.attribute_project)))
629             elif cmd == 'prjconf':
630                 sys.stdout.write(''.join(show_project_conf(apiurl, project)))
631             elif cmd == 'user':
632                 r = get_user_meta(apiurl, user)
633                 if r:
634                     sys.stdout.write(''.join(r))
635             elif cmd == 'pattern':
636                 if pattern:
637                     r = show_pattern_meta(apiurl, project, pattern)
638                     if r:
639                         sys.stdout.write(''.join(r))
640                 else:
641                     r = show_pattern_metalist(apiurl, project)
642                     if r:
643                         sys.stdout.write('\n'.join(r) + '\n')
644
645         # edit
646         if opts.edit and not opts.file:
647             if cmd == 'prj':
648                 edit_meta(metatype='prj',
649                           edit=True,
650                           force=opts.force,
651                           path_args=quote_plus(project),
652                           apiurl=apiurl,
653                           template_args=({
654                                   'name': project,
655                                   'user': conf.get_apiurl_usr(apiurl)}))
656             elif cmd == 'pkg':
657                 edit_meta(metatype='pkg',
658                           edit=True,
659                           path_args=(quote_plus(project), quote_plus(package)),
660                           apiurl=apiurl,
661                           template_args=({
662                                   'name': package,
663                                   'user': conf.get_apiurl_usr(apiurl)}))
664             elif cmd == 'prjconf':
665                 edit_meta(metatype='prjconf',
666                           edit=True,
667                           path_args=quote_plus(project),
668                           apiurl=apiurl,
669                           template_args=None)
670             elif cmd == 'user':
671                 edit_meta(metatype='user',
672                           edit=True,
673                           path_args=(quote_plus(user)),
674                           apiurl=apiurl,
675                           template_args=({'user': user}))
676             elif cmd == 'pattern':
677                 edit_meta(metatype='pattern',
678                           edit=True,
679                           path_args=(project, pattern),
680                           apiurl=apiurl,
681                           template_args=None)
682
683         # create attribute entry
684         if (opts.create or opts.set) and cmd == 'attribute':
685             if not opts.attribute:
686                 raise oscerr.WrongOptions('no attribute given to create')
687             values = ''
688             if opts.set:
689                 opts.set = opts.set.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
690                 for i in opts.set.split(','):
691                     values += '<value>%s</value>' % i
692             aname = opts.attribute.split(":")
693             d = '<attributes><attribute namespace=\'%s\' name=\'%s\' >%s</attribute></attributes>' % (aname[0], aname[1], values)
694             url = makeurl(apiurl, attributepath)
695             for data in streamfile(url, http_POST, data=d):
696                 sys.stdout.write(data)
697
698         # upload file
699         if opts.file:
700
701             if opts.file == '-':
702                 f = sys.stdin.read()
703             else:
704                 try:
705                     f = open(opts.file).read()
706                 except:
707                     sys.exit('could not open file \'%s\'.' % opts.file)
708
709             if cmd == 'prj':
710                 edit_meta(metatype='prj',
711                           data=f,
712                           edit=opts.edit,
713                           force=opts.force,
714                           apiurl=apiurl,
715                           path_args=quote_plus(project))
716             elif cmd == 'pkg':
717                 edit_meta(metatype='pkg',
718                           data=f,
719                           edit=opts.edit,
720                           apiurl=apiurl,
721                           path_args=(quote_plus(project), quote_plus(package)))
722             elif cmd == 'prjconf':
723                 edit_meta(metatype='prjconf',
724                           data=f,
725                           edit=opts.edit,
726                           apiurl=apiurl,
727                           path_args=quote_plus(project))
728             elif cmd == 'user':
729                 edit_meta(metatype='user',
730                           data=f,
731                           edit=opts.edit,
732                           apiurl=apiurl,
733                           path_args=(quote_plus(user)))
734             elif cmd == 'pattern':
735                 edit_meta(metatype='pattern',
736                           data=f,
737                           edit=opts.edit,
738                           apiurl=apiurl,
739                           path_args=(project, pattern))
740
741
742         # delete
743         if opts.delete:
744             path = metatypes[cmd]['path']
745             if cmd == 'pattern':
746                 path = path % (project, pattern)
747                 u = makeurl(apiurl, [path])
748                 http_DELETE(u)
749             elif cmd == 'attribute':
750                 if not opts.attribute:
751                     raise oscerr.WrongOptions('no attribute given to create')
752                 attributepath.append(opts.attribute)
753                 u = makeurl(apiurl, attributepath)
754                 for data in streamfile(u, http_DELETE):
755                     sys.stdout.write(data)
756             else:
757                 raise oscerr.WrongOptions('The --delete switch is only for pattern metadata or attributes.')
758
759
760     # TODO: rewrite and consolidate the current submitrequest/createrequest "mess"
761
762     @cmdln.option('-m', '--message', metavar='TEXT',
763                   help='specify message TEXT')
764     @cmdln.option('-r', '--revision', metavar='REV',
765                   help='specify a certain source revision ID (the md5 sum) for the source package')
766     @cmdln.option('-s', '--supersede', metavar='SUPERSEDE',
767                   help='Superseding another request by this one')
768     @cmdln.option('--nodevelproject', action='store_true',
769                   help='do not follow a defined devel project ' \
770                        '(primary project where a package is developed)')
771     @cmdln.option('--cleanup', action='store_true',
772                   help='remove package if submission gets accepted (default for home:<id>:branch projects)')
773     @cmdln.option('--no-cleanup', action='store_true',
774                   help='never remove source package on accept, but update its content')
775     @cmdln.option('--no-update', action='store_true',
776                   help='never touch source package on accept (will break source links)')
777     @cmdln.option('-d', '--diff', action='store_true',
778                   help='show diff only instead of creating the actual request')
779     @cmdln.option('--yes', action='store_true',
780                   help='proceed without asking.')
781     @cmdln.alias("sr")
782     @cmdln.alias("submitreq")
783     @cmdln.alias("submitpac")
784     def do_submitrequest(self, subcmd, opts, *args):
785         """${cmd_name}: Create request to submit source into another Project
786
787         [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration for information
788         on this topic.]
789
790         See the "request" command for showing and modifing existing requests.
791
792         usage:
793             osc submitreq [OPTIONS]
794             osc submitreq [OPTIONS] DESTPRJ [DESTPKG]
795             osc submitreq [OPTIONS] SOURCEPRJ SOURCEPKG DESTPRJ [DESTPKG]
796
797             osc submitpac ... is a shorthand for osc submitreq --cleanup ...
798
799         ${cmd_option_list}
800         """
801
802         if opts.cleanup and opts.no_cleanup:
803             raise oscerr.WrongOptions('\'--cleanup\' and \'--no-cleanup\' are mutually exclusive')
804
805         src_update = conf.config['submitrequest_on_accept_action'] or None
806         # we should check here for home:<id>:branch and default to update, but that would require OBS 1.7 server
807
808         if subcmd == 'submitpac' and not opts.no_cleanup:
809             opts.cleanup = True;
810
811         if opts.cleanup:
812             src_update = "cleanup"
813         elif opts.no_cleanup:
814             src_update = "update"
815         elif opts.no_update:
816             src_update = "noupdate"
817
818         args = slash_split(args)
819
820         # remove this block later again
821         oldcmds = ['create', 'list', 'log', 'show', 'decline', 'accept', 'delete', 'revoke']
822         if args and args[0] in oldcmds:
823             print >>sys.stderr, "************************************************************************"
824             print >>sys.stderr, "* WARNING: It looks that you are using this command with a             *"
825             print >>sys.stderr, "*          deprecated syntax.                                          *"
826             print >>sys.stderr, "*          Please run \"osc sr --help\" and \"osc rq --help\"              *"
827             print >>sys.stderr, "*          to see the new syntax.                                      *"
828             print >>sys.stderr, "************************************************************************"
829             if args[0] == 'create':
830                 args.pop(0)
831             else:
832                 sys.exit(1)
833
834         if len(args) > 4:
835             raise oscerr.WrongArgs('Too many arguments.')
836
837         if len(args) > 0 and len(args) <= 2 and is_project_dir(os.getcwd()):
838             sys.exit('osc submitrequest from project directory is only working without target specs and for source linked files\n')
839
840         apiurl = self.get_api_url()
841
842         if len(args) == 0 and is_project_dir(os.getcwd()):
843             import cgi
844             # submit requests for multiple packages are currently handled via multiple requests
845             # They could be also one request with multiple actions, but that avoids to accepts parts of it.
846             project = store_read_project(os.curdir)
847
848             sr_ids = []
849             pi = []
850             pac = []
851             targetprojects = []
852             # loop via all packages for checking their state
853             for p in meta_get_packagelist(apiurl, project):
854                 if p.startswith("_patchinfo:") or p.startswith("patchinfo"):
855                     pi.append(p)
856                 else:
857                     # get _link info from server, that knows about the local state ...
858                     u = makeurl(apiurl, ['source', project, p])
859                     f = http_GET(u)
860                     root = ET.parse(f).getroot()
861                     linkinfo = root.find('linkinfo')
862                     if linkinfo == None:
863                         print "Package ", p, " is not a source link."
864                         sys.exit("This is currently not supported.")
865                     if linkinfo.get('error'):
866                         print "Package ", p, " is a broken source link."
867                         sys.exit("Please fix this first")
868                     t = linkinfo.get('project')
869                     if t:
870                         if len(root.findall('entry')) > 1: # This is not really correct, but should work mostly
871                                                            # Real fix is to ask the api if sources are modificated
872                                                            # but there is no such call yet.
873                             targetprojects.append(t)
874                             pac.append(p)
875                             print "Submitting package ", p
876                         else:
877                             print "  Skipping package ", p
878                     else:
879                         print "Skipping package ", p,  " since it is a source link pointing inside the project."
880
881             # was this project created by clone request ?
882             u = makeurl(apiurl, ['source', project, '_attribute', 'OBS:RequestCloned'])
883             f = http_GET(u)
884             root = ET.parse(f).getroot()
885             value = root.findtext('attribute/value')
886             myreqs = {}
887             if value:
888                 myreqs = [ value ]
889
890             if not opts.yes:
891                 if pi:
892                     print "Submitting patchinfo ", ', '.join(pi), " to ", ', '.join(targetprojects)
893                 repl = raw_input('\nEverything fine? Can we create the requests ? (y/n) ')
894                 if repl.lower() != 'y':
895                     print >>sys.stderr, 'Aborted...'
896                     raise oscerr.UserAbort()
897
898
899             # loop via all packages to do the action
900             for p in pac:
901                 result = create_submit_request(apiurl, project, p)
902                 if not result:
903 #                    sys.exit(result)
904                     sys.exit("submit request creation failed")
905                 sr_ids.append(result)
906
907             # create submit requests for all found patchinfos
908             actionxml=""
909             options_block=""
910             if src_update:
911                 options_block="""<options><sourceupdate>%s</sourceupdate></options> """ % (src_update)
912
913             for p in pi:
914                 for t in targetprojects:
915                     s = """<action type="submit"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> %s </action>"""  % \
916                            (project, p, t, p, options_block)
917                     actionxml += s
918
919             if actionxml != "":
920                 xml = """<request> %s <state name="new"/> <description>%s</description> </request> """ % \
921                       (actionxml, cgi.escape(opts.message or ""))
922                 u = makeurl(apiurl, ['request'], query='cmd=create')
923                 f = http_POST(u, data=xml)
924
925                 root = ET.parse(f).getroot()
926                 sr_ids.append(root.get('id'))
927
928             print "Requests created: ",
929             for i in sr_ids:
930                 print i,
931
932             repl = ''
933             if len(myreqs) > 0:
934                 print '\n\nThere are already following submit request: %s.' % \
935                       ', '.join([str(i) for i in myreqs ])
936                 repl = raw_input('\nSupersede the old requests? (y/n) ')
937                 if repl.lower() == 'y':
938                     for req in myreqs:
939                         change_request_state(apiurl, str(req), 'superseded',
940                                              'superseded by %s' % result, result)
941
942             sys.exit('Successfully finished')
943
944         elif len(args) <= 2:
945             # try using the working copy at hand
946             p = findpacs(os.curdir)[0]
947             src_project = p.prjname
948             src_package = p.name
949             apiurl = p.apiurl
950             if len(args) == 0 and p.islink():
951                 dst_project = p.linkinfo.project
952                 dst_package = p.linkinfo.package
953             elif len(args) > 0:
954                 dst_project = args[0]
955                 if len(args) == 2:
956                     dst_package = args[1]
957                 else:
958                     dst_package = src_package
959             else:
960                 sys.exit('Package \'%s\' is not a source link, so I cannot guess the submit target.\n'
961                          'Please provide it the target via commandline arguments.' % p.name)
962
963             modified = [i for i in p.filenamelist if not p.status(i) in (' ', '?', 'S')]
964             if len(modified) > 0:
965                 print 'Your working copy has local modifications.'
966                 repl = raw_input('Proceed without committing the local changes? (y|N) ')
967                 if repl != 'y':
968                     raise oscerr.UserAbort()
969         elif len(args) >= 3:
970             # get the arguments from the commandline
971             src_project, src_package, dst_project = args[0:3]
972             if len(args) == 4:
973                 dst_package = args[3]
974             else:
975                 dst_package = src_package
976         else:
977             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
978                   + self.get_cmd_help('request'))
979
980         if not opts.nodevelproject:
981             devloc = None
982             try:
983                 devloc = show_develproject(apiurl, dst_project, dst_package)
984             except urllib2.HTTPError:
985                 print >>sys.stderr, """\
986 Warning: failed to fetch meta data for '%s' package '%s' (new package?) """ \
987                     % (dst_project, dst_package)
988                 pass
989
990             if devloc and \
991                dst_project != devloc and \
992                src_project != devloc:
993                 print """\
994 A different project, %s, is defined as the place where development
995 of the package %s primarily takes place.
996 Please submit there instead, or use --nodevelproject to force direct submission.""" \
997                 % (devloc, dst_package)
998                 if not opts.diff:
999                     sys.exit(1)
1000
1001         rdiff = None
1002         if opts.diff or not opts.message:
1003             try:
1004                 rdiff = 'old: %s/%s\nnew: %s/%s' %(dst_project, dst_package, src_project, src_package)
1005                 rdiff += server_diff(apiurl,
1006                                     dst_project, dst_package, opts.revision,
1007                                     src_project, src_package, None, True)
1008             except:
1009                 rdiff = ''
1010
1011         if opts.diff:
1012             run_pager(rdiff)
1013             return
1014
1015         # Are there already requests to this package ?
1016         reqs = get_request_list(apiurl, dst_project, dst_package, req_type='submit', req_state=['new','review'])
1017         user = conf.get_apiurl_usr(apiurl)
1018         myreqs = [ i for i in reqs if i.state.who == user ]
1019         repl = ''
1020
1021         if len(myreqs) > 0:
1022             print 'There are already following submit request: %s.' % \
1023                   ', '.join([i.reqid for i in myreqs ])
1024             repl = raw_input('Supersede the old requests? (y/n/c) ')
1025             if repl.lower() == 'c':
1026                 print >>sys.stderr, 'Aborting'
1027                 raise oscerr.UserAbort()
1028
1029         if not opts.message:
1030             difflines = []
1031             doappend = False
1032             changes_re = re.compile(r'^--- .*\.changes ')
1033             for line in rdiff.split('\n'):
1034                 if line.startswith('--- '):
1035                     if changes_re.match(line):
1036                         doappend = True
1037                     else:
1038                         doappend = False
1039                 if doappend:
1040                     difflines.append(line)
1041             opts.message = edit_message(footer=rdiff, template='\n'.join(parse_diff_for_commit_message('\n'.join(difflines))))
1042
1043         result = create_submit_request(apiurl,
1044                                        src_project, src_package,
1045                                        dst_project, dst_package,
1046                                        opts.message, orev=opts.revision, src_update=src_update)
1047         if repl.lower() == 'y':
1048             for req in myreqs:
1049                 change_request_state(apiurl, req.reqid, 'superseded',
1050                                      'superseded by %s' % result, result)
1051
1052         if opts.supersede:
1053             change_request_state(apiurl, opts.supersede, 'superseded',
1054                                  opts.message or '', result)
1055
1056         print 'created request id', result
1057
1058     def _actionparser(self, opt_str, value, parser):
1059         value = []
1060         if not hasattr(parser.values, 'actiondata'):
1061             setattr(parser.values, 'actiondata', [])
1062         if parser.values.actions == None:
1063             parser.values.actions = []
1064
1065         rargs = parser.rargs
1066         while rargs:
1067             arg = rargs[0]
1068             if ((arg[:2] == "--" and len(arg) > 2) or
1069                     (arg[:1] == "-" and len(arg) > 1 and arg[1] != "-")):
1070                 break
1071             else:
1072                 value.append(arg)
1073                 del rargs[0]
1074
1075         parser.values.actions.append(value[0])
1076         del value[0]
1077         parser.values.actiondata.append(value)
1078
1079     def _submit_request(self, args, opts, options_block):
1080         actionxml=""
1081         apiurl = self.get_api_url()
1082         if len(args) == 0 and is_project_dir(os.getcwd()):
1083             # submit requests for multiple packages are currently handled via multiple requests
1084             # They could be also one request with multiple actions, but that avoids to accepts parts of it.
1085             project = store_read_project(os.curdir)
1086
1087             pi = []
1088             pac = []
1089             targetprojects = []
1090             rdiffmsg = []
1091             # loop via all packages for checking their state
1092             for p in meta_get_packagelist(apiurl, project):
1093                 if p.startswith("_patchinfo:"):
1094                     pi.append(p)
1095                 else:
1096                     # get _link info from server, that knows about the local state ...
1097                     u = makeurl(apiurl, ['source', project, p])
1098                     f = http_GET(u)
1099                     root = ET.parse(f).getroot()
1100                     linkinfo = root.find('linkinfo')
1101                     if linkinfo == None:
1102                         print "Package ", p, " is not a source link."
1103                         sys.exit("This is currently not supported.")
1104                     if linkinfo.get('error'):
1105                         print "Package ", p, " is a broken source link."
1106                         sys.exit("Please fix this first")
1107                     t = linkinfo.get('project')
1108                     if t:
1109                         rdiff = ''
1110                         try:
1111                             rdiff = server_diff(apiurl, t, p, opts.revision, project, p, None, True)
1112                         except:
1113                             rdiff = ''
1114
1115                         if rdiff != '':
1116                             targetprojects.append(t)
1117                             pac.append(p)
1118                             rdiffmsg.append("old: %s/%s\nnew: %s/%s\n%s" %(t, p, project, p,rdiff))
1119                         else:
1120                             print "Skipping package ", p,  " since it has no difference with the target package."
1121                     else:
1122                         print "Skipping package ", p,  " since it is a source link pointing inside the project."
1123             if opts.diff:
1124                 print ''.join(rdiffmsg)
1125                 sys.exit(0)
1126
1127                 if not opts.yes:
1128                     if pi:
1129                         print "Submitting patchinfo ", ', '.join(pi), " to ", ', '.join(targetprojects)
1130                     print "\nEverything fine? Can we create the requests ? [y/n]"
1131                     if sys.stdin.read(1) != "y":
1132                         sys.exit("Aborted...")
1133
1134             # loop via all packages to do the action
1135             for p in pac:
1136                 s = """<action type="submit"> <source project="%s" package="%s"  rev="%s"/> <target project="%s" package="%s"/> %s </action>"""  % \
1137                        (project, p, opts.revision or show_upstream_rev(apiurl, project, p), t, p, options_block)
1138                 actionxml += s
1139
1140             # create submit requests for all found patchinfos
1141             for p in pi:
1142                 for t in targetprojects:
1143                     s = """<action type="submit"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> %s </action>"""  % \
1144                            (project, p, t, p, options_block)
1145                     actionxml += s
1146
1147             return actionxml
1148
1149         elif len(args) <= 2:
1150             # try using the working copy at hand
1151             p = findpacs(os.curdir)[0]
1152             src_project = p.prjname
1153             src_package = p.name
1154             if len(args) == 0 and p.islink():
1155                 dst_project = p.linkinfo.project
1156                 dst_package = p.linkinfo.package
1157             elif len(args) > 0:
1158                 dst_project = args[0]
1159                 if len(args) == 2:
1160                     dst_package = args[1]
1161                 else:
1162                     dst_package = src_package
1163             else:
1164                 sys.exit('Package \'%s\' is not a source link, so I cannot guess the submit target.\n'
1165                          'Please provide it the target via commandline arguments.' % p.name)
1166
1167             modified = [i for i in p.filenamelist if p.status(i) != ' ' and p.status(i) != '?']
1168             if len(modified) > 0:
1169                 print 'Your working copy has local modifications.'
1170                 repl = raw_input('Proceed without committing the local changes? (y|N) ')
1171                 if repl != 'y':
1172                     sys.exit(1)
1173         elif len(args) >= 3:
1174             # get the arguments from the commandline
1175             src_project, src_package, dst_project = args[0:3]
1176             if len(args) == 4:
1177                 dst_package = args[3]
1178             else:
1179                 dst_package = src_package
1180         else:
1181             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
1182                   + self.get_cmd_help('request'))
1183
1184         if not opts.nodevelproject:
1185             devloc = None
1186             try:
1187                 devloc = show_develproject(apiurl, dst_project, dst_package)
1188             except urllib2.HTTPError:
1189                 print >>sys.stderr, """\
1190 Warning: failed to fetch meta data for '%s' package '%s' (new package?) """ \
1191                     % (dst_project, dst_package)
1192                 pass
1193
1194             if devloc and \
1195                dst_project != devloc and \
1196                src_project != devloc:
1197                 print """\
1198 A different project, %s, is defined as the place where development
1199 of the package %s primarily takes place.
1200 Please submit there instead, or use --nodevelproject to force direct submission.""" \
1201                 % (devloc, dst_package)
1202                 if not opts.diff:
1203                     sys.exit(1)
1204
1205         rdiff = None
1206         if opts.diff:
1207             try:
1208                 rdiff = 'old: %s/%s\nnew: %s/%s' %(dst_project, dst_package, src_project, src_package)
1209                 rdiff += server_diff(apiurl,
1210                                     dst_project, dst_package, opts.revision,
1211                                     src_project, src_package, None, True)
1212             except:
1213                 rdiff = ''
1214         if opts.diff:
1215             run_pager(rdiff)
1216         else:
1217             reqs = get_request_list(apiurl, dst_project, dst_package, req_type='submit', req_state=['new','review'])
1218             user = conf.get_apiurl_usr(apiurl)
1219             myreqs = [ i for i in reqs if i.state.who == user ]
1220             repl = ''
1221             if len(myreqs) > 0:
1222                 print 'You already created the following submit request: %s.' % \
1223                       ', '.join([i.reqid for i in myreqs ])
1224                 repl = raw_input('Supersede the old requests? (y/n/c) ')
1225                 if repl.lower() == 'c':
1226                     print >>sys.stderr, 'Aborting'
1227                     sys.exit(1)
1228
1229             actionxml = """<action type="submit"> <source project="%s" package="%s"  rev="%s"/> <target project="%s" package="%s"/> %s </action>"""  % \
1230                     (src_project, src_package, opts.revision or show_upstream_rev(apiurl, src_project, src_package), dst_project, dst_package, options_block)
1231             if repl.lower() == 'y':
1232                 for req in myreqs:
1233                     change_request_state(apiurl, req.reqid, 'superseded',
1234                                          'superseded by %s' % result, result)
1235
1236             if opts.supersede:
1237                 change_request_state(apiurl, opts.supersede, 'superseded',  '', result)
1238
1239             #print 'created request id', result
1240             return actionxml
1241
1242     def _delete_request(self, args, opts):
1243         if len(args) < 1:
1244             raise oscerr.WrongArgs('Please specify at least a project.')
1245         if len(args) > 2:
1246             raise oscerr.WrongArgs('Too many arguments.')
1247
1248         package = ""
1249         if len(args) > 1:
1250             package = """package="%s" """ % (args[1])
1251         actionxml = """<action type="delete"> <target project="%s" %s/> </action> """ % (args[0], package)
1252         return actionxml
1253
1254     def _changedevel_request(self, args, opts):
1255         if len(args) > 4:
1256             raise oscerr.WrongArgs('Too many arguments.')
1257
1258         if len(args) == 0 and is_package_dir('.') and find_default_project():
1259             wd = os.curdir
1260             devel_project = store_read_project(wd)
1261             devel_package = package = store_read_package(wd)
1262             project = find_default_project(self.get_api_url(), package)
1263         else:
1264             if len(args) < 3:
1265                 raise oscerr.WrongArgs('Too few arguments.')
1266
1267             devel_project = args[2]
1268             project = args[0]
1269             package = args[1]
1270             devel_package = package
1271             if len(args) > 3:
1272                 devel_package = args[3]
1273
1274         actionxml = """ <action type="change_devel"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> </action> """ % \
1275                 (devel_project, devel_package, project, package)
1276
1277         return actionxml
1278
1279     def _add_me(self, args, opts):
1280         if len(args) > 3:
1281             raise oscerr.WrongArgs('Too many arguments.')
1282         if len(args) < 2:
1283             raise oscerr.WrongArgs('Too few arguments.')
1284
1285         apiurl = self.get_api_url()
1286
1287         user = conf.get_apiurl_usr(apiurl)
1288         role = args[0]
1289         project = args[1]
1290         actionxml = """ <action type="add_role"> <target project="%s" /> <person name="%s" role="%s" /> </action> """ % \
1291                 (project, user, role)
1292
1293         if len(args) > 2:
1294             package = args[2]
1295             actionxml = """ <action type="add_role"> <target project="%s" package="%s" /> <person name="%s" role="%s" /> </action> """ % \
1296                 (project, package, user, role)
1297
1298         if get_user_meta(apiurl, user) == None:
1299             raise oscerr.WrongArgs('osc: an error occured.')
1300
1301         return actionxml
1302
1303     def _add_user(self, args, opts):
1304         if len(args) > 4:
1305             raise oscerr.WrongArgs('Too many arguments.')
1306         if len(args) < 3:
1307             raise oscerr.WrongArgs('Too few arguments.')
1308
1309         apiurl = self.get_api_url()
1310
1311         user = args[0]
1312         role = args[1]
1313         project = args[2]
1314         actionxml = """ <action type="add_role"> <target project="%s" /> <person name="%s" role="%s" /> </action> """ % \
1315                 (project, user, role)
1316
1317         if len(args) > 3:
1318             package = args[3]
1319             actionxml = """ <action type="add_role"> <target project="%s" package="%s" /> <person name="%s" role="%s" /> </action> """ % \
1320                 (project, package, user, role)
1321
1322         if get_user_meta(apiurl, user) == None:
1323             raise oscerr.WrongArgs('osc: an error occured.')
1324
1325         return actionxml
1326
1327     def _add_group(self, args, opts):
1328         if len(args) > 4:
1329             raise oscerr.WrongArgs('Too many arguments.')
1330         if len(args) < 3:
1331             raise oscerr.WrongArgs('Too few arguments.')
1332
1333         apiurl = self.get_api_url()
1334
1335         group = args[0]
1336         role = args[1]
1337         project = args[2]
1338         actionxml = """ <action type="add_role"> <target project="%s" /> <group name="%s" role="%s" /> </action> """ % \
1339                 (project, group, role)
1340
1341         if len(args) > 3:
1342             package = args[3]
1343             actionxml = """ <action type="add_role"> <target project="%s" package="%s" /> <group name="%s" role="%s" /> </action> """ % \
1344                 (project, package, group, role)
1345
1346         if get_group(apiurl, group) == None:
1347             raise oscerr.WrongArgs('osc: an error occured.')
1348
1349         return actionxml
1350
1351     def _set_bugowner(self, args, opts):
1352         if len(args) > 3:
1353             raise oscerr.WrongArgs('Too many arguments.')
1354         if len(args) < 2:
1355             raise oscerr.WrongArgs('Too few arguments.')
1356
1357         apiurl = self.get_api_url()
1358
1359         user = args[0]
1360         project = args[1]
1361         if len(args) > 2:
1362             package = args[2]
1363
1364         if get_user_meta(apiurl, user) == None:
1365             raise oscerr.WrongArgs('osc: an error occured.')
1366
1367         actionxml = """ <action type="set_bugowner"> <target project="%s" package="%s" /> <person name="%s" /> </action> """ % \
1368                 (project, package, user)
1369
1370         return actionxml
1371
1372     @cmdln.option('-a', '--action', action='callback', callback = _actionparser,dest = 'actions',
1373                   help='specify action type of a request, can be : submit/delete/change_devel/add_role/set_bugowner')
1374     @cmdln.option('-m', '--message', metavar='TEXT',
1375                   help='specify message TEXT')
1376     @cmdln.option('-r', '--revision', metavar='REV',
1377                   help='for "create", specify a certain source revision ID (the md5 sum)')
1378     @cmdln.option('-s', '--supersede', metavar='SUPERSEDE',
1379                   help='Superseding another request by this one')
1380     @cmdln.option('--nodevelproject', action='store_true',
1381                   help='do not follow a defined devel project ' \
1382                        '(primary project where a package is developed)')
1383     @cmdln.option('--cleanup', action='store_true',
1384                   help='remove package if submission gets accepted (default for home:<id>:branch projects)')
1385     @cmdln.option('--no-cleanup', action='store_true',
1386                   help='never remove source package on accept, but update its content')
1387     @cmdln.option('--no-update', action='store_true',
1388                   help='never touch source package on accept (will break source links)')
1389     @cmdln.option('-d', '--diff', action='store_true',
1390                   help='show diff only instead of creating the actual request')
1391     @cmdln.option('--yes', action='store_true',
1392                   help='proceed without asking.')
1393     @cmdln.alias("creq")
1394     def do_createrequest(self, subcmd, opts, *args):
1395         """${cmd_name}: create multiple requests with a single command
1396
1397         usage:
1398             osc creq [OPTIONS] [ 
1399                 -a submit SOURCEPRJ SOURCEPKG DESTPRJ [DESTPKG] 
1400                 -a delete PROJECT [PACKAGE] 
1401                 -a change_devel PROJECT PACKAGE DEVEL_PROJECT [DEVEL_PACKAGE] 
1402                 -a add_me ROLE PROJECT [PACKAGE]
1403                 -a add_group GROUP ROLE PROJECT [PACKAGE]
1404                 -a add_role USER ROLE PROJECT [PACKAGE]
1405                 -a set_bugowner USER PROJECT [PACKAGE]
1406                 ]
1407
1408             Option -m works for all types of request, the rest work only for submit.
1409         example:
1410             osc creq -a submit -a delete home:someone:branches:openSUSE:Tools -a change_devel openSUSE:Tools osc home:someone:branches:openSUSE:Tools -m ok
1411
1412             This will submit all modified packages under current directory, delete project home:someone:branches:openSUSE:Tools and change the devel project to home:someone:branches:openSUSE:Tools for package osc in project openSUSE:Tools.
1413         ${cmd_option_list}
1414         """
1415         src_update = conf.config['submitrequest_on_accept_action'] or None
1416         # we should check here for home:<id>:branch and default to update, but that would require OBS 1.7 server
1417         if opts.cleanup:
1418             src_update = "cleanup"
1419         elif opts.no_cleanup:
1420             src_update = "update"
1421         elif opts.no_update:
1422             src_update = "noupdate"
1423
1424         options_block=""
1425         if src_update:
1426             options_block="""<options><sourceupdate>%s</sourceupdate></options> """ % (src_update)
1427
1428         args = slash_split(args)
1429
1430         apiurl = self.get_api_url()
1431         
1432         i = 0
1433         actionsxml = ""
1434         for ai in opts.actions:
1435             if ai == 'submit':
1436                 args = opts.actiondata[i]
1437                 i = i+1
1438                 actionsxml += self._submit_request(args,opts, options_block)
1439             elif ai == 'delete':
1440                 args = opts.actiondata[i]
1441                 actionsxml += self._delete_request(args,opts)
1442                 i = i+1
1443             elif ai == 'change_devel':
1444                 args = opts.actiondata[i]
1445                 actionsxml += self._changedevel_request(args,opts)
1446                 i = i+1
1447             elif ai == 'add_me':
1448                 args = opts.actiondata[i]
1449                 actionsxml += self._add_me(args,opts)
1450                 i = i+1
1451             elif ai == 'add_group':
1452                 args = opts.actiondata[i]
1453                 actionsxml += self._add_group(args,opts)
1454                 i = i+1
1455             elif ai == 'add_role':
1456                 args = opts.actiondata[i]
1457                 actionsxml += self._add_user(args,opts)
1458                 i = i+1
1459             elif ai == 'set_bugowner':
1460                 args = opts.actiondata[i]
1461                 actionsxml += self._set_bugowner(args,opts)
1462                 i = i+1
1463             else:
1464                 raise oscerr.WrongArgs('Unsupported action %s' % ai)
1465         if actionsxml == "":
1466             sys.exit('No actions need to be taken.')
1467
1468         if not opts.message:
1469             opts.message = edit_message()
1470
1471         import cgi
1472         xml = """<request> %s <state name="new"/> <description>%s</description> </request> """ % \
1473               (actionsxml, cgi.escape(opts.message or ""))
1474         u = makeurl(apiurl, ['request'], query='cmd=create')
1475         f = http_POST(u, data=xml)
1476
1477         root = ET.parse(f).getroot()
1478         return root.get('id')
1479
1480
1481     @cmdln.option('-m', '--message', metavar='TEXT',
1482                   help='specify message TEXT')
1483     @cmdln.option('-r', '--role', metavar='role', default='maintainer',
1484                    help='specify user role (default: maintainer)')
1485     @cmdln.alias("reqmaintainership")
1486     @cmdln.alias("reqms")
1487     def do_requestmaintainership(self, subcmd, opts, *args):
1488         """${cmd_name}: requests to add user as maintainer
1489
1490         usage:
1491             osc requestmaintainership                           # for current user in checked out package
1492             osc requestmaintainership USER                      # for specified user in checked out package
1493             osc requestmaintainership PROJECT PACKAGE           # for current user
1494             osc requestmaintainership PROJECT PACKAGE USER      # request for specified user
1495
1496         ${cmd_option_list}
1497         """
1498         import cgi
1499         args = slash_split(args)
1500         apiurl = self.get_api_url()
1501
1502         if len(args) == 2:
1503             project = args[0]
1504             package = args[1]
1505             user = conf.get_apiurl_usr(apiurl)
1506         elif len(args) == 3:
1507             project = args[0]
1508             package = args[1]
1509             user = args[2]
1510         elif len(args) < 2 and is_package_dir(os.curdir):
1511             project = store_read_project(os.curdir)
1512             package = store_read_package(os.curdir)
1513             if len(args) == 0:
1514                 user = conf.get_apiurl_usr(apiurl)
1515             else:
1516                 user = args[0]
1517         else:
1518             raise oscerr.WrongArgs('Wrong number of arguments.')
1519
1520         if not opts.role in ('maintainer', 'bugowner'):
1521             raise oscerr.WrongOptions('invalid \'--role\': either specify \'maintainer\' or \'bugowner\'')
1522         if not opts.message:
1523             opts.message = edit_message()
1524
1525         r = Request()
1526         r.add_action('add_role', tgt_project=project, tgt_package=package,
1527             person_name=user, person_role=opts.role)
1528         r.description = cgi.escape(opts.message or '')
1529         r.create(apiurl)
1530         print r.reqid
1531
1532     @cmdln.option('-m', '--message', metavar='TEXT',
1533                   help='specify message TEXT')
1534     @cmdln.alias("dr")
1535     @cmdln.alias("dropreq")
1536     @cmdln.alias("droprequest")
1537     @cmdln.alias("deletereq")
1538     def do_deleterequest(self, subcmd, opts, *args):
1539         """${cmd_name}: Request to delete (or 'drop') a package or project
1540
1541         usage:
1542             osc deletereq [-m TEXT]                     # works in checked out project/package
1543             osc deletereq [-m TEXT] PROJECT [PACKAGE]
1544         ${cmd_option_list}
1545         """
1546         import cgi
1547
1548         args = slash_split(args)
1549
1550         project = None
1551         package = None
1552
1553         if len(args) > 2:
1554             raise oscerr.WrongArgs('Too many arguments.')
1555         elif len(args) == 1:
1556             project = args[0]
1557         elif len(args) == 2:
1558             project = args[0]
1559             package = args[1]
1560         elif is_project_dir(os.getcwd()):
1561             project = store_read_project(os.curdir)
1562         elif is_package_dir(os.getcwd()):
1563             project = store_read_project(os.curdir)
1564             package = store_read_package(os.curdir)
1565         else: 
1566             raise oscerr.WrongArgs('Please specify at least a project.')
1567
1568         if not opts.message:
1569             import textwrap
1570             if package is not None:
1571                 footer=textwrap.TextWrapper(width = 66).fill(
1572                         'please explain why you like to delete package %s of project %s'
1573                         % (package,project))
1574             else:
1575                 footer=textwrap.TextWrapper(width = 66).fill(
1576                         'please explain why you like to delete project %s' % project)
1577             opts.message = edit_message(footer)
1578
1579         r = Request()
1580         r.add_action('delete', tgt_project=project, tgt_package=package)
1581         r.description = cgi.escape(opts.message)
1582         r.create(self.get_api_url())
1583         print r.reqid
1584
1585
1586     @cmdln.option('-m', '--message', metavar='TEXT',
1587                   help='specify message TEXT')
1588     @cmdln.alias("cr")
1589     @cmdln.alias("changedevelreq")
1590     def do_changedevelrequest(self, subcmd, opts, *args):
1591         """${cmd_name}: Create request to change the devel package definition.
1592
1593         [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration 
1594         for information on this topic.]
1595
1596         See the "request" command for showing and modifing existing requests.
1597
1598         osc changedevelrequest PROJECT PACKAGE DEVEL_PROJECT [DEVEL_PACKAGE]
1599         """
1600         import cgi
1601
1602         if len(args) == 0 and is_package_dir('.') and find_default_project():
1603             wd = os.curdir
1604             devel_project = store_read_project(wd)
1605             devel_package = package = store_read_package(wd)
1606             project = find_default_project(self.get_api_url(), package)
1607         elif len(args) < 3:
1608             raise oscerr.WrongArgs('Too few arguments.')
1609         elif len(args) > 4:
1610             raise oscerr.WrongArgs('Too many arguments.')
1611         else:
1612             devel_project = args[2]
1613             project = args[0]
1614             package = args[1]
1615             devel_package = package
1616             if len(args) == 4:
1617                 devel_package = args[3]
1618
1619         if not opts.message:
1620             import textwrap
1621             footer=textwrap.TextWrapper(width = 66).fill(
1622                     'please explain why you like to change the devel project of %s/%s to %s/%s'
1623                     % (project,package,devel_project,devel_package))
1624             opts.message = edit_message(footer)
1625
1626         r = Request()
1627         r.add_action('change_devel', src_project=devel_project, src_package=devel_package,
1628             tgt_project=project, tgt_package=package)
1629         r.description = cgi.escape(opts.message)
1630         r.create(self.get_api_url())
1631         print r.reqid
1632
1633
1634     @cmdln.option('-d', '--diff', action='store_true',
1635                   help='generate a diff')
1636     @cmdln.option('-u', '--unified', action='store_true',
1637                   help='output the diff in the unified diff format')
1638     @cmdln.option('-m', '--message', metavar='TEXT',
1639                   help='specify message TEXT')
1640     @cmdln.option('-t', '--type', metavar='TYPE',
1641                   help='limit to requests which contain a given action type (submit/delete/change_devel)')
1642     @cmdln.option('-a', '--all', action='store_true',
1643                         help='all states. Same as\'-s all\'')
1644     @cmdln.option('-f', '--force', action='store_true',
1645                         help='enforce state change, can be used to ignore open reviews')
1646     @cmdln.option('-s', '--state', default='',  # default is 'all' if no args given, 'new,review' otherwise
1647                         help='only list requests in one of the comma separated given states (new/review/accepted/revoked/declined) or "all" [default="new,review", or "all", if no args given]')
1648     @cmdln.option('-D', '--days', metavar='DAYS',
1649                         help='only list requests in state "new" or changed in the last DAYS. [default=%(request_list_days)s]')
1650     @cmdln.option('-U', '--user', metavar='USER',
1651                         help='requests or reviews limited for the specified USER')
1652     @cmdln.option('-G', '--group', metavar='GROUP',
1653                         help='requests or reviews limited for the specified GROUP')
1654     @cmdln.option('-P', '--project', metavar='PROJECT',
1655                         help='requests or reviews limited for the specified PROJECT')
1656     @cmdln.option('-p', '--package', metavar='PACKAGE',
1657                         help='requests or reviews limited for the specified PACKAGE, requires also a PROJECT')
1658     @cmdln.option('-b', '--brief', action='store_true', default=False,
1659                         help='print output in list view as list subcommand')
1660     @cmdln.option('-M', '--mine', action='store_true',
1661                         help='only show requests created by yourself')
1662     @cmdln.option('-B', '--bugowner', action='store_true',
1663                         help='also show requests about packages where I am bugowner')
1664     @cmdln.option('-e', '--edit', action='store_true',
1665                         help='edit a submit action')
1666     @cmdln.option('-i', '--interactive', action='store_true',
1667                         help='interactive review of request')
1668     @cmdln.option('--non-interactive', action='store_true',
1669                         help='non-interactive review of request')
1670     @cmdln.option('--exclude-target-project', action='append',
1671                         help='exclude target project from request list')
1672     @cmdln.option('--involved-projects', action='store_true',
1673                         help='show all requests for project/packages where USER is involved')
1674     @cmdln.option('--source-buildstatus', action='store_true',
1675                         help='print the buildstatus of the source package (only works with "show")')
1676     @cmdln.alias("rq")
1677     @cmdln.alias("review")
1678     # FIXME: rewrite this mess and split request and review
1679     def do_request(self, subcmd, opts, *args):
1680         """${cmd_name}: Show or modify requests and reviews
1681
1682         [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration
1683         for information on this topic.]
1684
1685         The 'request' command has the following sub commands:
1686
1687         "list" lists open requests attached to a project or package or person.
1688         Uses the project/package of the current directory if none of
1689         -M, -U USER, project/package are given.
1690
1691         "log" will show the history of the given ID
1692
1693         "show" will show the request itself, and generate a diff for review, if
1694         used with the --diff option. The keyword show can be omitted if the ID is numeric.
1695
1696         "decline" will change the request state to "declined"
1697
1698         "reopen" will set the request back to new or review.
1699
1700         "supersede" will supersede one request with another existing one.
1701
1702         "revoke" will set the request state to "revoked"
1703
1704         "accept" will change the request state to "accepted" and will trigger
1705         the actual submit process. That would normally be a server-side copy of
1706         the source package to the target package.
1707
1708         "checkout" will checkout the request's source package ("submit" requests only).
1709
1710         The 'review' command has the following sub commands:
1711
1712         "list" lists open requests that need to be reviewed by the
1713         specified user or group 
1714
1715         "add" adds a person or group as reviewer to a request
1716
1717         "accept" mark the review positive
1718
1719         "decline" mark the review negative. A negative review will
1720         decline the request.
1721
1722         usage:
1723             osc request list [-M] [-U USER] [-s state] [-D DAYS] [-t type] [-B] [PRJ [PKG]]
1724             osc request log ID
1725             osc request [show] [-d] [-b] ID
1726
1727             osc request accept [-m TEXT] ID
1728             osc request decline [-m TEXT] ID
1729             osc request revoke [-m TEXT] ID
1730             osc request reopen [-m TEXT] ID
1731             osc request supersede [-m TEXT] ID SUPERSEDE_ID
1732             osc request approvenew [-m TEXT] PROJECT
1733
1734             osc request checkout/co ID
1735             osc request clone [-m TEXT] ID
1736
1737             osc review list [-U USER] [-G GROUP] [-s state]
1738             osc review add [-m TEXT] [-U USER] [-G GROUP] ID
1739             osc review accept [-m TEXT] ID
1740             osc review decline [-m TEXT] ID
1741             osc review reopen [-m TEXT] ID
1742             osc review supersede [-m TEXT] ID SUPERSEDE_ID
1743
1744         ${cmd_option_list}
1745         """
1746
1747         args = slash_split(args)
1748
1749         if opts.all and opts.state:
1750             raise oscerr.WrongOptions('Sorry, the options \'--all\' and \'--state\' ' \
1751                     'are mutually exclusive.')
1752         if opts.mine and opts.user:
1753             raise oscerr.WrongOptions('Sorry, the options \'--user\' and \'--mine\' ' \
1754                     'are mutually exclusive.')
1755         if opts.interactive and opts.non_interactive:
1756             raise oscerr.WrongOptions('Sorry, the options \'--interactive\' and ' \
1757                     '\'--non-interactive\' are mutually exclusive')
1758
1759         if not args:
1760             args = [ 'list' ]
1761             opts.mine = 1
1762             if opts.state == '':
1763                 opts.state = 'all'
1764
1765         if opts.state == '':
1766             opts.state = 'new,review'
1767
1768         if args[0] == 'help':
1769             return self.do_help(['help', 'request'])
1770
1771         cmds = [ 'list', 'log', 'show', 'decline', 'reopen', 'clone', 'accept', 'approvenew', 'wipe', 'supersede', 'revoke', 'checkout', 'co' ]
1772         if subcmd != 'review' and args[0] not in cmds:
1773             raise oscerr.WrongArgs('Unknown request action %s. Choose one of %s.' \
1774                                                % (args[0],', '.join(cmds)))
1775         cmds = [ 'list', 'add', 'decline', 'accept', 'reopen', 'supersede' ]
1776         if subcmd == 'review' and args[0] not in cmds:
1777             raise oscerr.WrongArgs('Unknown review action %s. Choose one of %s.' \
1778                                                % (args[0],', '.join(cmds)))
1779
1780         cmd = args[0]
1781         del args[0]
1782
1783         apiurl = self.get_api_url()
1784
1785         if cmd in ['list']:
1786             min_args, max_args = 0, 2
1787         elif cmd in ['supersede']:
1788             min_args, max_args = 2, 2
1789         else:
1790             min_args, max_args = 1, 1
1791         if len(args) < min_args:
1792             raise oscerr.WrongArgs('Too few arguments.')
1793         if len(args) > max_args:
1794             raise oscerr.WrongArgs('Too many arguments.')
1795         if cmd in ['add'] and not opts.user and not opts.group:
1796             opts.user = conf.get_apiurl_usr(apiurl)
1797
1798         reqid = None
1799         supersedid = None
1800         if cmd == 'list' or cmd == 'approvenew':
1801             package = None
1802             project = None
1803             if len(args) > 0:
1804                 project = args[0]
1805             elif not opts.mine and not opts.user:
1806                 try:
1807                     project = store_read_project(os.curdir)
1808                     package = store_read_package(os.curdir)
1809                 except oscerr.NoWorkingCopy:
1810                     pass
1811
1812             if len(args) > 1:
1813                 package = args[1]
1814         elif cmd == 'supersede':
1815             reqid = args[0]
1816             supersedid = args[1]
1817         elif cmd in ['log', 'add', 'show', 'decline', 'reopen', 'clone', 'accept', 'wipe', 'revoke', 'checkout', 'co']:
1818             reqid = args[0]
1819
1820         # clone all packages from a given request
1821         if cmd in ['clone']:
1822             # should we force a message?
1823             print 'Cloned packages are available in project: %s' % clone_request(apiurl, reqid, opts.message)
1824
1825         # add new reviewer to existing request
1826         elif cmd in ['add'] and subcmd == 'review':
1827             query = { 'cmd': 'addreview' }
1828             if opts.user:
1829                 query['by_user'] = opts.user
1830             if opts.group:
1831                 query['by_group'] = opts.group
1832             if opts.project:
1833                 query['by_project'] = opts.project
1834             if opts.package:
1835                 query['by_package'] = opts.package
1836             url = makeurl(apiurl, ['request', reqid], query)
1837             if not opts.message:
1838                 opts.message = edit_message()
1839             r = http_POST(url, data=opts.message)
1840             print ET.parse(r).getroot().get('code')
1841
1842         # list and approvenew
1843         elif cmd == 'list' or cmd == 'approvenew':
1844             states = ('new', 'accepted', 'revoked', 'declined', 'review', 'superseded')
1845             who = ''
1846             if cmd == 'approvenew':
1847                states = ('new')
1848                results = get_request_list(apiurl, project, package, '', ['new'])
1849             else:
1850                state_list = opts.state.split(',')
1851                if opts.all:
1852                    state_list = ['all']
1853                if subcmd == 'review':
1854                    state_list = ['review']
1855                elif opts.state == 'all':
1856                    state_list = ['all']
1857                else:
1858                    for s in state_list:
1859                        if not s in states and not s == 'all':
1860                            raise oscerr.WrongArgs('Unknown state \'%s\', try one of %s' % (s, ','.join(states)))
1861                if opts.mine:
1862                    who = conf.get_apiurl_usr(apiurl)
1863                if opts.user:
1864                    who = opts.user
1865
1866                ## FIXME -B not implemented!
1867                if opts.bugowner:
1868                    if (self.options.debug):
1869                        print 'list: option --bugowner ignored: not impl.'
1870
1871                if subcmd == 'review':
1872                        # FIXME: do the review list for the user and for all groups he belong to
1873                        results = get_review_list(apiurl, project, package, who, opts.group, opts.project, opts.package, state_list)
1874                else:
1875                    if opts.involved_projects:
1876                        who = who or conf.get_apiurl_usr(apiurl)
1877                        results = get_user_projpkgs_request_list(apiurl, who, req_state=state_list,
1878                                                                 req_type=opts.type, exclude_projects=opts.exclude_target_project or [])
1879                    else:
1880                        results = get_request_list(apiurl, project, package, who,
1881                                                   state_list, opts.type, opts.exclude_target_project or [])
1882
1883             results.sort(reverse=True)
1884             import time
1885             days = opts.days or conf.config['request_list_days']
1886             since = ''
1887             try:
1888                 days = int(days)
1889             except ValueError:
1890                 days = 0
1891             if days > 0:
1892                 since = time.strftime('%Y-%m-%dT%H:%M:%S',time.localtime(time.time()-days*24*3600))
1893
1894             skipped = 0
1895             ## bs has received 2009-09-20 a new xquery compare() function
1896             ## which allows us to limit the list inside of get_request_list
1897             ## That would be much faster for coolo. But counting the remainder
1898             ## would not be possible with current xquery implementation.
1899             ## Workaround: fetch all, and filter on client side.
1900
1901             ## FIXME: date filtering should become implemented on server side
1902             for result in results:
1903                 if days == 0 or result.state.when > since or result.state.name == 'new':
1904                     if (opts.interactive or conf.config['request_show_interactive']) and not opts.non_interactive:
1905                         request_interactive_review(apiurl, result)
1906                     else:
1907                         print result.list_view(), '\n'
1908                 else:
1909                     skipped += 1
1910             if skipped:
1911                 print "There are %d requests older than %s days.\n" % (skipped, days)
1912
1913             if cmd == 'approvenew':
1914                 print "\n *** Approve them all ? [y/n] ***"
1915                 if sys.stdin.read(1) == "y":
1916     
1917                     if not opts.message:
1918                         opts.message = edit_message()
1919                     for result in results:
1920                         print result.reqid, ": ",
1921                         r = change_request_state(apiurl,
1922                                 result.reqid, 'accepted', opts.message or '', force=opts.force)
1923                         print 'Result of change request state: %s' % r
1924                 else:
1925                     print >>sys.stderr, 'Aborted...'
1926                     raise oscerr.UserAbort()
1927
1928         elif cmd == 'log':
1929             for l in get_request_log(apiurl, reqid):
1930                 print l
1931
1932         # show
1933         elif cmd == 'show':
1934             r = get_request(apiurl, reqid)
1935             if opts.brief:
1936                 print r.list_view()
1937             elif opts.edit:
1938                 if not r.get_actions('submit'):
1939                     raise oscerr.WrongOptions('\'--edit\' not possible ' \
1940                         '(request has no \'submit\' action)')
1941                 return request_interactive_review(apiurl, r, 'e')
1942             elif (opts.interactive or conf.config['request_show_interactive']) and not opts.non_interactive:
1943                 return request_interactive_review(apiurl, r)
1944             else:
1945                 print r
1946             if opts.source_buildstatus:
1947                 sr_actions = r.get_actions('submit')
1948                 if not sr_actions:
1949                     raise oscerr.WrongOptions( '\'--source-buildstatus\' not possible ' \
1950                         '(request has no \'submit\' actions)')
1951                 for action in sr_actions:
1952                     print 'Buildstatus for \'%s/%s\':' % (action.src_project, action.src_package)
1953                     print '\n'.join(get_results(apiurl, action.src_project, action.src_package))
1954             if opts.diff:
1955                 diff = ''
1956                 try:
1957                     # works since OBS 2.1
1958                     diff = request_diff(apiurl, reqid)
1959                 except urllib2.HTTPError, e:
1960                     # for OBS 2.0 and before
1961                     sr_actions = r.get_actions('submit')
1962                     if not sr_actions:
1963                         raise oscerr.WrongOptions('\'--diff\' not possible (request has no \'submit\' actions)')
1964                     for action in sr_actions:
1965                         diff += 'old: %s/%s\nnew: %s/%s\n' % (action.src_project, action.src_package,
1966                             action.tgt_project, action.tgt_package)
1967                         diff += submit_action_diff(apiurl, action)
1968                         diff += '\n\n'
1969                 run_pager(diff)
1970
1971         # checkout
1972         elif cmd == 'checkout' or cmd == 'co':
1973             r = get_request(apiurl, reqid)
1974             sr_actions = r.get_actions('submit')
1975             if not sr_actions:
1976                 raise oscerr.WrongArgs('\'checkout\' not possible (request has no \'submit\' actions)')
1977             for action in sr_actions:
1978                 checkout_package(apiurl, action.src_project, action.src_package, \
1979                     action.src_rev, expand_link=True, prj_dir=action.src_project)
1980
1981         else:
1982             state_map = {'reopen' : 'new', 'accept' : 'accepted', 'decline' : 'declined', 'wipe' : 'deleted', 'revoke' : 'revoked', 'supersede' : 'superseded'}
1983             # Change review state only
1984             if subcmd == 'review':
1985                 if not opts.message:
1986                    opts.message = edit_message()
1987                 if cmd in ['accept', 'decline', 'reopen', 'supersede']:
1988                     r = change_review_state(apiurl,
1989                             reqid, state_map[cmd], conf.get_apiurl_usr(apiurl), opts.group, opts.project, opts.package, opts.message or '', supersed=supersedid)
1990                     print r
1991             # Change state of entire request
1992             elif cmd in ['reopen', 'accept', 'decline', 'wipe', 'revoke', 'supersede']:
1993                 rq = get_request(apiurl, reqid)
1994                 if rq.state.name == state_map[cmd]:
1995                     repl = raw_input("\n *** The state of the request (#%s) is already '%s'. Change state anyway?  [y/n] *** "  % (reqid, rq.state.name))
1996                     if repl.lower() != 'y':
1997                         print >>sys.stderr, 'Aborted...'
1998                         raise oscerr.UserAbort()
1999                                             
2000                 if not opts.message:
2001                     tmpl = change_request_state_template(rq, state_map[cmd])
2002                     opts.message = edit_message(template=tmpl)
2003                 r = change_request_state(apiurl,
2004                         reqid, state_map[cmd], opts.message or '', supersed=supersedid, force=opts.force)
2005                 print 'Result of change request state: %s' % r
2006
2007     # editmeta and its aliases are all depracated
2008     @cmdln.alias("editprj")
2009     @cmdln.alias("createprj")
2010     @cmdln.alias("editpac")
2011     @cmdln.alias("createpac")
2012     @cmdln.alias("edituser")
2013     @cmdln.alias("usermeta")
2014     @cmdln.hide(1)
2015     def do_editmeta(self, subcmd, opts, *args):
2016         """${cmd_name}:
2017
2018         Obsolete command to edit metadata. Use 'meta' now.
2019
2020         See the help output of 'meta'.
2021
2022         """
2023
2024         print >>sys.stderr, 'This command is obsolete. Use \'osc meta <metatype> ...\'.'
2025         print >>sys.stderr, 'See \'osc help meta\'.'
2026         #self.do_help([None, 'meta'])
2027         return 2
2028
2029
2030     @cmdln.option('-r', '--revision', metavar='rev',
2031                   help='use the specified revision.')
2032     @cmdln.option('-R', '--use-plain-revision', action='store_true',
2033                   help='Don\'t expand revsion based on baserev, the revision which was used when commit happened.')
2034     @cmdln.option('-b', '--use-baserev', action='store_true',
2035                   help='Use the revisions which exists when the original commit happend and don\'t try to merge later commits.')
2036     @cmdln.option('-u', '--unset', action='store_true',
2037                   help='remove revision in link, it will point always to latest revision')
2038     def do_setlinkrev(self, subcmd, opts, *args):
2039         """${cmd_name}: Updates a revision number in a source link.
2040
2041         This command adds or updates a specified revision number in a source link.
2042         The current revision of the source is used, if no revision number is specified.
2043
2044         usage:
2045             osc setlinkrev
2046             osc setlinkrev PROJECT [PACKAGE]
2047         ${cmd_option_list}
2048         """
2049
2050         args = slash_split(args)
2051         apiurl = self.get_api_url()
2052         package = None
2053         expand = True
2054         baserev = False
2055         if opts.use_plain_revision:
2056             expand = False
2057         if opts.use_baserev:
2058             baserev = True
2059
2060         rev = parseRevisionOption(opts.revision)[0] or ''
2061         if opts.unset:
2062             rev = None
2063
2064         if len(args) == 0:
2065             p = findpacs(os.curdir)[0]
2066             project = p.prjname
2067             package = p.name
2068             apiurl = p.apiurl
2069             if not p.islink():
2070                 sys.exit('Local directory is no checked out source link package, aborting')
2071         elif len(args) == 2:
2072             project = args[0]
2073             package = args[1]
2074         elif len(args) == 1:
2075             project = args[0]
2076         else:
2077             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
2078                   + self.get_cmd_help('setlinkrev'))
2079
2080         if package:
2081             packages = [package]
2082         else:
2083             packages = meta_get_packagelist(apiurl, project)
2084
2085         for p in packages:
2086             print 'setting revision for package %s' % p
2087             set_link_rev(apiurl, project, p, revision=rev, expand=expand, baserev=baserev)
2088
2089
2090     def do_linktobranch(self, subcmd, opts, *args):
2091         """${cmd_name}: Convert a package containing a classic link with patch to a branch
2092
2093         This command tells the server to convert a _link with or without a project.diff
2094         to a branch. This is a full copy with a _link file pointing to the branched place.
2095
2096         usage:
2097             osc linktobranch                    # can be used in checked out package
2098             osc linktobranch PROJECT PACKAGE
2099         ${cmd_option_list}
2100         """
2101         args = slash_split(args)
2102         apiurl = self.get_api_url()
2103
2104         if len(args) == 0:
2105             wd = os.curdir
2106             project = store_read_project(wd)
2107             package = store_read_package(wd)
2108             update_local_dir = True
2109         elif len(args) < 2:
2110             raise oscerr.WrongArgs('Too few arguments (required none or two)')
2111         elif len(args) > 2:
2112             raise oscerr.WrongArgs('Too many arguments (required none or two)')
2113         else:
2114             project = args[0]
2115             package = args[1]
2116             update_local_dir = False
2117
2118         # execute
2119         link_to_branch(apiurl, project, package)
2120         if update_local_dir:
2121             pac = Package(wd)
2122             pac.update(rev=pac.latest_rev())
2123
2124
2125     @cmdln.option('-m', '--message', metavar='TEXT',
2126                   help='specify message TEXT')
2127     def do_detachbranch(self, subcmd, opts, *args):
2128         """${cmd_name}: replace a link with its expanded sources
2129
2130         If a package is a link it is replaced with its expanded sources. The link
2131         does not exist anymore.
2132
2133         usage:
2134             osc detachbranch                    # can be used in package working copy
2135             osc detachbranch PROJECT PACKAGE
2136         ${cmd_option_list}
2137         """
2138         args = slash_split(args)
2139         apiurl = self.get_api_url()
2140         if len(args) == 0:
2141             project = store_read_project(os.curdir)
2142             package = store_read_package(os.curdir)
2143         elif len(args) == 2:
2144             project, package = args
2145         elif len(args) > 2:
2146             raise oscerr.WrongArgs('Too many arguments (required none or two)')
2147         else:
2148             raise oscerr.WrongArgs('Too few arguments (required none or two)')
2149
2150         try:
2151             copy_pac(apiurl, project, package, apiurl, project, package, expand=True, comment=opts.message)
2152         except urllib2.HTTPError, e:
2153             root = ET.fromstring(show_files_meta(apiurl, project, package, 'latest', expand=False))
2154             li = Linkinfo()
2155             li.read(root.find('linkinfo'))
2156             if li.islink() and li.haserror():
2157                 raise oscerr.LinkExpandError(project, package, li.error)
2158             elif not li.islink():
2159                 print >>sys.stderr, 'package \'%s/%s\' is no link' % (project, package)
2160             else:
2161                 raise e
2162
2163
2164     @cmdln.option('-C', '--cicount', choices=['add', 'copy', 'local'],
2165                   help='cicount attribute in the link, known values are add, copy, and local, default in buildservice is currently add.')
2166     @cmdln.option('-c', '--current', action='store_true',
2167                   help='link fixed against current revision.')
2168     @cmdln.option('-r', '--revision', metavar='rev',
2169                   help='link the specified revision.')
2170     @cmdln.option('-f', '--force', action='store_true',
2171                   help='overwrite an existing link file if it is there.')
2172     @cmdln.option('-d', '--disable-publish', action='store_true',
2173                   help='disable publishing of the linked package')
2174     def do_linkpac(self, subcmd, opts, *args):
2175         """${cmd_name}: "Link" a package to another package
2176
2177         A linked package is a clone of another package, but plus local
2178         modifications. It can be cross-project.
2179
2180         The DESTPAC name is optional; the source packages' name will be used if
2181         DESTPAC is omitted.
2182
2183         Afterwards, you will want to 'checkout DESTPRJ DESTPAC'.
2184
2185         To add a patch, add the patch as file and add it to the _link file.
2186         You can also specify text which will be inserted at the top of the spec file.
2187
2188         See the examples in the _link file.
2189
2190         NOTE: In case you are not aware about the difference of 'linkpac' and 'branch' command
2191               you should use the 'branch' command by default.
2192
2193         usage:
2194             osc linkpac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
2195         ${cmd_option_list}
2196         """
2197
2198         args = slash_split(args)
2199         apiurl = self.get_api_url()
2200
2201         if not args or len(args) < 3:
2202             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
2203                   + self.get_cmd_help('linkpac'))
2204
2205         rev, dummy = parseRevisionOption(opts.revision)
2206
2207         src_project = args[0]
2208         src_package = args[1]
2209         dst_project = args[2]
2210         if len(args) > 3:
2211             dst_package = args[3]
2212         else:
2213             dst_package = src_package
2214
2215         if src_project == dst_project and src_package == dst_package:
2216             raise oscerr.WrongArgs('Error: source and destination are the same.')
2217
2218         if src_project == dst_project and not opts.cicount:
2219             # in this case, the user usually wants to build different spec
2220             # files from the same source
2221             opts.cicount = "copy"
2222
2223         if opts.current:
2224             rev = show_upstream_rev(apiurl, src_project, src_package)
2225
2226         if rev and not checkRevision(src_project, src_package, rev):
2227             print >>sys.stderr, 'Revision \'%s\' does not exist' % rev
2228             sys.exit(1)
2229
2230         link_pac(src_project, src_package, dst_project, dst_package, opts.force, rev, opts.cicount, opts.disable_publish)
2231
2232     @cmdln.option('--nosources', action='store_true',
2233                   help='ignore source packages when copying build results to destination project')
2234     @cmdln.option('-m', '--map-repo', metavar='SRC=TARGET[,SRC=TARGET]',
2235                   help='Allows repository mapping(s) to be given as SRC=TARGET[,SRC=TARGET]')
2236     @cmdln.option('-d', '--disable-publish', action='store_true',
2237                   help='disable publishing of the aggregated package')
2238     def do_aggregatepac(self, subcmd, opts, *args):
2239         """${cmd_name}: "Aggregate" a package to another package
2240
2241         Aggregation of a package means that the build results (binaries) of a
2242         package are basically copied into another project.
2243         This can be used to make packages available from building that are
2244         needed in a project but available only in a different project. Note
2245         that this is done at the expense of disk space. See
2246         http://en.opensuse.org/openSUSE:Build_Service_Tips_and_Tricks#link_and_aggregate
2247         for more information.
2248
2249         The DESTPAC name is optional; the source packages' name will be used if
2250         DESTPAC is omitted.
2251
2252         usage:
2253             osc aggregatepac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
2254         ${cmd_option_list}
2255         """
2256
2257         args = slash_split(args)
2258
2259         if not args or len(args) < 3:
2260             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
2261                   + self.get_cmd_help('aggregatepac'))
2262
2263         src_project = args[0]
2264         src_package = args[1]
2265         dst_project = args[2]
2266         if len(args) > 3:
2267             dst_package = args[3]
2268         else:
2269             dst_package = src_package
2270
2271         if src_project == dst_project and src_package == dst_package:
2272             raise oscerr.WrongArgs('Error: source and destination are the same.')
2273
2274         repo_map = {}
2275         if opts.map_repo:
2276             for pair in opts.map_repo.split(','):
2277                 src_tgt = pair.split('=')
2278                 if len(src_tgt) != 2:
2279                     raise oscerr.WrongOptions('map "%s" must be SRC=TARGET[,SRC=TARGET]' % opts.map_repo)
2280                 repo_map[src_tgt[0]] = src_tgt[1]
2281
2282         aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map, opts.disable_publish, opts.nosources)
2283
2284
2285     @cmdln.option('-c', '--client-side-copy', action='store_true',
2286                         help='do a (slower) client-side copy')
2287     @cmdln.option('-k', '--keep-maintainers', action='store_true',
2288                         help='keep original maintainers. Default is remove all and replace with the one calling the script.')
2289     @cmdln.option('-d', '--keep-develproject', action='store_true',
2290                         help='keep develproject tag in the package metadata')
2291     @cmdln.option('-r', '--revision', metavar='rev',
2292                         help='link the specified revision.')
2293     @cmdln.option('-t', '--to-apiurl', metavar='URL',
2294                         help='URL of destination api server. Default is the source api server.')
2295     @cmdln.option('-m', '--message', metavar='TEXT',
2296                   help='specify message TEXT')
2297     @cmdln.option('-e', '--expand', action='store_true',
2298                         help='if the source package is a link then copy the expanded version of the link')
2299     def do_copypac(self, subcmd, opts, *args):
2300         """${cmd_name}: Copy a package
2301
2302         A way to copy package to somewhere else.
2303
2304         It can be done across buildservice instances, if the -t option is used.
2305         In that case, a client-side copy and link expansion are implied.
2306
2307         Using --client-side-copy always involves downloading all files, and
2308         uploading them to the target.
2309
2310         The DESTPAC name is optional; the source packages' name will be used if
2311         DESTPAC is omitted.
2312
2313         usage:
2314             osc copypac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
2315         ${cmd_option_list}
2316         """
2317
2318         args = slash_split(args)
2319
2320         if not args or len(args) < 3:
2321             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
2322                   + self.get_cmd_help('copypac'))
2323
2324         src_project = args[0]
2325         src_package = args[1]
2326         dst_project = args[2]
2327         if len(args) > 3:
2328             dst_package = args[3]
2329         else:
2330             dst_package = src_package
2331
2332         src_apiurl = conf.config['apiurl']
2333         if opts.to_apiurl:
2334             dst_apiurl = conf.config['apiurl_aliases'].get(opts.to_apiurl, opts.to_apiurl)
2335         else:
2336             dst_apiurl = src_apiurl
2337
2338         if src_apiurl != dst_apiurl:
2339             opts.client_side_copy = True
2340             opts.expand = True
2341
2342         rev, dummy = parseRevisionOption(opts.revision)
2343
2344         if opts.message:
2345             comment = opts.message
2346         else:
2347             if not rev:
2348                 rev = show_upstream_rev(src_apiurl, src_project, src_package)
2349             comment = 'osc copypac from project:%s package:%s revision:%s' % ( src_project, src_package, rev )
2350
2351         if src_project == dst_project and \
2352            src_package == dst_package and \
2353            not rev and \
2354            src_apiurl == dst_apiurl:
2355             raise oscerr.WrongArgs('Source and destination are the same.')
2356
2357         r = copy_pac(src_apiurl, src_project, src_package,
2358                      dst_apiurl, dst_project, dst_package,
2359                      client_side_copy=opts.client_side_copy,
2360                      keep_maintainers=opts.keep_maintainers,
2361                      keep_develproject=opts.keep_develproject,
2362                      expand=opts.expand,
2363                      revision=rev,
2364                      comment=comment)
2365         print r
2366
2367
2368     @cmdln.option('-m', '--message', metavar='TEXT',
2369                         help='specify message TEXT')
2370     def do_releaserequest(self, subcmd, opts, *args):
2371         """${cmd_name}: Create a request for releasing a maintenance update.
2372
2373         [See http://doc.opensuse.org/products/draft/OBS/obs-reference-guide/cha.obs.maintenance_setup.html
2374         for information on this topic.]
2375
2376         This command is used by the maintence team to start the release process of a maintenance update.
2377         This includes usually testing based on the defined reviewers of the update project.
2378
2379         usage:
2380             osc releaserequest [ SOURCEPROJECT ]
2381         ${cmd_option_list}
2382         """
2383        
2384         # FIXME: additional parameters can be a certain repo list to create a partitial release
2385
2386         args = slash_split(args)
2387         apiurl = self.get_api_url()
2388
2389         source_project = None
2390
2391         if len(args) > 1:
2392             raise oscerr.WrongArgs('Too many arguments.')
2393
2394         if len(args) == 0 and is_project_dir(os.curdir):
2395             source_project = store_read_project(os.curdir)
2396         elif len(args) == 0:
2397             raise oscerr.WrongArgs('Too few arguments.')
2398         if len(args) > 0:
2399             source_project = args[0]
2400
2401         if not opts.message:
2402             opts.message = edit_message()
2403
2404         r = create_release_request(apiurl, source_project, opts.message)
2405         print r.reqid
2406
2407
2408
2409     @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
2410                         help='Use this attribute to find default maintenance project (default is OBS:MaintenanceProject)')
2411     @cmdln.option('-m', '--message', metavar='TEXT',
2412                         help='specify message TEXT')
2413     def do_maintenancerequest(self, subcmd, opts, *args):
2414         """${cmd_name}: Create a request for starting a maintenance incident.
2415
2416         [See http://doc.opensuse.org/products/draft/OBS/obs-reference-guide/cha.obs.maintenance_setup.html
2417         for information on this topic.]
2418
2419         This command is asking the maintence team to start a maintence incident based on a 
2420         created maintenance update. Please see the "mbranch" command on how to create such a project and
2421         the "patchinfo" command how add the required maintenance update informations.
2422
2423         usage:
2424             osc maintenancerequest [ SOURCEPROJECT [ TARGETPROJECT ] ]
2425         ${cmd_option_list}
2426         """
2427
2428         args = slash_split(args)
2429         apiurl = self.get_api_url()
2430         attribute = "OBS:MaintenanceProject" # default attribute as defined in api code.
2431         if opts.attribute:
2432             attribute = opts.attribute
2433
2434         source_project = target_project = None
2435
2436         if len(args) > 2:
2437             raise oscerr.WrongArgs('Too many arguments.')
2438
2439         if len(args) == 0 and is_project_dir(os.curdir):
2440             source_project = store_read_project(os.curdir)
2441         elif len(args) == 0:
2442             raise oscerr.WrongArgs('Too few arguments.')
2443         if len(args) > 0:
2444             source_project = args[0]
2445
2446         if len(args) > 1:
2447             target_project = args[1]
2448         else:
2449             xpath = 'attribute/@name = \'%s\'' % attribute
2450             res = search(apiurl, project_id=xpath)
2451             root = res['project_id']
2452             project = root.find('project')
2453             if project is None:
2454                 sys.exit('Unable to find defined OBS:MaintenanceProject project on server.')
2455             target_project = project.get('name')
2456             print 'Using target project \'%s\'' % target_project
2457
2458         if not opts.message:
2459             opts.message = edit_message()
2460
2461         r = create_maintenance_request(apiurl, source_project, target_project, opts.message)
2462         print r.reqid
2463
2464
2465     @cmdln.option('-c', '--checkout', action='store_true',
2466                         help='Checkout branched package afterwards ' \
2467                                 '(\'osc bco\' is a shorthand for this option)' )
2468     @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
2469                         help='Use this attribute to find affected packages (default is OBS:Maintained)')
2470     @cmdln.option('-u', '--update-project-attribute', metavar='UPDATE_ATTRIBUTE',
2471                         help='Use this attribute to find update projects (default is OBS:UpdateProject) ')
2472     def do_mbranch(self, subcmd, opts, *args):
2473         """${cmd_name}: Multiple branch of a package
2474
2475         [See http://en.opensuse.org/openSUSE:Build_Service_Concept_Maintenance
2476         for information on this topic.]
2477
2478         This command is used for creating multiple links of defined version of a package
2479         in one project. This is esp. used for maintenance updates.
2480
2481         The branched package will live in
2482             home:USERNAME:branches:ATTRIBUTE:PACKAGE
2483         if nothing else specified.
2484
2485         usage:
2486             osc mbranch [ SOURCEPACKAGE [ TARGETPROJECT ] ]
2487         ${cmd_option_list}
2488         """
2489         args = slash_split(args)
2490         apiurl = self.get_api_url()
2491         tproject = None
2492
2493         maintained_attribute = conf.config['maintained_attribute']
2494         maintained_update_project_attribute = conf.config['maintained_update_project_attribute']
2495
2496         if not len(args) or len(args) > 2:
2497             raise oscerr.WrongArgs('Wrong number of arguments.')
2498         if len(args) >= 1:
2499             package = args[0]
2500         if len(args) >= 2:
2501             tproject = args[1]
2502
2503         r = attribute_branch_pkg(apiurl, maintained_attribute, maintained_update_project_attribute, \
2504                                  package, tproject)
2505
2506         if r is None:
2507             print >>sys.stderr, 'ERROR: Attribute branch call came not back with a project.'
2508             sys.exit(1)
2509
2510         print "Project " + r + " created."
2511
2512         if opts.checkout:
2513             Project.init_project(apiurl, r, r, conf.config['do_package_tracking'])
2514             print statfrmt('A', r)
2515
2516             # all packages
2517             for package in meta_get_packagelist(apiurl, r):
2518                 try:
2519                     checkout_package(apiurl, r, package, expand_link = True, prj_dir = r)
2520                 except:
2521                     print >>sys.stderr, 'Error while checkout package:\n', package
2522
2523             if conf.config['verbose']:
2524                 print 'Note: You can use "osc delete" or "osc submitpac" when done.\n'
2525
2526
2527     @cmdln.alias('branchco')
2528     @cmdln.alias('bco')
2529     @cmdln.alias('getpac')
2530     @cmdln.option('--nodevelproject', action='store_true',
2531                         help='do not follow a defined devel project ' \
2532                              '(primary project where a package is developed)')
2533     @cmdln.option('-c', '--checkout', action='store_true',
2534                         help='Checkout branched package afterwards using "co -e -S"' \
2535                                 '(\'osc bco\' is a shorthand for this option)' )
2536     @cmdln.option('-f', '--force', default=False, action="store_true",
2537                   help='force branch, overwrite target')
2538     @cmdln.option('-m', '--message', metavar='TEXT',
2539                         help='specify message TEXT')
2540     @cmdln.option('-r', '--revision', metavar='rev',
2541                         help='branch against a specific revision')
2542     def do_branch(self, subcmd, opts, *args):
2543         """${cmd_name}: Branch a package
2544
2545         [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration
2546         for information on this topic.]
2547
2548         Create a source link from a package of an existing project to a new
2549         subproject of the requesters home project (home:branches:)
2550
2551         The branched package will live in
2552             home:USERNAME:branches:PROJECT/PACKAGE
2553         if nothing else specified.
2554
2555         With getpac or bco, the branched package will come from one of
2556             %(getpac_default_project)s
2557         (list of projects from oscrc:getpac_default_project)
2558         if nothing else is specfied on the command line.
2559
2560         usage:
2561             osc branch
2562             osc branch SOURCEPROJECT SOURCEPACKAGE
2563             osc branch SOURCEPROJECT SOURCEPACKAGE TARGETPROJECT
2564             osc branch SOURCEPROJECT SOURCEPACKAGE TARGETPROJECT TARGETPACKAGE
2565             osc getpac SOURCEPACKAGE
2566             osc bco ...
2567         ${cmd_option_list}
2568         """
2569
2570         if subcmd == 'getpac' or subcmd == 'branchco' or subcmd == 'bco': opts.checkout = True
2571         args = slash_split(args)
2572         tproject = tpackage = None
2573
2574         if (subcmd == 'getpac' or subcmd == 'bco') and len(args) == 1:
2575             def_p = find_default_project(self.get_api_url(), args[0])
2576             print >>sys.stderr, 'defaulting to %s/%s' % (def_p, args[0])
2577             # python has no args.unshift ???
2578             args = [ def_p, args[0] ]
2579             
2580         if len(args) == 0 and is_package_dir('.'):
2581             args = (store_read_project('.'), store_read_package('.'))
2582
2583         if len(args) < 2 or len(args) > 4:
2584             raise oscerr.WrongArgs('Wrong number of arguments.')
2585
2586         apiurl = self.get_api_url()
2587
2588         expected = 'home:%s:branches:%s' % (conf.get_apiurl_usr(apiurl), args[0])
2589         if len(args) >= 3:
2590             expected = tproject = args[2]
2591         if len(args) >= 4:
2592             tpackage = args[3]
2593
2594         exists, targetprj, targetpkg, srcprj, srcpkg = \
2595                 branch_pkg(apiurl, args[0], args[1],
2596                            nodevelproject=opts.nodevelproject, rev=opts.revision,
2597                            target_project=tproject, target_package=tpackage,
2598                            return_existing=opts.checkout, msg=opts.message or '',
2599                            force=opts.force)
2600         if exists:
2601             print >>sys.stderr, 'Using existing branch project: %s' % targetprj
2602
2603         devloc = None
2604         if not exists and (srcprj != args[0] or srcpkg != args[1]):
2605             try:
2606                 root = ET.fromstring(''.join(show_attribute_meta(apiurl, args[0], None, None,
2607                     conf.config['maintained_update_project_attribute'], False, False)))
2608                 # this might raise an AttributeError
2609                 uproject = root.find('attribute').find('value').text
2610                 print '\nNote: The branch has been created from the configured update project: %s' \
2611                     % uproject
2612             except (AttributeError, urllib2.HTTPError), e:
2613                 devloc = srcprj
2614                 print '\nNote: The branch has been created of a different project,\n' \
2615                       '              %s,\n' \
2616                       '      which is the primary location of where development for\n' \
2617                       '      that package takes place.\n' \
2618                       '      That\'s also where you would normally make changes against.\n' \
2619                       '      A direct branch of the specified package can be forced\n' \
2620                       '      with the --nodevelproject option.\n' % devloc
2621
2622         package = tpackage or args[1]
2623         if opts.checkout:
2624             checkout_package(apiurl, targetprj, package, server_service_files=True,
2625                              expand_link=True, prj_dir=targetprj)
2626             if conf.config['verbose']:
2627                 print 'Note: You can use "osc delete" or "osc submitpac" when done.\n'
2628         else:
2629             apiopt = ''
2630             if conf.get_configParser().get('general', 'apiurl') != apiurl:
2631                 apiopt = '-A %s ' % apiurl
2632             print 'A working copy of the branched package can be checked out with:\n\n' \
2633                   'osc %sco %s/%s' \
2634                       % (apiopt, targetprj, package)
2635         print_request_list(apiurl, args[0], args[1])
2636         if devloc:
2637             print_request_list(apiurl, devloc, srcpkg)
2638
2639
2640     def do_undelete(self, subcmd, opts, *args):
2641         """${cmd_name}: Restores a deleted project or package on the server.
2642
2643         The server restores a package including the sources and meta configuration.
2644         Binaries remain to be lost and will be rebuild.
2645
2646         usage:
2647            osc undelete PROJECT
2648            osc undelete PROJECT PACKAGE [PACKAGE ...]
2649
2650         ${cmd_option_list}
2651         """
2652
2653         args = slash_split(args)
2654         if len(args) < 1:
2655             raise oscerr.WrongArgs('Missing argument.')
2656
2657         apiurl = self.get_api_url()
2658         prj = args[0]
2659         pkgs = args[1:]
2660
2661         if pkgs:
2662             for pkg in pkgs:
2663                 undelete_package(apiurl, prj, pkg)
2664         else:
2665             undelete_project(apiurl, prj)
2666
2667
2668     @cmdln.option('-f', '--force', action='store_true',
2669                         help='deletes a package or an empty project')
2670     def do_rdelete(self, subcmd, opts, *args):
2671         """${cmd_name}: Delete a project or packages on the server.
2672
2673         As a safety measure, project must be empty (i.e., you need to delete all
2674         packages first). Also, packages must have no requests pending (i.e., you need
2675         to accept/revoke such requests first).
2676         If you are sure that you want to remove this project and all
2677         its packages use \'--force\' switch.
2678
2679         usage:
2680            osc rdelete [-f] PROJECT [PACKAGE]
2681
2682         ${cmd_option_list}
2683         """
2684
2685         args = slash_split(args)
2686         if len(args) < 1 or len(args) > 2:
2687             raise oscerr.WrongArgs('Wrong number of arguments')
2688
2689         apiurl = self.get_api_url()
2690         prj = args[0]
2691
2692         # empty arguments result in recursive project delete ...
2693         if not len(prj):
2694             raise oscerr.WrongArgs('Project argument is empty')
2695
2696         if len(args) > 1:
2697             pkg = args[1]
2698
2699             if not len(pkg):
2700                 raise oscerr.WrongArgs('Package argument is empty')
2701
2702             ## FIXME: core.py:commitDelPackage() should have something similar
2703             rlist = get_request_list(apiurl, prj, pkg)
2704             for rq in rlist: print rq
2705             if len(rlist) >= 1 and not opts.force:
2706               print >>sys.stderr, 'Package has pending requests. Deleting the package will break them. '\
2707                                   'They should be accepted/declined/revoked before deleting the package. '\
2708                                   'Or just use \'--force\'.'
2709               sys.exit(1)
2710
2711             delete_package(apiurl, prj, pkg)
2712
2713         elif len(meta_get_packagelist(apiurl, prj)) >= 1 and not opts.force:
2714             print >>sys.stderr, 'Project contains packages. It must be empty before deleting it. ' \
2715                                 'If you are sure that you want to remove this project and all its ' \
2716                                 'packages use the \'--force\' switch.'
2717             sys.exit(1)
2718         else:
2719             delete_project(apiurl, prj)
2720
2721     @cmdln.hide(1)
2722     def do_deletepac(self, subcmd, opts, *args):
2723         print """${cmd_name} is obsolete !
2724
2725                  Please use either
2726                    osc delete       for checked out packages or projects
2727                  or
2728                    osc rdelete      for server side operations."""
2729
2730         sys.exit(1)
2731
2732     @cmdln.hide(1)
2733     @cmdln.option('-f', '--force', action='store_true',
2734                         help='deletes a project and its packages')
2735     def do_deleteprj(self, subcmd, opts, project):
2736         """${cmd_name} is obsolete !
2737
2738                  Please use
2739                    osc rdelete PROJECT
2740         """
2741         sys.exit(1)
2742
2743     @cmdln.alias('metafromspec')
2744     @cmdln.option('', '--specfile', metavar='FILE',
2745                       help='Path to specfile. (if you pass more than working copy this option is ignored)')
2746     def do_updatepacmetafromspec(self, subcmd, opts, *args):
2747         """${cmd_name}: Update package meta information from a specfile
2748
2749         ARG, if specified, is a package working copy.
2750
2751         ${cmd_usage}
2752         ${cmd_option_list}
2753         """
2754
2755         args = parseargs(args)
2756         if opts.specfile and len(args) == 1:
2757             specfile = opts.specfile
2758         else:
2759             specfile = None
2760         pacs = findpacs(args)
2761         for p in pacs:
2762             p.read_meta_from_spec(specfile)
2763             p.update_package_meta()
2764
2765
2766     @cmdln.alias('linkdiff')
2767     @cmdln.alias('ldiff')
2768     @cmdln.alias('di')
2769     @cmdln.option('-c', '--change', metavar='rev',
2770                         help='the change made by revision rev (like -r rev-1:rev).'
2771                              'If rev is negative this is like -r rev:rev-1.')
2772     @cmdln.option('-r', '--revision', metavar='rev1[:rev2]',
2773                         help='If rev1 is specified it will compare your working copy against '
2774                              'the revision (rev1) on the server. '
2775                              'If rev1 and rev2 are specified it will compare rev1 against rev2 '
2776                              '(NOTE: changes in your working copy are ignored in this case)')
2777     @cmdln.option('-p', '--plain', action='store_true',
2778                         help='output the diff in plain (not unified) diff format')
2779     @cmdln.option('-l', '--link', action='store_true',
2780                         help='(osc linkdiff): compare against the base revision of the link')
2781     @cmdln.option('--missingok', action='store_true',
2782                         help='do not fail if the source or target project/package does not exist on the server')
2783     def do_diff(self, subcmd, opts, *args):
2784         """${cmd_name}: Generates a diff
2785
2786         Generates a diff, comparing local changes against the repository
2787         server.
2788
2789         ${cmd_usage}
2790                 ARG, if specified, is a filename to include in the diff.
2791                 Default: all files.
2792
2793             osc diff --link
2794             osc linkdiff                
2795                 Compare current checkout directory against the link base.
2796
2797             osc diff --link PROJ PACK      
2798             osc linkdiff PROJ PACK      
2799                 Compare a package against the link base (ignoring working copy changes).
2800
2801         ${cmd_option_list}
2802         """
2803
2804         if (subcmd == 'ldiff' or subcmd == 'linkdiff'):
2805             opts.link = True
2806         args = parseargs(args)
2807         
2808         pacs = None
2809         if not opts.link or not len(args) == 2:
2810             pacs = findpacs(args)
2811
2812
2813         if opts.link:
2814             query = { 'rev': 'latest' }
2815             if pacs:
2816                 u = makeurl(pacs[0].apiurl, ['source', pacs[0].prjname, pacs[0].name], query=query)
2817             else:
2818                 u = makeurl(self.get_api_url(), ['source', args[0], args[1]], query=query)
2819             f = http_GET(u)
2820             root = ET.parse(f).getroot()
2821             linkinfo = root.find('linkinfo')
2822             if linkinfo == None:
2823                 raise oscerr.APIError('package is not a source link')
2824             baserev = linkinfo.get('baserev')
2825             opts.revision = baserev
2826             if pacs:
2827                 print "diff working copy against linked revision %s\n" % baserev
2828             else:
2829                 print "diff commited package against linked revision %s\n" % baserev
2830                 run_pager(server_diff(self.get_api_url(), args[0], args[1], baserev, 
2831                   args[0], args[1], linkinfo.get('lsrcmd5'), not opts.plain, opts.missingok))
2832                 return
2833
2834         if opts.change:
2835             try:
2836                 rev = int(opts.change)
2837                 if rev > 0:
2838                     rev1 = rev - 1
2839                     rev2 = rev
2840                 elif rev < 0:
2841                     rev1 = -rev
2842                     rev2 = -rev - 1
2843                 else:
2844                     return
2845             except:
2846                 print >>sys.stderr, 'Revision \'%s\' not an integer' % opts.change
2847                 return
2848         else:
2849             rev1, rev2 = parseRevisionOption(opts.revision)
2850         diff = ''
2851         for pac in pacs:
2852             if not rev2:
2853                 for i in pac.get_diff(rev1):
2854                     sys.stdout.write(''.join(i))
2855             else:
2856                 diff += server_diff_noex(pac.apiurl, pac.prjname, pac.name, rev1,
2857                                     pac.prjname, pac.name, rev2, not opts.plain, opts.missingok)
2858         run_pager(diff)
2859
2860
2861     @cmdln.option('--oldprj', metavar='OLDPRJ',
2862                   help='project to compare against'
2863                   ' (deprecated, use 3 argument form)')
2864     @cmdln.option('--oldpkg', metavar='OLDPKG',
2865                   help='package to compare against'
2866                   ' (deprecated, use 3 argument form)')
2867     @cmdln.option('-M', '--meta', action='store_true',
2868                         help='diff meta data')
2869     @cmdln.option('-r', '--revision', metavar='N[:M]',
2870                   help='revision id, where N = old revision and M = new revision')
2871     @cmdln.option('-p', '--plain', action='store_true',
2872                   help='output the diff in plain (not unified) diff format')
2873     @cmdln.option('-c', '--change', metavar='rev',
2874                         help='the change made by revision rev (like -r rev-1:rev). '
2875                              'If rev is negative this is like -r rev:rev-1.')
2876     @cmdln.option('--missingok', action='store_true',
2877                         help='do not fail if the source or target project/package does not exist on the server')
2878     @cmdln.option('-u', '--unexpand', action='store_true',
2879                         help='diff unexpanded version if sources are linked')
2880     def do_rdiff(self, subcmd, opts, *args):
2881         """${cmd_name}: Server-side "pretty" diff of two packages
2882
2883         Compares two packages (three or four arguments) or shows the
2884         changes of a specified revision of a package (two arguments)
2885
2886         If no revision is specified the latest revision is used.
2887
2888         Note that this command doesn't return a normal diff (which could be
2889         applied as patch), but a "pretty" diff, which also compares the content
2890         of tarballs.
2891
2892
2893         usage:
2894             osc ${cmd_name} OLDPRJ OLDPAC NEWPRJ [NEWPAC]
2895             osc ${cmd_name} PROJECT PACKAGE
2896         ${cmd_option_list}
2897         """
2898
2899         args = slash_split(args)
2900         apiurl = self.get_api_url()
2901
2902         rev1 = None
2903         rev2 = None
2904
2905         old_project = None
2906         old_package = None
2907         new_project = None
2908         new_package = None
2909
2910         if len(args) == 2:
2911             new_project = args[0]
2912             new_package = args[1]
2913             if opts.oldprj:
2914                 old_project = opts.oldprj
2915             if opts.oldpkg:
2916                 old_package = opts.oldpkg
2917         elif len(args) == 3 or len(args) == 4:
2918             if opts.oldprj or opts.oldpkg:
2919                 raise oscerr.WrongArgs('--oldpkg and --oldprj are only valid with two arguments')
2920             old_project = args[0]
2921             new_package = old_package = args[1]
2922             new_project = args[2]
2923             if len(args) == 4:
2924                 new_package = args[3]
2925         elif len(args) == 1 and opts.meta:
2926             new_project = args[0]
2927             new_package = '_project'
2928         else:
2929             raise oscerr.WrongArgs('Wrong number of arguments')
2930
2931         if opts.meta:
2932             opts.unexpand = True
2933
2934         if opts.change:
2935             try:
2936                 rev = int(opts.change)
2937                 if rev > 0:
2938                     rev1 = rev - 1
2939                     rev2 = rev
2940                 elif rev < 0:
2941                     rev1 = -rev
2942                     rev2 = -rev - 1
2943                 else:
2944                     return
2945             except:
2946                 print >>sys.stderr, 'Revision \'%s\' not an integer' % opts.change
2947                 return
2948         else:
2949             if opts.revision:
2950                 rev1, rev2 = parseRevisionOption(opts.revision)
2951
2952         rdiff = server_diff_noex(apiurl,
2953                             old_project, old_package, rev1,
2954                             new_project, new_package, rev2, not opts.plain, opts.missingok,
2955                             meta=opts.meta,
2956                             expand=not opts.unexpand)
2957
2958         run_pager(rdiff)
2959
2960     @cmdln.hide(1)
2961     @cmdln.alias('in')
2962     def do_install(self, subcmd, opts, *args):
2963         """${cmd_name}: install a package after build via zypper in -r
2964
2965         Not implemented here.  Please try 
2966         http://software.opensuse.org/search?q=osc-plugin-install&include_home=true
2967
2968
2969         ${cmd_usage}
2970         ${cmd_option_list}
2971         """
2972
2973         args = slash_split(args)
2974         args = expand_proj_pack(args)
2975
2976         ## FIXME:
2977         ## if there is only one argument, and it ends in .ymp
2978         ## then fetch it, Parse XML to get the first
2979         ##  metapackage.group.repositories.repository.url
2980         ## and construct zypper cmd's for all
2981         ##  metapackage.group.software.item.name
2982         ##
2983         ## if args[0] is already an url, the use it as is.
2984
2985         cmd = "sudo zypper -p http://download.opensuse.org/repositories/%s/%s --no-refresh -v in %s" % (re.sub(':',':/',args[0]), 'openSUSE_11.4', args[1])
2986         print self.do_install.__doc__
2987         print "Example: \n" + cmd
2988
2989
2990     def do_repourls(self, subcmd, opts, *args):
2991         """${cmd_name}: Shows URLs of .repo files
2992
2993         Shows URLs on which to access the project .repos files (yum-style
2994         metadata) on download.opensuse.org.
2995
2996         usage:
2997            osc repourls [PROJECT]
2998
2999         ${cmd_option_list}
3000         """
3001
3002         apiurl = self.get_api_url()
3003
3004         if len(args) == 1:
3005             project = args[0]
3006         elif len(args) == 0:
3007             project = store_read_project('.')
3008         else:
3009             raise oscerr.WrongArgs('Wrong number of arguments')
3010
3011         # XXX: API should somehow tell that
3012         url_tmpl = 'http://download.opensuse.org/repositories/%s/%s/%s.repo'
3013         repos = get_repositories_of_project(apiurl, project)
3014         for repo in repos:
3015             print url_tmpl % (project.replace(':', ':/'), repo, project)
3016
3017
3018     @cmdln.option('-r', '--revision', metavar='rev',
3019                         help='checkout the specified revision. '
3020                              'NOTE: if you checkout the complete project '
3021                              'this option is ignored!')
3022     @cmdln.option('-e', '--expand-link', action='store_true',
3023                         help='if a package is a link, check out the expanded '
3024                              'sources (no-op, since this became the default)')
3025     @cmdln.option('-u', '--unexpand-link', action='store_true',
3026                         help='if a package is a link, check out the _link file ' \
3027                              'instead of the expanded sources')
3028     @cmdln.option('-M', '--meta', action='store_true',
3029                         help='checkout out meta data instead of sources' )
3030     @cmdln.option('-c', '--current-dir', action='store_true',
3031                         help='place PACKAGE folder in the current directory' \
3032                              'instead of a PROJECT/PACKAGE directory')
3033     @cmdln.option('-s', '--source-service-files', action='store_true',
3034                         help='Run source services.' )
3035     @cmdln.option('-S', '--server-side-source-service-files', action='store_true',
3036                         help='Use server side generated sources instead of local generation.' )
3037     @cmdln.option('-l', '--limit-size', metavar='limit_size',
3038                         help='Skip all files with a given size')
3039     @cmdln.alias('co')
3040     def do_checkout(self, subcmd, opts, *args):
3041         """${cmd_name}: Check out content from the repository
3042
3043         Check out content from the repository server, creating a local working
3044         copy.
3045
3046         When checking out a single package, the option --revision can be used
3047         to specify a revision of the package to be checked out.
3048
3049         When a package is a source link, then it will be checked out in
3050         expanded form. If --unexpand-link option is used, the checkout will
3051         instead produce the raw _link file plus patches.
3052
3053         usage:
3054             osc co PROJECT [PACKAGE] [FILE]
3055                osc co PROJECT                    # entire project
3056                osc co PROJECT PACKAGE            # a package
3057                osc co PROJECT PACKAGE FILE       # single file -> to current dir
3058
3059             while inside a project directory:
3060                osc co PACKAGE                    # check out PACKAGE from project
3061             
3062             with the result of rpm -q --qf '%%{disturl}n' PACKAGE
3063                osc co obs://API/PROJECT/PLATFORM/REVISION-PACKAGE       
3064
3065         ${cmd_option_list}
3066         """
3067
3068         if opts.unexpand_link:
3069             expand_link = False
3070         else:
3071             expand_link = True
3072
3073         # XXX: this too openSUSE-setup specific...
3074         # FIXME: this should go into ~jw/patches/osc/osc.proj_pack_20101201.diff 
3075         #        to be available to all subcommands via @cmdline.prep(proj_pack)
3076         # obs://build.opensuse.org/openSUSE:11.3/standard/fc6c25e795a89503e99d59da5dc94a79-screen
3077         m = re.match(r"obs://([^/]+)/(\S+)/([^/]+)/([A-Fa-f\d]+)\-(\S+)", args[0])
3078         if m and len(args) == 1:
3079             apiurl   = "https://" + m.group(1)
3080             project = project_dir = m.group(2)
3081             # platform            = m.group(3)
3082             opts.revision         = m.group(4)
3083             package               = m.group(5)
3084             apiurl = apiurl.replace('/build.', '/api.')
3085             filename = None
3086         else:
3087             args = slash_split(args)
3088             project = package = filename = None
3089             apiurl = self.get_api_url()
3090             try:
3091                 project = project_dir = args[0]
3092                 package = args[1]
3093                 filename = args[2]
3094             except:
3095                 pass
3096
3097             if len(args) == 1 and is_project_dir(os.curdir):
3098                 project = store_read_project(os.curdir)
3099                 project_dir = os.curdir
3100                 package = args[0]
3101
3102         rev, dummy = parseRevisionOption(opts.revision)
3103         if rev==None:
3104             rev="latest"
3105
3106         if rev and rev != "latest" and not checkRevision(project, package, rev):
3107             print >>sys.stderr, 'Revision \'%s\' does not exist' % rev
3108             sys.exit(1)
3109
3110         if filename:
3111             # Note: same logic as with 'osc cat' (not 'osc ls', which never merges!)
3112             if expand_link:
3113                 rev = show_upstream_srcmd5(apiurl, project, package, expand=True, revision=rev)
3114             get_source_file(apiurl, project, package, filename, revision=rev, progress_obj=self.download_progress)
3115
3116         elif package:
3117             if opts.current_dir:
3118    &n