Mercurial > genshi > mirror
comparison examples/trac/trac/scripts/admin.py @ 39:93b4dcbafd7b trunk
Copy Trac to main branch.
author | cmlenz |
---|---|
date | Mon, 03 Jul 2006 18:53:27 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
38:ee669cb9cccc | 39:93b4dcbafd7b |
---|---|
1 # -*- coding: utf-8 -*- | |
2 # | |
3 # Copyright (C) 2003-2006 Edgewall Software | |
4 # All rights reserved. | |
5 # | |
6 # This software is licensed as described in the file COPYING, which | |
7 # you should have received as part of this distribution. The terms | |
8 # are also available at http://trac.edgewall.com/license.html. | |
9 # | |
10 # This software consists of voluntary contributions made by many | |
11 # individuals. For the exact contribution history, see the revision | |
12 # history and logs, available at http://projects.edgewall.com/trac/. | |
13 # | |
14 | |
15 __copyright__ = 'Copyright (c) 2003-2006 Edgewall Software' | |
16 | |
17 import cmd | |
18 import getpass | |
19 import os | |
20 import shlex | |
21 import shutil | |
22 import StringIO | |
23 import sys | |
24 import time | |
25 import traceback | |
26 import urllib | |
27 import locale | |
28 | |
29 import trac | |
30 from trac import perm, util, db_default | |
31 from trac.config import default_dir | |
32 from trac.core import TracError | |
33 from trac.env import Environment | |
34 from trac.perm import PermissionSystem | |
35 from trac.ticket.model import * | |
36 from trac.util.markup import html | |
37 from trac.util.text import to_unicode, wrap | |
38 from trac.wiki import WikiPage | |
39 from trac.wiki.macros import WikiMacroBase | |
40 | |
41 def copytree(src, dst, symlinks=False, skip=[]): | |
42 """Recursively copy a directory tree using copy2() (from shutil.copytree.) | |
43 | |
44 Added a `skip` parameter consisting of absolute paths | |
45 which we don't want to copy. | |
46 """ | |
47 names = os.listdir(src) | |
48 os.mkdir(dst) | |
49 errors = [] | |
50 for name in names: | |
51 srcname = os.path.join(src, name) | |
52 if srcname in skip: | |
53 continue | |
54 dstname = os.path.join(dst, name) | |
55 try: | |
56 if symlinks and os.path.islink(srcname): | |
57 linkto = os.readlink(srcname) | |
58 os.symlink(linkto, dstname) | |
59 elif os.path.isdir(srcname): | |
60 copytree(srcname, dstname, symlinks, skip) | |
61 else: | |
62 shutil.copy2(srcname, dstname) | |
63 # XXX What about devices, sockets etc.? | |
64 except (IOError, os.error), why: | |
65 errors.append((srcname, dstname, why)) | |
66 if errors: | |
67 raise shutil.Error, errors | |
68 | |
69 | |
70 class TracAdmin(cmd.Cmd): | |
71 intro = '' | |
72 license = trac.__license_long__ | |
73 doc_header = 'Trac Admin Console %(ver)s\n' \ | |
74 'Available Commands:\n' \ | |
75 % {'ver':trac.__version__ } | |
76 ruler = '' | |
77 prompt = "Trac> " | |
78 __env = None | |
79 _date_format = '%Y-%m-%d' | |
80 _datetime_format = '%Y-%m-%d %H:%M:%S' | |
81 _date_format_hint = 'YYYY-MM-DD' | |
82 | |
83 def __init__(self, envdir=None): | |
84 cmd.Cmd.__init__(self) | |
85 self.interactive = False | |
86 if envdir: | |
87 self.env_set(os.path.abspath(envdir)) | |
88 self._permsys = None | |
89 | |
90 def emptyline(self): | |
91 pass | |
92 | |
93 def onecmd(self, line): | |
94 """`line` may be a `str` or an `unicode` object""" | |
95 try: | |
96 if isinstance(line, str): | |
97 line = to_unicode(line, sys.stdin.encoding) | |
98 rv = cmd.Cmd.onecmd(self, line) or 0 | |
99 except SystemExit: | |
100 raise | |
101 except Exception, e: | |
102 print>>sys.stderr, 'Command failed: %s' % e | |
103 rv = 2 | |
104 if not self.interactive: | |
105 return rv | |
106 | |
107 def run(self): | |
108 self.interactive = True | |
109 print 'Welcome to trac-admin %(ver)s\n' \ | |
110 'Interactive Trac administration console.\n' \ | |
111 '%(copy)s\n\n' \ | |
112 "Type: '?' or 'help' for help on commands.\n" % \ | |
113 {'ver':trac.__version__,'copy':__copyright__} | |
114 self.cmdloop() | |
115 | |
116 ## | |
117 ## Environment methods | |
118 ## | |
119 | |
120 def env_set(self, envname, env=None): | |
121 self.envname = envname | |
122 self.prompt = "Trac [%s]> " % self.envname | |
123 if env is not None: | |
124 self.__env = env | |
125 | |
126 def env_check(self): | |
127 try: | |
128 self.__env = Environment(self.envname) | |
129 except: | |
130 return 0 | |
131 return 1 | |
132 | |
133 def env_open(self): | |
134 try: | |
135 if not self.__env: | |
136 self.__env = Environment(self.envname) | |
137 return self.__env | |
138 except Exception, e: | |
139 print 'Failed to open environment.', e | |
140 traceback.print_exc() | |
141 sys.exit(1) | |
142 | |
143 def db_open(self): | |
144 return self.env_open().get_db_cnx() | |
145 | |
146 def db_query(self, sql, cursor=None, params=None): | |
147 if not cursor: | |
148 cnx = self.db_open() | |
149 cursor = cnx.cursor() | |
150 if params: | |
151 cursor.execute(sql, params) | |
152 else: | |
153 cursor.execute(sql) | |
154 for row in cursor: | |
155 yield row | |
156 | |
157 def db_update(self, sql, cursor=None, params=None): | |
158 if not cursor: | |
159 cnx = self.db_open() | |
160 cursor = cnx.cursor() | |
161 else: | |
162 cnx = None | |
163 if params: | |
164 cursor.execute(sql, params) | |
165 else: | |
166 cursor.execute(sql) | |
167 if cnx: | |
168 cnx.commit() | |
169 | |
170 ## | |
171 ## Utility methods | |
172 ## | |
173 | |
174 def arg_tokenize (self, argstr): | |
175 """`argstr` is an `unicode` string | |
176 | |
177 ... but shlex is not unicode friendly. | |
178 """ | |
179 return [unicode(token, 'utf-8') | |
180 for token in shlex.split(argstr.encode('utf-8'))] or [''] | |
181 | |
182 def word_complete (self, text, words): | |
183 return [a for a in words if a.startswith (text)] | |
184 | |
185 def print_listing(self, headers, data, sep=' ', decor=True): | |
186 cons_charset = sys.stdout.encoding | |
187 ldata = list(data) | |
188 if decor: | |
189 ldata.insert(0, headers) | |
190 print | |
191 colw = [] | |
192 ncols = len(ldata[0]) # assumes all rows are of equal length | |
193 for cnum in xrange(0, ncols): | |
194 mw = 0 | |
195 for cell in [unicode(d[cnum]) or '' for d in ldata]: | |
196 if len(cell) > mw: | |
197 mw = len(cell) | |
198 colw.append(mw) | |
199 for rnum in xrange(len(ldata)): | |
200 for cnum in xrange(ncols): | |
201 if decor and rnum == 0: | |
202 sp = ('%%%ds' % len(sep)) % ' ' # No separator in header | |
203 else: | |
204 sp = sep | |
205 if cnum + 1 == ncols: | |
206 sp = '' # No separator after last column | |
207 pdata = ((u'%%-%ds%s' % (colw[cnum], sp)) | |
208 % (ldata[rnum][cnum] or '')) | |
209 if cons_charset and isinstance(pdata, unicode): | |
210 pdata = pdata.encode(cons_charset, 'replace') | |
211 print pdata, | |
212 print | |
213 if rnum == 0 and decor: | |
214 print ''.join(['-' for x in | |
215 xrange(0, (1 + len(sep)) * cnum + sum(colw))]) | |
216 print | |
217 | |
218 def print_doc(cls, docs, stream=None): | |
219 if stream is None: | |
220 stream = sys.stdout | |
221 if not docs: return | |
222 for cmd, doc in docs: | |
223 print>>stream, cmd | |
224 print>>stream, '\t-- %s\n' % doc | |
225 print_doc = classmethod(print_doc) | |
226 | |
227 def get_component_list(self): | |
228 rows = self.db_query("SELECT name FROM component") | |
229 return [row[0] for row in rows] | |
230 | |
231 def get_user_list(self): | |
232 rows = self.db_query("SELECT DISTINCT username FROM permission") | |
233 return [row[0] for row in rows] | |
234 | |
235 def get_wiki_list(self): | |
236 rows = self.db_query('SELECT DISTINCT name FROM wiki') | |
237 return [row[0] for row in rows] | |
238 | |
239 def get_dir_list(self, pathstr, justdirs=False): | |
240 dname = os.path.dirname(pathstr) | |
241 d = os.path.join(os.getcwd(), dname) | |
242 dlist = os.listdir(d) | |
243 if justdirs: | |
244 result = [] | |
245 for entry in dlist: | |
246 try: | |
247 if os.path.isdir(entry): | |
248 result.append(entry) | |
249 except: | |
250 pass | |
251 else: | |
252 result = dlist | |
253 return result | |
254 | |
255 def get_enum_list(self, type): | |
256 rows = self.db_query("SELECT name FROM enum WHERE type=%s", | |
257 params=[type]) | |
258 return [row[0] for row in rows] | |
259 | |
260 def get_milestone_list(self): | |
261 rows = self.db_query("SELECT name FROM milestone") | |
262 return [row[0] for row in rows] | |
263 | |
264 def get_version_list(self): | |
265 rows = self.db_query("SELECT name FROM version") | |
266 return [row[0] for row in rows] | |
267 | |
268 def _parse_date(self, t): | |
269 seconds = None | |
270 t = t.strip() | |
271 if t == 'now': | |
272 seconds = int(time.time()) | |
273 else: | |
274 for format in [self._date_format, '%x %X', '%x, %X', '%X %x', | |
275 '%X, %x', '%x', '%c', '%b %d, %Y']: | |
276 try: | |
277 pt = time.strptime(t, format) | |
278 seconds = int(time.mktime(pt)) | |
279 except ValueError: | |
280 continue | |
281 break | |
282 if seconds == None: | |
283 try: | |
284 seconds = int(t) | |
285 except ValueError: | |
286 pass | |
287 if seconds == None: | |
288 print>>sys.stderr, 'Unknown time format %s' % t | |
289 return seconds | |
290 | |
291 def _format_date(self, s): | |
292 return time.strftime(self._date_format, time.localtime(s)) | |
293 | |
294 def _format_datetime(self, s): | |
295 return time.strftime(self._datetime_format, time.localtime(s)) | |
296 | |
297 | |
298 ## | |
299 ## Available Commands | |
300 ## | |
301 | |
302 ## Help | |
303 _help_help = [('help', 'Show documentation')] | |
304 | |
305 def all_docs(cls): | |
306 return (cls._help_about + cls._help_help + | |
307 cls._help_initenv + cls._help_hotcopy + | |
308 cls._help_resync + cls._help_upgrade + | |
309 cls._help_wiki + | |
310 # cls._help_config + cls._help_wiki + | |
311 cls._help_permission + cls._help_component + | |
312 cls._help_ticket + | |
313 cls._help_ticket_type + cls._help_priority + | |
314 cls._help_severity + cls._help_version + | |
315 cls._help_milestone) | |
316 all_docs = classmethod(all_docs) | |
317 | |
318 def do_help(self, line=None): | |
319 arg = self.arg_tokenize(line) | |
320 if arg[0]: | |
321 try: | |
322 doc = getattr(self, "_help_" + arg[0]) | |
323 self.print_doc(doc) | |
324 except AttributeError: | |
325 print "No documentation found for '%s'" % arg[0] | |
326 else: | |
327 print 'trac-admin - The Trac Administration Console %s' \ | |
328 % trac.__version__ | |
329 if not self.interactive: | |
330 print | |
331 print "Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n" | |
332 print "Invoking trac-admin without command starts "\ | |
333 "interactive mode." | |
334 self.print_doc(self.all_docs()) | |
335 | |
336 | |
337 ## About / Version | |
338 _help_about = [('about', 'Shows information about trac-admin')] | |
339 | |
340 def do_about(self, line): | |
341 print | |
342 print 'Trac Admin Console %s' % trac.__version__ | |
343 print '=================================================================' | |
344 print self.license | |
345 | |
346 | |
347 ## Quit / EOF | |
348 _help_quit = [['quit', 'Exit the program']] | |
349 _help_exit = _help_quit | |
350 _help_EOF = _help_quit | |
351 | |
352 def do_quit(self, line): | |
353 print | |
354 sys.exit() | |
355 | |
356 do_exit = do_quit # Alias | |
357 do_EOF = do_quit # Alias | |
358 | |
359 | |
360 # Component | |
361 _help_component = [('component list', 'Show available components'), | |
362 ('component add <name> <owner>', 'Add a new component'), | |
363 ('component rename <name> <newname>', | |
364 'Rename a component'), | |
365 ('component remove <name>', | |
366 'Remove/uninstall component'), | |
367 ('component chown <name> <owner>', | |
368 'Change component ownership')] | |
369 | |
370 def complete_component(self, text, line, begidx, endidx): | |
371 if begidx in (16, 17): | |
372 comp = self.get_component_list() | |
373 elif begidx > 15 and line.startswith('component chown '): | |
374 comp = self.get_user_list() | |
375 else: | |
376 comp = ['list', 'add', 'rename', 'remove', 'chown'] | |
377 return self.word_complete(text, comp) | |
378 | |
379 def do_component(self, line): | |
380 arg = self.arg_tokenize(line) | |
381 if arg[0] == 'list': | |
382 self._do_component_list() | |
383 elif arg[0] == 'add' and len(arg)==3: | |
384 name = arg[1] | |
385 owner = arg[2] | |
386 self._do_component_add(name, owner) | |
387 elif arg[0] == 'rename' and len(arg)==3: | |
388 name = arg[1] | |
389 newname = arg[2] | |
390 self._do_component_rename(name, newname) | |
391 elif arg[0] == 'remove' and len(arg)==2: | |
392 name = arg[1] | |
393 self._do_component_remove(name) | |
394 elif arg[0] == 'chown' and len(arg)==3: | |
395 name = arg[1] | |
396 owner = arg[2] | |
397 self._do_component_set_owner(name, owner) | |
398 else: | |
399 self.do_help ('component') | |
400 | |
401 def _do_component_list(self): | |
402 data = [] | |
403 for c in Component.select(self.env_open()): | |
404 data.append((c.name, c.owner)) | |
405 self.print_listing(['Name', 'Owner'], data) | |
406 | |
407 def _do_component_add(self, name, owner): | |
408 component = Component(self.env_open()) | |
409 component.name = name | |
410 component.owner = owner | |
411 component.insert() | |
412 | |
413 def _do_component_rename(self, name, newname): | |
414 component = Component(self.env_open(), name) | |
415 component.name = newname | |
416 component.update() | |
417 | |
418 def _do_component_remove(self, name): | |
419 component = Component(self.env_open(), name) | |
420 component.delete() | |
421 | |
422 def _do_component_set_owner(self, name, owner): | |
423 component = Component(self.env_open(), name) | |
424 component.owner = owner | |
425 component.update() | |
426 | |
427 | |
428 ## Permission | |
429 _help_permission = [('permission list [user]', 'List permission rules'), | |
430 ('permission add <user> <action> [action] [...]', | |
431 'Add a new permission rule'), | |
432 ('permission remove <user> <action> [action] [...]', | |
433 'Remove permission rule')] | |
434 | |
435 def complete_permission(self, text, line, begidx, endidx): | |
436 argv = self.arg_tokenize(line) | |
437 argc = len(argv) | |
438 if line[-1] == ' ': # Space starts new argument | |
439 argc += 1 | |
440 if argc == 2: | |
441 comp = ['list', 'add', 'remove'] | |
442 elif argc >= 4: | |
443 comp = perm.permissions + perm.meta_permissions.keys() | |
444 comp.sort() | |
445 return self.word_complete(text, comp) | |
446 | |
447 def do_permission(self, line): | |
448 arg = self.arg_tokenize(line) | |
449 if arg[0] == 'list': | |
450 user = None | |
451 if len(arg) > 1: | |
452 user = arg[1] | |
453 self._do_permission_list(user) | |
454 elif arg[0] == 'add' and len(arg) >= 3: | |
455 user = arg[1] | |
456 for action in arg[2:]: | |
457 self._do_permission_add(user, action) | |
458 elif arg[0] == 'remove' and len(arg) >= 3: | |
459 user = arg[1] | |
460 for action in arg[2:]: | |
461 self._do_permission_remove(user, action) | |
462 else: | |
463 self.do_help('permission') | |
464 | |
465 def _do_permission_list(self, user=None): | |
466 if not self._permsys: | |
467 self._permsys = PermissionSystem(self.env_open()) | |
468 if user: | |
469 rows = [] | |
470 perms = self._permsys.get_user_permissions(user) | |
471 for action in perms: | |
472 if perms[action]: | |
473 rows.append((user, action)) | |
474 else: | |
475 rows = self._permsys.get_all_permissions() | |
476 rows.sort() | |
477 self.print_listing(['User', 'Action'], rows) | |
478 print | |
479 print 'Available actions:' | |
480 actions = self._permsys.get_actions() | |
481 actions.sort() | |
482 text = ', '.join(actions) | |
483 print wrap(text, initial_indent=' ', subsequent_indent=' ', | |
484 linesep='\n') | |
485 print | |
486 | |
487 def _do_permission_add(self, user, action): | |
488 if not self._permsys: | |
489 self._permsys = PermissionSystem(self.env_open()) | |
490 if not action.islower() and not action.isupper(): | |
491 print 'Group names must be in lower case and actions in upper case' | |
492 return | |
493 self._permsys.grant_permission(user, action) | |
494 | |
495 def _do_permission_remove(self, user, action): | |
496 if not self._permsys: | |
497 self._permsys = PermissionSystem(self.env_open()) | |
498 rows = self._permsys.get_all_permissions() | |
499 if action == '*': | |
500 for row in rows: | |
501 if user != '*' and user != row[0]: | |
502 continue | |
503 self._permsys.revoke_permission(row[0], row[1]) | |
504 else: | |
505 for row in rows: | |
506 if action != row[1]: | |
507 continue | |
508 if user != '*' and user != row[0]: | |
509 continue | |
510 self._permsys.revoke_permission(row[0], row[1]) | |
511 | |
512 ## Initenv | |
513 _help_initenv = [('initenv', | |
514 'Create and initialize a new environment interactively'), | |
515 ('initenv <projectname> <db> <repostype> <repospath> <templatepath>', | |
516 'Create and initialize a new environment from arguments')] | |
517 | |
518 def do_initdb(self, line): | |
519 self.do_initenv(line) | |
520 | |
521 def get_initenv_args(self): | |
522 returnvals = [] | |
523 print 'Creating a new Trac environment at %s' % self.envname | |
524 print | |
525 print 'Trac will first ask a few questions about your environment ' | |
526 print 'in order to initalize and prepare the project database.' | |
527 print | |
528 print " Please enter the name of your project." | |
529 print " This name will be used in page titles and descriptions." | |
530 print | |
531 dp = 'My Project' | |
532 returnvals.append(raw_input('Project Name [%s]> ' % dp).strip() or dp) | |
533 print | |
534 print ' Please specify the connection string for the database to use.' | |
535 print ' By default, a local SQLite database is created in the environment ' | |
536 print ' directory. It is also possible to use an already existing ' | |
537 print ' PostgreSQL database (check the Trac documentation for the exact ' | |
538 print ' connection string syntax).' | |
539 print | |
540 ddb = 'sqlite:db/trac.db' | |
541 prompt = 'Database connection string [%s]> ' % ddb | |
542 returnvals.append(raw_input(prompt).strip() or ddb) | |
543 print | |
544 print ' Please specify the type of version control system,' | |
545 print ' By default, it will be svn.' | |
546 print | |
547 print ' If you don\'t want to use Trac with version control integration, ' | |
548 print ' choose the default here and don\'t specify a repository directory. ' | |
549 print ' in the next question.' | |
550 print | |
551 drpt = 'svn' | |
552 prompt = 'Repository type [%s]> ' % drpt | |
553 returnvals.append(raw_input(prompt).strip() or drpt) | |
554 print | |
555 print ' Please specify the absolute path to the version control ' | |
556 print ' repository, or leave it blank to use Trac without a repository.' | |
557 print ' You can also set the repository location later.' | |
558 print | |
559 prompt = 'Path to repository [/path/to/repos]> ' | |
560 returnvals.append(raw_input(prompt).strip()) | |
561 print | |
562 print ' Please enter location of Trac page templates.' | |
563 print ' Default is the location of the site-wide templates installed with Trac.' | |
564 print | |
565 dt = default_dir('templates') | |
566 prompt = 'Templates directory [%s]> ' % dt | |
567 returnvals.append(raw_input(prompt).strip() or dt) | |
568 print | |
569 return returnvals | |
570 | |
571 def do_initenv(self, line): | |
572 if self.env_check(): | |
573 print "Initenv for '%s' failed." % self.envname | |
574 print "Does an environment already exist?" | |
575 return 2 | |
576 | |
577 if os.path.exists(self.envname) and os.listdir(self.envname): | |
578 print "Initenv for '%s' failed." % self.envname | |
579 print "Directory exists and is not empty." | |
580 return 2 | |
581 | |
582 arg = self.arg_tokenize(line) | |
583 project_name = None | |
584 db_str = None | |
585 repository_dir = None | |
586 templates_dir = None | |
587 if len(arg) == 1 and not arg[0]: | |
588 returnvals = self.get_initenv_args() | |
589 project_name, db_str, repository_type, repository_dir, \ | |
590 templates_dir = returnvals | |
591 elif len(arg) != 5: | |
592 print 'Wrong number of arguments to initenv: %d' % len(arg) | |
593 return 2 | |
594 else: | |
595 project_name, db_str, repository_type, repository_dir, \ | |
596 templates_dir = arg[:5] | |
597 | |
598 if not os.access(os.path.join(templates_dir, 'header.cs'), os.F_OK): | |
599 print templates_dir, "doesn't look like a Trac templates directory" | |
600 return 2 | |
601 | |
602 try: | |
603 print 'Creating and Initializing Project' | |
604 options = [ | |
605 ('trac', 'database', db_str), | |
606 ('trac', 'repository_type', repository_type), | |
607 ('trac', 'repository_dir', repository_dir), | |
608 ('trac', 'templates_dir', templates_dir), | |
609 ('project', 'name', project_name), | |
610 ] | |
611 try: | |
612 self.__env = Environment(self.envname, create=True, | |
613 options=options) | |
614 except Exception, e: | |
615 print 'Failed to create environment.', e | |
616 traceback.print_exc() | |
617 sys.exit(1) | |
618 | |
619 # Add a few default wiki pages | |
620 print ' Installing default wiki pages' | |
621 cnx = self.__env.get_db_cnx() | |
622 cursor = cnx.cursor() | |
623 self._do_wiki_load(default_dir('wiki'), cursor) | |
624 cnx.commit() | |
625 | |
626 if repository_dir: | |
627 try: | |
628 repos = self.__env.get_repository() | |
629 if repos: | |
630 print ' Indexing repository' | |
631 repos.sync() | |
632 except TracError, e: | |
633 print>>sys.stderr, "\nWarning:\n" | |
634 if repository_type == "svn": | |
635 print>>sys.stderr, "You should install the SVN bindings" | |
636 else: | |
637 print>>sys.stderr, "Repository type %s not supported" \ | |
638 % repository_type | |
639 except Exception, e: | |
640 print 'Failed to initialize environment.', e | |
641 traceback.print_exc() | |
642 return 2 | |
643 | |
644 print """ | |
645 --------------------------------------------------------------------- | |
646 Project environment for '%(project_name)s' created. | |
647 | |
648 You may now configure the environment by editing the file: | |
649 | |
650 %(config_path)s | |
651 | |
652 If you'd like to take this new project environment for a test drive, | |
653 try running the Trac standalone web server `tracd`: | |
654 | |
655 tracd --port 8000 %(project_path)s | |
656 | |
657 Then point your browser to http://localhost:8000/%(project_dir)s. | |
658 There you can also browse the documentation for your installed | |
659 version of Trac, including information on further setup (such as | |
660 deploying Trac to a real web server). | |
661 | |
662 The latest documentation can also always be found on the project | |
663 website: | |
664 | |
665 http://projects.edgewall.com/trac/ | |
666 | |
667 Congratulations! | |
668 """ % dict(project_name=project_name, project_path=self.envname, | |
669 project_dir=os.path.basename(self.envname), | |
670 config_path=os.path.join(self.envname, 'conf', 'trac.ini')) | |
671 | |
672 _help_resync = [('resync', 'Re-synchronize trac with the repository')] | |
673 | |
674 ## Resync | |
675 def do_resync(self, line): | |
676 print 'Resyncing repository history...' | |
677 cnx = self.db_open() | |
678 cursor = cnx.cursor() | |
679 cursor.execute("DELETE FROM revision") | |
680 cursor.execute("DELETE FROM node_change") | |
681 repos = self.__env.get_repository() | |
682 cursor.execute("DELETE FROM system WHERE name='repository_dir'") | |
683 cursor.execute("INSERT INTO system (name,value) " | |
684 "VALUES ('repository_dir',%s)", (repos.name,)) | |
685 repos.sync() | |
686 print 'Done.' | |
687 | |
688 ## Wiki | |
689 _help_wiki = [('wiki list', 'List wiki pages'), | |
690 ('wiki remove <name>', 'Remove wiki page'), | |
691 ('wiki export <page> [file]', | |
692 'Export wiki page to file or stdout'), | |
693 ('wiki import <page> [file]', | |
694 'Import wiki page from file or stdin'), | |
695 ('wiki dump <directory>', | |
696 'Export all wiki pages to files named by title'), | |
697 ('wiki load <directory>', | |
698 'Import all wiki pages from directory'), | |
699 ('wiki upgrade', | |
700 'Upgrade default wiki pages to current version')] | |
701 | |
702 def complete_wiki(self, text, line, begidx, endidx): | |
703 argv = self.arg_tokenize(line) | |
704 argc = len(argv) | |
705 if line[-1] == ' ': # Space starts new argument | |
706 argc += 1 | |
707 if argc == 2: | |
708 comp = ['list', 'remove', 'import', 'export', 'dump', 'load', | |
709 'upgrade'] | |
710 else: | |
711 if argv[1] in ('dump', 'load'): | |
712 comp = self.get_dir_list(argv[-1], 1) | |
713 elif argv[1] == 'remove': | |
714 comp = self.get_wiki_list() | |
715 elif argv[1] in ('export', 'import'): | |
716 if argc == 3: | |
717 comp = self.get_wiki_list() | |
718 elif argc == 4: | |
719 comp = self.get_dir_list(argv[-1]) | |
720 return self.word_complete(text, comp) | |
721 | |
722 def do_wiki(self, line): | |
723 arg = self.arg_tokenize(line) | |
724 if arg[0] == 'list': | |
725 self._do_wiki_list() | |
726 elif arg[0] == 'remove' and len(arg)==2: | |
727 name = arg[1] | |
728 self._do_wiki_remove(name) | |
729 elif arg[0] == 'import' and len(arg) == 3: | |
730 title = arg[1] | |
731 file = arg[2] | |
732 self._do_wiki_import(file, title) | |
733 elif arg[0] == 'export' and len(arg) in [2,3]: | |
734 page = arg[1] | |
735 file = (len(arg) == 3 and arg[2]) or None | |
736 self._do_wiki_export(page, file) | |
737 elif arg[0] == 'dump' and len(arg) in [1,2]: | |
738 dir = (len(arg) == 2 and arg[1]) or '' | |
739 self._do_wiki_dump(dir) | |
740 elif arg[0] == 'load' and len(arg) in [1,2]: | |
741 dir = (len(arg) == 2 and arg[1]) or '' | |
742 self._do_wiki_load(dir) | |
743 elif arg[0] == 'upgrade' and len(arg) == 1: | |
744 self._do_wiki_load(default_dir('wiki'), | |
745 ignore=['WikiStart', 'checkwiki.py'], | |
746 create_only=['InterMapTxt']) | |
747 else: | |
748 self.do_help ('wiki') | |
749 | |
750 def _do_wiki_list(self): | |
751 rows = self.db_query("SELECT name, max(version), max(time) " | |
752 "FROM wiki GROUP BY name ORDER BY name") | |
753 self.print_listing(['Title', 'Edits', 'Modified'], | |
754 [(r[0], r[1], self._format_datetime(r[2])) for r in rows]) | |
755 | |
756 def _do_wiki_remove(self, name): | |
757 page = WikiPage(self.env_open(), name) | |
758 page.delete() | |
759 | |
760 def _do_wiki_import(self, filename, title, cursor=None, | |
761 create_only=[]): | |
762 if not os.path.isfile(filename): | |
763 raise Exception, '%s is not a file' % filename | |
764 | |
765 f = open(filename,'r') | |
766 data = to_unicode(f.read(), 'utf-8') | |
767 | |
768 # Make sure we don't insert the exact same page twice | |
769 rows = self.db_query("SELECT text FROM wiki WHERE name=%s " | |
770 "ORDER BY version DESC LIMIT 1", cursor, | |
771 params=(title,)) | |
772 old = list(rows) | |
773 if old and title in create_only: | |
774 print ' %s already exists.' % title | |
775 return | |
776 if old and data == old[0][0]: | |
777 print ' %s already up to date.' % title | |
778 return | |
779 f.close() | |
780 | |
781 self.db_update("INSERT INTO wiki(version,name,time,author,ipnr,text) " | |
782 " SELECT 1+COALESCE(max(version),0),%s,%s," | |
783 " 'trac','127.0.0.1',%s FROM wiki " | |
784 " WHERE name=%s", | |
785 cursor, (title, int(time.time()), data, title)) | |
786 | |
787 def _do_wiki_export(self, page, filename=''): | |
788 data = self.db_query("SELECT text FROM wiki WHERE name=%s " | |
789 "ORDER BY version DESC LIMIT 1", params=[page]) | |
790 text = data.next()[0] | |
791 if not filename: | |
792 print text | |
793 else: | |
794 if os.path.isfile(filename): | |
795 raise Exception("File '%s' exists" % filename) | |
796 f = open(filename,'w') | |
797 f.write(text.encode('utf-8')) | |
798 f.close() | |
799 | |
800 def _do_wiki_dump(self, dir): | |
801 pages = self.get_wiki_list() | |
802 for p in pages: | |
803 dst = os.path.join(dir, urllib.quote(p, '')) | |
804 print " %s => %s" % (p, dst) | |
805 self._do_wiki_export(p, dst) | |
806 | |
807 def _do_wiki_load(self, dir, cursor=None, ignore=[], create_only=[]): | |
808 for page in os.listdir(dir): | |
809 if page in ignore: | |
810 continue | |
811 filename = os.path.join(dir, page) | |
812 page = urllib.unquote(page) | |
813 if os.path.isfile(filename): | |
814 print " %s => %s" % (filename, page) | |
815 self._do_wiki_import(filename, page, cursor, create_only) | |
816 | |
817 ## Ticket | |
818 _help_ticket = [('ticket remove <number>', 'Remove ticket')] | |
819 | |
820 def complete_ticket(self, text, line, begidx, endidx): | |
821 argv = self.arg_tokenize(line) | |
822 argc = len(argv) | |
823 if line[-1] == ' ': # Space starts new argument | |
824 argc += 1 | |
825 comp = [] | |
826 if argc == 2: | |
827 comp = ['remove'] | |
828 return self.word_complete(text, comp) | |
829 | |
830 def do_ticket(self, line): | |
831 arg = self.arg_tokenize(line) | |
832 if arg[0] == 'remove' and len(arg)==2: | |
833 try: | |
834 number = int(arg[1]) | |
835 except ValueError: | |
836 print>>sys.stderr, "<number> must be a number" | |
837 return | |
838 self._do_ticket_remove(number) | |
839 else: | |
840 self.do_help ('ticket') | |
841 | |
842 def _do_ticket_remove(self, number): | |
843 ticket = Ticket(self.env_open(), number) | |
844 ticket.delete() | |
845 print "Ticket %d and all associated data removed." % number | |
846 | |
847 | |
848 ## (Ticket) Type | |
849 _help_ticket_type = [('ticket_type list', 'Show possible ticket types'), | |
850 ('ticket_type add <value>', 'Add a ticket type'), | |
851 ('ticket_type change <value> <newvalue>', | |
852 'Change a ticket type'), | |
853 ('ticket_type remove <value>', 'Remove a ticket type'), | |
854 ('ticket_type order <value> up|down', | |
855 'Move a ticket type up or down in the list')] | |
856 | |
857 def complete_ticket_type (self, text, line, begidx, endidx): | |
858 if begidx == 16: | |
859 comp = self.get_enum_list ('ticket_type') | |
860 elif begidx < 15: | |
861 comp = ['list', 'add', 'change', 'remove', 'order'] | |
862 return self.word_complete(text, comp) | |
863 | |
864 def do_ticket_type(self, line): | |
865 self._do_enum('ticket_type', line) | |
866 | |
867 ## (Ticket) Priority | |
868 _help_priority = [('priority list', 'Show possible ticket priorities'), | |
869 ('priority add <value>', 'Add a priority value option'), | |
870 ('priority change <value> <newvalue>', | |
871 'Change a priority value'), | |
872 ('priority remove <value>', 'Remove priority value'), | |
873 ('priority order <value> up|down', | |
874 'Move a priority value up or down in the list')] | |
875 | |
876 def complete_priority (self, text, line, begidx, endidx): | |
877 if begidx == 16: | |
878 comp = self.get_enum_list ('priority') | |
879 elif begidx < 15: | |
880 comp = ['list', 'add', 'change', 'remove', 'order'] | |
881 return self.word_complete(text, comp) | |
882 | |
883 def do_priority(self, line): | |
884 self._do_enum('priority', line) | |
885 | |
886 ## (Ticket) Severity | |
887 _help_severity = [('severity list', 'Show possible ticket severities'), | |
888 ('severity add <value>', 'Add a severity value option'), | |
889 ('severity change <value> <newvalue>', | |
890 'Change a severity value'), | |
891 ('severity remove <value>', 'Remove severity value'), | |
892 ('severity order <value> up|down', | |
893 'Move a severity value up or down in the list')] | |
894 | |
895 def complete_severity (self, text, line, begidx, endidx): | |
896 if begidx == 16: | |
897 comp = self.get_enum_list ('severity') | |
898 elif begidx < 15: | |
899 comp = ['list', 'add', 'change', 'remove', 'order'] | |
900 return self.word_complete(text, comp) | |
901 | |
902 def do_severity(self, line): | |
903 self._do_enum('severity', line) | |
904 | |
905 # Type, priority, severity share the same datastructure and methods: | |
906 | |
907 _enum_map = {'ticket_type': Type, 'priority': Priority, | |
908 'severity': Severity} | |
909 | |
910 def _do_enum(self, type, line): | |
911 arg = self.arg_tokenize(line) | |
912 if arg[0] == 'list': | |
913 self._do_enum_list(type) | |
914 elif arg[0] == 'add' and len(arg) == 2: | |
915 name = arg[1] | |
916 self._do_enum_add(type, name) | |
917 elif arg[0] == 'change' and len(arg) == 3: | |
918 name = arg[1] | |
919 newname = arg[2] | |
920 self._do_enum_change(type, name, newname) | |
921 elif arg[0] == 'remove' and len(arg) == 2: | |
922 name = arg[1] | |
923 self._do_enum_remove(type, name) | |
924 elif arg[0] == 'order' and len(arg) == 3 and arg[2] in ('up', 'down'): | |
925 name = arg[1] | |
926 if arg[2] == 'up': | |
927 direction = -1 | |
928 else: | |
929 direction = 1 | |
930 self._do_enum_order(type, name, direction) | |
931 else: | |
932 self.do_help(type) | |
933 | |
934 def _do_enum_list(self, type): | |
935 enum_cls = self._enum_map[type] | |
936 self.print_listing(['Possible Values'], | |
937 [(e.name,) for e in enum_cls.select(self.env_open())]) | |
938 | |
939 def _do_enum_add(self, type, name): | |
940 cnx = self.db_open() | |
941 sql = ("INSERT INTO enum(value,type,name) " | |
942 " SELECT 1+COALESCE(max(%(cast)s),0),'%(type)s','%(name)s'" | |
943 " FROM enum WHERE type='%(type)s'" | |
944 % {'type':type, 'name':name, 'cast': cnx.cast('value', 'int')}) | |
945 cursor = cnx.cursor() | |
946 self.db_update(sql, cursor) | |
947 cnx.commit() | |
948 | |
949 def _do_enum_change(self, type, name, newname): | |
950 enum_cls = self._enum_map[type] | |
951 enum = enum_cls(self.env_open(), name) | |
952 enum.name = newname | |
953 enum.update() | |
954 | |
955 def _do_enum_remove(self, type, name): | |
956 enum_cls = self._enum_map[type] | |
957 enum = enum_cls(self.env_open(), name) | |
958 enum.delete() | |
959 | |
960 def _do_enum_order(self, type, name, direction): | |
961 env = self.env_open() | |
962 enum_cls = self._enum_map[type] | |
963 enum1 = enum_cls(env, name) | |
964 enum1.value = int(float(enum1.value) + direction) | |
965 for enum2 in enum_cls.select(env): | |
966 if int(float(enum2.value)) == enum1.value: | |
967 enum2.value = int(float(enum2.value) - direction) | |
968 break | |
969 else: | |
970 return | |
971 enum1.update() | |
972 enum2.update() | |
973 | |
974 ## Milestone | |
975 | |
976 _help_milestone = [('milestone list', 'Show milestones'), | |
977 ('milestone add <name> [due]', 'Add milestone'), | |
978 ('milestone rename <name> <newname>', | |
979 'Rename milestone'), | |
980 ('milestone due <name> <due>', | |
981 'Set milestone due date (Format: "%s" or "now")' | |
982 % _date_format_hint), | |
983 ('milestone completed <name> <completed>', | |
984 'Set milestone completed date (Format: "%s" or "now")' | |
985 % _date_format_hint), | |
986 ('milestone remove <name>', 'Remove milestone')] | |
987 | |
988 def complete_milestone (self, text, line, begidx, endidx): | |
989 if begidx in (15, 17): | |
990 comp = self.get_milestone_list() | |
991 elif begidx < 15: | |
992 comp = ['list', 'add', 'rename', 'time', 'remove'] | |
993 return self.word_complete(text, comp) | |
994 | |
995 def do_milestone(self, line): | |
996 arg = self.arg_tokenize(line) | |
997 if arg[0] == 'list': | |
998 self._do_milestone_list() | |
999 elif arg[0] == 'add' and len(arg) in [2,3]: | |
1000 self._do_milestone_add(arg[1]) | |
1001 if len(arg) == 3: | |
1002 self._do_milestone_set_due(arg[1], arg[2]) | |
1003 elif arg[0] == 'rename' and len(arg) == 3: | |
1004 self._do_milestone_rename(arg[1], arg[2]) | |
1005 elif arg[0] == 'remove' and len(arg) == 2: | |
1006 self._do_milestone_remove(arg[1]) | |
1007 elif arg[0] == 'due' and len(arg) == 3: | |
1008 self._do_milestone_set_due(arg[1], arg[2]) | |
1009 elif arg[0] == 'completed' and len(arg) == 3: | |
1010 self._do_milestone_set_completed(arg[1], arg[2]) | |
1011 else: | |
1012 self.do_help('milestone') | |
1013 | |
1014 def _do_milestone_list(self): | |
1015 data = [] | |
1016 for m in Milestone.select(self.env_open()): | |
1017 data.append((m.name, m.due and self._format_date(m.due), | |
1018 m.completed and self._format_datetime(m.completed))) | |
1019 | |
1020 self.print_listing(['Name', 'Due', 'Completed'], data) | |
1021 | |
1022 def _do_milestone_rename(self, name, newname): | |
1023 milestone = Milestone(self.env_open(), name) | |
1024 milestone.name = newname | |
1025 milestone.update() | |
1026 | |
1027 def _do_milestone_add(self, name): | |
1028 milestone = Milestone(self.env_open()) | |
1029 milestone.name = name | |
1030 milestone.insert() | |
1031 | |
1032 def _do_milestone_remove(self, name): | |
1033 milestone = Milestone(self.env_open(), name) | |
1034 milestone.delete(author=getpass.getuser()) | |
1035 | |
1036 def _do_milestone_set_due(self, name, t): | |
1037 milestone = Milestone(self.env_open(), name) | |
1038 milestone.due = self._parse_date(t) | |
1039 milestone.update() | |
1040 | |
1041 def _do_milestone_set_completed(self, name, t): | |
1042 milestone = Milestone(self.env_open(), name) | |
1043 milestone.completed = self._parse_date(t) | |
1044 milestone.update() | |
1045 | |
1046 ## Version | |
1047 _help_version = [('version list', 'Show versions'), | |
1048 ('version add <name> [time]', 'Add version'), | |
1049 ('version rename <name> <newname>', | |
1050 'Rename version'), | |
1051 ('version time <name> <time>', | |
1052 'Set version date (Format: "%s" or "now")' | |
1053 % _date_format_hint), | |
1054 ('version remove <name>', 'Remove version')] | |
1055 | |
1056 def complete_version (self, text, line, begidx, endidx): | |
1057 if begidx in (13, 15): | |
1058 comp = self.get_version_list() | |
1059 elif begidx < 13: | |
1060 comp = ['list', 'add', 'rename', 'time', 'remove'] | |
1061 return self.word_complete(text, comp) | |
1062 | |
1063 def do_version(self, line): | |
1064 arg = self.arg_tokenize(line) | |
1065 if arg[0] == 'list': | |
1066 self._do_version_list() | |
1067 elif arg[0] == 'add' and len(arg) in [2,3]: | |
1068 self._do_version_add(arg[1]) | |
1069 if len(arg) == 3: | |
1070 self._do_version_time(arg[1], arg[2]) | |
1071 elif arg[0] == 'rename' and len(arg) == 3: | |
1072 self._do_version_rename(arg[1], arg[2]) | |
1073 elif arg[0] == 'time' and len(arg) == 3: | |
1074 self._do_version_time(arg[1], arg[2]) | |
1075 elif arg[0] == 'remove' and len(arg) == 2: | |
1076 self._do_version_remove(arg[1]) | |
1077 else: | |
1078 self.do_help('version') | |
1079 | |
1080 def _do_version_list(self): | |
1081 data = [] | |
1082 for v in Version.select(self.env_open()): | |
1083 data.append((v.name, v.time and self._format_date(v.time))) | |
1084 self.print_listing(['Name', 'Time'], data) | |
1085 | |
1086 def _do_version_rename(self, name, newname): | |
1087 version = Version(self.env_open(), name) | |
1088 version.name = newname | |
1089 version.update() | |
1090 | |
1091 def _do_version_add(self, name): | |
1092 version = Version(self.env_open()) | |
1093 version.name = name | |
1094 version.insert() | |
1095 | |
1096 def _do_version_remove(self, name): | |
1097 version = Version(self.env_open(), name) | |
1098 version.delete() | |
1099 | |
1100 def _do_version_time(self, name, t): | |
1101 version = Version(self.env_open(), name) | |
1102 version.time = self._parse_date(t) | |
1103 version.update() | |
1104 | |
1105 _help_upgrade = [('upgrade', 'Upgrade database to current version')] | |
1106 def do_upgrade(self, line): | |
1107 arg = self.arg_tokenize(line) | |
1108 do_backup = True | |
1109 if arg[0] in ['-b', '--no-backup']: | |
1110 do_backup = False | |
1111 self.db_open() | |
1112 | |
1113 if not self.__env.needs_upgrade(): | |
1114 print "Database is up to date, no upgrade necessary." | |
1115 return | |
1116 | |
1117 self.__env.upgrade(backup=do_backup) | |
1118 print 'Upgrade done.' | |
1119 | |
1120 _help_hotcopy = [('hotcopy <backupdir>', | |
1121 'Make a hot backup copy of an environment')] | |
1122 def do_hotcopy(self, line): | |
1123 arg = self.arg_tokenize(line) | |
1124 if arg[0]: | |
1125 dest = arg[0] | |
1126 else: | |
1127 self.do_help('hotcopy') | |
1128 return | |
1129 | |
1130 # Bogus statement to lock the database while copying files | |
1131 cnx = self.db_open() | |
1132 cursor = cnx.cursor() | |
1133 cursor.execute("UPDATE system SET name=NULL WHERE name IS NULL") | |
1134 | |
1135 try: | |
1136 print 'Hotcopying %s to %s ...' % (self.__env.path, dest), | |
1137 db_str = self.__env.config.get('trac', 'database') | |
1138 prefix, db_path = db_str.split(':', 1) | |
1139 if prefix == 'sqlite': | |
1140 # don't copy the journal (also, this would fail on Windows) | |
1141 db_path = os.path.normpath(db_path) | |
1142 skip = ['%s-journal' % os.path.join(self.__env.path, db_path)] | |
1143 else: | |
1144 skip = [] | |
1145 copytree(self.__env.path, dest, symlinks=1, skip=skip) | |
1146 finally: | |
1147 # Unlock database | |
1148 cnx.rollback() | |
1149 | |
1150 print 'Hotcopy done.' | |
1151 | |
1152 | |
1153 class TracAdminHelpMacro(WikiMacroBase): | |
1154 """Displays help for trac-admin commands. | |
1155 | |
1156 Examples: | |
1157 {{{ | |
1158 [[TracAdminHelp]] # all commands | |
1159 [[TracAdminHelp(wiki)]] # all wiki commands | |
1160 [[TracAdminHelp(wiki export)]] # the "wiki export" command | |
1161 [[TracAdminHelp(upgrade)]] # the upgrade command | |
1162 }}} | |
1163 """ | |
1164 | |
1165 def render_macro(self, req, name, content): | |
1166 if content: | |
1167 try: | |
1168 arg = content.split(' ', 1)[0] | |
1169 doc = getattr(TracAdmin, '_help_' + arg) | |
1170 except AttributeError: | |
1171 raise TracError('Unknown trac-admin command "%s"' % content) | |
1172 if arg != content: | |
1173 for cmd, help in doc: | |
1174 if cmd.startswith(content): | |
1175 doc = [(cmd, help)] | |
1176 break | |
1177 else: | |
1178 doc = TracAdmin.all_docs() | |
1179 buf = StringIO.StringIO() | |
1180 TracAdmin.print_doc(doc, buf) | |
1181 return html.PRE(buf.getvalue(), class_='wiki') | |
1182 | |
1183 | |
1184 def run(args): | |
1185 """Main entry point.""" | |
1186 admin = TracAdmin() | |
1187 if len(args) > 0: | |
1188 if args[0] in ('-h', '--help', 'help'): | |
1189 return admin.onecmd("help") | |
1190 elif args[0] in ('-v','--version','about'): | |
1191 return admin.onecmd("about") | |
1192 else: | |
1193 admin.env_set(os.path.abspath(args[0])) | |
1194 if len(args) > 1: | |
1195 s_args = ' '.join(["'%s'" % c for c in args[2:]]) | |
1196 command = args[1] + ' ' +s_args | |
1197 return admin.onecmd(command) | |
1198 else: | |
1199 while True: | |
1200 admin.run() | |
1201 else: | |
1202 return admin.onecmd("help") | |
1203 | |
1204 | |
1205 if __name__ == '__main__': | |
1206 run(sys.argv[1:]) |