#!/usr/bin/python # # SvnCLBrowse -- graphical Subversion changelist browser # # ==================================================================== # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ==================================================================== # This script requires Python 2.5 import sys import os import getopt # Try to import the wxWidgets modules. try: import wx import wx.xrc except ImportError: sys.stderr.write(""" ERROR: This program requires the wxWidgets Python bindings, which you do not appear to have installed. """) raise # Try to import the Subversion modules. try: import svn.client, svn.wc, svn.core except ImportError: sys.stderr.write(""" ERROR: This program requires the Subversion Python bindings, which you do not appear to have installed. """) raise status_code_map = { svn.wc.status_none : ' ', svn.wc.status_normal : ' ', svn.wc.status_added : 'A', svn.wc.status_missing : '!', svn.wc.status_incomplete : '!', svn.wc.status_deleted : 'D', svn.wc.status_replaced : 'R', svn.wc.status_modified : 'M', svn.wc.status_merged : 'G', svn.wc.status_conflicted : 'C', svn.wc.status_obstructed : '~', svn.wc.status_ignored : 'I', svn.wc.status_external : 'X', svn.wc.status_unversioned : '?', } def output_info(path, info, window): window.AppendText("Path: %s\n" % os.path.normpath(path)) if info.kind != svn.core.svn_node_dir: window.AppendText("Name: %s\n" % os.path.basename(path)) if info.URL: window.AppendText("URL: %s\n" % info.URL) if info.repos_root_URL: window.AppendText("Repository Root: %s\n" % info.repos_root_URL) if info.repos_UUID: window.AppendText("Repository UUID: %s\n" % info.repos_UUID) if info.rev >= 0: window.AppendText("Revision: %ld\n" % info.rev) if info.kind == svn.core.svn_node_file: window.AppendText("Node Kind: file\n") elif info.kind == svn.core.svn_node_dir: window.AppendText("Node Kind: directory\n") elif info.kind == svn.core.svn_node_none: window.AppendText("Node Kind: none\n") else: window.AppendText("Node Kind: unknown\n") if info.has_wc_info: if info.schedule == svn.wc.schedule_normal: window.AppendText("Schedule: normal\n") elif info.schedule == svn.wc.schedule_add: window.AppendText("Schedule: add\n") elif info.schedule == svn.wc.schedule_delete: window.AppendText("Schedule: delete\n") elif info.schedule == svn.wc.schedule_replace: window.AppendText("Schedule: replace\n") if info.depth == svn.core.svn_depth_unknown: pass elif info.depth == svn.core.svn_depth_empty: window.AppendText("Depth: empty\n") elif info.depth == svn.core.svn_depth_files: window.AppendText("Depth: files\n") elif info.depth == svn.core.svn_depth_immediates: window.AppendText("Depth: immediates\n") elif info.depth == svn.core.svn_depth_infinity: pass else: window.AppendText("Depth: INVALID\n") if info.copyfrom_url: window.AppendText("Copied From URL: %s\n" % info.copyfrom_url) if info.copyfrom_rev >= 0: window.AppendText("Copied From Rev: %ld\n" % info.copyfrom_rev) if info.last_changed_author: window.AppendText("Last Changed Author: %s\n" % info.last_changed_author) if info.last_changed_rev >= 0: window.AppendText("Last Changed Rev: %ld\n" % info.last_changed_rev) if info.last_changed_date: window.AppendText("Last Changed Date: %s\n" % svn.core.svn_time_to_human_cstring(info.last_changed_date)) if info.has_wc_info: if info.text_time: window.AppendText("Text Last Updated: %s\n" % svn.core.svn_time_to_human_cstring(info.text_time)) if info.prop_time: window.AppendText("Properties Last Updated: %s\n" % svn.core.svn_time_to_human_cstring(info.prop_time)) if info.checksum: window.AppendText("Checksum: %s\n" % info.checksum) if info.conflict_old: window.AppendText("Conflict Previous Base File: %s\n" % info.conflict_old) if info.conflict_wrk: window.AppendText("Conflict Previous Working File: %s\n" % info.conflict_wrk) if info.conflict_new: window.AppendText("Conflict Current Base File: %s\n" % info.conflict_new) if info.prejfile: window.AppendText("Conflict Properties File: %s\n" % info.prejfile) if info.lock: if info.lock.token: window.AppendText("Lock Token: %s\n" % info.lock.token) if info.lock.owner: window.AppendText("Lock Owner: %s\n" % info.lock.owner) if info.lock.creation_date: window.AppendText("Lock Created: %s\n" % svn.core.svn_time_to_human_cstring(info.lock.creation_date)) if info.lock.expiration_date: window.AppendText("Lock Expires: %s\n" % svn.core.svn_time_to_human_cstring(info.lock.expiration_date)) if info.lock.comment: num_lines = len(info.lock.comment.split("\n")) window.AppendText("Lock Comment (%d line%s): %s\n" % (num_lines, num_lines > 1 and "s" or "", info.lock.comment)) if info.changelist: window.AppendText("Changelist: %s\n" % info.changelist) window.AppendText("\n") class _item: pass class SvnCLBrowse(wx.App): def __init__(self, wc_dir): svn.core.svn_config_ensure(None) self.svn_ctx = svn.client.ctx_t() self.svn_ctx.config = svn.core.svn_config_get_config(None) if wc_dir is not None: self.wc_dir = svn.core.svn_path_canonicalize(wc_dir) else: self.wc_dir = wc_dir wx.App.__init__(self) def OnInit(self): self.SetAppName("SvnCLBrowse") self.xrc = wx.xrc.EmptyXmlResource() wx.FileSystem.AddHandler(wx.MemoryFSHandler()) wx.MemoryFSHandler.AddFile('XRC/SvnCLBrowse.xrc', _XML_RESOURCE) self.xrc.Load('memory:XRC/SvnCLBrowse.xrc') # XML Resource stuff. self.resources = _item() self.resources.CLBFrame = self.xrc.LoadFrame(None, 'CLBFrame') self.resources.CLBMenuBar = self.xrc.LoadMenuBar('CLBMenuBar') self.resources.CLBMenuFileQuit = self.xrc.GetXRCID('CLBMenuFileQuit') self.resources.CLBMenuOpsInfo = self.xrc.GetXRCID('CLBMenuOpsInfo') self.resources.CLBMenuOpsMembers = self.xrc.GetXRCID('CLBMenuOpsMembers') self.resources.CLBMenuHelpAbout = self.xrc.GetXRCID('CLBMenuHelpAbout') self.resources.CLBDirNav = self.resources.CLBFrame.FindWindowById( self.xrc.GetXRCID('CLBDirNav')) self.resources.CLBChangelists = self.resources.CLBFrame.FindWindowById( self.xrc.GetXRCID('CLBChangelists')) self.resources.CLBVertSplitter = self.resources.CLBFrame.FindWindowById( self.xrc.GetXRCID('CLBVertSplitter')) self.resources.CLBHorzSplitter = self.resources.CLBFrame.FindWindowById( self.xrc.GetXRCID('CLBHorzSplitter')) self.resources.CLBOutput = self.resources.CLBFrame.FindWindowById( self.xrc.GetXRCID('CLBOutput')) self.resources.CLBStatusBar = self.resources.CLBFrame.CreateStatusBar(2) # Glue some of our extra stuff onto the main frame. self.resources.CLBFrame.SetMenuBar(self.resources.CLBMenuBar) self.resources.CLBStatusBar.SetStatusWidths([-1, 100]) # Event handlers. They are the key to the world. wx.EVT_CLOSE(self.resources.CLBFrame, self._FrameClosure) wx.EVT_MENU(self, self.resources.CLBMenuFileQuit, self._FileQuitMenu) wx.EVT_MENU(self, self.resources.CLBMenuOpsInfo, self._OpsInfoMenu) wx.EVT_MENU(self, self.resources.CLBMenuOpsMembers, self._OpsMembersMenu) wx.EVT_MENU(self, self.resources.CLBMenuHelpAbout, self._HelpAboutMenu) wx.EVT_TREE_ITEM_ACTIVATED(self, self.resources.CLBDirNav.GetTreeCtrl().Id, self._DirNavSelChanged) # Reset our working directory self._SetWorkingDirectory(self.wc_dir) # Resize and display our frame. self.resources.CLBFrame.SetSize(wx.Size(600, 400)) self.resources.CLBFrame.Center() self.resources.CLBFrame.Show(True) self.resources.CLBVertSplitter.SetSashPosition( self.resources.CLBVertSplitter.GetSize()[0] / 2) self.resources.CLBHorzSplitter.SetSashPosition( self.resources.CLBHorzSplitter.GetSize()[1] / 2) # Tell wxWidgets that this is our main window self.SetTopWindow(self.resources.CLBFrame) # Return a success flag return True def _SetWorkingDirectory(self, wc_dir): if wc_dir is None: return if not os.path.isdir(wc_dir): wc_dir = os.path.abspath('/') self.wc_dir = os.path.abspath(wc_dir) self.resources.CLBChangelists.Clear() self.resources.CLBDirNav.SetPath(self.wc_dir) self.resources.CLBFrame.SetTitle("SvnCLBrowse - %s" % (self.wc_dir)) changelists = {} self.resources.CLBFrame.SetStatusText("Checking '%s' for status..." \ % (self.wc_dir)) wx.BeginBusyCursor() def _status_callback(path, status, clists=changelists): if status.entry and status.entry.changelist: clists[status.entry.changelist] = None # Do the status crawl, using _status_callback() as our callback function. revision = svn.core.svn_opt_revision_t() revision.type = svn.core.svn_opt_revision_head try: svn.client.status2(self.wc_dir, revision, _status_callback, svn.core.svn_depth_infinity, False, False, False, True, self.svn_ctx) except svn.core.SubversionException: self.resources.CLBStatusBar.SetStatusText("UNVERSIONED", 2) else: changelist_names = changelists.keys() changelist_names.sort() for changelist in changelist_names: self.resources.CLBChangelists.Append(changelist) finally: wx.EndBusyCursor() self.resources.CLBFrame.SetStatusText("") def _Destroy(self): self.resources.CLBFrame.Destroy() def _DirNavSelChanged(self, event): self._SetWorkingDirectory(self.resources.CLBDirNav.GetPath()) def _GetSelectedChangelists(self): changelists = [] items = self.resources.CLBChangelists.GetSelections() for item in items: changelists.append(str(self.resources.CLBChangelists.GetString(item))) return changelists def _OpsMembersMenu(self, event): self.resources.CLBOutput.Clear() changelists = self._GetSelectedChangelists() if not changelists: return def _info_receiver(path, info, pool): self.resources.CLBOutput.AppendText(" %s\n" % (path)) for changelist in changelists: self.resources.CLBOutput.AppendText("Changelist: %s\n" % (changelist)) revision = svn.core.svn_opt_revision_t() revision.type = svn.core.svn_opt_revision_working svn.client.info2(self.wc_dir, revision, revision, _info_receiver, svn.core.svn_depth_infinity, [changelist], self.svn_ctx) self.resources.CLBOutput.AppendText("\n") def _OpsInfoMenu(self, event): self.resources.CLBOutput.Clear() changelists = self._GetSelectedChangelists() if not changelists: return def _info_receiver(path, info, pool): output_info(path, info, self.resources.CLBOutput) revision = svn.core.svn_opt_revision_t() revision.type = svn.core.svn_opt_revision_working svn.client.info2(self.wc_dir, revision, revision, _info_receiver, svn.core.svn_depth_infinity, changelists, self.svn_ctx) def _FrameClosure(self, event): self._Destroy() def _FileQuitMenu(self, event): self._Destroy() def _HelpAboutMenu(self, event): wx.MessageBox("SvnCLBrowse" " -- graphical Subversion changelist browser.\n\n", "About SvnCLBrowse", wx.OK | wx.CENTER, self.resources.CLBFrame) def OnExit(self): pass _XML_RESOURCE = """ CTRL+Q Quit SvnCLBrowse. Show information about members of the selected changelist(s). List the members of the selected changelist(s). About SvnCLBrowse. SvnCLBrowse -- graphical Subversion changelist browser 1 1 1 1 3 0 0 1 2 horizontal 200 50 wxHORIZONTAL wxEXPAND wxHORIZONTAL wxALL|wxEXPAND wxEXPAND 1 wxVERTICAL wxEXPAND wxALL|wxEXPAND 5 1 0 0 vertical 130 50 wxEXPAND 0 0 """ def usage_and_exit(errmsg=None): stream = errmsg and sys.stderr or sys.stdout progname = os.path.basename(sys.argv[0]) stream.write("""%s -- graphical Subversion changelist browser Usage: %s [DIRECTORY] Launch the SvnCLBrowse graphical changelist browser, using DIRECTORY (or the current working directory, if DIRECTORY is not provided) as the initial browse location. """ % (progname, progname)) if errmsg: stream.write("ERROR: %s\n" % (errmsg)) sys.exit(errmsg and 1 or 0) def main(): opts, args = getopt.gnu_getopt(sys.argv[1:], 'h?', ['help']) for name, value in opts: if name == '-h' or name == '-?' or name == '--help': usage_and_exit() argc = len(args) if argc == 0: wc_dir = '.' elif argc == 1: wc_dir = sys.argv[1] else: usage_and_exit("Too many arguments") app = SvnCLBrowse(wc_dir) app.MainLoop() app.OnExit() if __name__ == "__main__": main()