5 from PyQt4
import QtCore
6 from PyQt4
import QtGui
7 from PyQt4
.QtCore
import Qt
8 from PyQt4
.QtCore
import SIGNAL
12 from cola
import utils
13 from cola
import qtutils
15 # Custom event type for GitRepoInfoEvents
16 INFO_EVENT_TYPE
= QtCore
.QEvent
.User
+ 42
19 class Columns(object):
20 """Defines columns in the classic view"""
26 ALL
= (NAME
, STATUS
, AGE
, MESSAGE
, WHO
)
29 class GitRepoModel(QtGui
.QStandardItemModel
):
30 """Provides an interface into a git repository for browsing purposes."""
31 def __init__(self
, parent
):
32 QtGui
.QStandardItem
.__init
__(self
, parent
)
33 self
._interesting
_paths
= self
._get
_paths
()
34 self
._known
_paths
= set()
36 model
.add_message_observer(model
.message_updated
,
39 self
.setColumnCount(len(Columns
.ALL
))
40 for idx
, header
in enumerate(Columns
.ALL
):
41 self
.setHeaderData(idx
, Qt
.Horizontal
,
42 QtCore
.QVariant(self
.tr(header
.title())))
44 self
._direntries
= {'': self
.invisibleRootItem()}
47 def _create_column(self
, col
, path
):
48 """Creates a StandardItem for use in a treeview cell."""
49 # GitRepoNameItem is the only one that returns a custom type(),
50 # so we use to infer selections.
51 if col
== Columns
.NAME
:
52 return GitRepoNameItem(path
)
53 return GitRepoItem(col
, path
)
55 def _create_row(self
, path
):
56 """Return a list of items representing a row."""
57 return [self
._create
_column
(c
, path
) for c
in Columns
.ALL
]
59 def _add_file(self
, parent
, path
, insert
=False):
60 """Add a file entry to the model."""
63 row_items
= self
._create
_row
(path
)
65 # Use a standard file icon for the name field
66 row_items
[0].setIcon(qtutils
.file_icon())
69 # Add file paths at the end of the list
70 parent
.appendRow(row_items
)
71 self
.entry(path
).update_name()
72 self
._known
_paths
.add(path
)
74 # Entries exist so try to find an a good insertion point
76 for idx
in xrange(parent
.rowCount()):
77 child
= parent
.child(idx
, 0)
78 if child
.rowCount() > 0:
81 parent
.insertRow(idx
, row_items
)
85 # No adequate place found so simply append
87 parent
.appendRow(row_items
)
88 self
.entry(path
).update_name()
89 self
._known
_paths
.add(path
)
91 def add_directory(self
, parent
, path
):
92 """Add a directory entry to the model."""
95 row_items
= self
._create
_row
(path
)
97 # Use a standard directory icon
98 row_items
[0].setIcon(qtutils
.dir_icon())
100 # Insert directories before file paths
101 row
= self
._dir
_rows
.setdefault(parent
, 0)
102 parent
.insertRow(row
, row_items
)
103 self
._dir
_rows
[parent
] += 1
105 # Update the 'name' column for this entry
106 self
.entry(path
).update_name()
107 self
._known
_paths
.add(path
)
111 def path_is_interesting(self
, path
):
112 """Return True if path has a status."""
113 return path
in self
._interesting
_paths
115 def _get_paths(self
):
116 """Return paths of interest; e.g. paths with a status."""
118 paths
= set(model
.staged
+ model
.unstaged
)
119 return cola
.utils
.add_parents(paths
)
121 def _model_updated(self
):
122 """Observes model changes and updates paths accordingly."""
123 old_paths
= self
._interesting
_paths
124 new_paths
= self
._get
_paths
()
125 for path
in new_paths
.union(old_paths
):
126 if path
not in self
._known
_paths
:
128 self
.entry(path
).update()
130 self
._interesting
_paths
= new_paths
132 def _initialize(self
):
133 """Iterate over the cola model and create GitRepoItems."""
134 for path
in cola
.model().everything():
137 def add_file(self
, path
, insert
=False):
138 """Add a file to the model."""
139 dirname
= utils
.dirname(path
)
140 if dirname
in self
._direntries
:
141 parent
= self
._direntries
[dirname
]
143 parent
= self
._create
_dir
_entry
(dirname
, self
._direntries
)
144 self
._direntries
[dirname
] = parent
145 self
._add
_file
(parent
, path
, insert
=insert
)
147 def _create_dir_entry(self
, dirname
, direntries
):
149 Create a directory entry for the model.
151 This ensures that directories are always listed before files.
154 entries
= dirname
.split('/')
156 parent
= self
.invisibleRootItem()
157 for entry
in entries
:
159 path
= '/'.join(curdir
)
160 if path
in direntries
:
161 parent
= direntries
[path
]
164 parent_path
= '/'.join(curdir
[:-1])
165 parent
= self
.add_directory(grandparent
, path
)
166 direntries
[path
] = parent
169 def entry(self
, path
):
170 """Return the GitRepoEntry for a path."""
171 return GitRepoEntryManager
.entry(path
)
174 class GitRepoEntryManager(object):
176 Provides access to static instances of GitRepoEntry and model data.
181 def entry(cls
, path
):
182 """Return a static instance of a GitRepoEntry."""
183 if path
not in cls
.static_entries
:
184 cls
.static_entries
[path
] = GitRepoEntry(path
)
185 return cls
.static_entries
[path
]
188 class GitRepoEntry(QtCore
.QObject
):
190 Provides asynchronous lookup of repository data for a path.
192 Emits signal names matching those defined in Columns.
195 def __init__(self
, path
):
196 QtCore
.QObject
.__init
__(self
)
200 def update_name(self
):
201 """Emits a signal corresponding to the entry's name."""
202 # 'name' is cheap to calculate so simply emit a signal
203 self
.emit(SIGNAL(Columns
.NAME
), utils
.basename(self
.path
))
204 if '/' not in self
.path
:
208 """Starts a GitRepoInfoTask to calculate info for entries."""
209 # GitRepoInfoTask handles expensive lookups
210 threadpool
= QtCore
.QThreadPool
.globalInstance()
211 self
.task
= GitRepoInfoTask(self
.path
)
212 threadpool
.start(self
.task
)
215 """Receive GitRepoInfoEvents and emit corresponding Qt signals."""
216 if e
.type() == INFO_EVENT_TYPE
:
218 self
.emit(SIGNAL(e
.signal
), *e
.data
)
220 return QtCore
.QObject
.event(self
, e
)
223 class GitRepoInfoTask(QtCore
.QRunnable
):
224 """Handles expensive git lookups for a path."""
225 def __init__(self
, path
):
226 QtCore
.QRunnable
.__init
__(self
)
232 Return git data for a path.
234 Supported keys are 'date', 'message', and 'author'
238 log_line
= cola
.model().git
.log('-1', '--', self
.path
,
241 pretty
='format:%ar/%s/%an')
243 log_line
= core
.decode(log_line
)
244 date
, rest
= log_line
.split('/', 1)
245 message
, author
= rest
.rsplit('/', 1)
246 self
._data
['date'] = date
247 self
._data
['message'] = message
248 self
._data
['author'] = author
250 self
._data
['date'] = self
.date()
251 self
._data
['message'] = '-'
252 self
._data
['author'] = cola
.model().local_user_name
253 return self
._data
[key
]
256 """Calculate the name for an entry."""
257 return utils
.basename(self
.path
)
261 Returns a relative date for a file path.
263 This is typically used for new entries that do not have
264 'git log' information.
267 encpath
= core
.encode(self
.path
)
268 st
= os
.stat(encpath
)
269 elapsed
= time
.time() - st
.st_mtime
270 minutes
= int(elapsed
/ 60.)
272 return '%d minutes ago' % minutes
273 hours
= int(elapsed
/ 60. / 60.)
275 return '%d hours ago' % hours
276 return '%d days ago' % int(elapsed
/ 60. / 60. / 24.)
279 """Return the status for the entry's path."""
282 unmerged
= utils
.add_parents(set(model
.unmerged
))
283 modified
= utils
.add_parents(set(model
.modified
))
284 staged
= utils
.add_parents(set(model
.staged
))
285 untracked
= utils
.add_parents(set(model
.untracked
))
286 upstream_changed
= utils
.add_parents(set(model
.upstream_changed
))
288 if self
.path
in unmerged
:
289 return qtutils
.tr('Unmerged')
290 if self
.path
in modified
and self
.path
in staged
:
291 return qtutils
.tr('Partially Staged')
292 if self
.path
in modified
:
293 return qtutils
.tr('Modified')
294 if self
.path
in staged
:
295 return qtutils
.tr('Staged')
296 if self
.path
in untracked
:
297 return qtutils
.tr('Untracked')
298 if self
.path
in upstream_changed
:
299 return qtutils
.tr('Changed Upstream')
303 """Perform expensive lookups and post corresponding events."""
304 app
= QtGui
.QApplication
.instance()
305 app
.postEvent(GitRepoEntryManager
.entry(self
.path
),
306 GitRepoInfoEvent(Columns
.MESSAGE
, self
.data('message')))
307 app
.postEvent(GitRepoEntryManager
.entry(self
.path
),
308 GitRepoInfoEvent(Columns
.AGE
, self
.data('date')))
309 app
.postEvent(GitRepoEntryManager
.entry(self
.path
),
310 GitRepoInfoEvent(Columns
.WHO
, self
.data('author')))
311 app
.postEvent(GitRepoEntryManager
.entry(self
.path
),
312 GitRepoInfoEvent(Columns
.STATUS
, self
.status()))
315 class GitRepoInfoEvent(QtCore
.QEvent
):
316 """Transport mechanism for communicating from a GitRepoInfoTask."""
317 def __init__(self
, signal
, *data
):
318 QtCore
.QEvent
.__init
__(self
, QtCore
.QEvent
.User
+ 1)
323 return INFO_EVENT_TYPE
326 class GitRepoItem(QtGui
.QStandardItem
):
328 Represents a cell in a treeview.
330 Many GitRepoItems map to a single repository path.
331 Each GitRepoItem manages a different cell in the tree view.
332 One is created for each column -- Name, Status, Age, etc.
335 def __init__(self
, column
, path
):
336 QtGui
.QStandardItem
.__init
__(self
)
337 self
.setEditable(False)
338 self
.setDragEnabled(False)
339 entry
= GitRepoEntryManager
.entry(path
)
340 QtCore
.QObject
.connect(entry
, SIGNAL(column
), self
.setText
)
343 class GitRepoNameItem(GitRepoItem
):
344 """Subclass GitRepoItem to provide a custom type()."""
345 TYPE
= QtGui
.QStandardItem
.UserType
+ 1
347 def __init__(self
, path
):
348 GitRepoItem
.__init
__(self
, Columns
.NAME
, path
)
353 Indicate that this item is of a special user-defined type.
355 'name' is the only column that registers a user-defined type.
356 This is done to allow filtering out other columns when determining
357 which paths are selected.
360 return GitRepoNameItem
.TYPE