allow --force branch, even when target already exists
[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 decline [-m TEXT] ID
1107             osc request revoke [-m TEXT] ID
1108             osc request wipe ID
1109             osc request checkout/co ID
1110             osc review accept [-m TEXT] ID
1111             osc review decline [-m TEXT] ID
1112         ${cmd_option_list}
1113         """
1114
1115         args = slash_split(args)
1116
1117         if opts.all and opts.state:
1118             raise oscerr.WrongOptions('Sorry, the options \'--all\' and \'--state\' ' \
1119                     'are mutually exclusive.')
1120         if opts.mine and opts.user:
1121             raise oscerr.WrongOptions('Sorry, the options \'--user\' and \'--mine\' ' \
1122                     'are mutually exclusive.')
1123         if opts.interactive and opts.non_interactive:
1124             raise oscerr.WrongOptions('Sorry, the options \'--interactive\' and ' \
1125                     '\'--non-interactive\' are mutually exclusive')
1126
1127         if not args:
1128             args = [ 'list' ]
1129             opts.mine = 1
1130             if opts.state == '':
1131                 opts.state = 'all'
1132
1133         if opts.state == '':
1134             opts.state = 'new'
1135
1136         cmds = ['list', 'log', 'show', 'decline', 'accept', 'wipe', 'revoke', 'checkout', 'co', 'help']
1137         if not args or args[0] not in cmds:
1138             raise oscerr.WrongArgs('Unknown request action %s. Choose one of %s.' \
1139                                                % (args[0],', '.join(cmds)))
1140
1141         cmd = args[0]
1142         del args[0]
1143
1144         if cmd == 'help':
1145             return self.do_help(['help', 'request'])
1146
1147         if cmd in ['wipe']:
1148             min_args, max_args = 1, 1
1149         elif 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':
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
1178         if cmd == 'list':
1179             states = ('new', 'accepted', 'revoked', 'declined')
1180             state_list = opts.state.split(',')
1181             if opts.state == 'all':
1182                 state_list = ['all']
1183             else:
1184                 for s in state_list:
1185                     if not s in states:
1186                         raise oscerr.WrongArgs('Unknown state \'%s\', try one of %s' % (s, ','.join(states)))
1187             who = ''
1188             if opts.mine:
1189                 who = conf.get_apiurl_usr(apiurl)
1190             if opts.user:
1191                 who = opts.user
1192             if opts.all:
1193                 state_list = ['all']
1194
1195             ## FIXME -B not implemented!
1196             if opts.bugowner:
1197                 if (self.options.debug):
1198                     print 'list: option --bugowner ignored: not impl.'
1199
1200             if opts.involved_projects:
1201                 who = who or conf.get_apiurl_usr(apiurl)
1202                 results = get_user_projpkgs_request_list(apiurl, who, req_state=state_list,
1203                                                          req_type=opts.type, exclude_projects=opts.exclude_target_project or [])
1204             else:
1205                 results = get_request_list(apiurl, project, package, who,
1206                                            state_list, opts.type, opts.exclude_target_project or [])
1207             results.sort(reverse=True)
1208             import time
1209             days = opts.days or conf.config['request_list_days']
1210             since = ''
1211             try:
1212                 days = int(days)
1213             except ValueError:
1214                 days = 0
1215             if days > 0:
1216                 since = time.strftime('%Y-%m-%dT%H:%M:%S',time.localtime(time.time()-days*24*3600))
1217
1218             skipped = 0
1219             ## bs has received 2009-09-20 a new xquery compare() function
1220             ## which allows us to limit the list inside of get_request_list
1221             ## That would be much faster for coolo. But counting the remainder
1222             ## would not be possible with current xquery implementation.
1223             ## Workaround: fetch all, and filter on client side.
1224
1225             ## FIXME: date filtering should become implemented on server side
1226             for result in results:
1227                 if days == 0 or result.state.when > since or result.state.name == 'new':
1228                     print result.list_view()
1229                 else:
1230                     skipped += 1
1231             if skipped:
1232                 print "There are %d requests older than %s days.\n" % (skipped, days)
1233
1234         elif cmd == 'log':
1235             for l in get_request_log(conf.config['apiurl'], reqid):
1236                 print l
1237
1238         # show
1239         elif cmd == 'show':
1240             r = get_request(conf.config['apiurl'], reqid)
1241             if opts.brief:
1242                 print r.list_view()
1243             elif (opts.interactive or conf.config['request_show_interactive']) and not opts.non_interactive:
1244                 return request_interactive_review(conf.config['apiurl'], r)
1245             else:
1246                 print r
1247             # fixme: will inevitably fail if the given target doesn't exist
1248             if opts.diff and r.actions[0].type != 'submit':
1249                 raise oscerr.WrongOptions('\'--diff\' is not possible for request type: \'%s\'' % r.actions[0].type)
1250             elif opts.diff:
1251                 try:
1252                     print server_diff(conf.config['apiurl'],
1253                                       r.actions[0].dst_project, r.actions[0].dst_package, None,
1254                                       r.actions[0].src_project, r.actions[0].src_package, r.actions[0].src_rev, opts.unified, True)
1255                 except urllib2.HTTPError, e:
1256                     if e.code != 400:
1257                         e.osc_msg = 'Diff not possible'
1258                         raise e
1259                     # backward compatiblity: only a recent api/backend supports the missingok parameter
1260                     try:
1261                         print server_diff(conf.config['apiurl'],
1262                                           r.actions[0].dst_project, r.actions[0].dst_package, None,
1263                                           r.actions[0].src_project, r.actions[0].src_package, r.actions[0].src_rev, opts.unified, False)
1264                     except urllib2.HTTPError, e:
1265                         e.osc_msg = 'Diff not possible'
1266                         raise
1267
1268         # checkout
1269         elif cmd == 'checkout' or cmd == 'co':
1270             r = get_request(conf.config['apiurl'], reqid)
1271             submits = [ i for i in r.actions if i.type == 'submit' ]
1272             if not len(submits):
1273                 raise oscerr.WrongArgs('\'checkout\' only works for \'submit\' requests')
1274             checkout_package(conf.config['apiurl'], submits[0].src_project, submits[0].src_package, \
1275                 submits[0].src_rev, expand_link=True, prj_dir=submits[0].src_project)
1276
1277         else:
1278             if not opts.message:
1279                 opts.message = edit_message()
1280             state_map = {'accept' : 'accepted', 'decline' : 'declined', 'wipe' : 'deleted', 'revoke' : 'revoked'}
1281             # Change review state only
1282             if subcmd == 'review':
1283                 if cmd in ['accept', 'decline']:
1284                     r = change_review_state(conf.config['apiurl'],
1285                             reqid, state_map[cmd], conf.config['user'], '', opts.message or '')
1286                     print r
1287             # Change state of entire request
1288             elif cmd in ['accept', 'decline', 'wipe', 'revoke']:
1289                 r = change_request_state(conf.config['apiurl'],
1290                         reqid, state_map[cmd], opts.message or '')
1291                 print r
1292
1293     # editmeta and its aliases are all depracated
1294     @cmdln.alias("editprj")
1295     @cmdln.alias("createprj")
1296     @cmdln.alias("editpac")
1297     @cmdln.alias("createpac")
1298     @cmdln.alias("edituser")
1299     @cmdln.alias("usermeta")
1300     @cmdln.hide(1)
1301     def do_editmeta(self, subcmd, opts, *args):
1302         """${cmd_name}:
1303
1304         Obsolete command to edit metadata. Use 'meta' now.
1305
1306         See the help output of 'meta'.
1307
1308         """
1309
1310         print >>sys.stderr, 'This command is obsolete. Use \'osc meta <metatype> ...\'.'
1311         print >>sys.stderr, 'See \'osc help meta\'.'
1312         #self.do_help([None, 'meta'])
1313         return 2
1314
1315
1316     @cmdln.option('-r', '--revision', metavar='rev',
1317                   help='use the specified revision.')
1318     @cmdln.option('-u', '--unset', action='store_true',
1319                   help='remove revision in link, it will point always to latest revision')
1320     def do_setlinkrev(self, subcmd, opts, *args):
1321         """${cmd_name}: Updates a revision number in a source link.
1322
1323         This command adds or updates a specified revision number in a source link.
1324         The current revision of the source is used, if no revision number is specified.
1325
1326         usage:
1327             osc setlinkrev
1328             osc setlinkrev PROJECT [PACKAGE]
1329         ${cmd_option_list}
1330         """
1331
1332         args = slash_split(args)
1333         apiurl = conf.config['apiurl']
1334         package = None
1335         if len(args) == 0:
1336             p = findpacs(os.curdir)[0]
1337             project = p.prjname
1338             package = p.name
1339             apiurl = p.apiurl
1340             if not p.islink():
1341                 sys.exit('Local directory is no checked out source link package, aborting')
1342         elif len(args) == 2:
1343             project = args[0]
1344             package = args[1]
1345         elif len(args) == 1:
1346             project = args[0]
1347         else:
1348             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
1349                   + self.get_cmd_help('setlinkrev'))
1350
1351         if package:
1352             packages = [ package ]
1353         else:
1354             packages = meta_get_packagelist(apiurl, project)
1355
1356         for p in packages:
1357             print "setting revision for package", p
1358             if opts.unset:
1359                 rev=-1
1360             else:
1361                 rev, dummy = parseRevisionOption(opts.revision)
1362             set_link_rev(apiurl, project, p, rev)
1363
1364
1365     def do_linktobranch(self, subcmd, opts, *args):
1366         """${cmd_name}: Convert a package containing a classic link with patch to a branch
1367
1368         This command tells the server to convert a _link with or without a project.diff
1369         to a branch. This is a full copy with a _link file pointing to the branched place.
1370
1371         usage:
1372             osc linktobranch                    # can be used in checked out package
1373             osc linktobranch PROJECT PACKAGE
1374         ${cmd_option_list}
1375         """
1376         args = slash_split(args)
1377         apiurl = self.get_api_url()
1378
1379         if len(args) == 0:
1380             wd = os.curdir
1381             project = store_read_project(wd)
1382             package = store_read_package(wd)
1383             update_local_dir = True
1384         elif len(args) < 2:
1385             raise oscerr.WrongArgs('Too few arguments (required none or two)')
1386         elif len(args) > 2:
1387             raise oscerr.WrongArgs('Too many arguments (required none or two)')
1388         else:
1389             project = args[0]
1390             package = args[1]
1391             update_local_dir = False
1392
1393         # execute
1394         link_to_branch(apiurl, project, package)
1395         if update_local_dir:
1396             pac = Package(wd)
1397             pac.update(rev=pac.latest_rev())
1398
1399
1400     @cmdln.option('-C', '--cicount', choices=['add', 'copy', 'local'],
1401                   help='cicount attribute in the link, known values are add, copy, and local, default in buildservice is currently add.')
1402     @cmdln.option('-c', '--current', action='store_true',
1403                   help='link fixed against current revision.')
1404     @cmdln.option('-r', '--revision', metavar='rev',
1405                   help='link the specified revision.')
1406     @cmdln.option('-f', '--force', action='store_true',
1407                   help='overwrite an existing link file if it is there.')
1408     @cmdln.option('-d', '--disable-publish', action='store_true',
1409                   help='disable publishing of the linked package')
1410     def do_linkpac(self, subcmd, opts, *args):
1411         """${cmd_name}: "Link" a package to another package
1412
1413         A linked package is a clone of another package, but plus local
1414         modifications. It can be cross-project.
1415
1416         The DESTPAC name is optional; the source packages' name will be used if
1417         DESTPAC is omitted.
1418
1419         Afterwards, you will want to 'checkout DESTPRJ DESTPAC'.
1420
1421         To add a patch, add the patch as file and add it to the _link file.
1422         You can also specify text which will be inserted at the top of the spec file.
1423
1424         See the examples in the _link file.
1425
1426         usage:
1427             osc linkpac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
1428         ${cmd_option_list}
1429         """
1430
1431         args = slash_split(args)
1432
1433         if not args or len(args) < 3:
1434             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
1435                   + self.get_cmd_help('linkpac'))
1436
1437         rev, dummy = parseRevisionOption(opts.revision)
1438
1439         src_project = args[0]
1440         src_package = args[1]
1441         dst_project = args[2]
1442         if len(args) > 3:
1443             dst_package = args[3]
1444         else:
1445             dst_package = src_package
1446
1447         if src_project == dst_project and src_package == dst_package:
1448             raise oscerr.WrongArgs('Error: source and destination are the same.')
1449
1450         if src_project == dst_project and not opts.cicount:
1451             # in this case, the user usually wants to build different spec
1452             # files from the same source
1453             opts.cicount = "copy"
1454
1455         if opts.current:
1456             rev = show_upstream_rev(conf.config['apiurl'], src_project, src_package)
1457
1458         if rev and not checkRevision(src_project, src_package, rev):
1459             print >>sys.stderr, 'Revision \'%s\' does not exist' % rev
1460             sys.exit(1)
1461
1462         link_pac(src_project, src_package, dst_project, dst_package, opts.force, rev, opts.cicount, opts.disable_publish)
1463
1464     @cmdln.option('-m', '--map-repo', metavar='SRC=TARGET[,SRC=TARGET]',
1465                   help='Allows repository mapping(s) to be given as SRC=TARGET[,SRC=TARGET]')
1466     @cmdln.option('-d', '--disable-publish', action='store_true',
1467                   help='disable publishing of the aggregated package')
1468     def do_aggregatepac(self, subcmd, opts, *args):
1469         """${cmd_name}: "Aggregate" a package to another package
1470
1471         Aggregation of a package means that the build results (binaries) of a
1472         package are basically copied into another project.
1473         This can be used to make packages available from building that are
1474         needed in a project but available only in a different project. Note
1475         that this is done at the expense of disk space. See
1476         http://en.opensuse.org/Build_Service/Tips_and_Tricks#_link_and__aggregate
1477         for more information.
1478
1479         The DESTPAC name is optional; the source packages' name will be used if
1480         DESTPAC is omitted.
1481
1482         usage:
1483             osc aggregatepac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
1484         ${cmd_option_list}
1485         """
1486
1487         args = slash_split(args)
1488
1489         if not args or len(args) < 3:
1490             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
1491                   + self.get_cmd_help('aggregatepac'))
1492
1493         src_project = args[0]
1494         src_package = args[1]
1495         dst_project = args[2]
1496         if len(args) > 3:
1497             dst_package = args[3]
1498         else:
1499             dst_package = src_package
1500
1501         if src_project == dst_project and src_package == dst_package:
1502             raise oscerr.WrongArgs('Error: source and destination are the same.')
1503
1504         repo_map = {}
1505         if opts.map_repo:
1506             for pair in opts.map_repo.split(','):
1507                 src_tgt = pair.split('=')
1508                 if len(src_tgt) != 2:
1509                     raise oscerr.WrongOptions('map "%s" must be SRC=TARGET[,SRC=TARGET]' % opts.map_repo)
1510                 repo_map[src_tgt[0]] = src_tgt[1]
1511
1512         aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map, opts.disable_publish)
1513
1514
1515     @cmdln.option('-c', '--client-side-copy', action='store_true',
1516                         help='do a (slower) client-side copy')
1517     @cmdln.option('-k', '--keep-maintainers', action='store_true',
1518                         help='keep original maintainers. Default is remove all and replace with the one calling the script.')
1519     @cmdln.option('-d', '--keep-develproject', action='store_true',
1520                         help='keep develproject tag in the package metadata')
1521     @cmdln.option('-r', '--revision', metavar='rev',
1522                         help='link the specified revision.')
1523     @cmdln.option('-t', '--to-apiurl', metavar='URL',
1524                         help='URL of destination api server. Default is the source api server.')
1525     @cmdln.option('-m', '--message', metavar='TEXT',
1526                   help='specify message TEXT')
1527     @cmdln.option('-e', '--expand', action='store_true',
1528                         help='if the source package is a link then copy the expanded version of the link')
1529     def do_copypac(self, subcmd, opts, *args):
1530         """${cmd_name}: Copy a package
1531
1532         A way to copy package to somewhere else.
1533
1534         It can be done across buildservice instances, if the -t option is used.
1535         In that case, a client-side copy is implied.
1536
1537         Using --client-side-copy always involves downloading all files, and
1538         uploading them to the target.
1539
1540         The DESTPAC name is optional; the source packages' name will be used if
1541         DESTPAC is omitted.
1542
1543         usage:
1544             osc copypac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
1545         ${cmd_option_list}
1546         """
1547
1548         args = slash_split(args)
1549
1550         if not args or len(args) < 3:
1551             raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
1552                   + self.get_cmd_help('copypac'))
1553
1554         src_project = args[0]
1555         src_package = args[1]
1556         dst_project = args[2]
1557         if len(args) > 3:
1558             dst_package = args[3]
1559         else:
1560             dst_package = src_package
1561
1562         src_apiurl = conf.config['apiurl']
1563         if opts.to_apiurl:
1564             dst_apiurl = conf.config['apiurl_aliases'].get(opts.to_apiurl, opts.to_apiurl)
1565         else:
1566             dst_apiurl = src_apiurl
1567
1568         if src_apiurl != dst_apiurl:
1569             opts.client_side_copy = True
1570
1571         rev, dummy = parseRevisionOption(opts.revision)
1572
1573         if opts.message:
1574             comment = opts.message
1575         else:
1576             if not rev:
1577                 rev = show_upstream_rev(src_apiurl, src_project, src_package)
1578             comment = 'osc copypac from project:%s package:%s revision:%s' % ( src_project, src_package, rev )
1579
1580         if src_project == dst_project and \
1581            src_package == dst_package and \
1582            not rev and \
1583            src_apiurl == dst_apiurl:
1584             raise oscerr.WrongArgs('Source and destination are the same.')
1585
1586         r = copy_pac(src_apiurl, src_project, src_package,
1587                      dst_apiurl, dst_project, dst_package,
1588                      client_side_copy=opts.client_side_copy,
1589                      keep_maintainers=opts.keep_maintainers,
1590                      keep_develproject=opts.keep_develproject,
1591                      expand=opts.expand,
1592                      revision=rev,
1593                      comment=comment)
1594         print r
1595
1596
1597     @cmdln.option('-c', '--checkout', action='store_true',
1598                         help='Checkout branched package afterwards ' \
1599                                 '(\'osc bco\' is a shorthand for this option)' )
1600     @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
1601                         help='Use this attribute to find affected packages (default is OBS:Maintained)')
1602     @cmdln.option('-u', '--update-project-attribute', metavar='UPDATE_ATTRIBUTE',
1603                         help='Use this attribute to find update projects (default is OBS:UpdateProject) ')
1604     def do_mbranch(self, subcmd, opts, *args):
1605         """${cmd_name}: Multiple branch of a package
1606
1607         [See http://en.opensuse.org/Build_Service/Concepts/Maintenance for information
1608         on this topic.]
1609
1610         This command is used for creating multiple links of defined version of a package
1611         in one project. This is esp. used for maintenance updates.
1612
1613         The branched package will live in
1614             home:USERNAME:branches:ATTRIBUTE:PACKAGE
1615         if nothing else specified.
1616
1617         usage:
1618             osc mbranch [ SOURCEPACKAGE [ TARGETPROJECT ] ]
1619         ${cmd_option_list}
1620         """
1621         args = slash_split(args)
1622         tproject = None
1623
1624         maintained_attribute = conf.config['maintained_attribute']
1625         maintained_update_project_attribute = conf.config['maintained_update_project_attribute']
1626
1627         if not len(args) or len(args) > 2:
1628             raise oscerr.WrongArgs('Wrong number of arguments.')
1629         if len(args) >= 1:
1630             package = args[0]
1631         if len(args) >= 2:
1632             tproject = args[1]
1633
1634         r = attribute_branch_pkg(conf.config['apiurl'], maintained_attribute, maintained_update_project_attribute, \
1635                                  package, tproject)
1636
1637         if r is None:
1638             print >>sys.stderr, 'ERROR: Attribute branch call came not back with a project.'
1639             sys.exit(1)
1640
1641         print "Project " + r + " created."
1642
1643         if opts.checkout:
1644             init_project_dir(conf.config['apiurl'], r, r)
1645             print statfrmt('A', r)
1646
1647             # all packages
1648             for package in meta_get_packagelist(conf.config['apiurl'], r):
1649                 try:
1650                     checkout_package(conf.config['apiurl'], r, package, expand_link = True, prj_dir = r)
1651                 except:
1652                     print >>sys.stderr, 'Error while checkout package:\n', package
1653
1654             if conf.config['verbose']:
1655                 print 'Note: You can use "osc delete" or "osc submitpac" when done.\n'
1656
1657
1658     @cmdln.alias('branchco')
1659     @cmdln.alias('bco')
1660     @cmdln.alias('getpac')
1661     @cmdln.option('--nodevelproject', action='store_true',
1662                         help='do not follow a defined devel project ' \
1663                              '(primary project where a package is developed)')
1664     @cmdln.option('-c', '--checkout', action='store_true',
1665                         help='Checkout branched package afterwards ' \
1666                                 '(\'osc bco\' is a shorthand for this option)' )
1667     @cmdln.option('-f', '--force', default=False, action="store_true",
1668                   help='force commit - do not tests a file list')
1669     @cmdln.option('-m', '--message', metavar='TEXT',
1670                         help='specify message TEXT')
1671     @cmdln.option('-r', '--revision', metavar='rev',
1672                         help='branch against a specific revision')
1673     def do_branch(self, subcmd, opts, *args):
1674         """${cmd_name}: Branch a package
1675
1676         [See http://en.opensuse.org/Build_Service/Collaboration for information
1677         on this topic.]
1678
1679         Create a source link from a package of an existing project to a new
1680         subproject of the requesters home project (home:branches:)
1681
1682         The branched package will live in
1683             home:USERNAME:branches:PROJECT/PACKAGE
1684         if nothing else specified.
1685
1686         With getpac or bco, the branched package will come from
1687             %(getpac_default_project)s
1688         if nothing else specified.
1689
1690         usage:
1691             osc branch
1692             osc branch SOURCEPROJECT SOURCEPACKAGE
1693             osc branch SOURCEPROJECT SOURCEPACKAGE TARGETPROJECT
1694             osc branch SOURCEPROJECT SOURCEPACKAGE TARGETPROJECT TARGETPACKAGE
1695             osc getpac  SOURCEPACKAGE
1696             osc bco ...
1697         ${cmd_option_list}
1698         """
1699
1700         if subcmd == 'getpac' or subcmd == 'branchco' or subcmd == 'bco': opts.checkout = True
1701         args = slash_split(args)
1702         tproject = tpackage = None
1703
1704         if (subcmd == 'getpac' or subcmd == 'bco') and len(args) == 1:
1705             print >>sys.stderr, 'defaulting to %s/%s' % (conf.config['getpac_default_project'], args[0])
1706             # python has no args.unshift ???
1707             args = [ conf.config['getpac_default_project'] , args[0] ]
1708             
1709         if len(args) == 0 and is_package_dir('.'):
1710             args = (store_read_project('.'), store_read_package('.'))
1711
1712         if len(args) < 2 or len(args) > 4:
1713             raise oscerr.WrongArgs('Wrong number of arguments.')
1714
1715         expected = 'home:%s:branches:%s' % (conf.config['user'], args[0])
1716         if len(args) >= 3:
1717             expected = tproject = args[2]
1718         if len(args) >= 4:
1719             tpackage = args[3]
1720
1721         if not opts.message:
1722                 footer='please specify the purpose of your branch'
1723                 template='This package was branched from %s in order to ...\n' % args[0]
1724                 opts.message = edit_message(footer, template)
1725
1726         exists, targetprj, targetpkg, srcprj, srcpkg = \
1727                 branch_pkg(conf.config['apiurl'], args[0], args[1],
1728                            nodevelproject=opts.nodevelproject, rev=opts.revision,
1729                            target_project=tproject, target_package=tpackage,
1730                            return_existing=opts.checkout, msg=opts.message or '',
1731                            force=opts.force)
1732         if exists:
1733             print >>sys.stderr, 'Using existing branch project: %s' % targetprj
1734
1735         devloc = None
1736         if not exists and (srcprj is not None and srcprj != args[0] or \
1737                            srcprj is None and targetprj != expected):
1738             devloc = srcprj or targetprj
1739             if not srcprj and 'branches:' in targetprj:
1740                 devloc = targetprj.split('branches:')[1]
1741             print '\nNote: The branch has been created of a different project,\n' \
1742                   '              %s,\n' \
1743                   '      which is the primary location of where development for\n' \
1744                   '      that package takes place.\n' \
1745                   '      That\'s also where you would normally make changes against.\n' \
1746                   '      A direct branch of the specified package can be forced\n' \
1747                   '      with the --nodevelproject option.\n' % devloc
1748
1749         package = tpackage or args[1]
1750         if opts.checkout:
1751             checkout_package(conf.config['apiurl'], targetprj, package,
1752                              expand_link=True, prj_dir=targetprj)
1753             if conf.config['verbose']:
1754                 print 'Note: You can use "osc delete" or "osc submitpac" when done.\n'
1755         else:
1756             apiopt = ''
1757             if conf.get_configParser().get('general', 'apiurl') != conf.config['apiurl']:
1758                 apiopt = '-A %s ' % conf.config['apiurl']
1759             print 'A working copy of the branched package can be checked out with:\n\n' \
1760                   'osc %sco %s/%s' \
1761                       % (apiopt, targetprj, package)
1762         print_request_list(conf.config['apiurl'], args[0], args[1])
1763         if devloc:
1764             print_request_list(conf.config['apiurl'], devloc, args[1])
1765
1766
1767
1768     @cmdln.option('-f', '--force', action='store_true',
1769                         help='deletes a package or an empty project')
1770     def do_rdelete(self, subcmd, opts, *args):
1771         """${cmd_name}: Delete a project or packages on the server.
1772
1773         As a safety measure, project must be empty (i.e., you need to delete all
1774         packages first). If you are sure that you want to remove this project and all
1775         its packages use \'--force\' switch.
1776
1777         usage:
1778            osc rdelete -f PROJECT
1779            osc rdelete PROJECT PACKAGE [PACKAGE ...]
1780
1781         ${cmd_option_list}
1782         """
1783
1784         args = slash_split(args)
1785         if len(args) < 1:
1786             raise oscerr.WrongArgs('Missing argument.')
1787         prj = args[0]
1788         pkgs = args[1:]
1789
1790         if pkgs:
1791             for pkg in pkgs:
1792                # careful: if pkg is an empty string, the package delete request results
1793                # into a project delete request - which works recursively...
1794                 if pkg:
1795                     delete_package(conf.config['apiurl'], prj, pkg)
1796         elif len(meta_get_packagelist(conf.config['apiurl'], prj)) >= 1 and not opts.force:
1797             print >>sys.stderr, 'Project contains packages. It must be empty before deleting it. ' \
1798                                 'If you are sure that you want to remove this project and all its ' \
1799                                 'packages use the \'--force\' switch'
1800             sys.exit(1)
1801         else:
1802             delete_project(conf.config['apiurl'], prj)
1803
1804     @cmdln.hide(1)
1805     def do_deletepac(self, subcmd, opts, *args):
1806         print """${cmd_name} is obsolete !
1807
1808                  Please use either
1809                    osc delete       for checked out packages or projects
1810                  or
1811                    osc rdelete      for server side operations."""
1812
1813         sys.exit(1)
1814
1815     @cmdln.hide(1)
1816     @cmdln.option('-f', '--force', action='store_true',
1817                         help='deletes a project and its packages')
1818     def do_deleteprj(self, subcmd, opts, project):
1819         """${cmd_name} is obsolete !
1820
1821                  Please use
1822                    osc rdelete PROJECT
1823         """
1824         sys.exit(1)
1825
1826     @cmdln.alias('metafromspec')
1827     @cmdln.option('', '--specfile', metavar='FILE',
1828                       help='Path to specfile. (if you pass more than working copy this option is ignored)')
1829     def do_updatepacmetafromspec(self, subcmd, opts, *args):
1830         """${cmd_name}: Update package meta information from a specfile
1831
1832         ARG, if specified, is a package working copy.
1833
1834         ${cmd_usage}
1835         ${cmd_option_list}
1836         """
1837
1838         args = parseargs(args)
1839         if opts.specfile and len(args) == 1:
1840             specfile = opts.specfile
1841         else:
1842             specfile = None
1843         pacs = findpacs(args)
1844         for p in pacs:
1845             p.read_meta_from_spec(specfile)
1846             p.update_package_meta()
1847
1848
1849     @cmdln.alias('di')
1850     @cmdln.option('-c', '--change', metavar='rev',
1851                         help='the change made by revision rev (like -r rev-1:rev).'
1852                              'If rev is negative this is like -r rev:rev-1.')
1853     @cmdln.option('-r', '--revision', metavar='rev1[:rev2]',
1854                         help='If rev1 is specified it will compare your working copy against '
1855                              'the revision (rev1) on the server. '
1856                              'If rev1 and rev2 are specified it will compare rev1 against rev2 '
1857                              '(NOTE: changes in your working copy are ignored in this case)')
1858     @cmdln.option('-p', '--plain', action='store_true',
1859                         help='output the diff in plain (not unified) diff format')
1860     @cmdln.option('--missingok', action='store_true',
1861                         help='do not fail if the source or target project/package does not exist on the server')
1862     def do_diff(self, subcmd, opts, *args):
1863         """${cmd_name}: Generates a diff
1864
1865         Generates a diff, comparing local changes against the repository
1866         server.
1867
1868         ARG, specified, is a filename to include in the diff.
1869
1870         ${cmd_usage}
1871         ${cmd_option_list}
1872         """
1873
1874         args = parseargs(args)
1875         pacs = findpacs(args)
1876
1877         if opts.change:
1878             try:
1879                 rev = int(opts.change)
1880                 if rev > 0:
1881                     rev1 = rev - 1
1882                     rev2 = rev
1883                 elif rev < 0:
1884                     rev1 = -rev
1885                     rev2 = -rev - 1
1886                 else:
1887                     return
1888             except:
1889                 print >>sys.stderr, 'Revision \'%s\' not an integer' % opts.change
1890                 return
1891         else:
1892             rev1, rev2 = parseRevisionOption(opts.revision)
1893         diff = ''
1894         for pac in pacs:
1895             if not rev2:
1896                 diff += ''.join(make_diff(pac, rev1))
1897             else:
1898                 diff += server_diff(pac.apiurl, pac.prjname, pac.name, rev1,
1899                                     pac.prjname, pac.name, rev2, not opts.plain, opts.missingok)
1900         if len(diff) > 0:
1901             run_pager(diff)
1902
1903
1904     @cmdln.option('--oldprj', metavar='OLDPRJ',
1905                   help='project to compare against'
1906                   ' (deprecated, use 3 argument form)')
1907     @cmdln.option('--oldpkg', metavar='OLDPKG',
1908                   help='package to compare against'
1909                   ' (deprecated, use 3 argument form)')
1910     @cmdln.option('-r', '--revision', metavar='N[:M]',
1911                   help='revision id, where N = old revision and M = new revision')
1912     @cmdln.option('-p', '--plain', action='store_true',
1913                   help='output the diff in plain (not unified) diff format')
1914     @cmdln.option('-c', '--change', metavar='rev',
1915                         help='the change made by revision rev (like -r rev-1:rev). '
1916                              'If rev is negative this is like -r rev:rev-1.')
1917     @cmdln.option('--missingok', action='store_true',
1918                         help='do not fail if the source or target project/package does not exist on the server')
1919     def do_rdiff(self, subcmd, opts, *args):
1920         """${cmd_name}: Server-side "pretty" diff of two packages
1921
1922         Compares two packages (three or four arguments) or shows the
1923         changes of a specified revision of a package (two arguments)
1924
1925         If no revision is specified the latest revision is used.
1926
1927         Note that this command doesn't return a normal diff (which could be
1928         applied as patch), but a "pretty" diff, which also compares the content
1929         of tarballs.
1930
1931
1932         usage:
1933             osc ${cmd_name} OLDPRJ OLDPAC NEWPRJ [NEWPAC]
1934             osc ${cmd_name} PROJECT PACKAGE
1935         ${cmd_option_list}
1936         """
1937
1938         args = slash_split(args)
1939
1940         rev1 = None
1941         rev2 = None
1942
1943         old_project = None
1944         old_package = None
1945         new_project = None
1946         new_package = None
1947
1948         if len(args) == 2:
1949             new_project = args[0]
1950             new_package = args[1]
1951             if opts.oldprj:
1952                 old_project = opts.oldprj
1953             if opts.oldpkg:
1954                 old_package = opts.oldpkg
1955         elif len(args) == 3 or len(args) == 4:
1956             if opts.oldprj or opts.oldpkg:
1957                 raise oscerr.WrongArgs('--oldpkg and --oldprj are only valid with two arguments')
1958             old_project = args[0]
1959             new_package = old_package = args[1]
1960             new_project = args[2]
1961             if len(args) == 4:
1962                 new_package = args[3]
1963         else:
1964             raise oscerr.WrongArgs('Wrong number of arguments')
1965
1966
1967         if opts.change:
1968             try:
1969                 rev = int(opts.change)
1970                 if rev > 0:
1971                     rev1 = rev - 1
1972                     rev2 = rev
1973                 elif rev < 0:
1974                     rev1 = -rev
1975                     rev2 = -rev - 1
1976                 else:
1977                     return
1978             except:
1979                 print >>sys.stderr, 'Revision \'%s\' not an integer' % opts.change
1980                 return
1981         else:
1982             if opts.revision:
1983                 rev1, rev2 = parseRevisionOption(opts.revision)
1984
1985         rdiff = server_diff(conf.config['apiurl'],
1986                             old_project, old_package, rev1,
1987                             new_project, new_package, rev2, not opts.plain, opts.missingok)
1988         print rdiff
1989
1990     @cmdln.hide(1)
1991     @cmdln.alias('in')
1992     def do_install(self, subcmd, opts, *args):
1993         """${cmd_name}: install a package after build via zypper in -r
1994
1995         Not implemented yet. Use osc repourls,
1996         select the url you best like (standard),
1997         chop off after the last /, this should work with zypper.
1998
1999
2000         ${cmd_usage}
2001         ${cmd_option_list}
2002         """
2003
2004         args = slash_split(args)
2005         args = expand_proj_pack(args)
2006
2007         ## FIXME:
2008         ## if there is only one argument, and it ends in .ymp
2009         ## then fetch it, Parse XML to get the first
2010         ##  metapackage.group.repositories.repository.url
2011         ## and construct zypper cmd's for all
2012         ##  metapackage.group.software.item.name
2013         ##
2014         ## if args[0] is already an url, the use it as is.
2015
2016         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])
2017         print self.do_install.__doc__
2018         print "Example: \n" + cmd
2019
2020
2021     def do_repourls(self, subcmd, opts, *args):
2022         """${cmd_name}: Shows URLs of .repo files
2023
2024         Shows URLs on which to access the project .repos files (yum-style
2025         metadata) on download.opensuse.org.
2026
2027         usage:
2028            osc repourls [PROJECT]
2029
2030         ${cmd_option_list}
2031         """
2032
2033         apiurl = self.get_api_url()
2034
2035         if len(args) == 1:
2036             project = args[0]
2037         elif len(args) == 0:
2038             project = store_read_project('.')
2039         else:
2040             raise oscerr.WrongArgs('Wrong number of arguments')
2041
2042         # XXX: API should somehow tell that
2043         url_tmpl = 'http://download.opensuse.org/repositories/%s/%s/%s.repo'
2044         repos = get_repositories_of_project(apiurl, project)
2045         for repo in repos:
2046             print url_tmpl % (project.replace(':', ':/'), repo, project)
2047
2048
2049     @cmdln.option('-r', '--revision', metavar='rev',
2050                         help='checkout the specified revision. '
2051                              'NOTE: if you checkout the complete project '
2052                              'this option is ignored!')
2053     @cmdln.option('-e', '--expand-link', action='store_true',
2054                         help='if a package is a link, check out the expanded '
2055                              'sources (no-op, since this became the default)')
2056     @cmdln.option('-u', '--unexpand-link', action='store_true',
2057                         help='if a package is a link, check out the _link file ' \
2058                              'instead of the expanded sources')
2059     @cmdln.option('-c', '--current-dir', action='store_true',
2060                         help='place PACKAGE folder in the current directory' \
2061                              'instead of a PROJECT/PACKAGE directory')
2062     @cmdln.option('-s', '--source-service-files', action='store_true',
2063                         help='server side generated files of source services' \
2064                              'gets downloaded as well' )
2065     @cmdln.option('-l', '--limit-size', metavar='limit_size',
2066                         help='Skip all files with a given size')
2067     @cmdln.alias('co')
2068     def do_checkout(self, subcmd, opts, *args):
2069         """${cmd_name}: Check out content from the repository
2070
2071         Check out content from the repository server, creating a local working
2072         copy.
2073
2074         When checking out a single package, the option --revision can be used
2075         to specify a revision of the package to be checked out.
2076
2077         When a package is a source link, then it will be checked out in
2078         expanded form. If --unexpand-link option is used, the checkout will
2079         instead produce the raw _link file plus patches.
2080
2081         usage:
2082             osc co PROJECT [PACKAGE] [FILE]
2083                osc co PROJECT                    # entire project
2084                osc co PROJECT PACKAGE            # a package
2085                osc co PROJECT PACKAGE FILE       # single file -> to current dir
2086
2087             while inside a project directory:
2088                osc co PACKAGE                    # check out PACKAGE from project
2089
2090         ${cmd_option_list}
2091         """
2092
2093         if opts.unexpand_link:
2094             expand_link = False
2095         else:
2096             expand_link = True
2097         if opts.source_service_files:
2098             service_files = True
2099         else:
2100             service_files = False
2101
2102         args = slash_split(args)
2103         project = package = filename = None
2104
2105         apiurl = self.get_api_url()
2106
2107         try:
2108             project = project_dir = args[0]
2109             package = args[1]
2110             filename = args[2]
2111         except:
2112             pass
2113
2114         if args and len(args) == 1:
2115             localdir = os.getcwd()
2116             if is_project_dir(localdir):
2117                 project = store_read_project(localdir)
2118                 project_dir = localdir
2119                 package = args[0]
2120
2121         rev, dummy = parseRevisionOption(opts.revision)
2122         if rev==None:
2123             rev="latest"
2124
2125         if rev and rev != "latest" and not checkRevision(project, package, rev):
2126             print >>sys.stderr, 'Revision \'%s\' does not exist' % rev
2127             sys.exit(1)
2128
2129         if filename:
2130             get_source_file(apiurl, project, package, filename, revision=rev, progress_obj=self.download_progress)
2131
2132         elif package:
2133             if opts.current_dir:
2134                 project_dir = None
2135             checkout_package(apiurl, project, package, rev, expand_link=expand_link, \
2136                              prj_dir=project_dir, service_files=service_files, progress_obj=self.download_progress, limit_size=opts.limit_size)
2137             print_request_list(apiurl, project, package)
2138
2139         elif project:
2140             prj_dir = project
2141             if sys.platform[:3] == 'win':
2142                 prj_dir = prj_dir.replace(':', ';')
2143             if os.path.exists(prj_dir):
2144                 sys.exit('osc: project \'%s\' already exists' % project)
2145
2146             # check if the project does exist (show_project_meta will throw an exception)
2147             show_project_meta(apiurl, project)
2148
2149             init_project_dir(apiurl, prj_dir, project)
2150             print statfrmt('A', prj_dir)
2151
2152             # all packages
2153             for package in meta_get_packagelist(apiurl, project):
2154                 try:
2155                     checkout_package(apiurl, project, package, expand_link = expand_link, \
2156                                      prj_dir = prj_dir, service_files = service_files, progress_obj=self.download_progress, limit_size=opts.limit_size)
2157                 except oscerr.LinkExpandError, e:
2158                     print >>sys.stderr, 'Link cannot be expanded:\n', e
2159                     print >>sys.stderr, 'Use "osc repairlink" for fixing merge conflicts:\n'
2160                     # check out in unexpanded form at least
2161                     checkout_package(apiurl, project, package, expand_link = False, \
2162                                      prj_dir = prj_dir, service_files = service_files, progress_obj=self.download_progress, limit_size=opts.limit_size)
2163             print_request_list(apiurl, project)
2164
2165         else:
2166             raise oscerr.WrongArgs('Missing argument.\n\n' \
2167                   + self.get_cmd_help('checkout'))
2168
2169
2170     @cmdln.option('-q', '--quiet', action='store_true',
2171                         help='print as little as possible')
2172     @cmdln.option('-v', '--verbose', action='store_true',
2173                         help='print extra information')
2174     @cmdln.alias('st')
2175     def do_status(self, subcmd, opts, *args):
2176         """${cmd_name}: Show status of files in working copy
2177
2178         Show the status of files in a local working copy, indicating whether
2179         files have been changed locally, deleted, added, ...
2180
2181         The first column in the output specifies the status and is one of the
2182         following characters:
2183           ' ' no modifications
2184           'A' Added
2185           'C' Conflicted
2186           'D' Deleted
2187           'M' Modified
2188           '?' item is not under version control
2189           '!' item is missing (removed by non-osc command) or incomplete
2190
2191         examples:
2192           osc st
2193           osc st <directory>
2194           osc st file1 file2 ...
2195
2196         usage:
2197             osc status [OPTS] [PATH...]
2198         ${cmd_option_list}
2199         """
2200
2201         args = parseargs(args)
2202
2203         # storage for single Package() objects
2204         pacpaths = []
2205         # storage for a project dir ( { prj_instance : [ package objects ] } )
2206         prjpacs = {}
2207         for arg in args:
2208             # when 'status' is run inside a project dir, it should
2209             # stat all packages existing in the wc
2210             if is_project_dir(arg):
2211                 prj = Project(arg, False)
2212
2213                 if conf.config['do_package_tracking']:
2214                     prjpacs[prj] = []
2215                     for pac in prj.pacs_have:
2216                         # we cannot create package objects if the dir does not exist
2217                         if not pac in prj.pacs_broken:
2218                             prjpacs[prj].append(os.path.join(arg, pac))
2219                 else:
2220                     pacpaths += [arg + '/' + n for n in prj.pacs_have]
2221             elif is_package_dir(arg):
2222                 pacpaths.append(arg)
2223             elif os.path.isfile(arg):
2224                 pacpaths.append(arg)
2225             else:
2226                 msg = '\'%s\' is neither a project or a package directory' % arg
2227                 raise oscerr.NoWorkingCopy, msg
2228         lines = []
2229         # process single packages
2230         lines = getStatus(findpacs(pacpaths), None, opts.verbose, opts.quiet)
2231         # process project dirs
2232         for prj, pacs in prjpacs.iteritems():
2233             lines += getStatus(findpacs(pacs), prj, opts.verbose, opts.quiet)
2234         if lines:
2235             print '\n'.join(lines)
2236
2237
2238     def do_add(self, subcmd, opts, *args):
2239         """${cmd_name}: Mark files to be added upon the next commit
2240
2241         In case a URL is given the file will get downloaded and registered to be downloaded
2242         by the server as well via the download_url source service.
2243
2244         This is recommended for release tar balls to track their source and to help
2245         others to review your changes esp. on version upgrades.
2246
2247         usage:
2248             osc add URL [URL...]
2249             osc add FILE [FILE...]
2250         ${cmd_option_list}
2251         """
2252         if not args:
2253             raise oscerr.WrongArgs('Missing argument.\n\n' \
2254                   + self.get_cmd_help('add'))
2255
2256         # Do some magic here, when adding a url. We want that the server to download the tar ball and to verify it
2257         for arg in parseargs(args):
2258             if arg.startswith('http://') or arg.startswith('https://') or arg.startswith('ftp://'):
2259                 addDownloadUrlService(arg)
2260             else:
2261                 addFiles(arg)
2262
2263
2264     def do_mkpac(self, subcmd, opts, *args):
2265         """${cmd_name}: Create a new package under version control
2266
2267         usage:
2268             osc mkpac new_package
2269         ${cmd_option_list}
2270         """
2271         if not conf.config['do_package_tracking']:
2272             print >>sys.stderr, "to use this feature you have to enable \'do_package_tracking\' " \
2273                                 "in the [general] section in the configuration file"
2274             sys.exit(1)
2275
2276         if len(args) != 1:
2277             raise oscerr.WrongArgs('Wrong number of arguments.')
2278
2279         createPackageDir(args[0])
2280
2281     @cmdln.option('-r', '--recursive', action='store_true',
2282                         help='If CWD is a project dir then scan all package dirs as well')
2283     @cmdln.alias('ar')
2284     def do_addremove(self, subcmd, opts, *args):
2285         """${cmd_name}: Adds new files, removes disappeared files
2286
2287         Adds all files new in the local copy, and removes all disappeared files.
2288
2289         ARG, if specified, is a package working copy.
2290
2291         ${cmd_usage}
2292         ${cmd_option_list}
2293         """
2294
2295         args = parseargs(args)
2296         arg_list = args[:]
2297         for arg in arg_list:
2298             if is_project_dir(arg) and conf.config['do_package_tracking']:
2299                 prj = Project(arg, False)
2300                 for pac in prj.pacs_unvers:
2301                     pac_dir = getTransActPath(os.path.join(prj.dir, pac))
2302                     if os.path.isdir(pac_dir):
2303                         addFiles([pac_dir], prj)
2304                 for pac in prj.pacs_broken:
2305                     if prj.get_state(pac) != 'D':
2306                         prj.set_state(pac, 'D')
2307                         print statfrmt('D', getTransActPath(os.path.join(prj.dir, pac)))
2308                 if opts.recursive:
2309                     for pac in prj.pacs_have:
2310                         state = prj.get_state(pac)
2311                         if state != None and state != 'D':
2312                             pac_dir = getTransActPath(os.path.join(prj.dir, pac))
2313                             args.append(pac_dir)
2314                 args.remove(arg)
2315                 prj.write_packages()
2316             elif is_project_dir(arg):
2317                 print >>sys.stderr, 'osc: addremove is not supported in a project dir unless ' \
2318                                     '\'do_package_tracking\' is enabled in the configuration file'
2319                 sys.exit(1)
2320
2321         pacs = findpacs(args)
2322         for p in pacs:
2323             p.todo = p.filenamelist + p.filenamelist_unvers
2324
2325             for filename in p.todo:
2326                 if os.path.isdir(filename):
2327                     continue
2328                 # ignore foo.rXX, foo.mine for files which are in 'C' state
2329                 if os.path.splitext(filename)[0] in p.in_conflict:
2330                     continue
2331                 state = p.status(filename)
2332
2333                 if state == '?':
2334                     # TODO: should ignore typical backup files suffix ~ or .orig
2335                     p.addfile(filename)
2336                     print statfrmt('A', getTransActPath(os.path.join(p.dir, filename)))
2337                 elif state == '!':
2338                     p.put_on_deletelist(filename)
2339                     p.write_deletelist()
2340                     os.unlink(os.path.join(p.storedir, filename))
2341                     print statfrmt('D', getTransActPath(os.path.join(p.dir, filename)))
2342
2343
2344
2345     @cmdln.alias('ci')
2346     @cmdln.alias('checkin')
2347     @cmdln.option('-m', '--message', metavar='TEXT',
2348                   help='specify log message TEXT')
2349     @cmdln.option('-F', '--file', metavar='FILE',
2350                   help='read log message from FILE')
2351     @cmdln.option('-f', '--force', default=False, action="store_true",
2352                   help='force commit - do not tests a file list')
2353     def do_commit(self, subcmd, opts, *args):
2354         """${cmd_name}: Upload content to the repository server
2355
2356         Upload content which is changed in your working copy, to the repository
2357         server.
2358
2359         Optionally checks the state of a working copy, if found a file with
2360         unknown state, it requests an user input:
2361          * skip - don't change anything, just move to another file
2362          * remove - remove a file from dir
2363          * edit file list - edit filelist using EDITOR
2364          * commit - don't check anything and commit package
2365          * abort - abort commit - this is default value
2366         This can be supressed by check_filelist config item, or -f/--force
2367         command line option.
2368
2369         examples:
2370            osc ci                   # current dir
2371            osc ci <dir>
2372            osc ci file1 file2 ...
2373
2374         ${cmd_usage}
2375         ${cmd_option_list}
2376         """
2377
2378         args = parseargs(args)
2379
2380         msg = ''
2381         if opts.message:
2382             msg = opts.message
2383         elif opts.file:
2384             try:
2385                 msg = open(opts.file).read()
2386             except:
2387                 sys.exit('could not open file \'%s\'.' % opts.file)
2388
2389         arg_list = args[:]
2390         for arg in arg_list:
2391             if conf.config['do_package_tracking'] and is_project_dir(arg):
2392                 Project(arg).commit(msg=msg)
2393                 if not msg:
2394                     msg = edit_message()
2395                 args.remove(arg)
2396
2397         pacs = findpacs(args)
2398
2399         if conf.config['check_filelist'] and not opts.force:
2400             check_filelist_before_commit(pacs)
2401
2402         if not msg:
2403             template = store_read_file(os.path.abspath('.'), '_commit_msg')
2404             # open editor for commit message
2405             # but first, produce status and diff to append to the template
2406             footer = diffs = []
2407             lines = []
2408             for pac in pacs:
2409                 changed = getStatus([pac], quiet=True)
2410                 if changed:
2411                     footer += changed
2412                     diffs += ['\nDiff for working copy: %s' % pac.dir]
2413                     diffs += make_diff(pac, 0)
2414                     lines.extend(get_commit_message_template(pac))
2415             if template == None:
2416                 template='\n'.join(lines)
2417             # if footer is empty, there is nothing to commit, and no edit needed.
2418             if footer:
2419                 msg = edit_message(footer='\n'.join(footer), template=template)
2420
2421             if msg:
2422                 store_write_string(os.path.abspath('.'), '_commit_msg', msg)
2423             else:
2424                 store_unlink_file(os.path.abspath('.'), '_commit_msg')
2425
2426         if conf.config['do_package_tracking'] and len(pacs) > 0:
2427             prj_paths = {}
2428             single_paths = []
2429             files = {}
2430             # it is possible to commit packages from different projects at the same
2431             # time: iterate over all pacs and put each pac to the right project in the dict
2432             for pac in pacs:
2433                 path = os.path.normpath(os.path.join(pac.dir, os.pardir))
2434                 if is_project_dir(path):
2435                     pac_path = os.path.basename(os.path.normpath(pac.absdir))
2436                     prj_paths.setdefault(path, []).append(pac_path)
2437                     files[pac_path] = pac.todo
2438                 else:
2439                     single_paths.append(pac.dir)
2440             for prj, packages in prj_paths.iteritems():
2441                 Project(prj).commit(tuple(packages), msg, files)
2442             for pac in single_paths:
2443                 Package(pac).commit(msg)
2444         else:
2445             for p in pacs:
2446                 p.commit(msg)
2447
2448         store_unlink_file(os.path.abspath('.'), '_commit_msg')
2449
2450     @cmdln.option('-r', '--revision', metavar='REV',
2451                         help='update to specified revision (this option will be ignored '
2452                              'if you are going to update the complete project or more than '
2453                              'one package)')
2454     @cmdln.option('-u', '--unexpand-link', action='store_true',
2455                         help='if a package is an expanded link, update to the raw _link file')
2456     @cmdln.option('-e', '--expand-link', action='store_true',
2457                         help='if a package is a link, update to the expanded sources')
2458     @cmdln.option('-s', '--source-service-files', action='store_true',
2459                         help='Use server side generated sources instead of local generation.' )
2460     @cmdln.option('-l', '--limit-size', metavar='limit_size',
2461                         help='Skip all files with a given size')
2462     @cmdln.alias('up')
2463     def do_update(self, subcmd, opts, *args):
2464         """${cmd_name}: Update a working copy
2465
2466         examples:
2467
2468         1. osc up
2469                 If the current working directory is a package, update it.
2470                 If the directory is a project directory, update all contained
2471                 packages, AND check out newly added packages.
2472
2473                 To update only checked out packages, without checking out new
2474                 ones, you might want to use "osc up *" from within the project
2475                 dir.
2476
2477         2. osc up PAC
2478                 Update the packages specified by the path argument(s)
2479
2480         When --expand-link is used with source link packages, the expanded
2481         sources will be checked out. Without this option, the _link file and
2482         patches will be checked out. The option --unexpand-link can be used to
2483         switch back to the "raw" source with a _link file plus patch(es).
2484
2485         ${cmd_usage}
2486         ${cmd_option_list}
2487         """
2488
2489         if (opts.expand_link and opts.unexpand_link) \
2490             or (opts.expand_link and opts.revision) \
2491             or (opts.unexpand_link and opts.revision):
2492             raise oscerr.WrongOptions('Sorry, the options --expand-link, --unexpand-link and '
2493                      '--revision are mutually exclusive.')
2494
2495         if opts.source_service_files: service_files = True
2496         else: service_files = False
2497
2498         args = parseargs(args)
2499         arg_list = args[:]
2500
2501         for arg in arg_list:
2502             if is_project_dir(arg):
2503                 prj = Project(arg, progress_obj=self.download_progress)
2504
2505                 if conf.config['do_package_tracking']:
2506                     prj.update(expand_link=opts.expand_link,
2507                                unexpand_link=opts.unexpand_link)
2508                     args.remove(arg)
2509                 else:
2510                     # if not tracking package, and 'update' is run inside a project dir,
2511                     # it should do the following:
2512                     # (a) update all packages
2513                     args += prj.pacs_have
2514                     # (b) fetch new packages
2515                     prj.checkout_missing_pacs(expand_link = not opts.unexpand_link)
2516                     args.remove(arg)
2517                 print_request_list(prj.apiurl, prj.name)
2518
2519         args.sort()
2520         pacs = findpacs(args, progress_obj=self.download_progress)
2521
2522         if opts.revision and len(args) == 1:
2523             rev, dummy = parseRevisionOption(opts.revision)
2524             if not checkRevision(pacs[0].prjname, pacs[0].name, rev, pacs[0].apiurl):
2525                 print >>sys.stderr, 'Revision \'%s\' does not exist' % rev
2526                 sys.exit(1)
2527         else:
2528             rev = None
2529
2530         for p in pacs:
2531             if len(pacs) > 1:
2532                 print 'Updating %s' % p.name
2533
2534             # FIXME: ugly workaround for #399247
2535             if opts.expand_link or opts.unexpand_link:
2536                 if [ i for i in p.filenamelist+p.filenamelist_unvers if p.status(i) != ' ' and p.status(i) != '?']:
2537                     print >>sys.stderr, 'osc: cannot expand/unexpand because your working ' \
2538                                         'copy has local modifications.\nPlease revert/commit them ' \
2539                                         'and try again.'
2540                     sys.exit(1)
2541
2542             if not rev:
2543                 if opts.expand_link and p.islink() and not p.isexpanded():
2544                     if p.haslinkerror():
2545                         try:
2546                             rev = show_upstream_xsrcmd5(p.apiurl, p.prjname, p.name, revision=p.rev)
2547                         except:
2548                             rev = show_upstream_xsrcmd5(p.apiurl, p.prjname, p.name, revision=p.rev, linkrev="base")
2549                             p.mark_frozen()
2550                     else:
2551                         rev = p.linkinfo.xsrcmd5
2552                     print 'Expanding to rev', rev
2553                 elif opts.unexpand_link and p.islink() and p.isexpanded():
2554                     print 'Unexpanding to rev', p.linkinfo.lsrcmd5
2555                     rev = p.linkinfo.lsrcmd5
2556                 elif p.islink() and p.isexpanded():
2557                     rev = p.latest_rev()
2558
2559             p.update(rev, service_files, opts.limit_size)
2560             if opts.unexpand_link:
2561                 p.unmark_frozen()
2562             rev = None
2563             print_request_list(p.apiurl, p.prjname, p.name)
2564
2565
2566     @cmdln.option('-f', '--force', action='store_true',
2567                         help='forces removal of entire package and its files')
2568     @cmdln.alias('rm')
2569     @cmdln.alias('del')
2570     @cmdln.alias('remove')
2571     def do_delete(self, subcmd, opts, *args):
2572         """${cmd_name}: Mark files or package directories to be deleted upon the next 'checkin'
2573
2574         usage:
2575             cd .../PROJECT/PACKAGE
2576             osc delete FILE [...]
2577             cd .../PROJECT
2578             osc delete PACKAGE [...]
2579
2580         This command works on check out copies. Use "rdelete" for working on server
2581         side only. This is needed for removing the entire project.
2582
2583         As a safety measure, projects must be empty (i.e., you need to delete all
2584         packages first).
2585
2586         If you are sure that you want to remove a package and all
2587         its files use \'--force\' switch. Sometimes this also works without --force.
2588
2589         ${cmd_option_list}
2590         """
2591
2592         if not args:
2593             raise oscerr.WrongArgs('Missing argument.\n\n' \
2594                   + self.get_cmd_help('delete'))
2595
2596         args = parseargs(args)
2597         # check if args contains a package which was removed by
2598         # a non-osc command and mark it with the 'D'-state
2599         arg_list = args[:]
2600         for i in arg_list:
2601             if not os.path.exists(i):
2602                 prj_dir, pac_dir = getPrjPacPaths(i)
2603                 if is_project_dir(prj_dir):
2604                     prj = Project(prj_dir, False)
2605                     if i in prj.pacs_broken:
2606                         if prj.get_state(i) != 'A':
2607                             prj.set_state(pac_dir, 'D')
2608                         else:
2609                             prj.del_package_node(i)
2610                         print statfrmt('D', getTransActPath(i))
2611                         args.remove(i)
2612                         prj.write_packages()
2613         pacs = findpacs(args)
2614
2615         for p in pacs:
2616             if not p.todo:
2617                 prj_dir, pac_dir = getPrjPacPaths(p.absdir)
2618                 if is_project_dir(prj_dir):
2619                     if conf.config['do_package_tracking']:
2620                         prj = Project(prj_dir, False)
2621                         prj.delPackage(p, opts.force)
2622                     else:
2623                         print "WARNING: package tracking is disabled, operation skipped !"
2624             else:
2625                 pathn = getTransActPath(p.dir)
2626                 for filename in p.todo:
2627                     ret, state = p.delete_file(filename, opts.force)
2628                     if ret:
2629                         print statfrmt('D', os.path.join(pathn, filename))
2630                         continue
2631                     if state == '?':
2632                         sys.exit('\'%s\' is not under version control' % filename)
2633                     elif state in ['A', 'M'] and not opts.force:
2634                         sys.exit('\'%s\' has local modifications (use --force to remove this file)' % filename)
2635
2636
2637     def do_resolved(self, subcmd, opts, *args):
2638         """${cmd_name}: Remove 'conflicted' state on working copy files
2639
2640         If an upstream change can't be merged automatically, a file is put into
2641         in 'conflicted' ('C') state. Within the file, conflicts are marked with
2642         special <<<<<<< as well as ======== and >>>>>>> lines.
2643
2644         After manually resolving all conflicting parts, use this command to
2645         remove the 'conflicted' state.
2646
2647         Note:  this subcommand does not semantically resolve conflicts or
2648         remove conflict markers; it merely removes the conflict-related
2649         artifact files and allows PATH to be committed again.
2650
2651         usage:
2652             osc resolved FILE [FILE...]
2653         ${cmd_option_list}
2654         """
2655
2656         if not args:
2657             raise oscerr.WrongArgs('Missing argument.\n\n' \
2658                   + self.get_cmd_help('resolved'))
2659
2660         args = parseargs(args)
2661         pacs = findpacs(args)
2662
2663         for p in pacs:
2664             for filename in p.todo:
2665                 print 'Resolved conflicted state of "%s"' % filename
2666                 p.clear_from_conflictlist(filename)
2667
2668
2669     @cmdln.alias('platforms')
2670     def do_repositories(self, subcmd, opts, *args):
2671         """${cmd_name}: Shows available repositories
2672
2673         Examples:
2674         1. osc repositories
2675                 Shows all available repositories/build targets
2676
2677         2. osc repositories <project>
2678                 Shows the configured repositories/build targets of a project
2679
2680         ${cmd_usage}
2681         ${cmd_option_list}
2682         """
2683
2684         if args:
2685             project = args[0]
2686             print '\n'.join(get_repositories_of_project(conf.config['apiurl'], project))
2687         else:
2688             print '\n'.join(get_repositories(conf.config['apiurl']))
2689
2690
2691     @cmdln.hide(1)
2692     def do_results_meta(self, subcmd, opts, *args):
2693         print "Command results_meta is obsolete. Please use: osc results --xml"
2694         sys.exit(1)
2695
2696     @cmdln.hide(1)
2697     @cmdln.option('-l', '--last-build', action='store_true',
2698                         help='show last build results (succeeded/failed/unknown)')
2699     @cmdln.option('-r', '--repo', action='append', default = [],
2700                         help='Show results only for specified repo(s)')
2701     @cmdln.option('-a', '--arch', action='append', default = [],
2702                         help='Show results only for specified architecture(s)')
2703     @cmdln.option('', '--xml', action='store_true',
2704                         help='generate output in XML (former results_meta)')
2705     def do_rresults(self, subcmd, opts, *args):
2706         print "Command rresults is obsolete. Running 'osc results' instead"
2707         self.do_results('results', opts, *args)
2708         sys.exit(1)
2709
2710
2711     @cmdln.option('-f', '--force', action='store_true', default=False,
2712                         help="Don't ask and delete files")
2713     def do_rremove(self, subcmd, opts, project, package, *files):
2714         """${cmd_name}: Remove source files from selected package
2715
2716         ${cmd_usage}
2717         ${cmd_option_list}
2718         """
2719
2720         if len(files) == 0:
2721             if not '/' in project:
2722                 raise oscerr.WrongArgs("Missing operand, type osc help rremove for help")
2723             else:
2724                 files = (package, )
2725                 project, package = project.split('/')
2726
2727         for file in files:
2728             if not opts.force:
2729                 resp = raw_input("rm: remove source file `%s' from `%s/%s'? (yY|nN) " % (file, project, package))
2730                 if resp not in ('y', 'Y'):
2731                     continue
2732             try:
2733                 delete_files(conf.config['apiurl'], project, package, (file, ))
2734             except urllib2.HTTPError, e:
2735                 if opts.force:
2736                     print >>sys.stderr, e
2737                     body = e.read()
2738                     if e.code in [ 400, 403, 404, 500 ]:
2739                         if '<summary>' in body:
2740                             msg = body.split('<summary>')[1]
2741                             msg = msg.split('</summary>')[0]
2742                             print >>sys.stderr, msg
2743                 else:
2744                     raise e
2745
2746     @cmdln.alias('r')
2747     @cmdln.option('-l', '--last-build', action='store_true',
2748                         help='show last build results (succeeded/failed/unknown)')
2749     @cmdln.option('-r', '--repo', action='append', default = [],
2750                         help='Show results only for specified repo(s)')
2751     @cmdln.option('-a', '--arch', action='append', default = [],
2752                         help='Show results only for specified architecture(s)')
2753     @cmdln.option('', '--xml', action='store_true', default=False,
2754                         help='generate output in XML (former results_meta)')
2755     @cmdln.option('', '--csv', action='store_true', default=False,
2756                         help='generate output in CSV format')
2757     @cmdln.option('', '--format', default='%(repository)s|%(arch)s|%(state)s|%(dirty)s|%(code)s|%(details)s',
2758                         help='format string for csv output')
2759     def do_results(self, subcmd, opts, *args):
2760         """${cmd_name}: Shows the build results of a package
2761
2762         Usage:
2763             osc results (inside working copy)
2764             osc results remote_project remote_package
2765
2766         ${cmd_option_list}
2767         """
2768
2769         args = slash_split(args)
2770
2771         apiurl = self.get_api_url()
2772         if len(args) == 0:
2773             wd = os.curdir
2774             if is_project_dir(wd):
2775                 opts.csv = None
2776                 opts.arch = None
2777                 opts.repo = None
2778                 opts.hide_legend = None
2779                 opts.name_filter = None
2780                 opts.status_filter = None
2781                 opts.vertical = None
2782                 self.do_prjresults('prjresults', opts, *args)
2783                 sys.exit(0)
2784             else:
2785                 project = store_read_project(wd)
2786                 package = store_read_package(wd)
2787         elif len(args) < 2:
2788             raise oscerr.WrongArgs('Too few arguments (required none or two)')
2789         elif len(args) > 2:
2790             raise oscerr.WrongArgs('Too many arguments (required none or two)')
2791         else:
2792             project = args[0]
2793             package = args[1]
2794
2795         if opts.xml and opts.csv:
2796             raise oscerr.WrongOptions("--xml and --csv are mutual exclusive")
2797
2798         if opts.xml:
2799             func = show_results_meta
2800             delim = ''
2801         elif opts.csv:
2802             def _func(*args):
2803                 return format_results(get_package_results(*args), opts.format)
2804             func = _func
2805             delim = '\n'
2806         else:
2807             func = get_results
2808             delim = '\n'
2809
2810         print delim.join(func(apiurl, project, package, opts.last_build, opts.repo, opts.arch))
2811
2812     # WARNING: this function is also called by do_results. You need to set a default there
2813     #          as well when adding a new option!
2814     @cmdln.option('-q', '--hide-legend', action='store_true',
2815                         help='hide the legend')
2816     @cmdln.option('-c', '--csv', action='store_true',
2817                         help='csv output')
2818     @cmdln.option('-s', '--status-filter', metavar='STATUS',
2819                         help='show only packages with buildstatus STATUS (see legend)')
2820     @cmdln.option('-n', '--name-filter', metavar='EXPR',
2821                         help='show only packages whose names match EXPR')
2822     @cmdln.option('-a', '--arch', metavar='ARCH',
2823                         help='show results only for specified architecture(s)')
2824     @cmdln.option('-r', '--repo', metavar='REPO',
2825                         help='show results only for specified repo(s)')
2826     @cmdln.option('-V', '--vertical', action='store_true',
2827                         help='list packages vertically instead horizontally')
2828     @cmdln.alias('pr')
2829     def do_prjresults(self, subcmd, opts, *args):
2830         """${cmd_name}: Shows project-wide build results
2831
2832         Usage:
2833             osc prjresults (inside working copy)
2834             osc prjresults PROJECT
2835
2836         ${cmd_option_list}
2837         """
2838         apiurl = self.get_api_url()
2839
2840         if args:
2841             if len(args) == 1:
2842                 project = args[0]
2843             else:
2844                 raise oscerr.WrongArgs('Wrong number of arguments.')
2845         else:
2846             wd = os.curdir
2847             project = store_read_project(wd)
2848
2849         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))
2850
2851
2852     @cmdln.option('-q', '--hide-legend', action='store_true',
2853                         help='hide the legend')
2854     @cmdln.option('-c', '--csv', action='store_true',
2855                         help='csv output')
2856     @cmdln.option('-s', '--status-filter', metavar='STATUS',
2857                         help='show only packages with buildstatus STATUS (see legend)')
2858     @cmdln.option('-n', '--name-filter', metavar='EXPR',
2859                         help='show only packages whose names match EXPR')
2860
2861     @cmdln.hide(1)
2862     def do_rprjresults(self, subcmd, opts, *args):
2863         print "Command rprjresults is obsolete. Please use 'osc prjresults'"
2864         sys.exit(1)
2865
2866     @cmdln.alias('bl')
2867     @cmdln.option('-s', '--start', metavar='START',
2868                     help='get log starting from the offset')
2869     def do_buildlog(self, subcmd, opts, *args):
2870         """${cmd_name}: Shows the build log of a package
2871
2872         Shows the log file of the build of a package. Can be used to follow the
2873         log while it is being written.
2874         Needs to be called from within a package directory.
2875
2876         The arguments REPOSITORY and ARCH are the first two columns in the 'osc
2877         results' output. If the buildlog url is used buildlog command has the
2878         same behavior as remotebuildlog.
2879
2880         ${cmd_usage} [REPOSITORY ARCH | BUILDLOGURL]
2881         ${cmd_option_list}
2882         """
2883
2884         repository = arch = None
2885
2886         apiurl = self.get_api_url()
2887
2888         if len(args) == 1 and args[0].startswith('http'):
2889             apiurl, project, package, repository, arch = parse_buildlogurl(args[0])
2890         else:
2891             wd = os.curdir
2892             package = store_read_package(wd)
2893             project = store_read_project(wd)
2894
2895         offset=0
2896         if opts.start:
2897             offset = int(opts.start)
2898
2899         if not repository or not arch:
2900             if len(args) < 2:
2901                 self.print_repos()
2902             else:
2903                 repository = args[0]
2904                 arch = args[1]
2905
2906         print_buildlog(apiurl, project, package, repository, arch, offset)
2907
2908
2909     def print_repos(self):
2910         wd = os.curdir
2911         doprint = False
2912         if is_package_dir(wd):
2913             str = "package"
2914             doprint = True
2915         elif is_project_dir(wd):
2916             str = "project"
2917             doprint = True
2918
2919         if doprint:
2920             print 'Valid arguments for this %s are:' % str
2921             print
2922             self.do_repos(None, None)
2923             print
2924         raise oscerr.WrongArgs('Missing arguments')
2925
2926     @cmdln.alias('rbl')
2927     @cmdln.alias('rbuildlog')
2928     @cmdln.option('-s', '--start', metavar='START',
2929                     help='get log starting from the offset')
2930     def do_remotebuildlog(self, subcmd, opts, *args):
2931         """${cmd_name}: Shows the build log of a package
2932
2933         Shows the log file of the build of a package. Can be used to follow the
2934         log while it is being written.
2935
2936         usage:
2937             osc remotebuildlog project package repository arch
2938             or
2939             osc remotebuildlog project/package/repository/arch
2940             or
2941             osc remotebuildlog buildlogurl
2942         ${cmd_option_list}
2943         """
2944         if len(args) == 1 and args[0].startswith('http'):
2945             apiurl, project, package, repository, arch = parse_buildlogurl(args[0])
2946         else:
2947             args = slash_split(args)
2948             apiurl = conf.config['apiurl']
2949             if len(args) < 4:
2950                 raise oscerr.WrongArgs('Too few arguments.')
2951             elif len(args) > 4:
2952                 raise oscerr.WrongArgs('Too many arguments.')
2953             else:
2954                 project, package, repository, arch = args
2955
2956         offset=0
2957         if opts.start:
2958             offset = int(opts.start)
2959
2960         print_buildlog(apiurl, project, package, repository, arch, offset)
2961
2962     @cmdln.alias('lbl')
2963     @cmdln.option('-s', '--start', metavar='START',
2964                   help='get log starting from offset')
2965     def do_localbuildlog(self, subcmd, opts, *args):
2966         """${cmd_name}: Shows the build log of a local buildchroot
2967
2968         usage:
2969             osc lbl [REPOSITORY ARCH]
2970             osc lbl # show log of newest last local build
2971
2972         ${cmd_option_list}
2973         """
2974         if conf.config['build-type']:
2975             # FIXME: raise Exception instead
2976             print >>sys.stderr, 'Not implemented for VMs'
2977             sys.exit(1)
2978
2979         if len(args) == 0:
2980             package = store_read_package('.')
2981             import glob
2982             files = glob.glob(os.path.join(os.getcwd(), store, "_buildinfo-*"))
2983             if not files:
2984                 self.print_repos()
2985                 raise oscerr.WrongArgs('No buildconfig found, please specify repo and arch manually.')
2986             cfg = files[0]
2987             # find newest file
2988             for f in files[1:]:
2989                 if os.stat(f).st_mtime > os.stat(cfg).st_mtime:
2990                     cfg = f
2991             root = ET.parse(cfg).getroot()
2992             project = root.get("project")
2993             repo = root.get("repository")
2994             arch = root.find("arch").text
2995         elif len(args) == 2:
2996             project = store_read_project('.')
2997             package = store_read_package('.')
2998             repo = args[0]
2999             arch = args[1]
3000         else:
3001             if is_package_dir(os.curdir):
3002                 self.print_repos()
3003             raise oscerr.WrongArgs('Wrong number of arguments.')
3004
3005         buildroot = os.environ.get('OSC_BUILD_ROOT', conf.config['build-root'])
3006         buildroot = buildroot % {'project': project, 'package': package,
3007                                  'repo': repo, 'arch': arch}
3008         offset = 0
3009         if opts.start:
3010             offset = int(opts.start)
3011         logfile = os.path.join(buildroot, '.build.log')
3012         if not os.path.isfile(logfile):
3013             raise oscerr.OscIOError(None, 'logfile \'%s\' does not exist' % logfile)
3014         f = open(logfile, 'r')
3015         f.seek(offset)
3016         data = f.read(BUFSIZE)
3017         while len(data):
3018             sys.stdout.write(data)
3019             data = f.read(BUFSIZE)
3020         f.close()
3021
3022     @cmdln.alias('tr')
3023     def do_triggerreason(self, subcmd, opts, *args):
3024         """${cmd_name}: Show reason why a package got triggered to build
3025
3026         The server decides when a package needs to get rebuild, this command
3027         shows the detailed reason for a package. A brief reason is also stored
3028         in the jobhistory, which can be accessed via "osc jobhistory".
3029
3030         Trigger reasons might be:
3031           - new build (never build yet or rebuild manually forced)
3032           - source change (eg. on updating sources)
3033           - meta change (packages which are used for building have changed)
3034           - rebuild count sync (In case that it is configured to sync release numbers)
3035
3036         usage in package or project directory:
3037             osc reason REPOSITORY ARCH
3038             osc reason PROJECT PACKAGE REPOSITORY ARCH
3039
3040         ${cmd_option_list}
3041         """
3042         wd = os.curdir
3043         args = slash_split(args)
3044         project = package = repository = arch = None
3045
3046         if len(args) < 2:
3047             self.print_repos()
3048         
3049         apiurl = self.get_api_url()
3050
3051         if len(args) == 2: # 2
3052             if is_package_dir('.'):
3053                 package = store_read_package(wd)
3054             else:
3055                 raise oscerr.WrongArgs('package is not specified.')
3056             project = store_read_project(wd)
3057             repository = args[0]
3058             arch = args[1]
3059         elif len(args) == 4:
3060             project = args[0]
3061             package = args[1]
3062             repository = args[2]
3063             arch = args[3]
3064         else:
3065             raise oscerr.WrongArgs('Too many arguments.')
3066
3067         print apiurl, project, package, repository, arch
3068         xml = show_package_trigger_reason(apiurl, project, package, repository, arch)
3069         root = ET.fromstring(xml)
3070         reason = root.find('explain').text
3071         print reason
3072         if reason == "meta change":
3073             print "changed keys:"
3074             for package in root.findall('packagechange'):
3075                 print "  ", package.get('change'), package.get('key')
3076
3077
3078     # FIXME: the new osc syntax should allow to specify multiple packages
3079     # FIXME: the command should optionally use buildinfo data to show all dependencies
3080     @cmdln.alias('whatdependson')
3081     def do_dependson(self, subcmd, opts, *args):
3082         """${cmd_name}: Show the build dependencies
3083
3084         The command dependson and whatdependson can be used to find out what
3085         will be triggered when a certain package changes.
3086         This is no guarantee, since the new build might have changed dependencies.
3087
3088         dependson shows the build dependencies inside of a project, valid for a
3089         given repository and architecture.
3090         NOTE: to see all binary packages, which can trigger a build you need to
3091               refer the buildinfo, since this command shows only the dependencies
3092               inside of a project.
3093
3094