The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/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 = """<?xml version="1.0" ?>
<resource>
  <object class="wxMenuBar" name="CLBMenuBar">
    <object class="wxMenu">
      <label>&amp;File</label>
      <object class="wxMenuItem" name="CLBMenuFileQuit">
        <label>&amp;Quit</label>
        <accel>CTRL+Q</accel>
        <help>Quit SvnCLBrowse.</help>
      </object>
    </object>
    <object class="wxMenu">
      <label>&amp;Subversion</label>
      <object class="wxMenuItem" name="CLBMenuOpsInfo">
        <label>&amp;Info</label>
        <help>Show information about members of the selected changelist(s).</help>
      </object>
      <object class="wxMenuItem" name="CLBMenuOpsMembers">
        <label>&amp;Members</label>
        <help>List the members of the selected changelist(s).</help>
      </object>
    </object>
    <object class="wxMenu">
      <label>&amp;Help</label>
      <object class="wxMenuItem" name="CLBMenuHelpAbout">
        <label>&amp;About...</label>
        <help>About SvnCLBrowse.</help>
      </object>
    </object>
  </object>
  <object class="wxFrame" name="CLBFrame">
    <title>SvnCLBrowse -- graphical Subversion changelist browser</title>
    <centered>1</centered>
    <style>wxDEFAULT_FRAME_STYLE|wxCAPTION|wxSYSTEM_MENU|wxRESIZE_BORDER|wxRESIZE_BOX|wxMAXIMIZE_BOX|wxMINIMIZE_BOX|wxTAB_TRAVERSAL</style>
    <object class="wxFlexGridSizer">
      <cols>1</cols>
      <rows>1</rows>
      <object class="sizeritem">
        <object class="wxSplitterWindow" name="CLBVertSplitter">
          <object class="wxPanel">
            <object class="wxFlexGridSizer">
              <cols>1</cols>
              <rows>3</rows>
              <growablecols>0</growablecols>
              <growablerows>0</growablerows>
              <growablerows>1</growablerows>
              <growablerows>2</growablerows>
              <object class="sizeritem">
                <object class="wxSplitterWindow" name="CLBHorzSplitter">
                  <orientation>horizontal</orientation>
                  <sashpos>200</sashpos>
                  <minsize>50</minsize>
                  <style>wxSP_NOBORDER|wxSP_LIVE_UPDATE</style>
                  <object class="wxPanel">
                    <object class="wxStaticBoxSizer">
                      <label>Local Modifications</label>
                      <orient>wxHORIZONTAL</orient>
                      <object class="sizeritem">
                        <object class="wxGenericDirCtrl" name="CLBDirNav">
                          <style>wxDIRCTRL_DIR_ONLY</style>
                        </object>
                        <flag>wxEXPAND</flag>
                        <option>1</option>
                      </object>
                    </object>
                  </object>
                  <object class="wxPanel">
                    <object class="wxStaticBoxSizer">
                      <label>Changelists</label>
                      <orient>wxHORIZONTAL</orient>
                      <object class="sizeritem">
                        <object class="wxListBox" name="CLBChangelists">
                          <content>
                            <item/></content>
                          <style>wxLB_MULTIPLE</style>
                        </object>
                        <option>1</option>
                        <flag>wxALL|wxEXPAND</flag>
                      </object>
                    </object>
                  </object>
                </object>
                <flag>wxEXPAND</flag>
                <option>1</option>
              </object>
            </object>
          </object>
          <object class="wxPanel">
            <object class="wxFlexGridSizer">
              <cols>1</cols>
              <object class="sizeritem">
                <object class="wxStaticBoxSizer">
                  <label>Output</label>
                  <orient>wxVERTICAL</orient>
                  <object class="sizeritem">
                    <object class="wxTextCtrl" name="CLBOutput">
                      <style>wxTE_MULTILINE|wxTE_READONLY|wxTE_LEFT|wxTE_DONTWRAP</style>
                    </object>
                    <option>1</option>
                    <flag>wxEXPAND</flag>
                  </object>
                </object>
                <option>1</option>
                <flag>wxALL|wxEXPAND</flag>
                <border>5</border>
              </object>
              <rows>1</rows>
              <growablecols>0</growablecols>
              <growablerows>0</growablerows>
            </object>
          </object>
          <orientation>vertical</orientation>
          <sashpos>130</sashpos>
          <minsize>50</minsize>
          <style>wxSP_NOBORDER|wxSP_LIVE_UPDATE</style>
        </object>
        <option>1</option>
        <flag>wxEXPAND</flag>
      </object>
      <growablecols>0</growablecols>
      <growablerows>0</growablerows>
    </object>
  </object>
</resource>
"""

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()