core: make getcwd() fail-safe
[git-cola.git] / cola / widgets / patch.py
blobe4f49906de9b938ed11ba49237e160127375981f
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 = [p for p in paths
40 if core.isfile(p) and
41 (p.endswith('.patch') or p.endswith('.mbox'))]
42 dirs = [p for p in paths if core.isdir(p)]
43 dirs.sort()
44 for d in dirs:
45 patches.extend(get_patches_from_dir(d))
46 return patches
49 def get_patches_from_mimedata(mimedata):
50 urls = mimedata.urls()
51 if not urls:
52 return []
53 paths = [x.path() for x in urls]
54 return get_patches_from_paths(paths)
57 def get_patches_from_dir(path):
58 """Find patches in a subdirectory"""
59 patches = []
60 for root, _, files in core.walk(path):
61 for name in [f for f in files if f.endswith('.patch')]:
62 patches.append(core.decode(os.path.join(root, name)))
63 return patches
66 class ApplyPatches(Dialog):
68 def __init__(self, context, parent=None):
69 super(ApplyPatches, self).__init__(parent=parent)
70 self.context = context
71 self.setWindowTitle(N_('Apply Patches'))
72 self.setAcceptDrops(True)
73 if parent is not None:
74 self.setWindowModality(Qt.WindowModal)
76 self.curdir = core.getcwd()
77 self.inner_drag = False
79 self.usage = QtWidgets.QLabel()
80 self.usage.setText(N_("""
81 <p>
82 Drag and drop or use the <strong>Add</strong> button to add
83 patches to the list
84 </p>
85 """))
87 self.tree = PatchTreeWidget(parent=self)
88 self.tree.setHeaderHidden(True)
89 self.tree.itemSelectionChanged.connect(self._tree_selection_changed)
91 self.notifier = notifier = observable.Observable()
92 self.diffwidget = diff.DiffWidget(context, notifier, self,
93 is_commit=True)
95 self.add_button = qtutils.create_toolbutton(
96 text=N_('Add'), icon=icons.add(),
97 tooltip=N_('Add patches (+)'))
99 self.remove_button = qtutils.create_toolbutton(
100 text=N_('Remove'), icon=icons.remove(),
101 tooltip=N_('Remove selected (Delete)'))
103 self.apply_button = qtutils.create_button(
104 text=N_('Apply'), icon=icons.ok())
106 self.close_button = qtutils.close_button()
108 self.add_action = qtutils.add_action(
109 self, N_('Add'), self.add_files, hotkeys.ADD_ITEM)
111 self.remove_action = qtutils.add_action(
112 self, N_('Remove'), self.tree.remove_selected,
113 hotkeys.DELETE, hotkeys.BACKSPACE, hotkeys.REMOVE_ITEM)
115 self.top_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
116 self.add_button, self.remove_button,
117 qtutils.STRETCH, self.usage)
119 self.bottom_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
120 self.close_button, qtutils.STRETCH,
121 self.apply_button)
123 self.splitter = qtutils.splitter(Qt.Vertical,
124 self.tree, self.diffwidget)
126 self.main_layout = qtutils.vbox(defs.margin, defs.spacing,
127 self.top_layout, self.splitter,
128 self.bottom_layout)
129 self.setLayout(self.main_layout)
131 qtutils.connect_button(self.add_button, self.add_files)
132 qtutils.connect_button(self.remove_button, self.tree.remove_selected)
133 qtutils.connect_button(self.apply_button, self.apply_patches)
134 qtutils.connect_button(self.close_button, self.close)
136 self.init_state(None, self.resize, 666, 420)
138 def apply_patches(self):
139 items = self.tree.items()
140 if not items:
141 return
142 context = self.context
143 patches = [i.data(0, Qt.UserRole) for i in items]
144 cmds.do(cmds.ApplyPatches, context, patches)
145 self.accept()
147 def add_files(self):
148 files = qtutils.open_files(N_('Select patch file(s)...'),
149 directory=self.curdir,
150 filters='Patches (*.patch *.mbox)')
151 if not files:
152 return
153 self.curdir = os.path.dirname(files[0])
154 self.add_paths([core.relpath(f) for f in files])
156 def dragEnterEvent(self, event):
157 """Accepts drops if the mimedata contains patches"""
158 super(ApplyPatches, self).dragEnterEvent(event)
159 patches = get_patches_from_mimedata(event.mimeData())
160 if patches:
161 event.acceptProposedAction()
163 def dropEvent(self, event):
164 """Add dropped patches"""
165 event.accept()
166 patches = get_patches_from_mimedata(event.mimeData())
167 if not patches:
168 return
169 self.add_paths(patches)
171 def add_paths(self, paths):
172 self.tree.add_paths(paths)
174 def _tree_selection_changed(self):
175 items = self.tree.selected_items()
176 if not items:
177 return
178 item = items[-1] # take the last item
179 path = item.data(0, Qt.UserRole)
180 if not core.exists(path):
181 return
182 commit = parse_patch(path)
183 self.diffwidget.set_details(commit.oid, commit.author, commit.email,
184 commit.date, commit.summary)
185 self.diffwidget.set_diff(commit.diff)
187 def export_state(self):
188 """Export persistent settings"""
189 state = super(ApplyPatches, self).export_state()
190 state['sizes'] = get(self.splitter)
191 return state
193 def apply_state(self, state):
194 """Apply persistent settings"""
195 result = super(ApplyPatches, self).apply_state(state)
196 try:
197 self.splitter.setSizes(state['sizes'])
198 except (AttributeError, KeyError, ValueError, TypeError):
199 pass
200 return result
203 class PatchTreeWidget(DraggableTreeWidget):
205 def add_paths(self, paths):
206 patches = get_patches_from_paths(paths)
207 if not patches:
208 return
209 items = []
210 icon = icons.file_text()
211 for patch in patches:
212 item = QtWidgets.QTreeWidgetItem()
213 flags = item.flags() & ~Qt.ItemIsDropEnabled
214 item.setFlags(flags)
215 item.setIcon(0, icon)
216 item.setText(0, os.path.basename(patch))
217 item.setData(0, Qt.UserRole, patch)
218 item.setToolTip(0, patch)
219 items.append(item)
220 self.addTopLevelItems(items)
222 def remove_selected(self):
223 idxs = self.selectedIndexes()
224 rows = [idx.row() for idx in idxs]
225 for row in reversed(sorted(rows)):
226 self.invisibleRootItem().takeChild(row)
229 class Commit(object):
230 """Container for commit details"""
232 def __init__(self):
233 self.content = ''
234 self.author = ''
235 self.email = ''
236 self.oid = ''
237 self.summary = ''
238 self.diff = ''
239 self.date = ''
242 def parse_patch(path):
243 content = core.read(path)
244 commit = Commit()
245 parse(content, commit)
246 return commit
249 def parse(content, commit):
250 """Parse commit details from a patch"""
251 from_rgx = re.compile(r'^From (?P<oid>[a-f0-9]{40}) .*$')
252 author_rgx = re.compile(r'^From: (?P<author>[^<]+) <(?P<email>[^>]+)>$')
253 date_rgx = re.compile(r'^Date: (?P<date>.*)$')
254 subject_rgx = re.compile(r'^Subject: (?P<summary>.*)$')
256 commit.content = content
258 lines = content.splitlines()
259 for idx, line in enumerate(lines):
260 match = from_rgx.match(line)
261 if match:
262 commit.oid = match.group('oid')
263 continue
265 match = author_rgx.match(line)
266 if match:
267 commit.author = match.group('author')
268 commit.email = match.group('email')
269 continue
271 match = date_rgx.match(line)
272 if match:
273 commit.date = match.group('date')
274 continue
276 match = subject_rgx.match(line)
277 if match:
278 commit.summary = match.group('summary')
279 commit.diff = '\n'.join(lines[idx + 1:])
280 break