cvsview.py 29.1 KB
Newer Older
Stephen Kennedy's avatar
Stephen Kennedy committed
1
### Copyright (C) 2002-2004 Stephen Kennedy <stevek@gnome.org>
steve9000's avatar
steve9000 committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15

### This program is free software; you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation; either version 2 of the License, or
### (at your option) any later version.

### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.

### You should have received a copy of the GNU General Public License
### along with this program; if not, write to the Free Software
### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
steve9000's avatar
steve9000 committed
16

steve9000's avatar
steve9000 committed
17 18
from __future__ import generators

steve9000's avatar
steve9000 committed
19 20 21 22
import tempfile
import gobject
import shutil
import time
steve9000's avatar
steve9000 committed
23
import copy
steve9000's avatar
steve9000 committed
24 25 26 27
import gtk
import os
import re

28
import tree
steve9000's avatar
steve9000 committed
29 30
import misc
import gnomeglade
steve9000's avatar
steve9000 committed
31
import melddoc
32
import paths
steve9000's avatar
steve9000 committed
33 34 35 36 37 38

################################################################################
#
# Local Functions
#
################################################################################
39
class Entry(object):
40 41
    states = _("Ignored:Non CVS:::Error::Newly added:Modified:<b>Conflict</b>:Removed:Missing").split(":")
    assert len(states)==tree.STATE_MAX
steve9000's avatar
steve9000 committed
42
    def __str__(self):
43
        return "<%s:%s %s>\n" % (self.__class__, self.name, (self.path, self.state))
44
    def __repr__(self):
45 46
        return "%s %s\n" % (self.name, (self.path, self.state))
    def get_status(self):
steve9000's avatar
steve9000 committed
47
        return self.states[self.state]
steve9000's avatar
steve9000 committed
48 49

class Dir(Entry):
50
    def __init__(self, path, name, state):
51 52
        self.path = path
        self.parent, self.name = os.path.split(path[:-1])
53
        self.state = state
steve9000's avatar
steve9000 committed
54
        self.isdir = 1
55 56 57
        self.rev = ""
        self.tag = ""
        self.options = ""
steve9000's avatar
steve9000 committed
58 59

class File(Entry):
60
    def __init__(self, path, name, state, rev="", tag="", options=""):
61 62 63
        assert path[-1] != "/"
        self.path = path
        self.parent, self.name = os.path.split(path)
64
        self.state = state
65
        self.isdir = 0
66 67 68
        self.rev = rev
        self.tag = tag
        self.options = options
steve9000's avatar
steve9000 committed
69

70
def _lookup_cvs_files(dirs, files):
steve9000's avatar
steve9000 committed
71 72 73 74 75 76 77 78 79 80
    "files is array of (name, path). assume all files in same dir"
    if len(files):
        directory = os.path.dirname(files[0][1])
    elif len(dirs):
        directory = os.path.dirname(dirs[0][1])
    else:
        return [],[]

    try:
        entries = open( os.path.join(directory, "CVS/Entries")).read()
81 82
        # poor mans universal newline
        entries = entries.replace("\r","\n").replace("\n\n","\n")
83
    except IOError, e: # no cvs dir
84 85
        d = map(lambda x: Dir(x[1],x[0], tree.STATE_NONE), dirs) 
        f = map(lambda x: File(x[1],x[0], tree.STATE_NONE, None), files) 
steve9000's avatar
steve9000 committed
86
        return d,f
87

88
    try:
89
        logentries = open( os.path.join(directory, "CVS/Entries.Log")).read()
90 91 92
    except IOError, e:
        pass
    else:
93 94 95 96 97 98 99 100 101 102 103 104 105
        matches = re.findall("^([AR])\s*(.+)$(?m)", logentries)
        toadd = []
        for match in matches:
            if match[0] == "A":
                toadd.append( match[1] )
            elif match[0] == "R":
                try:
                    toadd.remove( match[1] )
                except ValueError:
                    pass
            else:
                print "Unknown Entries.Log line '%s'" % match[0]
        entries += "\n".join(toadd)
steve9000's avatar
steve9000 committed
106 107 108

    retfiles = []
    retdirs = []
109
    matches = re.findall("^(D?)/([^/]+)/(.+)$(?m)", entries)
steve9000's avatar
steve9000 committed
110
    matches.sort()
steve9000's avatar
steve9000 committed
111 112 113 114 115

    for match in matches:
        isdir = match[0]
        name = match[1]
        path = os.path.join(directory, name)
116 117 118
        rev, date, options, tag = match[2].split("/")
        if tag:
            tag = tag[1:]
steve9000's avatar
steve9000 committed
119
        if isdir:
120 121 122 123
            if os.path.exists(path):
                state = tree.STATE_NORMAL
            else:
                state = tree.STATE_MISSING
steve9000's avatar
steve9000 committed
124 125
            retdirs.append( Dir(path,name,state) )
        else:
steve9000's avatar
steve9000 committed
126 127 128
            if rev.startswith("-"):
                state = tree.STATE_REMOVED
            elif date=="dummy timestamp":
steve9000's avatar
steve9000 committed
129 130 131 132
                if rev[0] == "0":
                    state = tree.STATE_NEW
                else:
                    print "Revision '%s' not understood" % rev
133
            elif date=="dummy timestamp from new-entry":
134
                state = tree.STATE_MODIFIED
steve9000's avatar
steve9000 committed
135
            else:
steve9000's avatar
steve9000 committed
136 137
                plus = date.find("+")
                if plus >= 0:
138
                    state = tree.STATE_CONFLICT
steve9000's avatar
steve9000 committed
139
                else:
140 141 142 143
                    try:
                        mtime = os.stat(path).st_mtime
                    except OSError:
                        state = tree.STATE_MISSING
steve9000's avatar
steve9000 committed
144
                    else:
145
                        if time.asctime(time.gmtime(mtime))==date:
146 147 148
                            state = tree.STATE_NORMAL
                        else:
                            state = tree.STATE_MODIFIED
149
            retfiles.append( File(path, name, state, rev, tag, options) )
steve9000's avatar
steve9000 committed
150
    # known
steve9000's avatar
steve9000 committed
151
    cvsfiles = map(lambda x: x[1], matches)
steve9000's avatar
steve9000 committed
152 153
    # ignored
    try:
154 155
        ignored = open( os.path.join(directory, "%s/.cvsignore" % os.environ["HOME"] )).read().split()
    except (IOError,KeyError):
steve9000's avatar
steve9000 committed
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
        ignored = []
    try:
        ignored += open( os.path.join(directory, ".cvsignore")).read().split()
    except IOError:
        pass

    if len(ignored):
        try:
            regexes = [ misc.shell_to_regex(i)[:-1] for i in ignored ]
            ignore_re = re.compile( "(" + "|".join(regexes) + ")" )
        except re.error, e:
            misc.run_dialog(_("Error converting to a regular expression\n" \
                              "The pattern was '%s'\n" \
                              "The error was '%s'") % (",".join(ignored), e))
    else:
171
        class dummy(object):
steve9000's avatar
steve9000 committed
172 173 174
            def match(*args): return None
        ignore_re = dummy()

steve9000's avatar
steve9000 committed
175 176
    for f,path in files:
        if f not in cvsfiles:
steve9000's avatar
steve9000 committed
177 178 179 180
            if ignore_re.match(f) == None:
                retfiles.append( File(path, f, tree.STATE_NONE, "") )
            else:
                retfiles.append( File(path, f, tree.STATE_IGNORED, "") )
steve9000's avatar
steve9000 committed
181 182
    for d,path in dirs:
        if d not in cvsfiles:
steve9000's avatar
steve9000 committed
183 184 185 186
            if ignore_re.match(d) == None:
                retdirs.append( Dir(path, d, tree.STATE_NONE) )
            else:
                retfiles.append( Dir(path, d, tree.STATE_IGNORED) )
steve9000's avatar
steve9000 committed
187 188

    return retdirs, retfiles
steve9000's avatar
steve9000 committed
189 190 191 192 193 194

################################################################################
#
# Local Functions
#
################################################################################
steve9000's avatar
steve9000 committed
195
def _find(start):
196
    if start[-1] != "/": start+="/"
steve9000's avatar
steve9000 committed
197 198
    cfiles = []
    cdirs = []
steve9000's avatar
steve9000 committed
199 200
    try:
        entries = os.listdir(start)
steve9000's avatar
steve9000 committed
201
        entries.sort()
steve9000's avatar
steve9000 committed
202 203
    except OSError:
        entries = []
204
    for f in filter(lambda x: x!="CVS" and x[0]!=".", entries):
205
        fname = start + f
steve9000's avatar
steve9000 committed
206
        lname = fname
steve9000's avatar
steve9000 committed
207 208 209 210
        if os.path.isdir(fname):
            cdirs.append( (f, lname) )
        else:
            cfiles.append( (f, lname) )
211
    return _lookup_cvs_files(cdirs, cfiles)
steve9000's avatar
steve9000 committed
212

213
def recursive_find(start):
steve9000's avatar
steve9000 committed
214 215 216 217 218 219
    if start=="":
        start="."
    ret = []
    def visit(arg, dirname, names):
        try: names.remove("CVS")
        except ValueError: pass
steve9000's avatar
steve9000 committed
220
        dirs, files = _find(dirname)
steve9000's avatar
steve9000 committed
221 222 223 224 225
        ret.extend( dirs )
        ret.extend( files )
    os.path.walk(start, visit, ret)
    return ret

226
def listdir_cvs(start):
steve9000's avatar
steve9000 committed
227 228 229 230 231
    if start=="":
        start="."
    dirs, files = _find(start)
    return dirs+files

232 233 234 235 236 237 238 239 240
def _expand_to_root( treeview, path ):
    """Expand rows from path up to root"""
    start = path[:]
    while len(start) and not treeview.row_expanded(start):
        start = start[:-1]
    level = len(start)
    while level < len(path):
        level += 1
        treeview.expand_row( path[:level], 0)
steve9000's avatar
steve9000 committed
241

steve9000's avatar
steve9000 committed
242 243 244 245 246 247 248
def _commonprefix(files):
    if len(files) != 1:
        workdir = misc.commonprefix(files)
    else:
        workdir = os.path.dirname(files[0])
    return workdir

249 250 251 252 253
################################################################################
#
# CommitDialog
#
################################################################################
steve9000's avatar
steve9000 committed
254
class CommitDialog(gnomeglade.Component):
255
    def __init__(self, parent):
256
        gnomeglade.Component.__init__(self, paths.share_dir("glade2/cvsview.glade"), "commitdialog")
257 258
        self.parent = parent
        self.widget.set_transient_for( parent.widget.get_toplevel() )
259 260 261 262
        selected = parent._get_selected_files()
        topdir = _commonprefix(selected)
        selected = [ s[len(topdir):] for s in selected ]
        self.changedfiles.set_text( ("(in %s) "%topdir) + " ".join(selected) )
263 264 265
        self.widget.show_all()

    def run(self):
266 267
        self.previousentry.list.select_item(0)
        self.textview.grab_focus()
268
        buf = self.textview.get_buffer()
269 270 271
        buf.place_cursor( buf.get_start_iter() )
        buf.move_mark( buf.get_selection_bound(), buf.get_end_iter() )
        response = self.widget.run()
272 273
        msg = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0)
        if response == gtk.RESPONSE_OK:
274
            self.parent._command_on_selected(self.parent.prefs.get_cvs_command("commit") + ["-m", msg] )
steve9000's avatar
steve9000 committed
275 276
        if len(msg.strip()):
            self.previousentry.prepend_history(1, msg)
277
        self.widget.destroy()
steve9000's avatar
steve9000 committed
278 279 280
    def on_previousentry_activate(self, gentry):
        buf = self.textview.get_buffer()
        buf.set_text( gentry.gtk_entry().get_text() )
281

282 283 284 285 286 287 288
################################################################################
#
# CvsTreeStore
#
################################################################################

COL_LOCATION, COL_STATUS, COL_REVISION, COL_TAG, COL_OPTIONS, COL_END = range(tree.COL_END, tree.COL_END+6)
289

290 291 292 293 294 295
class CvsTreeStore(tree.DiffTreeStore):
    def __init__(self):
        types = [type("")] * COL_END
        types[tree.COL_ICON] = type(tree.pixbuf_file)
        gtk.TreeStore.__init__(self, *types)
        self.ntree = 1
steve9000's avatar
steve9000 committed
296 297
        self._setup_default_styles()
        self.textstyle[tree.STATE_MISSING] = '<span foreground="#000088" strikethrough="true" weight="bold">%s</span>'
298

steve9000's avatar
steve9000 committed
299 300 301 302 303 304 305
################################################################################
#
# DirDiffMenu
#
################################################################################
class CvsMenu(gnomeglade.Component):
    def __init__(self, app, event):
306
        gladefile = paths.share_dir("glade2/cvsview.glade")
steve9000's avatar
steve9000 committed
307 308 309 310 311
        gnomeglade.Component.__init__(self, gladefile, "menu_popup")
        self.parent = app
        self.widget.popup( None, None, None, 3, event.time )
    def on_diff_activate(self, menuitem):
        self.parent.on_button_diff_clicked( menuitem )
312 313
    def on_edit_activate(self, menuitem):
        self.parent._edit_files( self.parent._get_selected_files() )
steve9000's avatar
steve9000 committed
314 315 316 317 318 319 320 321 322 323 324 325 326
    def on_update_activate(self, menuitem):
        self.parent.on_button_update_clicked( menuitem )
    def on_commit_activate(self, menuitem):
        self.parent.on_button_commit_clicked( menuitem )
    def on_add_activate(self, menuitem):
        self.parent.on_button_add_clicked( menuitem )
    def on_add_binary_activate(self, menuitem):
        self.parent.on_button_add_binary_clicked( menuitem )
    def on_remove_activate(self, menuitem):
        self.parent.on_button_remove_clicked( menuitem )
    def on_remove_locally_activate(self, menuitem):
        self.parent.on_button_delete_clicked( menuitem )

327 328 329 330 331 332 333 334
################################################################################
# filters
################################################################################
entry_modified = lambda x: (x.state >= tree.STATE_NEW) or (x.isdir and (x.state > tree.STATE_NONE))
entry_normal   = lambda x: (x.state == tree.STATE_NORMAL) 
entry_noncvs   = lambda x: (x.state == tree.STATE_NONE) or (x.isdir and (x.state > tree.STATE_IGNORED))
entry_ignored  = lambda x: (x.state == tree.STATE_IGNORED) or x.isdir

steve9000's avatar
steve9000 committed
335 336 337 338 339
################################################################################
#
# CvsView
#
################################################################################
steve9000's avatar
steve9000 committed
340
class CvsView(melddoc.MeldDoc, gnomeglade.Component):
steve9000's avatar
steve9000 committed
341

steve9000's avatar
steve9000 committed
342 343
    def __init__(self, prefs):
        melddoc.MeldDoc.__init__(self, prefs)
344
        gnomeglade.Component.__init__(self, paths.share_dir("glade2/cvsview.glade"), "cvsview")
steve9000's avatar
steve9000 committed
345
        self.toolbar.set_style( self.prefs.get_toolbar_style() )
steve9000's avatar
steve9000 committed
346
        self.tempfiles = []
347 348
        self.model = CvsTreeStore()
        self.treeview.set_model(self.model)
steve9000's avatar
steve9000 committed
349
        self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
350
        self.treeview.set_headers_visible(1)
steve9000's avatar
steve9000 committed
351
        column = gtk.TreeViewColumn( _("Name") )
steve9000's avatar
steve9000 committed
352 353
        renpix = gtk.CellRendererPixbuf()
        rentext = gtk.CellRendererText()
354 355 356 357 358
        column.pack_start(renpix, expand=0)
        column.pack_start(rentext, expand=1)
        column.set_attributes(renpix, pixbuf=self.model.column_index(tree.COL_ICON, 0))
        column.set_attributes(rentext, markup=self.model.column_index(tree.COL_TEXT, 0))
        self.treeview.append_column(column)
359

steve9000's avatar
steve9000 committed
360
        def addCol(name, num):
361
            column = gtk.TreeViewColumn(name)
steve9000's avatar
steve9000 committed
362
            rentext = gtk.CellRendererText()
363 364 365 366 367
            column.pack_start(rentext, expand=0)
            column.set_attributes(rentext, markup=self.model.column_index(num, 0))
            self.treeview.append_column(column)
            return column

steve9000's avatar
steve9000 committed
368 369 370 371 372
        self.treeview_column_location = addCol( _("Location"), COL_LOCATION)
        addCol(_("Status"), COL_STATUS)
        addCol(_("Rev"), COL_REVISION)
        addCol(_("Tag"), COL_TAG)
        addCol(_("Options"), COL_OPTIONS)
373

374
        class ConsoleStream(object):
375 376 377 378 379 380 381 382 383 384
            def __init__(this, textview):
                this.textview = textview
                b = textview.get_buffer()
                this.mark = b.create_mark("END", b.get_end_iter(), 0)
            def write(this, s):
                if s:
                    b = this.textview.get_buffer()
                    b.insert(b.get_end_iter(), s)
                    this.textview.scroll_mark_onscreen( this.mark )
        self.consolestream = ConsoleStream(self.consoleview)
steve9000's avatar
steve9000 committed
385
        self.location = None
386
        self.treeview_column_location.set_visible( self.button_flatten.get_active() )
steve9000's avatar
steve9000 committed
387 388 389
        size = self.fileentry.size_request()[1]
        self.button_jump.set_size_request(size, size)
        self.button_jump.hide()
390 391
        if not self.prefs.cvs_console_visible:
            self.on_console_view_toggle(self.console_hide_box)
392 393 394 395 396 397

    def set_location(self, location):
        self.model.clear()
        self.location = location = os.path.abspath(location or ".")
        self.fileentry.gtk_entry().set_text(location)
        iter = self.model.add_entries( None, [location] )
steve9000's avatar
steve9000 committed
398
        self.treeview.get_selection().select_iter(iter)
399 400 401
        self.model.set_state(iter, 0, tree.STATE_NORMAL, isdir=1)
        self.recompute_label()
        self.scheduler.remove_all_tasks()
402
        self.scheduler.add_task( self._search_recursively_iter(self.model.get_iter_root()).next )
403 404

    def recompute_label(self):
steve9000's avatar
steve9000 committed
405
        self.label_text = os.path.basename(self.location)
406 407
        self.label_changed()

408
    def _search_recursively_iter(self, iterstart):
steve9000's avatar
steve9000 committed
409
        yield _("[%s] Scanning") % self.label_text
410
        rootpath = self.model.get_path( iterstart  )
411
        rootname = self.model.value_path( self.model.get_iter(rootpath), 0 )
412
        prefixlen = 1 + len( self.model.value_path( self.model.get_iter_root(), 0 ) )
413
        todo = [ (rootpath, rootname) ]
414
        filters = []
415
        if self.button_modified.get_active():
416 417 418
            filters.append( entry_modified )
        if self.button_normal.get_active():
            filters.append( entry_normal )
419
        if self.button_noncvs.get_active():
420 421 422 423 424 425 426
            filters.append( entry_noncvs )
        if self.button_ignored.get_active():
            filters.append( entry_ignored )
        def showable(entry):
            for f in filters:
                if f(entry): return 1
        recursive = self.button_flatten.get_active()
427 428 429 430 431 432 433 434 435
        while len(todo):
            todo.sort() # depth first
            path, name = todo.pop(0)
            if path:
                iter = self.model.get_iter( path )
                root = self.model.value_path( iter, 0 )
            else:
                iter = self.model.get_iter_root()
                root = name
steve9000's avatar
steve9000 committed
436
            yield _("[%s] Scanning %s") % (self.label_text, root[prefixlen:])
437 438 439 440 441 442 443 444 445 446
            #import time; time.sleep(1.0)
            
            entries = filter(showable, listdir_cvs(root))
            differences = 0
            for e in entries:
                differences |= (e.state != tree.STATE_NORMAL)
                if e.isdir and recursive:
                    todo.append( (None, e.path) )
                    continue
                child = self.model.add_entries(iter, [e.path])
447
                self._update_item_state( child, e, root[prefixlen:] )
448 449
                if e.isdir:
                    todo.append( (self.model.get_path(child), None) )
450
            if not recursive: # expand parents
451
                if len(entries) == 0:
452
                    self.model.add_empty(iter, _("(Empty)"))
453
                if differences or len(path)==1:
454 455
                    _expand_to_root( self.treeview, path )
            else: # just the root
456 457
                self.treeview.expand_row( (0,), 0)

458 459 460 461
    def on_preference_changed(self, key, value):
        if key == "toolbar_style":
            self.toolbar.set_style( self.prefs.get_toolbar_style() )

462 463 464
    def on_fileentry_activate(self, fileentry):
        path = fileentry.get_full_path(0)
        self.set_location(path)
steve9000's avatar
steve9000 committed
465

466
    def on_quit_event(self):
467
        self.scheduler.remove_all_tasks()
468 469 470 471
        for f in self.tempfiles:
            if os.path.exists(f):
                shutil.rmtree(f, ignore_errors=1)

steve9000's avatar
steve9000 committed
472
    def on_delete_event(self, appquit=0):
473
        self.on_quit_event()
steve9000's avatar
steve9000 committed
474
        return gtk.RESPONSE_OK
475

steve9000's avatar
steve9000 committed
476
    def on_row_activated(self, treeview, path, tvc):
477 478
        iter = self.model.get_iter(path)
        if self.model.iter_has_child(iter):
steve9000's avatar
steve9000 committed
479 480 481 482 483
            if self.treeview.row_expanded(path):
                self.treeview.collapse_row(path)
            else:
                self.treeview.expand_row(path,0)
        else:
484 485
            path = self.model.value_path(iter, 0)
            self.run_cvs_diff( [path] )
steve9000's avatar
steve9000 committed
486

steve9000's avatar
steve9000 committed
487
    def run_cvs_diff_iter(self, paths, empty_patch_ok):
steve9000's avatar
steve9000 committed
488
        yield _("[%s] Fetching differences") % self.label_text
489
        difffunc = self._command_iter(self.prefs.get_cvs_command("diff") + ["-u"], paths, 0).next
steve9000's avatar
steve9000 committed
490 491 492 493 494
        diff = None
        while type(diff) != type(()):
            diff = difffunc()
            yield 1
        prefix, patch = diff[0], diff[1]
steve9000's avatar
steve9000 committed
495
        yield _("[%s] Applying patch") % self.label_text
496
        if patch:
497
            self.show_patch(prefix, patch)
steve9000's avatar
steve9000 committed
498
        elif empty_patch_ok:
499
            misc.run_dialog( _("No differences found."), parent=self, messagetype=gtk.MESSAGE_INFO)
500
        else:
steve9000's avatar
steve9000 committed
501 502 503 504
            for path in paths:
                self.emit("create-diff", [path])

    def run_cvs_diff(self, paths, empty_patch_ok=0):
505
        self.scheduler.add_task( self.run_cvs_diff_iter(paths, empty_patch_ok).next, atfront=1 )
506

507 508
    def on_button_press_event(self, text, event):
        if event.button==3:
steve9000's avatar
steve9000 committed
509
            CvsMenu(self, event)
510 511
        return 0

512 513
    def on_button_flatten_toggled(self, button):
        self.treeview_column_location.set_visible( self.button_flatten.get_active() )
steve9000's avatar
steve9000 committed
514
        self.refresh()
515
    def on_button_filter_toggled(self, button):
steve9000's avatar
steve9000 committed
516 517
        self.refresh()

518 519 520 521 522 523 524 525
    def _get_selected_treepaths(self):
        sel = []
        def gather(model, path, iter):
            sel.append( model.get_path(iter) )
        s = self.treeview.get_selection()
        s.selected_foreach(gather)
        return sel

steve9000's avatar
steve9000 committed
526
    def _get_selected_files(self):
527
        sel = []
steve9000's avatar
steve9000 committed
528
        def gather(model, path, iter):
529
            sel.append( model.value_path(iter,0) )
steve9000's avatar
steve9000 committed
530 531
        s = self.treeview.get_selection()
        s.selected_foreach(gather)
steve9000's avatar
steve9000 committed
532
        # remove empty entries and remove trailing slashes
533
        return [ x[-1]!="/" and x or x[:-1] for x in sel if x != None ]
steve9000's avatar
steve9000 committed
534

steve9000's avatar
steve9000 committed
535
    def _command_iter(self, command, files, refresh):
steve9000's avatar
steve9000 committed
536
        """Run 'command' on 'files'. Return a tuple of the directory the
steve9000's avatar
steve9000 committed
537 538
           command was executed in and the output of the command.
        """
steve9000's avatar
steve9000 committed
539
        msg = misc.shelljoin(command)
540
        yield "[%s] %s" % (self.label_text, msg.replace("\n", u"\u21b2") )
541
        if len(files) == 1 and os.path.isdir(files[0]):
542 543
            workdir = os.path.dirname( files[0] )
            files = [ os.path.basename( files[0] ) ]
544 545
        else:
            workdir = _commonprefix(files)
546 547
            kill = len(workdir) and (len(workdir)+1) or 0
            files = filter(lambda x: len(x), map(lambda x: x[kill:], files))
steve9000's avatar
steve9000 committed
548
        r = None
549
        self.consolestream.write( misc.shelljoin(command+files) + " (in %s)\n" % workdir)
550
        readfunc = misc.read_pipe_iter(command + files, self.consolestream, workdir=workdir).next
551 552 553
        try:
            while r == None:
                r = readfunc()
554
                self.consolestream.write(r)
555 556
                yield 1
        except IOError, e:
steve9000's avatar
steve9000 committed
557
            misc.run_dialog("Error running command.\n'%s'\n\nThe error was:\n%s" % ( misc.shelljoin(command), e),
steve9000's avatar
steve9000 committed
558
                parent=self, messagetype=gtk.MESSAGE_ERROR)
559
        if refresh:
steve9000's avatar
steve9000 committed
560
            self.refresh_partial(workdir)
561
        yield workdir, r
steve9000's avatar
steve9000 committed
562 563 564 565 566

    def _command(self, command, files, refresh=1):
        """Run 'command' on 'files'.
        """
        self.scheduler.add_task( self._command_iter(command, files, refresh).next )
steve9000's avatar
steve9000 committed
567
        
568
    def _command_on_selected(self, command, refresh=1):
steve9000's avatar
steve9000 committed
569 570
        files = self._get_selected_files()
        if len(files):
steve9000's avatar
steve9000 committed
571
            self._command(command, files, refresh)
steve9000's avatar
steve9000 committed
572
        else:
steve9000's avatar
steve9000 committed
573
            misc.run_dialog( _("Select some files first."), parent=self, messagetype=gtk.MESSAGE_INFO)
steve9000's avatar
steve9000 committed
574

steve9000's avatar
steve9000 committed
575
    def on_button_update_clicked(self, object):
576
        self._command_on_selected( self.prefs.get_cvs_command("update") )
steve9000's avatar
steve9000 committed
577
    def on_button_commit_clicked(self, object):
578 579 580
        dialog = CommitDialog( self )
        dialog.run()

steve9000's avatar
steve9000 committed
581
    def on_button_add_clicked(self, object):
582
        self._command_on_selected(self.prefs.get_cvs_command("add") )
steve9000's avatar
steve9000 committed
583
    def on_button_add_binary_clicked(self, object):
584
        self._command_on_selected(self.prefs.get_cvs_command("add") + ["-kb"] )
steve9000's avatar
steve9000 committed
585
    def on_button_remove_clicked(self, object):
586
        self._command_on_selected(self.prefs.get_cvs_command("rm") + ["-f"] )
steve9000's avatar
steve9000 committed
587
    def on_button_delete_clicked(self, object):
steve9000's avatar
steve9000 committed
588
        files = self._get_selected_files()
589 590 591 592 593 594 595 596 597 598 599
        for name in files:
            try:
                if os.path.isfile(name):
                    os.remove(name)
                elif os.path.isdir(name):
                    if misc.run_dialog(_("'%s' is a directory.\nRemove recusively?") % os.path.basename(name),
                            parent = self,
                            buttonstype=gtk.BUTTONS_OK_CANCEL) == gtk.RESPONSE_OK:
                        shutil.rmtree(name)
            except OSError, e:
                misc.run_dialog(_("Error removing %s\n\n%s.") % (name,e), parent = self)
steve9000's avatar
steve9000 committed
600 601
        workdir = _commonprefix(files)
        self.refresh_partial(workdir)
steve9000's avatar
steve9000 committed
602

steve9000's avatar
steve9000 committed
603
    def on_button_diff_clicked(self, object):
steve9000's avatar
steve9000 committed
604 605 606
        files = self._get_selected_files()
        if len(files):
            self.run_cvs_diff(files, empty_patch_ok=1)
steve9000's avatar
steve9000 committed
607

608
    def show_patch(self, prefix, patch):
steve9000's avatar
steve9000 committed
609 610 611
        if not patch: return

        tmpdir = tempfile.mktemp("-meld")
612
        self.tempfiles.append(tmpdir)
steve9000's avatar
steve9000 committed
613 614 615 616 617 618 619 620 621 622 623
        os.mkdir(tmpdir)

        regex = re.compile("^Index:\s+(.*$)", re.M)
        files = regex.findall(patch)
        diffs = []
        for file in files:
            destfile = os.path.join(tmpdir,file)
            destdir = os.path.dirname( destfile )

            if not os.path.exists(destdir):
                os.makedirs(destdir)
624
            pathtofile = os.path.join(prefix, file)
steve9000's avatar
steve9000 committed
625
            try:
626 627
                shutil.copyfile( pathtofile, destfile)
            except IOError: # it is missing, create empty file
steve9000's avatar
steve9000 committed
628
                open(destfile,"w").close()
629
            diffs.append( (destfile, pathtofile) )
steve9000's avatar
steve9000 committed
630

steve9000's avatar
steve9000 committed
631
        misc.write_pipe(["patch","--strip=0","--reverse","--directory=%s" % tmpdir], patch)
steve9000's avatar
steve9000 committed
632 633 634 635
        for d in diffs:
            self.emit("create-diff", d)

    def refresh(self):
636
        self.set_location( self.model.value_path( self.model.get_iter_root(), 0 ) )
637

638
    def refresh_partial(self, where):
639
        if not self.button_flatten.get_active():
640
            iter = self.find_iter_by_name( where )
steve9000's avatar
steve9000 committed
641 642 643 644 645 646
            if iter:
                newiter = self.model.insert_after( None, iter) 
                self.model.set_value(newiter, self.model.column_index( tree.COL_PATH, 0), where)
                self.model.set_state(newiter, 0, tree.STATE_NORMAL, isdir=1)
                self.model.remove(iter)
                self.scheduler.add_task( self._search_recursively_iter(newiter).next )
647
        else: # XXX fixme
648
            self.refresh()
649

steve9000's avatar
steve9000 committed
650 651 652
    def next_diff(self,*args):
        pass

steve9000's avatar
steve9000 committed
653 654
    def on_button_jump_press_event(self, button, event):
        class MyMenu(gtk.Menu):
655
            def __init__(self, parent, where, showup=1):
steve9000's avatar
steve9000 committed
656 657
                gtk.Menu.__init__(self)
                self.cvsview = parent 
658 659 660 661 662 663 664 665 666 667
                self.map_id = self.connect("map", lambda item: self.on_map(item,where,showup) )
            def add_item(self, name, submenu, showup):
                item = gtk.MenuItem(name)
                if submenu:
                    item.set_submenu( MyMenu(self.cvsview, submenu, showup ) )
                self.append( item )
            def on_map(self, item, where, showup):
                if showup:
                    self.add_item("..", os.path.dirname(where), 1 )
                self.populate( where, self.listdir(where) )
steve9000's avatar
steve9000 committed
668
                self.show_all()
669 670 671 672 673 674 675 676 677 678 679 680
                self.disconnect(self.map_id)
                del self.map_id
            def listdir(self, d):
                try:
                    return [p for p in os.listdir(d) if os.path.basename(p) != "CVS" and os.path.isdir( os.path.join(d,p))]
                except OSError:
                    return []
            def populate(self, where, children):
                for child in children:
                    cc = self.listdir( os.path.join(where, child) )
                    self.add_item( child, len(cc) and os.path.join(where,child), 0 )
        menu = MyMenu( self, os.path.abspath(self.location) )
steve9000's avatar
steve9000 committed
681 682
        menu.popup(None, None, None, event.button, event.time)

683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707
    def _update_item_state(self, iter, cvsentry, location):
        e = cvsentry
        self.model.set_state( iter, 0, e.state, e.isdir )
        def set(col, val):
            self.model.set_value( iter, self.model.column_index(col,0), val)
        set( COL_LOCATION, location )
        set( COL_STATUS, e.get_status())
        set( COL_REVISION, e.rev)
        set( COL_TAG, e.tag)
        set( COL_OPTIONS, e.options)

    def on_file_changed(self, filename):
        iter = self.find_iter_by_name(filename)
        if iter:
            path = self.model.value_path(iter, 0)
            dirs, files = _lookup_cvs_files( [], [ (os.path.basename(path), path)] )
            for e in files:
                if e.path == path:
                    prefixlen = 1 + len( self.model.value_path( self.model.get_iter_root(), 0 ) )
                    self._update_item_state( iter, e, e.parent[prefixlen:])
                    return

    def find_iter_by_name(self, name):
        iter = self.model.get_iter_root()
        path = self.model.value_path(iter, 0)
steve9000's avatar
steve9000 committed
708 709 710
        while iter:
            if name == path:
                return iter
711
            elif name.startswith(path): 
712 713 714 715 716 717 718 719 720 721
                child = self.model.iter_children( iter )
                while child:
                    path = self.model.value_path(child, 0)
                    if name == path:
                        return child
                    elif name.startswith(path):
                        break
                    else:
                        child = self.model.iter_next( child )
                iter = child
722 723
            else:
                break
724 725
        return None

726
    def on_console_view_toggle(self, box, event=None):
727
        if box == self.console_hide_box:
728
            self.prefs.cvs_console_visible = 0
729 730 731
            self.console_hbox.hide()
            self.console_show_box.show()
        else:
732
            self.prefs.cvs_console_visible = 1
733 734 735 736 737 738 739
            self.console_hbox.show()
            self.console_show_box.hide()

    def on_consoleview_populate_popup(self, text, menu):
        item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
        def activate(*args):
            buf = text.get_buffer()
steve9000's avatar
steve9000 committed
740
            buf.delete( buf.get_start_iter(), buf.get_end_iter() )
741 742 743 744 745 746 747
        item.connect("activate", activate)
        item.show()
        menu.insert( item, 0 )
        item = gtk.SeparatorMenuItem()
        item.show()
        menu.insert( item, 1 )

748 749 750 751 752 753 754 755 756 757 758 759
    def next_diff(self, direction):
        start_iter = self.model.get_iter( (self._get_selected_treepaths() or [(0,)])[-1] )

        def goto_iter(it):
            curpath = self.model.get_path(it)
            for i in range(len(curpath)-1):
                self.treeview.expand_row( curpath[:i+1], 0)
            self.treeview.set_cursor(curpath)

        search = {gtk.gdk.SCROLL_UP : self.model.inorder_search_up}.get(direction, self.model.inorder_search_down)
        for it in search( start_iter ):
            state = int(self.model.get_state( it, 0))
760
            if state not in (tree.STATE_NORMAL, tree.STATE_EMPTY):
761 762 763
                goto_iter(it)
                return

steve9000's avatar
steve9000 committed
764 765
gobject.type_register(CvsView)