1 from __future__
import division
, absolute_import
, unicode_literals
4 from qtpy
import QtCore
6 from qtpy
import QtWidgets
7 from qtpy
.QtCore
import Qt
8 from qtpy
.QtCore
import Signal
10 from .. import gitcmds
14 from .. import qtutils
15 from ..git
import STDOUT
19 class Columns(object):
20 """Defines columns in the worktree browser"""
28 ALL
= (NAME
, STATUS
, MESSAGE
, AUTHOR
, AGE
)
29 ATTRS
= ('name', 'status', 'message', 'author', 'age')
49 def text(cls
, column
):
51 value
= cls
.TEXT
[column
]
53 # Defer translation until runtime
55 value
= cls
.TEXT
[column
]
59 def attr(cls
, column
):
60 """Return the attribute for the column"""
61 return cls
.ATTRS
[column
]
64 class GitRepoModel(QtGui
.QStandardItemModel
):
65 """Provides an interface into a git repository for browsing purposes."""
67 model_updated
= Signal()
70 def __init__(self
, context
, parent
):
71 QtGui
.QStandardItemModel
.__init
__(self
, parent
)
72 self
.setColumnCount(len(Columns
.ALL
))
74 self
.context
= context
75 self
.model
= model
= context
.model
78 self
.turbo
= cfg
.get('cola.turbo', False)
79 self
.default_author
= cfg
.get('user.name', N_('Author'))
81 self
._interesting
_paths
= set()
82 self
._interesting
_files
= set()
83 self
._runtask
= qtutils
.RunTask(parent
=parent
)
85 self
.model_updated
.connect(self
.refresh
, type=Qt
.QueuedConnection
)
88 model
.add_observer(model
.message_updated
, self
._model
_updated
)
90 self
.file_icon
= icons
.file_text()
91 self
.dir_icon
= icons
.directory()
93 def mimeData(self
, indexes
):
94 context
= self
.context
95 paths
= qtutils
.paths_from_indexes(self
, indexes
,
96 item_type
=GitRepoNameItem
.TYPE
)
97 return qtutils
.mimedata_from_paths(context
, paths
)
100 return qtutils
.path_mimetypes()
104 super(GitRepoModel
, self
).clear()
106 def hasChildren(self
, index
):
108 item
= self
.itemFromIndex(index
)
109 result
= item
.hasChildren()
114 def get(self
, path
, default
=None):
116 item
= self
.invisibleRootItem()
118 item
= self
.entries
.get(path
, default
)
121 def create_row(self
, path
, create
=True, is_dir
=False):
123 row
= self
.entries
[path
]
126 column
= create_column
127 row
= self
.entries
[path
] = [
128 column(c
, path
, is_dir
) for c
in Columns
.ALL
]
133 def populate(self
, item
):
134 self
.populate_dir(item
, item
.path
+ '/')
136 def add_directory(self
, parent
, path
):
137 """Add a directory entry to the model."""
138 # First, try returning an existing item
139 current_item
= self
.get(path
)
140 if current_item
is not None:
141 return current_item
[0]
144 row_items
= self
.create_row(path
, is_dir
=True)
146 # Use a standard directory icon
147 name_item
= row_items
[0]
148 name_item
.setIcon(self
.dir_icon
)
149 parent
.appendRow(row_items
)
153 def add_file(self
, parent
, path
):
154 """Add a file entry to the model."""
156 file_entry
= self
.get(path
)
157 if file_entry
is not None:
161 row_items
= self
.create_row(path
)
162 name_item
= row_items
[0]
164 # Use a standard file icon for the name field
165 name_item
.setIcon(self
.file_icon
)
167 # Add file paths at the end of the list
168 parent
.appendRow(row_items
)
172 def populate_dir(self
, parent
, path
):
173 """Populate a subtree"""
174 context
= self
.context
175 dirs
, paths
= gitcmds
.listdir(context
, path
)
177 # Insert directories before file paths
181 dir_parent
= self
.add_parent_directories(parent
, dirname
)
182 self
.add_directory(dir_parent
, dirname
)
183 self
.update_entry(dirname
)
185 for filename
in paths
:
188 file_parent
= self
.add_parent_directories(parent
, filename
)
189 self
.add_file(file_parent
, filename
)
190 self
.update_entry(filename
)
192 def add_parent_directories(self
, parent
, dirname
):
193 """Ensure that all parent directory entries exist"""
195 parent_dir
= utils
.dirname(dirname
)
196 for path
in utils
.pathset(parent_dir
):
197 sub_parent
= self
.add_directory(sub_parent
, path
)
200 def path_is_interesting(self
, path
):
201 """Return True if path has a status."""
202 return path
in self
._interesting
_paths
204 def get_paths(self
, files
=None):
205 """Return paths of interest; e.g. paths with a status."""
207 files
= self
.get_files()
208 return utils
.add_parents(files
)
212 return set(model
.staged
+ model
.unstaged
)
214 def _model_updated(self
):
215 """Observes model changes and updates paths accordingly."""
216 self
.model_updated
.emit()
219 old_files
= self
._interesting
_files
220 old_paths
= self
._interesting
_paths
221 new_files
= self
.get_files()
222 new_paths
= self
.get_paths(files
=new_files
)
224 if new_files
!= old_files
or not old_paths
:
230 for path
in sorted(new_paths
.union(old_paths
)):
231 self
.update_entry(path
)
233 self
._interesting
_files
= new_files
234 self
._interesting
_paths
= new_paths
236 def _initialize(self
):
237 self
.setHorizontalHeaderLabels(Columns
.text_values())
239 self
._interesting
_files
= files
= self
.get_files()
240 self
._interesting
_paths
= self
.get_paths(files
=files
)
242 root
= self
.invisibleRootItem()
243 self
.populate_dir(root
, './')
245 def update_entry(self
, path
):
246 if self
.turbo
or path
not in self
.entries
:
247 return # entry doesn't currently exist
248 context
= self
.context
249 task
= GitRepoInfoTask(
250 context
, self
._parent
, path
, self
.default_author
)
251 self
._runtask
.start(task
)
254 def create_column(col
, path
, is_dir
):
255 """Creates a StandardItem for use in a treeview cell."""
256 # GitRepoNameItem is the only one that returns a custom type()
257 # and is used to infer selections.
258 if col
== Columns
.NAME
:
259 item
= GitRepoNameItem(path
, is_dir
)
261 item
= GitRepoItem(path
)
265 class GitRepoInfoTask(qtutils
.Task
):
266 """Handles expensive git lookups for a path."""
268 def __init__(self
, context
, parent
, path
, default_author
):
269 qtutils
.Task
.__init
__(self
, parent
)
270 self
.context
= context
272 self
._parent
= parent
273 self
._default
_author
= default_author
277 """Return git data for a path
279 Supported keys are 'date', 'message', and 'author'
282 git
= self
.context
.git
285 '-1', '--', self
.path
, no_color
=True,
286 pretty
=r
'format:%ar%x01%s%x01%an', _readonly
=True)[STDOUT
]
289 date
, message
, author
= log_line
.split(chr(0x01), 2)
290 self
._data
['date'] = date
291 self
._data
['message'] = message
292 self
._data
['author'] = author
294 self
._data
['date'] = self
.date()
295 self
._data
['message'] = '-'
296 self
._data
['author'] = self
._default
_author
298 return self
._data
[key
]
301 """Returns a relative date for a file path
303 This is typically used for new entries that do not have
304 'git log' information.
308 st
= core
.stat(self
.path
)
310 return N_('%d minutes ago') % 0
311 elapsed
= time
.time() - st
.st_mtime
312 minutes
= int(elapsed
/ 60)
314 return N_('%d minutes ago') % minutes
315 hours
= int(elapsed
/ 60 / 60)
317 return N_('%d hours ago') % hours
318 return N_('%d days ago') % int(elapsed
/ 60 / 60 / 24)
321 """Return the status for the entry's path."""
322 model
= self
.context
.model
323 unmerged
= utils
.add_parents(model
.unmerged
)
324 modified
= utils
.add_parents(model
.modified
)
325 staged
= utils
.add_parents(model
.staged
)
326 untracked
= utils
.add_parents(model
.untracked
)
327 upstream_changed
= utils
.add_parents(model
.upstream_changed
)
331 status
= (icons
.modified_name(), N_('Unmerged'))
332 elif path
in modified
and self
.path
in staged
:
333 status
= (icons
.partial_name(), N_('Partially Staged'))
334 elif path
in modified
:
335 status
= (icons
.modified_name(), N_('Modified'))
337 status
= (icons
.staged_name(), N_('Staged'))
338 elif path
in upstream_changed
:
339 status
= (icons
.upstream_name(), N_('Changed Upstream'))
340 elif path
in untracked
:
347 """Perform expensive lookups and post corresponding events."""
351 self
.data('message'),
355 app
= QtWidgets
.QApplication
.instance()
357 app
.postEvent(self
._parent
, GitRepoInfoEvent(data
))
359 pass # The app exited before this task finished
362 class GitRepoInfoEvent(QtCore
.QEvent
):
363 """Transport mechanism for communicating from a GitRepoInfoTask."""
365 TYPE
= QtCore
.QEvent
.Type(QtCore
.QEvent
.registerEventType())
367 def __init__(self
, data
):
368 QtCore
.QEvent
.__init
__(self
, self
.TYPE
)
375 class GitRepoItem(QtGui
.QStandardItem
):
376 """Represents a cell in a treeview.
378 Many GitRepoItems map to a single repository path.
379 Each GitRepoItem manages a different cell in the tree view.
380 One is created for each column -- Name, Status, Age, etc.
383 def __init__(self
, path
):
384 QtGui
.QStandardItem
.__init
__(self
)
387 self
.setDragEnabled(False)
388 self
.setEditable(False)
390 def set_status(self
, data
):
393 self
.setIcon(QtGui
.QIcon(icon
))
395 self
.setIcon(QtGui
.QIcon())
399 class GitRepoNameItem(GitRepoItem
):
400 """Subclass GitRepoItem to provide a custom type()."""
401 TYPE
= QtGui
.QStandardItem
.ItemType(QtGui
.QStandardItem
.UserType
+ 1)
403 def __init__(self
, path
, is_dir
):
404 GitRepoItem
.__init
__(self
, path
)
406 self
.setDragEnabled(True)
407 self
.setText(utils
.basename(path
))
411 Indicate that this item is of a special user-defined type.
413 'name' is the only column that registers a user-defined type.
414 This is done to allow filtering out other columns when determining
415 which paths are selected.
420 def hasChildren(self
):