gitcmd: Add a git_path() method for finding .git-relative paths
[git-cola.git] / cola / controllers / repobrowser.py
blobbb123387ff678acd670b7d2b6f24cd73c354abe3
1 """This controller handles the repository file browser."""
4 import os
6 from PyQt4 import QtGui
8 import cola
9 from cola import gitcmd
10 from cola import utils
11 from cola import resources
12 from cola import qtutils
13 from cola.models import browser
14 from cola.views.selectcommits import SelectCommitsView
15 from cola.qobserver import QObserver
17 git = gitcmd.instance()
20 def select_file_from_repo():
21 """Launche a dialog to selecting a filename from a branch."""
22 model = cola.model().clone()
23 parent = QtGui.QApplication.instance().activeWindow()
24 view = SelectCommitsView(parent, syntax=False)
25 controller = RepoBrowserController(model, view,
26 title='Select File',
27 get_file=True)
28 view.show()
29 if view.exec_() == QtGui.QDialog.Accepted:
30 return controller.filename
31 else:
32 return None
34 def browse_git_branch(branch):
35 """Launch a dialog to browse files in a specific branch."""
36 if not branch:
37 return
38 # Clone the model to allow opening multiple browsers
39 # with different sets of data
40 model = browser.BrowserModel(branch)
41 parent = QtGui.QApplication.instance().activeWindow()
42 view = SelectCommitsView(parent, syntax=False)
43 controller = RepoBrowserController(model, view)
44 view.show()
45 return view.exec_() == QtGui.QDialog.Accepted
47 class RepoBrowserController(QObserver):
48 """Provides control to the Repository Browser."""
49 def __init__(self, model, view,
50 title='File Browser', get_file=False):
51 QObserver.__init__(self, model, view)
53 self.get_file = get_file
54 """Whether we should returns a selected file"""
56 self.filename = None
57 """The last selected filename"""
59 view.setWindowTitle(title)
60 self.add_signals('itemSelectionChanged()', view.commit_list,)
61 self.add_actions(directory = self.action_directory_changed)
62 self.add_callbacks(commit_list = self.item_changed)
63 view.commit_list.contextMenuEvent = self.context_menu_event
64 # Start at the root of the tree
65 model.set_directory('')
66 self.refresh_view()
68 def context_menu_event(self, event):
69 """Generate a context menu for the repository browser."""
70 menu = QtGui.QMenu(self.view);
71 menu.addAction(self.tr('Blame'), self.blame)
72 menu.exec_(self.view.commit_list.mapToGlobal(event.pos()))
74 def blame(self):
75 """Show git-blame output for a file path."""
76 current = self.view.commit_list.currentRow()
77 item = self.view.commit_list.item(current)
78 if item is None or not item.isSelected():
79 return
80 directories = self.model.directories
81 directory_entries = self.model.directory_entries
82 if current < len(directories):
83 # ignore directories
84 return
85 idx = current - len(directories)
86 if idx >= len(self.model.subtree_sha1s):
87 return
88 objtype, sha1, name = self.model.subtree_node(idx)
89 curdir = self.model.directory
90 if curdir:
91 filename = os.path.join(curdir, name)
92 else:
93 filename = name
94 blame = git.blame(self.model.currentbranch, filename)
95 self.view.commit_text.setText(blame)
97 ######################################################################
98 # Actions
99 def action_directory_changed(self):
100 """Called in response to a change in the model's directory."""
101 self.model.init_browser_data()
102 self._display_items()
104 ######################################################################
105 # Qt callbacks
106 def item_changed(self,*rest):
107 """Called when the current item changes"""
108 current = self.view.commit_list.currentRow()
109 item = self.view.commit_list.item(current)
110 if item is None or not item.isSelected():
111 self.view.revision.setText('')
112 self.view.commit_text.setText('')
113 return
114 directories = self.model.directories
115 directory_entries = self.model.directory_entries
116 if current < len(directories):
117 # This is a directory...
118 self.filename = None
119 dirent = directories[current]
120 if dirent != '..':
121 # This is a real directory for which
122 # we have child entries
123 entries = directory_entries[dirent]
124 else:
125 # This is '..' which is a special case
126 # since it doesn't really exist
127 entries = []
128 self.view.commit_text.setText('\n'.join(entries))
129 self.view.revision.setText('')
130 else:
131 # This is a file entry. The current row is absolute,
132 # so get a relative index by subtracting the number
133 # of directory entries
134 idx = current - len(directories)
135 if idx >= len(self.model.subtree_sha1s):
136 # This can happen when changing directories
137 self.filename = None
138 return
139 objtype, sha1, name = self.model.subtree_node(idx)
141 curdir = self.model.directory
142 if curdir:
143 self.filename = os.path.join(curdir, name)
144 else:
145 self.filename = name
147 catguts = git.cat_file(objtype, sha1, with_raw_output=True)
148 self.view.commit_text.setText(catguts)
150 self.view.revision.setText(sha1)
151 self.view.revision.selectAll()
153 # Copy the sha1 into the clipboard
154 qtutils.set_clipboard(sha1)
156 # automatically called by qobserver
157 def commit_list_doubleclick(self,*rest):
159 Called when an entry is double-clicked.
161 This callback changes the model's directory when
162 invoked on a directory item. When invoked on a file
163 it allows the file to be saved.
166 current = self.view.commit_list.currentRow()
167 directories = self.model.directories
169 # A file item was double-clicked.
170 # Create a save-as dialog and export the file,
171 # or if in get_file mode, grab the filename and finish the dialog.
172 if current >= len(directories):
173 idx = current - len(directories)
175 objtype, sha1, name = self.model.subtree_node(idx)
177 if self.get_file:
178 if self.model.directory:
179 curdir = self.model.directory
180 self.filename = os.path.join(curdir, name)
181 else:
182 self.filename = name
183 self.view.accept()
184 return
186 nameguess = os.path.join(self.model.directory, name)
187 filename = qtutils.save_dialog(self.view, 'Save', nameguess)
188 if not filename:
189 return
190 self.model.set_directory(os.path.dirname(filename))
191 contents = git.cat_file(objtype, sha1, with_raw_output=True)
192 utils.write(filename, contents)
193 return
195 dirent = directories[current]
196 curdir = self.model.directory
198 # "change directories"
199 # '..' is a special case--it doesn't really exist...
200 if dirent == '..':
201 newdir = os.path.dirname(os.path.dirname(curdir))
202 if newdir == '':
203 self.model.set_directory(newdir)
204 else:
205 self.model.set_directory(newdir + os.sep)
206 else:
207 self.model.set_directory(curdir + dirent)
209 def _display_items(self):
211 Populate the commit_list with the current directories and items
213 Directories are always listed first.
217 # Clear commit/revision fields
218 self.view.commit_text.setText('')
219 self.view.revision.setText('')
221 dir_icon = resources.icon('dir.png')
222 file_icon = resources.icon('generic.png')
223 # Factory method for creating items
224 creator = qtutils.create_listwidget_item
226 # First the directories,
227 qtutils.set_items(self.view.commit_list,
228 map(lambda d: creator(d, dir_icon),
229 self.model.directories))
230 # and now the filenames
231 qtutils.add_items(self.view.commit_list,
232 map(lambda s: creator(s, file_icon),
233 self.model.subtree_names))