3 # svnshell.py : a Python-based shell interface for cruising 'round in
6 ######################################################################
7 # Licensed to the Apache Software Foundation (ASF) under one
8 # or more contributor license agreements. See the NOTICE file
9 # distributed with this work for additional information
10 # regarding copyright ownership. The ASF licenses this file
11 # to you under the Apache License, Version 2.0 (the
12 # "License"); you may not use this file except in compliance
13 # with the License. You may obtain a copy of the License at
15 # http://www.apache.org/licenses/LICENSE-2.0
17 # Unless required by applicable law or agreed to in writing,
18 # software distributed under the License is distributed on an
19 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20 # KIND, either express or implied. See the License for the
21 # specific language governing permissions and limitations
23 ######################################################################
30 from random
import randint
31 from svn
import fs
, core
, repos
35 def __init__(self
, path
):
36 """initialize an SVNShell object"""
38 path
= core
.svn_path_canonicalize(path
)
39 self
.fs_ptr
= repos
.fs(repos
.open(path
))
41 self
.rev
= fs
.youngest_rev(self
.fs_ptr
)
43 self
.root
= fs
.revision_root(self
.fs_ptr
, self
.rev
)
48 def precmd(self
, line
):
50 # Ctrl-D is a command without a newline. Print a newline, so the next
51 # shell prompt is not on the same line as the last svnshell prompt.
56 def postcmd(self
, stop
, line
):
60 "Whatchoo talkin' 'bout, Willis?",
62 "Nope. Not gonna do it.",
63 "Ehh...I don't think so, chief."]
65 def default(self
, line
):
66 print(self
._errors
[randint(0, len(self
._errors
) - 1)])
68 def do_cat(self
, arg
):
69 """dump the contents of a file"""
71 print("You must supply a file path.")
73 catpath
= self
._parse
_path
(arg
)
74 kind
= fs
.check_path(self
.root
, catpath
)
75 if kind
== core
.svn_node_none
:
76 print("Path '%s' does not exist." % catpath
)
78 if kind
== core
.svn_node_dir
:
79 print("Path '%s' is not a file." % catpath
)
81 ### be nice to get some paging in here.
82 stream
= fs
.file_contents(self
.root
, catpath
)
84 data
= core
.svn_stream_read(stream
, core
.SVN_STREAM_CHUNK_SIZE
)
85 sys
.stdout
.write(data
)
86 if len(data
) < core
.SVN_STREAM_CHUNK_SIZE
:
90 """change directory"""
91 newpath
= self
._parse
_path
(arg
)
93 # make sure that path actually exists in the filesystem as a directory
94 kind
= fs
.check_path(self
.root
, newpath
)
95 if kind
!= core
.svn_node_dir
:
96 print("Path '%s' is not a valid filesystem directory." % newpath
)
100 def do_ls(self
, arg
):
101 """list the contents of the current directory or provided path"""
104 # no arg -- show a listing for the current directory.
105 entries
= fs
.dir_entries(self
.root
, self
.path
)
107 # arg? show a listing of that path.
108 newpath
= self
._parse
_path
(arg
)
109 kind
= fs
.check_path(self
.root
, newpath
)
110 if kind
== core
.svn_node_dir
:
112 entries
= fs
.dir_entries(self
.root
, parent
)
113 elif kind
== core
.svn_node_file
:
114 parts
= self
._path
_to
_parts
(newpath
)
116 parent
= self
._parts
_to
_path
(parts
)
117 print(parent
+ ':' + name
)
118 tmpentries
= fs
.dir_entries(self
.root
, parent
)
119 if not tmpentries
.get(name
, None):
122 entries
[name
] = tmpentries
[name
]
124 print("Path '%s' not found." % newpath
)
127 keys
= sorted(entries
.keys())
129 print(" REV AUTHOR NODE-REV-ID SIZE DATE NAME")
130 print("----------------------------------------------------------------------------")
133 fullpath
= parent
+ '/' + entry
135 is_dir
= fs
.is_dir(self
.root
, fullpath
)
139 size
= str(fs
.file_length(self
.root
, fullpath
))
141 node_id
= fs
.unparse_id(entries
[entry
].id)
142 created_rev
= fs
.node_created_rev(self
.root
, fullpath
)
143 author
= fs
.revision_prop(self
.fs_ptr
, created_rev
,
144 core
.SVN_PROP_REVISION_AUTHOR
)
147 date
= fs
.revision_prop(self
.fs_ptr
, created_rev
,
148 core
.SVN_PROP_REVISION_DATE
)
152 date
= self
._format
_date
(date
)
154 print("%6s %8s %12s %8s %12s %s" % (created_rev
, author
[:8],
155 node_id
, size
, date
, name
))
157 def do_lstxns(self
, arg
):
158 """list the transactions available for browsing"""
159 txns
= sorted(fs
.list_transactions(self
.fs_ptr
))
162 counter
= counter
+ 1
163 sys
.stdout
.write("%8s " % txn
)
169 def do_pcat(self
, arg
):
170 """list the properties of a path"""
173 catpath
= self
._parse
_path
(arg
)
174 kind
= fs
.check_path(self
.root
, catpath
)
175 if kind
== core
.svn_node_none
:
176 print("Path '%s' does not exist." % catpath
)
178 plist
= fs
.node_proplist(self
.root
, catpath
)
181 for pkey
, pval
in plist
.items():
182 print('K ' + str(len(pkey
)))
184 print('P ' + str(len(pval
)))
188 def do_setrev(self
, arg
):
189 """set the current revision to view"""
191 if arg
.lower() == 'head':
192 rev
= fs
.youngest_rev(self
.fs_ptr
)
195 newroot
= fs
.revision_root(self
.fs_ptr
, rev
)
197 print("Error setting the revision to '" + arg
+ "'.")
199 fs
.close_root(self
.root
)
203 self
._do
_path
_landing
()
205 def do_settxn(self
, arg
):
206 """set the current transaction to view"""
208 txnobj
= fs
.open_txn(self
.fs_ptr
, arg
)
209 newroot
= fs
.txn_root(txnobj
)
211 print("Error setting the transaction to '" + arg
+ "'.")
213 fs
.close_root(self
.root
)
217 self
._do
_path
_landing
()
219 def do_youngest(self
, arg
):
220 """list the youngest revision available for browsing"""
221 rev
= fs
.youngest_rev(self
.fs_ptr
)
224 def do_exit(self
, arg
):
227 def _path_to_parts(self
, path
):
228 return [_f
for _f
in path
.split('/') if _f
]
230 def _parts_to_path(self
, parts
):
231 return '/' + '/'.join(parts
)
233 def _parse_path(self
, path
):
234 # cleanup leading, trailing, and duplicate '/' characters
235 newpath
= self
._parts
_to
_path
(self
._path
_to
_parts
(path
))
237 # if PATH is absolute, use it, else append it to the existing path.
238 if path
.startswith('/') or self
.path
== '/':
239 newpath
= '/' + newpath
241 newpath
= self
.path
+ '/' + newpath
243 # cleanup '.' and '..'
244 parts
= self
._path
_to
_parts
(newpath
)
250 if len(finalparts
) != 0:
253 finalparts
.append(part
)
255 # finally, return the calculated path
256 return self
._parts
_to
_path
(finalparts
)
258 def _format_date(self
, date
):
259 date
= core
.svn_time_from_cstring(date
)
260 date
= time
.asctime(time
.localtime(date
/ 1000000))
263 def _do_path_landing(self
):
264 """try to land on self.path as a directory in root, failing up to '/'"""
268 kind
= fs
.check_path(self
.root
, newpath
)
269 if kind
== core
.svn_node_dir
:
272 parts
= self
._path
_to
_parts
(newpath
)
274 newpath
= self
._parts
_to
_path
(parts
)
277 def _setup_prompt(self
):
278 """present the prompt and handle the user's input"""
280 self
.prompt
= "<rev: " + str(self
.rev
)
282 self
.prompt
= "<txn: " + self
.txn
283 self
.prompt
+= " " + self
.path
+ ">$ "
285 def _complete(self
, text
, line
, begidx
, endidx
, limit_node_kind
=None):
286 """Generic tab completer. Takes the 4 standard parameters passed to a
287 cmd.Cmd completer function, plus LIMIT_NODE_KIND, which should be a
288 svn.core.svn_node_foo constant to restrict the returned completions to, or
289 None for no limit. Catches and displays exceptions, because otherwise
290 they are silently ignored - which is quite frustrating when debugging!"""
297 dirs
= arg
.split('/')
299 user_dir
= "/".join(dirs
[:-1] + [''])
301 canon_dir
= self
._parse
_path
(user_dir
)
303 entries
= fs
.dir_entries(self
.root
, canon_dir
)
304 acceptable_completions
= []
305 for name
, dirent_t
in entries
.items():
306 if not name
.startswith(user_elem
):
308 if limit_node_kind
and dirent_t
.kind
!= limit_node_kind
:
310 if dirent_t
.kind
== core
.svn_node_dir
:
312 acceptable_completions
.append(name
)
313 if limit_node_kind
== core
.svn_node_dir
or not limit_node_kind
:
314 if user_elem
in ('.', '..'):
315 for extraname
in ('.', '..'):
316 if extraname
.startswith(user_elem
):
317 acceptable_completions
.append(extraname
+ '/')
318 return acceptable_completions
321 sys
.stderr
.write("EXCEPTION WHILST COMPLETING\n")
323 traceback
.print_tb(ei
[2])
324 sys
.stderr
.write("%s: %s\n" % (ei
[0], ei
[1]))
327 def complete_cd(self
, text
, line
, begidx
, endidx
):
328 return self
._complete
(text
, line
, begidx
, endidx
, core
.svn_node_dir
)
330 def complete_cat(self
, text
, line
, begidx
, endidx
):
331 return self
._complete
(text
, line
, begidx
, endidx
, core
.svn_node_file
)
333 def complete_ls(self
, text
, line
, begidx
, endidx
):
334 return self
._complete
(text
, line
, begidx
, endidx
)
336 def complete_pcat(self
, text
, line
, begidx
, endidx
):
337 return self
._complete
(text
, line
, begidx
, endidx
)
341 "Return the basename for a '/'-separated path."
342 idx
= path
.rfind('/')
354 "usage: %s REPOS_PATH\n"
356 "Once the program has started, type 'help' at the prompt for hints on\n"
357 "using the shell.\n" % sys
.argv
[0])
361 if len(sys
.argv
) != 2:
364 SVNShell(sys
.argv
[1])
366 if __name__
== '__main__':