tests: use pytest fixtures in browse_model_test
[git-cola.git] / cola / widgets / patch.py
blob622ad8b2664fc1746c8a122b78541653abbb1caa
1 from __future__ import division, absolute_import, unicode_literals
2 import os
3 import re
5 from qtpy import QtWidgets
6 from qtpy.QtCore import Qt
8 from ..i18n import N_
9 from ..qtutils import get
10 from .. import core
11 from .. import cmds
12 from .. import hotkeys
13 from .. import observable
14 from .. import icons
15 from .. import qtutils
16 from .standard import Dialog
17 from .standard import DraggableTreeWidget
18 from . import defs
19 from . import diff
22 def apply_patches(context):
23 parent = qtutils.active_window()
24 dlg = new_apply_patches(context, parent=parent)
25 dlg.show()
26 dlg.raise_()
27 return dlg
30 def new_apply_patches(context, patches=None, parent=None):
31 dlg = ApplyPatches(context, parent=parent)
32 if patches:
33 dlg.add_paths(patches)
34 return dlg
37 def get_patches_from_paths(paths):
38 paths = [core.decode(p) for p in paths]
39 patches = [
41 for p in paths
42 if core.isfile(p) and (p.endswith('.patch') or p.endswith('.mbox'))
44 dirs = [p for p in paths if core.isdir(p)]
45 dirs.sort()
46 for d in dirs:
47 patches.extend(get_patches_from_dir(d))
48 return patches
51 def get_patches_from_mimedata(mimedata):
52 urls = mimedata.urls()
53 if not urls:
54 return []
55 paths = [x.path() for x in urls]
56 return get_patches_from_paths(paths)
59 def get_patches_from_dir(path):
60 """Find patches in a subdirectory"""
61 patches = []
62 for root, _, files in core.walk(path):
63 for name in [f for f in files if f.endswith('.patch')]:
64 patches.append(core.decode(os.path.join(root, name)))
65 return patches
68 class ApplyPatches(Dialog):
69 def __init__(self, context, parent=None):
70 super(ApplyPatches, self).__init__(parent=parent)
71 self.context = context
72 self.setWindowTitle(N_('Apply Patches'))
73 self.setAcceptDrops(True)
74 if parent is not None:
75 self.setWindowModality(Qt.WindowModal)
77 self.curdir = core.getcwd()
78 self.inner_drag = False
80 self.usage = QtWidgets.QLabel()
81 self.usage.setText(
82 N_(
83 """
84 <p>
85 Drag and drop or use the <strong>Add</strong> button to add
86 patches to the list
87 </p>
88 """
92 self.tree = PatchTreeWidget(parent=self)
93 self.tree.setHeaderHidden(True)
94 # pylint: disable=no-member
95 self.tree.itemSelectionChanged.connect(self._tree_selection_changed)
97 self.notifier = notifier = observable.Observable()
98 self.diffwidget = diff.DiffWidget(context, notifier, self, is_commit=True)
100 self.add_button = qtutils.create_toolbutton(
101 text=N_('Add'), icon=icons.add(), tooltip=N_('Add patches (+)')
104 self.remove_button = qtutils.create_toolbutton(
105 text=N_('Remove'),
106 icon=icons.remove(),
107 tooltip=N_('Remove selected (Delete)'),
110 self.apply_button = qtutils.create_button(text=N_('Apply'), icon=icons.ok())
112 self.close_button = qtutils.close_button()
114 self.add_action = qtutils.add_action(
115 self, N_('Add'), self.add_files, hotkeys.ADD_ITEM
118 self.remove_action = qtutils.add_action(
119 self,
120 N_('Remove'),
121 self.tree.remove_selected,
122 hotkeys.DELETE,
123 hotkeys.BACKSPACE,
124 hotkeys.REMOVE_ITEM,
127 self.top_layout = qtutils.hbox(
128 defs.no_margin,
129 defs.button_spacing,
130 self.add_button,
131 self.remove_button,
132 qtutils.STRETCH,
133 self.usage,
136 self.bottom_layout = qtutils.hbox(
137 defs.no_margin,
138 defs.button_spacing,
139 self.close_button,
140 qtutils.STRETCH,
141 self.apply_button,
144 self.splitter = qtutils.splitter(Qt.Vertical, self.tree, self.diffwidget)
146 self.main_layout = qtutils.vbox(
147 defs.margin,
148 defs.spacing,
149 self.top_layout,
150 self.splitter,
151 self.bottom_layout,
153 self.setLayout(self.main_layout)
155 qtutils.connect_button(self.add_button, self.add_files)
156 qtutils.connect_button(self.remove_button, self.tree.remove_selected)
157 qtutils.connect_button(self.apply_button, self.apply_patches)
158 qtutils.connect_button(self.close_button, self.close)
160 self.init_state(None, self.resize, 666, 420)
162 def apply_patches(self):
163 items = self.tree.items()
164 if not items:
165 return
166 context = self.context
167 patches = [i.data(0, Qt.UserRole) for i in items]
168 cmds.do(cmds.ApplyPatches, context, patches)
169 self.accept()
171 def add_files(self):
172 files = qtutils.open_files(
173 N_('Select patch file(s)...'),
174 directory=self.curdir,
175 filters='Patches (*.patch *.mbox)',
177 if not files:
178 return
179 self.curdir = os.path.dirname(files[0])
180 self.add_paths([core.relpath(f) for f in files])
182 def dragEnterEvent(self, event):
183 """Accepts drops if the mimedata contains patches"""
184 super(ApplyPatches, self).dragEnterEvent(event)
185 patches = get_patches_from_mimedata(event.mimeData())
186 if patches:
187 event.acceptProposedAction()
189 def dropEvent(self, event):
190 """Add dropped patches"""
191 event.accept()
192 patches = get_patches_from_mimedata(event.mimeData())
193 if not patches:
194 return
195 self.add_paths(patches)
197 def add_paths(self, paths):
198 self.tree.add_paths(paths)
200 def _tree_selection_changed(self):
201 items = self.tree.selected_items()
202 if not items:
203 return
204 item = items[-1] # take the last item
205 path = item.data(0, Qt.UserRole)
206 if not core.exists(path):
207 return
208 commit = parse_patch(path)
209 self.diffwidget.set_details(
210 commit.oid, commit.author, commit.email, commit.date, commit.summary
212 self.diffwidget.set_diff(commit.diff)
214 def export_state(self):
215 """Export persistent settings"""
216 state = super(ApplyPatches, self).export_state()
217 state['sizes'] = get(self.splitter)
218 return state
220 def apply_state(self, state):
221 """Apply persistent settings"""
222 result = super(ApplyPatches, self).apply_state(state)
223 try:
224 self.splitter.setSizes(state['sizes'])
225 except (AttributeError, KeyError, ValueError, TypeError):
226 pass
227 return result
230 # pylint: disable=too-many-ancestors
231 class PatchTreeWidget(DraggableTreeWidget):
232 def add_paths(self, paths):
233 patches = get_patches_from_paths(paths)
234 if not patches:
235 return
236 items = []
237 icon = icons.file_text()
238 for patch in patches:
239 item = QtWidgets.QTreeWidgetItem()
240 flags = item.flags() & ~Qt.ItemIsDropEnabled
241 item.setFlags(flags)
242 item.setIcon(0, icon)
243 item.setText(0, os.path.basename(patch))
244 item.setData(0, Qt.UserRole, patch)
245 item.setToolTip(0, patch)
246 items.append(item)
247 self.addTopLevelItems(items)
249 def remove_selected(self):
250 idxs = self.selectedIndexes()
251 rows = [idx.row() for idx in idxs]
252 for row in reversed(sorted(rows)):
253 self.invisibleRootItem().takeChild(row)
256 class Commit(object):
257 """Container for commit details"""
259 def __init__(self):
260 self.content = ''
261 self.author = ''
262 self.email = ''
263 self.oid = ''
264 self.summary = ''
265 self.diff = ''
266 self.date = ''
269 def parse_patch(path):
270 content = core.read(path)
271 commit = Commit()
272 parse(content, commit)
273 return commit
276 def parse(content, commit):
277 """Parse commit details from a patch"""
278 from_rgx = re.compile(r'^From (?P<oid>[a-f0-9]{40}) .*$')
279 author_rgx = re.compile(r'^From: (?P<author>[^<]+) <(?P<email>[^>]+)>$')
280 date_rgx = re.compile(r'^Date: (?P<date>.*)$')
281 subject_rgx = re.compile(r'^Subject: (?P<summary>.*)$')
283 commit.content = content
285 lines = content.splitlines()
286 for idx, line in enumerate(lines):
287 match = from_rgx.match(line)
288 if match:
289 commit.oid = match.group('oid')
290 continue
292 match = author_rgx.match(line)
293 if match:
294 commit.author = match.group('author')
295 commit.email = match.group('email')
296 continue
298 match = date_rgx.match(line)
299 if match:
300 commit.date = match.group('date')
301 continue
303 match = subject_rgx.match(line)
304 if match:
305 commit.summary = match.group('summary')
306 commit.diff = '\n'.join(lines[idx + 1 :])
307 break