browse: display errors when saving blobs
[git-cola.git] / cola / widgets / browse.py
blob54d5c6594a17809269463739788dab19f9eb5be1
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 import shlex
4 from qtpy.QtCore import Qt
5 from qtpy.QtCore import Signal
6 from qtpy import QtCore
7 from qtpy import QtGui
8 from qtpy import QtWidgets
10 from ..models.browse import GitRepoModel
11 from ..models.browse import GitRepoNameItem
12 from ..models.selection import State
13 from ..i18n import N_
14 from ..interaction import Interaction
15 from .. import cmds
16 from .. import core
17 from .. import gitcmds
18 from .. import hotkeys
19 from .. import icons
20 from .. import utils
21 from .. import qtutils
22 from .selectcommits import select_commits
23 from . import common
24 from . import defs
25 from . import standard
28 def worktree_browser(context, parent=None, update=True, show=True):
29 """Create a new worktree browser"""
30 view = Browser(context, parent, update=update)
31 if parent is None:
32 context.browser_windows.append(view)
33 view.closed.connect(context.browser_windows.remove)
34 model = GitRepoModel(context, view.tree)
35 view.set_model(model)
36 if update:
37 view.refresh()
38 if show:
39 view.show()
40 return view
43 def save_path(context, path, model):
44 """Choose an output filename based on the selected path"""
45 filename = qtutils.save_as(path)
46 if filename:
47 model.filename = filename
48 cmds.do(SaveBlob, context, model)
49 result = True
50 else:
51 result = False
52 return result
55 class Browser(standard.Widget):
56 # Read-only mode property
57 mode = property(lambda self: self.model.mode)
59 def __init__(self, context, parent, update=True):
60 standard.Widget.__init__(self, parent)
61 self.tree = RepoTreeView(context, self)
62 self.mainlayout = qtutils.hbox(defs.no_margin, defs.spacing, self.tree)
63 self.setLayout(self.mainlayout)
65 self.model = context.model
66 self.model.updated.connect(self._updated_callback, type=Qt.QueuedConnection)
67 if parent is None:
68 qtutils.add_close_action(self)
69 if update:
70 self._updated_callback()
72 self.init_state(context.settings, self.resize, 720, 420)
74 def set_model(self, model):
75 """Set the model"""
76 self.tree.set_model(model)
78 def refresh(self):
79 """Refresh the model triggering view updates"""
80 self.tree.refresh()
82 def _updated_callback(self):
83 branch = self.model.currentbranch
84 curdir = core.getcwd()
85 msg = N_('Repository: %s') % curdir
86 msg += '\n'
87 msg += N_('Branch: %s') % branch
88 self.setToolTip(msg)
90 scope = dict(project=self.model.project, branch=branch)
91 title = N_('%(project)s: %(branch)s - Browse') % scope
92 if self.mode == self.model.mode_amend:
93 title += ' %s' % N_('(Amending)')
94 self.setWindowTitle(title)
97 # pylint: disable=too-many-ancestors
98 class RepoTreeView(standard.TreeView):
99 """Provides a filesystem-like view of a git repository."""
101 def __init__(self, context, parent):
102 standard.TreeView.__init__(self, parent)
104 self.context = context
105 self.selection = context.selection
106 self.saved_selection = []
107 self.saved_current_path = None
108 self.saved_open_folders = set()
109 self.restoring_selection = False
110 self._columns_sized = False
112 self.setDragEnabled(True)
113 self.setRootIsDecorated(False)
114 self.setSortingEnabled(False)
115 self.setSelectionMode(self.ExtendedSelection)
117 # Observe model updates
118 model = context.model
119 model.about_to_update.connect(self.save_selection, type=Qt.QueuedConnection)
120 model.updated.connect(self.update_actions, type=Qt.QueuedConnection)
121 self.expanded.connect(self.index_expanded)
123 self.collapsed.connect(lambda idx: self.size_columns())
124 self.collapsed.connect(self.index_collapsed)
126 # Sync selection before the key press event changes the model index
127 queued = Qt.QueuedConnection
128 self.index_about_to_change.connect(self.sync_selection, type=queued)
130 self.action_history = qtutils.add_action_with_status_tip(
131 self,
132 N_('View History...'),
133 N_('View history for selected paths'),
134 self.view_history,
135 hotkeys.HISTORY,
138 self.action_stage = qtutils.add_action_with_status_tip(
139 self,
140 cmds.StageOrUnstage.name(),
141 N_('Stage/unstage selected paths for commit'),
142 cmds.run(cmds.StageOrUnstage, context),
143 hotkeys.STAGE_SELECTION,
146 self.action_untrack = qtutils.add_action_with_status_tip(
147 self,
148 N_('Untrack Selected'),
149 N_('Stop tracking paths'),
150 self.untrack_selected,
153 self.action_rename = qtutils.add_action_with_status_tip(
154 self, N_('Rename'), N_('Rename selected paths'), self.rename_selected
157 self.action_difftool = qtutils.add_action_with_status_tip(
158 self,
159 cmds.LaunchDifftool.name(),
160 N_('Launch git-difftool on the current path'),
161 cmds.run(cmds.LaunchDifftool, context),
162 hotkeys.DIFF,
165 self.action_difftool_predecessor = qtutils.add_action_with_status_tip(
166 self,
167 N_('Diff Against Predecessor...'),
168 N_('Launch git-difftool against previous versions'),
169 self.diff_predecessor,
170 hotkeys.DIFF_SECONDARY,
173 self.action_revert_unstaged = qtutils.add_action_with_status_tip(
174 self,
175 cmds.RevertUnstagedEdits.name(),
176 N_('Revert unstaged changes to selected paths'),
177 cmds.run(cmds.RevertUnstagedEdits, context),
178 hotkeys.REVERT,
181 self.action_revert_uncommitted = qtutils.add_action_with_status_tip(
182 self,
183 cmds.RevertUncommittedEdits.name(),
184 N_('Revert uncommitted changes to selected paths'),
185 cmds.run(cmds.RevertUncommittedEdits, context),
186 hotkeys.UNDO,
189 self.action_editor = qtutils.add_action_with_status_tip(
190 self,
191 cmds.LaunchEditor.name(),
192 N_('Edit selected paths'),
193 cmds.run(cmds.LaunchEditor, context),
194 hotkeys.EDIT,
197 self.action_blame = qtutils.add_action_with_status_tip(
198 self,
199 cmds.BlamePaths.name(),
200 N_('Blame selected paths'),
201 cmds.run(cmds.BlamePaths, context),
204 self.action_refresh = common.refresh_action(context, self)
206 self.action_default_app = common.default_app_action(
207 context, self, self.selected_paths
210 self.action_parent_dir = common.parent_dir_action(
211 context, self, self.selected_paths
214 self.action_terminal = common.terminal_action(
215 context, self, func=self.selected_paths
218 self.x_width = QtGui.QFontMetrics(self.font()).width('x')
219 self.size_columns(force=True)
221 def index_expanded(self, index):
222 """Update information about a directory as it is expanded."""
223 # Remember open folders so that we can restore them when refreshing
224 item = self.name_item_from_index(index)
225 self.saved_open_folders.add(item.path)
226 self.size_columns()
228 # update information about a directory as it is expanded
229 if item.cached:
230 return
231 path = item.path
233 model = self.model()
234 model.populate(item)
235 model.update_entry(path)
237 for row in range(item.rowCount()):
238 path = item.child(row, 0).path
239 model.update_entry(path)
241 item.cached = True
243 def index_collapsed(self, index):
244 item = self.name_item_from_index(index)
245 self.saved_open_folders.remove(item.path)
247 def refresh(self):
248 self.model().refresh()
250 def size_columns(self, force=False):
251 """Set the column widths."""
252 cfg = self.context.cfg
253 should_resize = cfg.get('cola.resizebrowsercolumns', default=False)
254 if not force and not should_resize:
255 return
256 self.resizeColumnToContents(0)
257 self.resizeColumnToContents(1)
258 self.resizeColumnToContents(2)
259 self.resizeColumnToContents(3)
260 self.resizeColumnToContents(4)
262 def sizeHintForColumn(self, column):
263 x_width = self.x_width
265 if column == 1:
266 # Status
267 size = x_width * 11
268 elif column == 2:
269 # Summary
270 size = x_width * 64
271 elif column == 3:
272 # Author
273 size = x_width * 18
274 elif column == 4:
275 # Age
276 size = x_width * 16
277 else:
278 # Filename and others use the actual content
279 size = super(RepoTreeView, self).sizeHintForColumn(column)
280 return size
282 def save_selection(self):
283 selection = self.selected_paths()
284 if selection:
285 self.saved_selection = selection
287 current = self.current_item()
288 if current:
289 self.saved_current_path = current.path
291 def restore(self):
292 selection = self.selectionModel()
293 flags = selection.Select | selection.Rows
295 self.restoring_selection = True
297 # Restore opened folders
298 model = self.model()
299 for path in sorted(self.saved_open_folders):
300 row = model.get(path)
301 if not row:
302 continue
303 index = row[0].index()
304 if index.isValid():
305 self.setExpanded(index, True)
307 # Restore the current item. We do this first, otherwise
308 # setCurrentIndex() can mess with the selection we set below
309 current_index = None
310 current_path = self.saved_current_path
311 if current_path:
312 row = model.get(current_path)
313 if row:
314 current_index = row[0].index()
316 if current_index and current_index.isValid():
317 self.setCurrentIndex(current_index)
319 # Restore selected items
320 for path in self.saved_selection:
321 row = model.get(path)
322 if not row:
323 continue
324 index = row[0].index()
325 if index.isValid():
326 self.scrollTo(index)
327 selection.select(index, flags)
329 self.restoring_selection = False
331 # Resize the columns once when cola.resizebrowsercolumns is False.
332 # This provides a good initial size since we will not be resizing
333 # the columns during expand/collapse.
334 if not self._columns_sized:
335 self._columns_sized = True
336 self.size_columns(force=True)
338 self.update_diff()
340 def update_actions(self):
341 """Enable/disable actions."""
342 selection = self.selected_paths()
343 selected = bool(selection)
344 staged = bool(self.selected_staged_paths(selection=selection))
345 modified = bool(self.selected_modified_paths(selection=selection))
346 unstaged = bool(self.selected_unstaged_paths(selection=selection))
347 tracked = bool(self.selected_tracked_paths(selection=selection))
348 revertable = staged or modified
350 self.action_editor.setEnabled(selected)
351 self.action_history.setEnabled(selected)
352 self.action_default_app.setEnabled(selected)
353 self.action_parent_dir.setEnabled(selected)
355 if self.action_terminal is not None:
356 self.action_terminal.setEnabled(selected)
358 self.action_stage.setEnabled(staged or unstaged)
359 self.action_untrack.setEnabled(tracked)
360 self.action_rename.setEnabled(tracked)
361 self.action_difftool.setEnabled(staged or modified)
362 self.action_difftool_predecessor.setEnabled(tracked)
363 self.action_revert_unstaged.setEnabled(revertable)
364 self.action_revert_uncommitted.setEnabled(revertable)
366 def contextMenuEvent(self, event):
367 """Create a context menu."""
368 self.update_actions()
369 menu = qtutils.create_menu(N_('Actions'), self)
370 menu.addAction(self.action_editor)
371 menu.addAction(self.action_stage)
372 menu.addSeparator()
373 menu.addAction(self.action_history)
374 menu.addAction(self.action_difftool)
375 menu.addAction(self.action_difftool_predecessor)
376 menu.addAction(self.action_blame)
377 menu.addSeparator()
378 menu.addAction(self.action_revert_unstaged)
379 menu.addAction(self.action_revert_uncommitted)
380 menu.addAction(self.action_untrack)
381 menu.addAction(self.action_rename)
382 menu.addSeparator()
383 menu.addAction(self.action_default_app)
384 menu.addAction(self.action_parent_dir)
386 if self.action_terminal is not None:
387 menu.addAction(self.action_terminal)
388 menu.exec_(self.mapToGlobal(event.pos()))
390 def mousePressEvent(self, event):
391 """Synchronize the selection on mouse-press."""
392 result = QtWidgets.QTreeView.mousePressEvent(self, event)
393 self.sync_selection()
394 return result
396 def sync_selection(self):
397 """Push selection into the selection model."""
398 staged = []
399 unmerged = []
400 modified = []
401 untracked = []
402 state = State(staged, unmerged, modified, untracked)
404 paths = self.selected_paths()
405 model = self.context.model
406 model_staged = utils.add_parents(model.staged)
407 model_modified = utils.add_parents(model.modified)
408 model_unmerged = utils.add_parents(model.unmerged)
409 model_untracked = utils.add_parents(model.untracked)
411 for path in paths:
412 if path in model_unmerged:
413 unmerged.append(path)
414 elif path in model_untracked:
415 untracked.append(path)
416 elif path in model_staged:
417 staged.append(path)
418 elif path in model_modified:
419 modified.append(path)
420 else:
421 staged.append(path)
422 # Push the new selection into the model.
423 self.selection.set_selection(state)
424 return paths
426 def selectionChanged(self, old, new):
427 """Override selectionChanged to update available actions."""
428 result = QtWidgets.QTreeView.selectionChanged(self, old, new)
429 if not self.restoring_selection:
430 self.update_actions()
431 self.update_diff()
432 return result
434 def update_diff(self):
435 context = self.context
436 model = context.model
437 paths = self.sync_selection()
438 if paths and self.model().path_is_interesting(paths[0]):
439 cached = paths[0] in model.staged
440 cmds.do(cmds.Diff, context, paths[0], cached)
442 def set_model(self, model):
443 """Set the concrete QAbstractItemModel instance."""
444 self.setModel(model)
445 model.restore.connect(self.restore, type=Qt.QueuedConnection)
447 def name_item_from_index(self, model_index):
448 """Return the name item corresponding to the model index."""
449 index = model_index.sibling(model_index.row(), 0)
450 return self.model().itemFromIndex(index)
452 def paths_from_indexes(self, indexes):
453 return qtutils.paths_from_indexes(
454 self.model(), indexes, item_type=GitRepoNameItem.TYPE
457 def selected_paths(self):
458 """Return the selected paths."""
459 return self.paths_from_indexes(self.selectedIndexes())
461 def selected_staged_paths(self, selection=None):
462 """Return selected staged paths."""
463 if selection is None:
464 selection = self.selected_paths()
465 model = self.context.model
466 staged = utils.add_parents(model.staged)
467 return [p for p in selection if p in staged]
469 def selected_modified_paths(self, selection=None):
470 """Return selected modified paths."""
471 if selection is None:
472 selection = self.selected_paths()
473 model = self.context.model
474 modified = utils.add_parents(model.modified)
475 return [p for p in selection if p in modified]
477 def selected_unstaged_paths(self, selection=None):
478 """Return selected unstaged paths."""
479 if selection is None:
480 selection = self.selected_paths()
481 model = self.context.model
482 modified = utils.add_parents(model.modified)
483 untracked = utils.add_parents(model.untracked)
484 unstaged = modified.union(untracked)
485 return [p for p in selection if p in unstaged]
487 def selected_tracked_paths(self, selection=None):
488 """Return selected tracked paths."""
489 if selection is None:
490 selection = self.selected_paths()
491 model = self.context.model
492 staged = set(self.selected_staged_paths(selection=selection))
493 modified = set(self.selected_modified_paths(selection=selection))
494 untracked = utils.add_parents(model.untracked)
495 tracked = staged.union(modified)
496 return [p for p in selection if p not in untracked or p in tracked]
498 def view_history(self):
499 """Launch the configured history browser path-limited to entries."""
500 paths = self.selected_paths()
501 cmds.do(cmds.VisualizePaths, self.context, paths)
503 def untrack_selected(self):
504 """untrack selected paths."""
505 context = self.context
506 cmds.do(cmds.Untrack, context, self.selected_tracked_paths())
508 def rename_selected(self):
509 """untrack selected paths."""
510 context = self.context
511 cmds.do(cmds.Rename, context, self.selected_tracked_paths())
513 def diff_predecessor(self):
514 """Diff paths against previous versions."""
515 context = self.context
516 paths = self.selected_tracked_paths()
517 args = ['--'] + paths
518 revs, summaries = gitcmds.log_helper(context, all=False, extra_args=args)
519 commits = select_commits(
520 context, N_('Select Previous Version'), revs, summaries, multiselect=False
522 if not commits:
523 return
524 commit = commits[0]
525 cmds.difftool_launch(context, left=commit, paths=paths)
527 def current_path(self):
528 """Return the path for the current item."""
529 index = self.currentIndex()
530 if not index.isValid():
531 return None
532 return self.name_item_from_index(index).path
535 class BrowseModel(object):
536 """Context data used for browsing branches via git-ls-tree"""
538 def __init__(self, ref, filename=None):
539 self.ref = ref
540 self.relpath = filename
541 self.filename = filename
544 class SaveBlob(cmds.ContextCommand):
545 def __init__(self, context, model):
546 super(SaveBlob, self).__init__(context)
547 self.browse_model = model
549 def do(self):
550 git = self.context.git
551 model = self.browse_model
552 ref = '%s:%s' % (model.ref, model.relpath)
553 with core.xopen(model.filename, 'wb') as fp:
554 status, output, err = git.show(ref, _stdout=fp)
556 out = '# git show %s >%s\n%s' % (
557 shlex.quote(ref),
558 shlex.quote(model.filename),
559 output,
561 Interaction.command(N_('Error Saving File'), 'git show', status, out, err)
562 if status != 0:
563 return
565 msg = N_('Saved "%(filename)s" from "%(ref)s" to "%(destination)s"') % dict(
566 filename=model.relpath, ref=model.ref, destination=model.filename
568 Interaction.log_status(status, msg, '')
570 Interaction.information(
571 N_('File Saved'), N_('File saved to "%s"') % model.filename
575 class BrowseBranch(standard.Dialog):
576 @classmethod
577 def browse(cls, context, ref):
578 model = BrowseModel(ref)
579 dlg = cls(context, model, parent=qtutils.active_window())
580 dlg_model = GitTreeModel(context, ref, dlg)
581 dlg.setModel(dlg_model)
582 dlg.setWindowTitle(N_('Browsing %s') % model.ref)
583 dlg.show()
584 dlg.raise_()
585 if dlg.exec_() != dlg.Accepted:
586 return None
587 return dlg
589 def __init__(self, context, model, parent=None):
590 standard.Dialog.__init__(self, parent=parent)
591 if parent is not None:
592 self.setWindowModality(Qt.WindowModal)
594 # updated for use by commands
595 self.context = context
596 self.model = model
598 # widgets
599 self.tree = GitTreeWidget(parent=self)
600 self.close_button = qtutils.close_button()
602 text = N_('Save')
603 self.save = qtutils.create_button(text=text, enabled=False, default=True)
605 # layouts
606 self.btnlayt = qtutils.hbox(
607 defs.margin, defs.spacing, self.close_button, qtutils.STRETCH, self.save
610 self.layt = qtutils.vbox(defs.margin, defs.spacing, self.tree, self.btnlayt)
611 self.setLayout(self.layt)
613 # connections
614 self.tree.path_chosen.connect(self.save_path)
616 self.tree.selection_changed.connect(
617 self.selection_changed, type=Qt.QueuedConnection
620 qtutils.connect_button(self.close_button, self.close)
621 qtutils.connect_button(self.save, self.save_blob)
622 self.init_size(parent=parent)
624 def expandAll(self):
625 self.tree.expandAll()
627 def setModel(self, model):
628 self.tree.setModel(model)
630 def path_chosen(self, path, close=True):
631 """Update the model from the view"""
632 model = self.model
633 model.relpath = path
634 model.filename = path
635 if close:
636 self.accept()
638 def save_path(self, path):
639 """Choose an output filename based on the selected path"""
640 self.path_chosen(path, close=False)
641 if save_path(self.context, path, self.model):
642 self.accept()
644 def save_blob(self):
645 """Save the currently selected file"""
646 filenames = self.tree.selected_files()
647 if not filenames:
648 return
649 self.save_path(filenames[0])
651 def selection_changed(self):
652 """Update actions based on the current selection"""
653 filenames = self.tree.selected_files()
654 self.save.setEnabled(bool(filenames))
657 # pylint: disable=too-many-ancestors
658 class GitTreeWidget(standard.TreeView):
660 selection_changed = Signal()
661 path_chosen = Signal(object)
663 def __init__(self, parent=None):
664 standard.TreeView.__init__(self, parent)
665 self.setHeaderHidden(True)
666 # pylint: disable=no-member
667 self.doubleClicked.connect(self.double_clicked)
669 def double_clicked(self, index):
670 item = self.model().itemFromIndex(index)
671 if item is None:
672 return
673 if item.is_dir:
674 return
675 self.path_chosen.emit(item.path)
677 def selected_files(self):
678 items = self.selected_items()
679 return [i.path for i in items if not i.is_dir]
681 def selectionChanged(self, old_selection, new_selection):
682 QtWidgets.QTreeView.selectionChanged(self, old_selection, new_selection)
683 self.selection_changed.emit()
685 def select_first_file(self):
686 """Select the first filename in the tree"""
687 model = self.model()
688 idx = self.indexAt(QtCore.QPoint(0, 0))
689 item = model.itemFromIndex(idx)
690 while idx and idx.isValid() and item and item.is_dir:
691 idx = self.indexBelow(idx)
692 item = model.itemFromIndex(idx)
694 if idx and idx.isValid() and item:
695 self.setCurrentIndex(idx)
698 class GitFileTreeModel(QtGui.QStandardItemModel):
699 """Presents a list of file paths as a hierarchical tree."""
701 def __init__(self, parent):
702 QtGui.QStandardItemModel.__init__(self, parent)
703 self.dir_entries = {'': self.invisibleRootItem()}
704 self.dir_rows = {}
706 def clear(self):
707 QtGui.QStandardItemModel.clear(self)
708 self.dir_rows = {}
709 self.dir_entries = {'': self.invisibleRootItem()}
711 def add_files(self, files):
712 """Add a list of files"""
713 add_file = self.add_file
714 for f in files:
715 add_file(f)
717 def add_file(self, path):
718 """Add a file to the model."""
719 dirname = utils.dirname(path)
720 dir_entries = self.dir_entries
721 try:
722 parent = dir_entries[dirname]
723 except KeyError:
724 parent = dir_entries[dirname] = self.create_dir_entry(dirname)
726 row_items = create_row(path, False)
727 parent.appendRow(row_items)
729 def add_directory(self, parent, path):
730 """Add a directory entry to the model."""
731 # Create model items
732 row_items = create_row(path, True)
734 try:
735 parent_path = parent.path
736 except AttributeError: # root QStandardItem
737 parent_path = ''
739 # Insert directories before file paths
740 try:
741 row = self.dir_rows[parent_path]
742 except KeyError:
743 row = self.dir_rows[parent_path] = 0
745 parent.insertRow(row, row_items)
746 self.dir_rows[parent_path] += 1
747 self.dir_entries[path] = row_items[0]
749 return row_items[0]
751 def create_dir_entry(self, dirname):
753 Create a directory entry for the model.
755 This ensures that directories are always listed before files.
758 entries = dirname.split('/')
759 curdir = []
760 parent = self.invisibleRootItem()
761 curdir_append = curdir.append
762 self_add_directory = self.add_directory
763 dir_entries = self.dir_entries
764 for entry in entries:
765 curdir_append(entry)
766 path = '/'.join(curdir)
767 try:
768 parent = dir_entries[path]
769 except KeyError:
770 grandparent = parent
771 parent = self_add_directory(grandparent, path)
772 dir_entries[path] = parent
773 return parent
776 def create_row(path, is_dir):
777 """Return a list of items representing a row."""
778 return [GitTreeItem(path, is_dir)]
781 class GitTreeModel(GitFileTreeModel):
782 def __init__(self, context, ref, parent):
783 GitFileTreeModel.__init__(self, parent)
784 self.context = context
785 self.ref = ref
786 self._initialize()
788 def _initialize(self):
789 """Iterate over git-ls-tree and create GitTreeItems."""
790 git = self.context.git
791 status, out, err = git.ls_tree('--full-tree', '-r', '-t', '-z', self.ref)
792 if status != 0:
793 Interaction.log_status(status, out, err)
794 return
796 if not out:
797 return
799 for line in out[:-1].split('\0'):
800 # .....6 ...4 ......................................40
801 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
802 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
803 objtype = line[7]
804 relpath = line[6 + 1 + 4 + 1 + 40 + 1 :]
805 if objtype == 't':
806 parent = self.dir_entries[utils.dirname(relpath)]
807 self.add_directory(parent, relpath)
808 elif objtype == 'b':
809 self.add_file(relpath)
812 class GitTreeItem(QtGui.QStandardItem):
814 Represents a cell in a treeview.
816 Many GitRepoItems could map to a single repository path,
817 but this tree only has a single column.
818 Each GitRepoItem manages a different cell in the tree view.
822 def __init__(self, path, is_dir):
823 QtGui.QStandardItem.__init__(self)
824 self.is_dir = is_dir
825 self.path = path
826 self.setEditable(False)
827 self.setDragEnabled(False)
828 self.setText(utils.basename(path))
829 if is_dir:
830 icon = icons.directory()
831 else:
832 icon = icons.file_text()
833 self.setIcon(icon)