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>&amp;File</label>
335      <object class="wxMenuItem" name="CLBMenuFileQuit">
336        <label>&amp;Quit</label>
337        <accel>CTRL+Q</accel>
338        <help>Quit SvnCLBrowse.</help>
339      </object>
340    </object>
341    <object class="wxMenu">
342      <label>&amp;Subversion</label>
343      <object class="wxMenuItem" name="CLBMenuOpsInfo">
344        <label>&amp;Info</label>
345        <help>Show information about members of the selected changelist(s).</help>
346      </object>
347      <object class="wxMenuItem" name="CLBMenuOpsMembers">
348        <label>&amp;Members</label>
349        <help>List the members of the selected changelist(s).</help>
350      </object>
351    </object>
352    <object class="wxMenu">
353      <label>&amp;Help</label>
354      <object class="wxMenuItem" name="CLBMenuHelpAbout">
355        <label>&amp;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