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