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