1#!/usr/local/bin/python 2# 3# SvnCLBrowse -- graphical Subversion changelist browser 4# 5# ==================================================================== 6# Licensed to the Apache Software Foundation (ASF) under one 7# or more contributor license agreements. See the NOTICE file 8# distributed with this work for additional information 9# regarding copyright ownership. The ASF licenses this file 10# to you under the Apache License, Version 2.0 (the 11# "License"); you may not use this file except in compliance 12# with the License. You may obtain a copy of the License at 13# 14# http://www.apache.org/licenses/LICENSE-2.0 15# 16# Unless required by applicable law or agreed to in writing, 17# software distributed under the License is distributed on an 18# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 19# KIND, either express or implied. See the License for the 20# specific language governing permissions and limitations 21# under the License. 22# ==================================================================== 23 24# This script requires Python 2.5 25 26import sys 27import os 28import getopt 29 30# Try to import the wxWidgets modules. 31try: 32 import wx 33 import wx.xrc 34except ImportError: 35 sys.stderr.write(""" 36ERROR: This program requires the wxWidgets Python bindings, which you 37 do not appear to have installed. 38 39""") 40 raise 41 42# Try to import the Subversion modules. 43try: 44 import svn.client, svn.wc, svn.core 45except ImportError: 46 sys.stderr.write(""" 47ERROR: This program requires the Subversion Python bindings, which you 48 do not appear to have installed. 49 50""") 51 raise 52 53status_code_map = { 54 svn.wc.status_none : ' ', 55 svn.wc.status_normal : ' ', 56 svn.wc.status_added : 'A', 57 svn.wc.status_missing : '!', 58 svn.wc.status_incomplete : '!', 59 svn.wc.status_deleted : 'D', 60 svn.wc.status_replaced : 'R', 61 svn.wc.status_modified : 'M', 62 svn.wc.status_merged : 'G', 63 svn.wc.status_conflicted : 'C', 64 svn.wc.status_obstructed : '~', 65 svn.wc.status_ignored : 'I', 66 svn.wc.status_external : 'X', 67 svn.wc.status_unversioned : '?', 68 } 69 70def output_info(path, info, window): 71 window.AppendText("Path: %s\n" % os.path.normpath(path)) 72 if info.kind != svn.core.svn_node_dir: 73 window.AppendText("Name: %s\n" % os.path.basename(path)) 74 if info.URL: 75 window.AppendText("URL: %s\n" % info.URL) 76 if info.repos_root_URL: 77 window.AppendText("Repository Root: %s\n" % info.repos_root_URL) 78 if info.repos_UUID: 79 window.AppendText("Repository UUID: %s\n" % info.repos_UUID) 80 if info.rev >= 0: 81 window.AppendText("Revision: %ld\n" % info.rev) 82 if info.kind == svn.core.svn_node_file: 83 window.AppendText("Node Kind: file\n") 84 elif info.kind == svn.core.svn_node_dir: 85 window.AppendText("Node Kind: directory\n") 86 elif info.kind == svn.core.svn_node_none: 87 window.AppendText("Node Kind: none\n") 88 else: 89 window.AppendText("Node Kind: unknown\n") 90 if info.has_wc_info: 91 if info.schedule == svn.wc.schedule_normal: 92 window.AppendText("Schedule: normal\n") 93 elif info.schedule == svn.wc.schedule_add: 94 window.AppendText("Schedule: add\n") 95 elif info.schedule == svn.wc.schedule_delete: 96 window.AppendText("Schedule: delete\n") 97 elif info.schedule == svn.wc.schedule_replace: 98 window.AppendText("Schedule: replace\n") 99 if info.depth == svn.core.svn_depth_unknown: 100 pass 101 elif info.depth == svn.core.svn_depth_empty: 102 window.AppendText("Depth: empty\n") 103 elif info.depth == svn.core.svn_depth_files: 104 window.AppendText("Depth: files\n") 105 elif info.depth == svn.core.svn_depth_immediates: 106 window.AppendText("Depth: immediates\n") 107 elif info.depth == svn.core.svn_depth_infinity: 108 pass 109 else: 110 window.AppendText("Depth: INVALID\n") 111 if info.copyfrom_url: 112 window.AppendText("Copied From URL: %s\n" % info.copyfrom_url) 113 if info.copyfrom_rev >= 0: 114 window.AppendText("Copied From Rev: %ld\n" % info.copyfrom_rev) 115 if info.last_changed_author: 116 window.AppendText("Last Changed Author: %s\n" % info.last_changed_author) 117 if info.last_changed_rev >= 0: 118 window.AppendText("Last Changed Rev: %ld\n" % info.last_changed_rev) 119 if info.last_changed_date: 120 window.AppendText("Last Changed Date: %s\n" % 121 svn.core.svn_time_to_human_cstring(info.last_changed_date)) 122 if info.has_wc_info: 123 if info.text_time: 124 window.AppendText("Text Last Updated: %s\n" % 125 svn.core.svn_time_to_human_cstring(info.text_time)) 126 if info.prop_time: 127 window.AppendText("Properties Last Updated: %s\n" % 128 svn.core.svn_time_to_human_cstring(info.prop_time)) 129 if info.checksum: 130 window.AppendText("Checksum: %s\n" % info.checksum) 131 if info.conflict_old: 132 window.AppendText("Conflict Previous Base File: %s\n" % info.conflict_old) 133 if info.conflict_wrk: 134 window.AppendText("Conflict Previous Working File: %s\n" % info.conflict_wrk) 135 if info.conflict_new: 136 window.AppendText("Conflict Current Base File: %s\n" % info.conflict_new) 137 if info.prejfile: 138 window.AppendText("Conflict Properties File: %s\n" % info.prejfile) 139 if info.lock: 140 if info.lock.token: 141 window.AppendText("Lock Token: %s\n" % info.lock.token) 142 if info.lock.owner: 143 window.AppendText("Lock Owner: %s\n" % info.lock.owner) 144 if info.lock.creation_date: 145 window.AppendText("Lock Created: %s\n" % 146 svn.core.svn_time_to_human_cstring(info.lock.creation_date)) 147 if info.lock.expiration_date: 148 window.AppendText("Lock Expires: %s\n" % 149 svn.core.svn_time_to_human_cstring(info.lock.expiration_date)) 150 if info.lock.comment: 151 num_lines = len(info.lock.comment.split("\n")) 152 window.AppendText("Lock Comment (%d line%s): %s\n" 153 % (num_lines, num_lines > 1 and "s" or "", info.lock.comment)) 154 if info.changelist: 155 window.AppendText("Changelist: %s\n" % info.changelist) 156 window.AppendText("\n") 157 158class _item: 159 pass 160 161class SvnCLBrowse(wx.App): 162 def __init__(self, wc_dir): 163 svn.core.svn_config_ensure(None) 164 self.svn_ctx = svn.client.svn_client_create_context() 165 self.svn_ctx.config = svn.core.svn_config_get_config(None) 166 if wc_dir is not None: 167 self.wc_dir = svn.core.svn_path_canonicalize(wc_dir) 168 else: 169 self.wc_dir = wc_dir 170 wx.App.__init__(self) 171 172 def OnInit(self): 173 self.SetAppName("SvnCLBrowse") 174 175 self.xrc = wx.xrc.EmptyXmlResource() 176 wx.FileSystem.AddHandler(wx.MemoryFSHandler()) 177 wx.MemoryFSHandler.AddFile('XRC/SvnCLBrowse.xrc', _XML_RESOURCE) 178 self.xrc.Load('memory:XRC/SvnCLBrowse.xrc') 179 180 # XML Resource stuff. 181 self.resources = _item() 182 self.resources.CLBFrame = self.xrc.LoadFrame(None, 'CLBFrame') 183 self.resources.CLBMenuBar = self.xrc.LoadMenuBar('CLBMenuBar') 184 self.resources.CLBMenuFileQuit = self.xrc.GetXRCID('CLBMenuFileQuit') 185 self.resources.CLBMenuOpsInfo = self.xrc.GetXRCID('CLBMenuOpsInfo') 186 self.resources.CLBMenuOpsMembers = self.xrc.GetXRCID('CLBMenuOpsMembers') 187 self.resources.CLBMenuHelpAbout = self.xrc.GetXRCID('CLBMenuHelpAbout') 188 self.resources.CLBDirNav = self.resources.CLBFrame.FindWindowById( 189 self.xrc.GetXRCID('CLBDirNav')) 190 self.resources.CLBChangelists = self.resources.CLBFrame.FindWindowById( 191 self.xrc.GetXRCID('CLBChangelists')) 192 self.resources.CLBVertSplitter = self.resources.CLBFrame.FindWindowById( 193 self.xrc.GetXRCID('CLBVertSplitter')) 194 self.resources.CLBHorzSplitter = self.resources.CLBFrame.FindWindowById( 195 self.xrc.GetXRCID('CLBHorzSplitter')) 196 self.resources.CLBOutput = self.resources.CLBFrame.FindWindowById( 197 self.xrc.GetXRCID('CLBOutput')) 198 self.resources.CLBStatusBar = self.resources.CLBFrame.CreateStatusBar(2) 199 200 # Glue some of our extra stuff onto the main frame. 201 self.resources.CLBFrame.SetMenuBar(self.resources.CLBMenuBar) 202 self.resources.CLBStatusBar.SetStatusWidths([-1, 100]) 203 204 # Event handlers. They are the key to the world. 205 wx.EVT_CLOSE(self.resources.CLBFrame, self._FrameClosure) 206 wx.EVT_MENU(self, self.resources.CLBMenuFileQuit, self._FileQuitMenu) 207 wx.EVT_MENU(self, self.resources.CLBMenuOpsInfo, self._OpsInfoMenu) 208 wx.EVT_MENU(self, self.resources.CLBMenuOpsMembers, self._OpsMembersMenu) 209 wx.EVT_MENU(self, self.resources.CLBMenuHelpAbout, self._HelpAboutMenu) 210 wx.EVT_TREE_ITEM_ACTIVATED(self, self.resources.CLBDirNav.GetTreeCtrl().Id, 211 self._DirNavSelChanged) 212 213 # Reset our working directory 214 self._SetWorkingDirectory(self.wc_dir) 215 216 # Resize and display our frame. 217 self.resources.CLBFrame.SetSize(wx.Size(600, 400)) 218 self.resources.CLBFrame.Center() 219 self.resources.CLBFrame.Show(True) 220 self.resources.CLBVertSplitter.SetSashPosition( 221 self.resources.CLBVertSplitter.GetSize()[0] / 2) 222 self.resources.CLBHorzSplitter.SetSashPosition( 223 self.resources.CLBHorzSplitter.GetSize()[1] / 2) 224 225 # Tell wxWidgets that this is our main window 226 self.SetTopWindow(self.resources.CLBFrame) 227 228 # Return a success flag 229 return True 230 231 def _SetWorkingDirectory(self, wc_dir): 232 if wc_dir is None: 233 return 234 if not os.path.isdir(wc_dir): 235 wc_dir = os.path.abspath('/') 236 self.wc_dir = os.path.abspath(wc_dir) 237 self.resources.CLBChangelists.Clear() 238 self.resources.CLBDirNav.SetPath(self.wc_dir) 239 self.resources.CLBFrame.SetTitle("SvnCLBrowse - %s" % (self.wc_dir)) 240 changelists = {} 241 self.resources.CLBFrame.SetStatusText("Checking '%s' for status..." \ 242 % (self.wc_dir)) 243 wx.BeginBusyCursor() 244 245 def _status_callback(path, status, clists=changelists): 246 if status.entry and status.entry.changelist: 247 clists[status.entry.changelist] = None 248 249 # Do the status crawl, using _status_callback() as our callback function. 250 revision = svn.core.svn_opt_revision_t() 251 revision.type = svn.core.svn_opt_revision_head 252 try: 253 svn.client.status2(self.wc_dir, revision, _status_callback, 254 svn.core.svn_depth_infinity, 255 False, False, False, True, self.svn_ctx) 256 except svn.core.SubversionException: 257 self.resources.CLBStatusBar.SetStatusText("UNVERSIONED", 2) 258 else: 259 changelist_names = changelists.keys() 260 changelist_names.sort() 261 for changelist in changelist_names: 262 self.resources.CLBChangelists.Append(changelist) 263 finally: 264 wx.EndBusyCursor() 265 self.resources.CLBFrame.SetStatusText("") 266 267 def _Destroy(self): 268 self.resources.CLBFrame.Destroy() 269 270 def _DirNavSelChanged(self, event): 271 self._SetWorkingDirectory(self.resources.CLBDirNav.GetPath()) 272 273 def _GetSelectedChangelists(self): 274 changelists = [] 275 items = self.resources.CLBChangelists.GetSelections() 276 for item in items: 277 changelists.append(str(self.resources.CLBChangelists.GetString(item))) 278 return changelists 279 280 def _OpsMembersMenu(self, event): 281 self.resources.CLBOutput.Clear() 282 changelists = self._GetSelectedChangelists() 283 if not changelists: 284 return 285 286 def _info_receiver(path, info, pool): 287 self.resources.CLBOutput.AppendText(" %s\n" % (path)) 288 289 for changelist in changelists: 290 self.resources.CLBOutput.AppendText("Changelist: %s\n" % (changelist)) 291 revision = svn.core.svn_opt_revision_t() 292 revision.type = svn.core.svn_opt_revision_working 293 svn.client.info2(self.wc_dir, revision, revision, 294 _info_receiver, svn.core.svn_depth_infinity, 295 [changelist], self.svn_ctx) 296 self.resources.CLBOutput.AppendText("\n") 297 298 def _OpsInfoMenu(self, event): 299 self.resources.CLBOutput.Clear() 300 changelists = self._GetSelectedChangelists() 301 if not changelists: 302 return 303 304 def _info_receiver(path, info, pool): 305 output_info(path, info, self.resources.CLBOutput) 306 307 revision = svn.core.svn_opt_revision_t() 308 revision.type = svn.core.svn_opt_revision_working 309 svn.client.info2(self.wc_dir, revision, revision, 310 _info_receiver, svn.core.svn_depth_infinity, 311 changelists, self.svn_ctx) 312 313 def _FrameClosure(self, event): 314 self._Destroy() 315 316 def _FileQuitMenu(self, event): 317 self._Destroy() 318 319 def _HelpAboutMenu(self, event): 320 wx.MessageBox("SvnCLBrowse" 321 " -- graphical Subversion changelist browser.\n\n", 322 "About SvnCLBrowse", 323 wx.OK | wx.CENTER, 324 self.resources.CLBFrame) 325 326 def OnExit(self): 327 pass 328 329 330_XML_RESOURCE = """<?xml version="1.0" ?> 331<resource> 332 <object class="wxMenuBar" name="CLBMenuBar"> 333 <object class="wxMenu"> 334 <label>&File</label> 335 <object class="wxMenuItem" name="CLBMenuFileQuit"> 336 <label>&Quit</label> 337 <accel>CTRL+Q</accel> 338 <help>Quit SvnCLBrowse.</help> 339 </object> 340 </object> 341 <object class="wxMenu"> 342 <label>&Subversion</label> 343 <object class="wxMenuItem" name="CLBMenuOpsInfo"> 344 <label>&Info</label> 345 <help>Show information about members of the selected changelist(s).</help> 346 </object> 347 <object class="wxMenuItem" name="CLBMenuOpsMembers"> 348 <label>&Members</label> 349 <help>List the members of the selected changelist(s).</help> 350 </object> 351 </object> 352 <object class="wxMenu"> 353 <label>&Help</label> 354 <object class="wxMenuItem" name="CLBMenuHelpAbout"> 355 <label>&About...</label> 356 <help>About SvnCLBrowse.</help> 357 </object> 358 </object> 359 </object> 360 <object class="wxFrame" name="CLBFrame"> 361 <title>SvnCLBrowse -- graphical Subversion changelist browser</title> 362 <centered>1</centered> 363 <style>wxDEFAULT_FRAME_STYLE|wxCAPTION|wxSYSTEM_MENU|wxRESIZE_BORDER|wxRESIZE_BOX|wxMAXIMIZE_BOX|wxMINIMIZE_BOX|wxTAB_TRAVERSAL</style> 364 <object class="wxFlexGridSizer"> 365 <cols>1</cols> 366 <rows>1</rows> 367 <object class="sizeritem"> 368 <object class="wxSplitterWindow" name="CLBVertSplitter"> 369 <object class="wxPanel"> 370 <object class="wxFlexGridSizer"> 371 <cols>1</cols> 372 <rows>3</rows> 373 <growablecols>0</growablecols> 374 <growablerows>0</growablerows> 375 <growablerows>1</growablerows> 376 <growablerows>2</growablerows> 377 <object class="sizeritem"> 378 <object class="wxSplitterWindow" name="CLBHorzSplitter"> 379 <orientation>horizontal</orientation> 380 <sashpos>200</sashpos> 381 <minsize>50</minsize> 382 <style>wxSP_NOBORDER|wxSP_LIVE_UPDATE</style> 383 <object class="wxPanel"> 384 <object class="wxStaticBoxSizer"> 385 <label>Local Modifications</label> 386 <orient>wxHORIZONTAL</orient> 387 <object class="sizeritem"> 388 <object class="wxGenericDirCtrl" name="CLBDirNav"> 389 <style>wxDIRCTRL_DIR_ONLY</style> 390 </object> 391 <flag>wxEXPAND</flag> 392 <option>1</option> 393 </object> 394 </object> 395 </object> 396 <object class="wxPanel"> 397 <object class="wxStaticBoxSizer"> 398 <label>Changelists</label> 399 <orient>wxHORIZONTAL</orient> 400 <object class="sizeritem"> 401 <object class="wxListBox" name="CLBChangelists"> 402 <content> 403 <item/></content> 404 <style>wxLB_MULTIPLE</style> 405 </object> 406 <option>1</option> 407 <flag>wxALL|wxEXPAND</flag> 408 </object> 409 </object> 410 </object> 411 </object> 412 <flag>wxEXPAND</flag> 413 <option>1</option> 414 </object> 415 </object> 416 </object> 417 <object class="wxPanel"> 418 <object class="wxFlexGridSizer"> 419 <cols>1</cols> 420 <object class="sizeritem"> 421 <object class="wxStaticBoxSizer"> 422 <label>Output</label> 423 <orient>wxVERTICAL</orient> 424 <object class="sizeritem"> 425 <object class="wxTextCtrl" name="CLBOutput"> 426 <style>wxTE_MULTILINE|wxTE_READONLY|wxTE_LEFT|wxTE_DONTWRAP</style> 427 </object> 428 <option>1</option> 429 <flag>wxEXPAND</flag> 430 </object> 431 </object> 432 <option>1</option> 433 <flag>wxALL|wxEXPAND</flag> 434 <border>5</border> 435 </object> 436 <rows>1</rows> 437 <growablecols>0</growablecols> 438 <growablerows>0</growablerows> 439 </object> 440 </object> 441 <orientation>vertical</orientation> 442 <sashpos>130</sashpos> 443 <minsize>50</minsize> 444 <style>wxSP_NOBORDER|wxSP_LIVE_UPDATE</style> 445 </object> 446 <option>1</option> 447 <flag>wxEXPAND</flag> 448 </object> 449 <growablecols>0</growablecols> 450 <growablerows>0</growablerows> 451 </object> 452 </object> 453</resource> 454""" 455 456def usage_and_exit(errmsg=None): 457 stream = errmsg and sys.stderr or sys.stdout 458 progname = os.path.basename(sys.argv[0]) 459 stream.write("""%s -- graphical Subversion changelist browser 460 461Usage: %s [DIRECTORY] 462 463Launch the SvnCLBrowse graphical changelist browser, using DIRECTORY 464(or the current working directory, if DIRECTORY is not provided) as 465the initial browse location. 466 467""" % (progname, progname)) 468 if errmsg: 469 stream.write("ERROR: %s\n" % (errmsg)) 470 sys.exit(errmsg and 1 or 0) 471 472def main(): 473 opts, args = getopt.gnu_getopt(sys.argv[1:], 'h?', ['help']) 474 for name, value in opts: 475 if name == '-h' or name == '-?' or name == '--help': 476 usage_and_exit() 477 argc = len(args) 478 if argc == 0: 479 wc_dir = '.' 480 elif argc == 1: 481 wc_dir = sys.argv[1] 482 else: 483 usage_and_exit("Too many arguments") 484 app = SvnCLBrowse(wc_dir) 485 app.MainLoop() 486 app.OnExit() 487 488if __name__ == "__main__": 489 main() 490