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