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