/tortoisehg/hgtk/thgimport.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 362 lines · 276 code · 60 blank · 26 comment · 61 complexity · 4e245521791c3656ec90849034a3c64d MD5 · raw file

  1. # thgimport.py - TortoiseHg's dialog for (q)importing patches
  2. #
  3. # Copyright 2009 Yuki KODAMA <endflow.net@gmail.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import os
  8. import gtk
  9. import gobject
  10. import tempfile
  11. from tortoisehg.util.i18n import _
  12. from tortoisehg.hgtk import gtklib, gdialog, cslist
  13. COL_NAME = 0
  14. COL_LABEL = 1
  15. DEST_REPO = 'repo'
  16. DEST_MQ = 'mq'
  17. class ImportDialog(gdialog.GDialog):
  18. """ Dialog to import patches """
  19. def __init__(self, repo=None, dest=DEST_REPO, sources=None):
  20. gdialog.GDialog.__init__(self, resizable=True)
  21. self.done = False
  22. self.mqloaded = hasattr(self.repo, 'mq')
  23. self.clipboard = gtk.Clipboard()
  24. self.tempfiles = []
  25. self.initdest = dest
  26. if not self.mqloaded and dest == DEST_MQ:
  27. self.initdest = DEST_REPO
  28. self.initsrc = sources
  29. # persistent settings
  30. self.recent = self.settings.mrul('src_paths')
  31. ### Start of Overriding Section ###
  32. def get_title(self, reponame):
  33. return _('Import - %s') % reponame
  34. def get_icon(self):
  35. return 'menuimport.ico'
  36. def get_defsize(self):
  37. return (500, 390)
  38. def get_setting_name(self):
  39. return 'import'
  40. def get_body(self, vbox):
  41. # layout table
  42. self.table = table = gtklib.LayoutTable()
  43. vbox.pack_start(table, True, True, 2)
  44. ## source path combo & browse buttons
  45. self.src_list = gtk.ListStore(str)
  46. self.src_combo = gtk.ComboBoxEntry(self.src_list, 0)
  47. self.files_btn = gtk.Button(_('Browse...'))
  48. ## set given sources (but preview later)
  49. if self.initsrc:
  50. src = os.pathsep.join(self.initsrc)
  51. self.src_combo.child.set_text(src)
  52. ## other sources
  53. menubtn = gtk.ToggleButton()
  54. menubtn.set_focus_on_click(False)
  55. menubtn.add(gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE))
  56. m = gtklib.MenuBuilder()
  57. m.append(_('Browse Directory...'), self.dir_clicked,
  58. gtk.STOCK_DIRECTORY)
  59. m.append(_('Import from Clipboard'), self.clip_clicked,
  60. gtk.STOCK_PASTE)
  61. self.menu = m.build()
  62. table.add_row(_('Source:'), self.src_combo, 1,
  63. self.files_btn, menubtn, expand=0)
  64. self.p0check = gtk.CheckButton(_('Do not strip paths '
  65. '(-p0), required for SVN patches'))
  66. table.add_row(None, self.p0check, 1, expand=0)
  67. ## add MRU paths to source combo
  68. for path in self.recent:
  69. self.src_list.append([path])
  70. # copy form thgstrip.py
  71. def createlabel():
  72. label = gtk.Label()
  73. label.set_alignment(0, 0.5)
  74. label.set_size_request(-1, 25)
  75. label.size_request()
  76. return label
  77. ## info label
  78. self.infolbl = createlabel()
  79. self.infobox = gtk.HBox()
  80. self.infobox.pack_start(self.infolbl, False, False)
  81. table.add_row(_('Preview:'), self.infobox, padding=False)
  82. ## dest combo
  83. self.dest_model = gtk.ListStore(gobject.TYPE_STRING, # dest name
  84. gobject.TYPE_STRING) # dest label
  85. for row in {DEST_REPO: _('Repository'),
  86. DEST_MQ: _('Patch Queue')}.items():
  87. self.dest_model.append(row)
  88. self.dest_combo = gtk.ComboBox(self.dest_model)
  89. cell = gtk.CellRendererText()
  90. self.dest_combo.pack_start(cell, True)
  91. self.dest_combo.add_attribute(cell, 'text', COL_LABEL)
  92. for row in self.dest_model:
  93. name, label = self.dest_model[row.path]
  94. if name == self.initdest:
  95. self.dest_combo.set_active_iter(row.iter)
  96. break
  97. ## patch preview
  98. self.cslist = cslist.ChangesetList()
  99. table.add_row(None, self.cslist, padding=False,
  100. yopt=gtk.FILL|gtk.EXPAND)
  101. self.cslist.set_dnd_enable(True)
  102. self.cslist.set_checkbox_enable(True)
  103. # signal handlers
  104. self.files_btn.connect('clicked', self.files_clicked)
  105. self.cslist.connect('list-updated', self.list_updated)
  106. self.cslist.connect('files-dropped', self.files_dropped)
  107. self.src_combo.connect('changed', lambda e: self.preview(queue=True))
  108. menubtn.connect('button-press-event', self.extra_pressed)
  109. popdown = lambda m: menubtn.set_active(False)
  110. self.menu.connect('selection-done', popdown)
  111. self.menu.connect('cancel', popdown)
  112. # hook thg-close/thg-exit
  113. def hook(dialog, orig):
  114. self.unlink_all_tempfiles()
  115. return orig(dialog)
  116. self.connect('thg-close', hook, gtklib.thgclose)
  117. self.connect('thg-exit', hook, gtklib.thgexit)
  118. # prepare to show
  119. self.cslist.clear()
  120. def get_extras(self, vbox):
  121. # dest combo
  122. if self.mqloaded:
  123. self.dest_combo.show_all()
  124. self.dest_combo.hide()
  125. self.infobox.pack_start(self.dest_combo, False, False, 6)
  126. # start preview
  127. if self.initsrc:
  128. self.preview()
  129. def get_buttons(self):
  130. return [('import', _('Import'), gtk.RESPONSE_OK),
  131. ('close', gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)]
  132. def get_default_button(self):
  133. return 'import'
  134. def get_action_map(self):
  135. return {gtk.RESPONSE_OK: self.doimport}
  136. def switch_to(self, normal, working, cmd):
  137. self.table.set_sensitive(normal)
  138. self.buttons['import'].set_property('visible', normal)
  139. self.buttons['close'].set_property('visible', normal)
  140. if normal:
  141. self.buttons['close'].grab_focus()
  142. def command_done(self, returncode, useraborted, *args):
  143. self.done = True
  144. self.add_to_mru()
  145. if returncode == 0:
  146. self.cmd.set_result(_('Imported successfully'), style='ok')
  147. elif useraborted:
  148. self.cmd.set_result(_('Canceled importing'), style='error')
  149. else:
  150. self.cmd.set_result(_('Failed to import'), style='error')
  151. def before_close(self):
  152. if len(self.cslist.get_items()) != 0 and not self.done:
  153. ret = gdialog.Confirm(_('Confirm Close'), [], self,
  154. _('Do you want to close?')).run()
  155. if ret != gtk.RESPONSE_YES:
  156. return False # don't close
  157. self.unlink_all_tempfiles()
  158. return True
  159. ### End of Overriding Section ###
  160. def files_clicked(self, button):
  161. initdir = self.get_initial_dir()
  162. result = gtklib.NativeSaveFileDialogWrapper(title=_('Select Patches'),
  163. initial=initdir, open=True, multi=True).run()
  164. if result and result != initdir:
  165. if not isinstance(result, basestring):
  166. result = os.pathsep.join(result)
  167. self.src_combo.child.set_text(result)
  168. self.preview()
  169. def dir_clicked(self, menuitem):
  170. initdir = self.get_initial_dir()
  171. result = gtklib.NativeFolderSelectDialog(
  172. title=_('Select Directory contains patches:'),
  173. initial=initdir).run()
  174. if result and result != initdir:
  175. self.src_combo.child.set_text(result)
  176. self.preview()
  177. def clip_clicked(self, menuitem):
  178. text = self.clipboard.wait_for_text()
  179. if not text:
  180. return
  181. filepath = self.save_as_tempfile(text)
  182. cur_text = self.src_combo.child.get_text()
  183. self.src_combo.child.set_text(cur_text + os.pathsep + filepath)
  184. self.preview()
  185. def extra_pressed(self, button, event):
  186. if not button.get_active():
  187. button.set_active(True)
  188. self.menu.show_all()
  189. def pos(*args):
  190. x, y = button.window.get_origin()
  191. r = button.allocation
  192. return (x + r.x, y + r.y + r.height, True)
  193. self.menu.popup(None, None, pos, event.button, event.time)
  194. def list_updated(self, cslist, total, sel, *args):
  195. self.update_status(sel)
  196. def files_dropped(self, cslist, files, *args):
  197. src = self.src_combo.child.get_text()
  198. if src:
  199. files = [src] + files
  200. self.src_combo.child.set_text(os.pathsep.join(files))
  201. self.preview()
  202. def get_initial_dir(self):
  203. src = self.src_combo.child.get_text()
  204. if src and os.path.exists(src):
  205. if os.path.isdir(src):
  206. return src
  207. parent = os.path.dirname(src)
  208. if parent and os.path.exists(parent):
  209. return parent
  210. return None
  211. def add_to_mru(self):
  212. dirs = self.get_dirpaths()
  213. for dir in dirs:
  214. if dir.find(tempfile.gettempdir()) != -1:
  215. continue
  216. self.recent.add(dir)
  217. self.src_list.append([dir])
  218. self.settings.write()
  219. def save_as_tempfile(self, text):
  220. fd, filepath = tempfile.mkstemp(prefix='thg-patch-')
  221. os.write(fd, text)
  222. os.close(fd)
  223. self.tempfiles.append(filepath)
  224. return filepath
  225. def unlink_all_tempfiles(self):
  226. for path in self.tempfiles:
  227. os.unlink(path)
  228. def update_status(self, count):
  229. if count:
  230. inner = gtklib.markup(_('%s patches') % count, weight='bold')
  231. if self.mqloaded:
  232. info = _('%s will be imported to the') % inner
  233. else:
  234. info = _('%s will be imported to the repository') % inner
  235. else:
  236. info = gtklib.markup(_('Nothing to import'),
  237. weight='bold', color=gtklib.DRED)
  238. self.infolbl.set_markup(info)
  239. if self.mqloaded:
  240. self.dest_combo.set_property('visible', bool(count))
  241. self.buttons['import'].set_sensitive(bool(count))
  242. def get_filepaths(self):
  243. src = self.src_combo.child.get_text()
  244. if not src:
  245. return []
  246. files = []
  247. for path in src.split(os.pathsep):
  248. path = path.strip('\r\n\t ')
  249. if not os.path.exists(path) or path in files:
  250. continue
  251. if os.path.isdir(path):
  252. entries = os.listdir(path)
  253. for entry in entries:
  254. file = os.path.join(path, entry)
  255. if os.path.isfile(file):
  256. files.append(file)
  257. elif os.path.isfile(path):
  258. files.append(path)
  259. return files
  260. def get_dirpaths(self):
  261. dirs = []
  262. files = self.get_filepaths()
  263. for file in files:
  264. dir = os.path.dirname(file)
  265. if os.path.isdir(dir) and dir not in dirs:
  266. dirs.append(dir)
  267. return dirs
  268. def get_dest(self):
  269. iter = self.dest_combo.get_active_iter()
  270. return self.dest_model.get(iter, COL_NAME)[0]
  271. def preview(self, queue=False):
  272. files = self.get_filepaths()
  273. if files:
  274. self.cslist.update(files, self.repo, queue=queue)
  275. else:
  276. self.cslist.clear()
  277. def doimport(self):
  278. items = self.cslist.get_items(sel=True)
  279. files = [file for file, sel in items if sel]
  280. if not files:
  281. return
  282. dest = self.get_dest()
  283. if DEST_REPO == dest:
  284. cmd = 'import'
  285. elif DEST_MQ == dest:
  286. cmd = 'qimport'
  287. else:
  288. raise _('unexpected destination name: %s') % dest
  289. # prepare command line
  290. cmdline = ['hg', cmd, '--verbose', '--']
  291. cmdline.extend(files)
  292. if self.p0check.get_active():
  293. cmdline.insert(2, '-p0')
  294. # start importing
  295. self.execute_command(cmdline)
  296. def run(ui, *pats, **opts):
  297. dest = opts.get('mq', False) and DEST_MQ or DEST_REPO
  298. sources = []
  299. for item in pats:
  300. if os.path.exists(item):
  301. sources.append(os.path.abspath(item))
  302. return ImportDialog(dest=dest, sources=sources)