3 # svnshell.py : a Python-based shell interface for cruising 'round in
6 ######################################################################
8 # Copyright (c) 2000-2004 CollabNet. All rights reserved.
10 # This software is licensed as described in the file COPYING, which
11 # you should have received as part of this distribution. The terms
12 # are also available at http://subversion.tigris.org/license-1.html.
13 # If newer versions of this license are posted there, you may use a
14 # newer version instead, at your option.
16 ######################################################################
24 from random
import randint
25 from svn
import fs
, core
, repos
29 def __init__(self
, path
):
30 """initialize an SVNShell object"""
32 path
= core
.svn_path_canonicalize(path
)
33 self
.fs_ptr
= repos
.fs(repos
.open(path
))
35 self
.rev
= fs
.youngest_rev(self
.fs_ptr
)
37 self
.root
= fs
.revision_root(self
.fs_ptr
, self
.rev
)
42 def precmd(self
, line
):
44 # Ctrl-D is a command without a newline. Print a newline, so the next
45 # shell prompt is not on the same line as the last svnshell prompt.
50 def postcmd(self
, stop
, line
):
54 "Whatchoo talkin' 'bout, Willis?",
56 "Nope. Not gonna do it.",
57 "Ehh...I don't think so, chief."]
59 def default(self
, line
):
60 print self
._errors
[randint(0, len(self
._errors
) - 1)]
62 def do_cat(self
, arg
):
63 """dump the contents of a file"""
65 print "You must supply a file path."
67 catpath
= self
._parse
_path
(arg
)
68 kind
= fs
.check_path(self
.root
, catpath
)
69 if kind
== core
.svn_node_none
:
70 print "Path '%s' does not exist." % catpath
72 if kind
== core
.svn_node_dir
:
73 print "Path '%s' is not a file." % catpath
75 ### be nice to get some paging in here.
76 stream
= fs
.file_contents(self
.root
, catpath
)
78 data
= core
.svn_stream_read(stream
, core
.SVN_STREAM_CHUNK_SIZE
)
79 sys
.stdout
.write(data
)
80 if len(data
) < core
.SVN_STREAM_CHUNK_SIZE
:
84 """change directory"""
85 newpath
= self
._parse
_path
(arg
)
87 # make sure that path actually exists in the filesystem as a directory
88 kind
= fs
.check_path(self
.root
, newpath
)
89 if kind
!= core
.svn_node_dir
:
90 print "Path '%s' is not a valid filesystem directory." % newpath
95 """list the contents of the current directory or provided path"""
98 # no arg -- show a listing for the current directory.
99 entries
= fs
.dir_entries(self
.root
, self
.path
)
101 # arg? show a listing of that path.
102 newpath
= self
._parse
_path
(arg
)
103 kind
= fs
.check_path(self
.root
, newpath
)
104 if kind
== core
.svn_node_dir
:
106 entries
= fs
.dir_entries(self
.root
, parent
)
107 elif kind
== core
.svn_node_file
:
108 parts
= self
._path
_to
_parts
(newpath
)
110 parent
= self
._parts
_to
_path
(parts
)
111 print parent
+ ':' + name
112 tmpentries
= fs
.dir_entries(self
.root
, parent
)
113 if not tmpentries
.get(name
, None):
116 entries
[name
] = tmpentries
[name
]
118 print "Path '%s' not found." % newpath
121 keys
= entries
.keys()
124 print " REV AUTHOR NODE-REV-ID SIZE DATE NAME"
125 print "----------------------------------------------------------------------------"
128 fullpath
= parent
+ '/' + entry
130 is_dir
= fs
.is_dir(self
.root
, fullpath
)
134 size
= str(fs
.file_length(self
.root
, fullpath
))
136 node_id
= fs
.unparse_id(entries
[entry
].id)
137 created_rev
= fs
.node_created_rev(self
.root
, fullpath
)
138 author
= fs
.revision_prop(self
.fs_ptr
, created_rev
,
139 core
.SVN_PROP_REVISION_AUTHOR
)
142 date
= fs
.revision_prop(self
.fs_ptr
, created_rev
,
143 core
.SVN_PROP_REVISION_DATE
)
147 date
= self
._format
_date
(date
)
149 print "%6s %8s %12s %8s %12s %s" % (created_rev
, author
[:8],
150 node_id
, size
, date
, name
)
152 def do_lstxns(self
, arg
):
153 """list the transactions available for browsing"""
154 txns
= fs
.list_transactions(self
.fs_ptr
)
158 counter
= counter
+ 1
165 def do_pcat(self
, arg
):
166 """list the properties of a path"""
169 catpath
= self
._parse
_path
(arg
)
170 kind
= fs
.check_path(self
.root
, catpath
)
171 if kind
== core
.svn_node_none
:
172 print "Path '%s' does not exist." % catpath
174 plist
= fs
.node_proplist(self
.root
, catpath
)
177 for pkey
, pval
in plist
.items():
178 print 'K ' + str(len(pkey
))
180 print 'P ' + str(len(pval
))
184 def do_setrev(self
, arg
):
185 """set the current revision to view"""
187 if arg
.lower() == 'head':
188 rev
= fs
.youngest_rev(self
.fs_ptr
)
191 newroot
= fs
.revision_root(self
.fs_ptr
, rev
)
193 print "Error setting the revision to '" + arg
+ "'."
195 fs
.close_root(self
.root
)
199 self
._do
_path
_landing
()
201 def do_settxn(self
, arg
):
202 """set the current transaction to view"""
204 txnobj
= fs
.open_txn(self
.fs_ptr
, arg
)
205 newroot
= fs
.txn_root(txnobj
)
207 print "Error setting the transaction to '" + arg
+ "'."
209 fs
.close_root(self
.root
)
213 self
._do
_path
_landing
()
215 def do_youngest(self
, arg
):
216 """list the youngest revision available for browsing"""
217 rev
= fs
.youngest_rev(self
.fs_ptr
)
220 def do_exit(self
, arg
):
223 def _path_to_parts(self
, path
):
224 return filter(None, string
.split(path
, '/'))
226 def _parts_to_path(self
, parts
):
227 return '/' + string
.join(parts
, '/')
229 def _parse_path(self
, path
):
230 # cleanup leading, trailing, and duplicate '/' characters
231 newpath
= self
._parts
_to
_path
(self
._path
_to
_parts
(path
))
233 # if PATH is absolute, use it, else append it to the existing path.
234 if path
.startswith('/') or self
.path
== '/':
235 newpath
= '/' + newpath
237 newpath
= self
.path
+ '/' + newpath
239 # cleanup '.' and '..'
240 parts
= self
._path
_to
_parts
(newpath
)
246 if len(finalparts
) != 0:
249 finalparts
.append(part
)
251 # finally, return the calculated path
252 return self
._parts
_to
_path
(finalparts
)
254 def _format_date(self
, date
):
255 date
= core
.svn_time_from_cstring(date
)
256 date
= time
.asctime(time
.localtime(date
/ 1000000))
259 def _do_path_landing(self
):
260 """try to land on self.path as a directory in root, failing up to '/'"""
264 kind
= fs
.check_path(self
.root
, newpath
)
265 if kind
== core
.svn_node_dir
:
268 parts
= self
._path
_to
_parts
(newpath
)
270 newpath
= self
._parts
_to
_path
(parts
)
273 def _setup_prompt(self
):
274 """present the prompt and handle the user's input"""
276 self
.prompt
= "<rev: " + str(self
.rev
)
278 self
.prompt
= "<txn: " + self
.txn
279 self
.prompt
+= " " + self
.path
+ ">$ "
281 def _complete(self
, text
, line
, begidx
, endidx
, limit_node_kind
=None):
282 """Generic tab completer. Takes the 4 standard parameters passed to a
283 cmd.Cmd completer function, plus LIMIT_NODE_KIND, which should be a
284 svn.core.svn_node_foo constant to restrict the returned completions to, or
285 None for no limit. Catches and displays exceptions, because otherwise
286 they are silently ignored - which is quite frustrating when debugging!"""
293 dirs
= arg
.split('/')
295 user_dir
= "/".join(dirs
[:-1] + [''])
297 canon_dir
= self
._parse
_path
(user_dir
)
299 entries
= fs
.dir_entries(self
.root
, canon_dir
)
300 acceptable_completions
= []
301 for name
, dirent_t
in entries
.items():
302 if not name
.startswith(user_elem
):
304 if limit_node_kind
and dirent_t
.kind
!= limit_node_kind
:
306 if dirent_t
.kind
== core
.svn_node_dir
:
308 acceptable_completions
.append(name
)
309 if limit_node_kind
== core
.svn_node_dir
or not limit_node_kind
:
310 if user_elem
in ('.', '..'):
311 for extraname
in ('.', '..'):
312 if extraname
.startswith(user_elem
):
313 acceptable_completions
.append(extraname
+ '/')
314 return acceptable_completions
317 sys
.stderr
.write("EXCEPTION WHILST COMPLETING\n")
319 traceback
.print_tb(ei
[2])
320 sys
.stderr
.write("%s: %s\n" % (ei
[0], ei
[1]))
323 def complete_cd(self
, text
, line
, begidx
, endidx
):
324 return self
._complete
(text
, line
, begidx
, endidx
, core
.svn_node_dir
)
326 def complete_cat(self
, text
, line
, begidx
, endidx
):
327 return self
._complete
(text
, line
, begidx
, endidx
, core
.svn_node_file
)
329 def complete_ls(self
, text
, line
, begidx
, endidx
):
330 return self
._complete
(text
, line
, begidx
, endidx
)
332 def complete_pcat(self
, text
, line
, begidx
, endidx
):
333 return self
._complete
(text
, line
, begidx
, endidx
)
337 "Return the basename for a '/'-separated path."
338 idx
= string
.rfind(path
, '/')
350 "usage: %s REPOS_PATH\n"
352 "Once the program has started, type 'help' at the prompt for hints on\n"
353 "using the shell.\n" % sys
.argv
[0])
357 if len(sys
.argv
) != 2:
360 SVNShell(sys
.argv
[1])
362 if __name__
== '__main__':