/git-cola-1.7.7/cola/cmds.py

# · Python · 921 lines · 840 code · 19 blank · 62 comment · 14 complexity · 94a72dadc03876f8e2ad53924fb15ff6 MD5 · raw file

  1. import os
  2. import sys
  3. import platform
  4. from fnmatch import fnmatch
  5. from cStringIO import StringIO
  6. import cola
  7. from cola import i18n
  8. from cola import core
  9. from cola import errors
  10. from cola import gitcfg
  11. from cola import gitcmds
  12. from cola import utils
  13. from cola import signals
  14. from cola import cmdfactory
  15. from cola import difftool
  16. from cola.diffparse import DiffParser
  17. from cola.models import selection
  18. _notifier = cola.notifier()
  19. _factory = cmdfactory.factory()
  20. _config = gitcfg.instance()
  21. class BaseCommand(object):
  22. """Base class for all commands; provides the command pattern"""
  23. def __init__(self):
  24. self.undoable = False
  25. def is_undoable(self):
  26. """Can this be undone?"""
  27. return self.undoable
  28. def name(self):
  29. """Return this command's name."""
  30. return self.__class__.__name__
  31. def do(self):
  32. raise NotImplementedError('%s.do() is unimplemented' % self.name())
  33. def undo(self):
  34. raise NotImplementedError('%s.undo() is unimplemented' % self.name())
  35. class Command(BaseCommand):
  36. """Base class for commands that modify the main model"""
  37. def __init__(self):
  38. """Initialize the command and stash away values for use in do()"""
  39. # These are commonly used so let's make it easier to write new commands.
  40. BaseCommand.__init__(self)
  41. self.model = cola.model()
  42. self.old_diff_text = self.model.diff_text
  43. self.old_filename = self.model.filename
  44. self.old_mode = self.model.mode
  45. self.old_head = self.model.head
  46. self.new_diff_text = self.old_diff_text
  47. self.new_filename = self.old_filename
  48. self.new_head = self.old_head
  49. self.new_mode = self.old_mode
  50. def do(self):
  51. """Perform the operation."""
  52. self.model.set_diff_text(self.new_diff_text)
  53. self.model.set_filename(self.new_filename)
  54. self.model.set_head(self.new_head)
  55. self.model.set_mode(self.new_mode)
  56. def undo(self):
  57. """Undo the operation."""
  58. self.model.set_diff_text(self.old_diff_text)
  59. self.model.set_filename(self.old_filename)
  60. self.model.set_head(self.old_head)
  61. self.model.set_mode(self.old_mode)
  62. class AmendMode(Command):
  63. """Try to amend a commit."""
  64. def __init__(self, amend):
  65. Command.__init__(self)
  66. self.undoable = True
  67. self.skip = False
  68. self.amending = amend
  69. self.old_commitmsg = self.model.commitmsg
  70. if self.amending:
  71. self.new_mode = self.model.mode_amend
  72. self.new_head = 'HEAD^'
  73. self.new_commitmsg = self.model.prev_commitmsg()
  74. return
  75. # else, amend unchecked, regular commit
  76. self.new_mode = self.model.mode_none
  77. self.new_head = 'HEAD'
  78. self.new_diff_text = ''
  79. self.new_commitmsg = self.model.commitmsg
  80. # If we're going back into new-commit-mode then search the
  81. # undo stack for a previous amend-commit-mode and grab the
  82. # commit message at that point in time.
  83. if not _factory.undostack:
  84. return
  85. undo_count = len(_factory.undostack)
  86. for i in xrange(undo_count):
  87. # Find the latest AmendMode command
  88. idx = undo_count - i - 1
  89. cmdobj = _factory.undostack[idx]
  90. if type(cmdobj) is not AmendMode:
  91. continue
  92. if cmdobj.amending:
  93. self.new_commitmsg = cmdobj.old_commitmsg
  94. break
  95. def do(self):
  96. """Leave/enter amend mode."""
  97. """Attempt to enter amend mode. Do not allow this when merging."""
  98. if self.amending:
  99. if os.path.exists(self.model.git.git_path('MERGE_HEAD')):
  100. self.skip = True
  101. _notifier.broadcast(signals.amend, False)
  102. _factory.prompt_user(signals.information,
  103. 'Oops! Unmerged',
  104. 'You are in the middle of a merge.\n'
  105. 'You cannot amend while merging.')
  106. return
  107. self.skip = False
  108. _notifier.broadcast(signals.amend, self.amending)
  109. self.model.set_commitmsg(self.new_commitmsg)
  110. Command.do(self)
  111. self.model.update_file_status()
  112. def undo(self):
  113. if self.skip:
  114. return
  115. self.model.set_commitmsg(self.old_commitmsg)
  116. Command.undo(self)
  117. self.model.update_file_status()
  118. class ApplyDiffSelection(Command):
  119. def __init__(self, staged, selected, offset, selection, apply_to_worktree):
  120. Command.__init__(self)
  121. self.staged = staged
  122. self.selected = selected
  123. self.offset = offset
  124. self.selection = selection
  125. self.apply_to_worktree = apply_to_worktree
  126. def do(self):
  127. # The normal worktree vs index scenario
  128. parser = DiffParser(self.model,
  129. filename=self.model.filename,
  130. cached=self.staged,
  131. reverse=self.apply_to_worktree)
  132. status, output = \
  133. parser.process_diff_selection(self.selected,
  134. self.offset,
  135. self.selection,
  136. apply_to_worktree=self.apply_to_worktree)
  137. _notifier.broadcast(signals.log_cmd, status, output)
  138. # Redo the diff to show changes
  139. if self.staged:
  140. diffcmd = DiffStaged([self.model.filename])
  141. else:
  142. diffcmd = Diff([self.model.filename])
  143. diffcmd.do()
  144. self.model.update_file_status()
  145. class ApplyPatches(Command):
  146. def __init__(self, patches):
  147. Command.__init__(self)
  148. patches.sort()
  149. self.patches = patches
  150. def do(self):
  151. diff_text = ''
  152. num_patches = len(self.patches)
  153. orig_head = self.model.git.rev_parse('HEAD')
  154. for idx, patch in enumerate(self.patches):
  155. status, output = self.model.git.am(patch,
  156. with_status=True,
  157. with_stderr=True)
  158. # Log the git-am command
  159. _notifier.broadcast(signals.log_cmd, status, output)
  160. if num_patches > 1:
  161. diff = self.model.git.diff('HEAD^!', stat=True)
  162. diff_text += 'Patch %d/%d - ' % (idx+1, num_patches)
  163. diff_text += '%s:\n%s\n\n' % (os.path.basename(patch), diff)
  164. diff_text += 'Summary:\n'
  165. diff_text += self.model.git.diff(orig_head, stat=True)
  166. # Display a diffstat
  167. self.model.set_diff_text(diff_text)
  168. self.model.update_file_status()
  169. _factory.prompt_user(signals.information,
  170. 'Patch(es) Applied',
  171. '%d patch(es) applied:\n\n%s' %
  172. (len(self.patches),
  173. '\n'.join(map(os.path.basename, self.patches))))
  174. class Checkout(Command):
  175. """
  176. A command object for git-checkout.
  177. 'argv' is handed off directly to git.
  178. """
  179. def __init__(self, argv, checkout_branch=False):
  180. Command.__init__(self)
  181. self.argv = argv
  182. self.checkout_branch = checkout_branch
  183. self.new_diff_text = ''
  184. def do(self):
  185. status, output = self.model.git.checkout(with_stderr=True,
  186. with_status=True, *self.argv)
  187. _notifier.broadcast(signals.log_cmd, status, output)
  188. if self.checkout_branch:
  189. self.model.update_status()
  190. else:
  191. self.model.update_file_status()
  192. class CheckoutBranch(Checkout):
  193. """Checkout a branch."""
  194. def __init__(self, branch, checkout_branch=True):
  195. Checkout.__init__(self, [branch])
  196. class CherryPick(Command):
  197. """Cherry pick commits into the current branch."""
  198. def __init__(self, commits):
  199. Command.__init__(self)
  200. self.commits = commits
  201. def do(self):
  202. self.model.cherry_pick_list(self.commits)
  203. self.model.update_file_status()
  204. class ResetMode(Command):
  205. """Reset the mode and clear the model's diff text."""
  206. def __init__(self):
  207. Command.__init__(self)
  208. self.new_mode = self.model.mode_none
  209. self.new_head = 'HEAD'
  210. self.new_diff_text = ''
  211. def do(self):
  212. Command.do(self)
  213. self.model.update_file_status()
  214. class Commit(ResetMode):
  215. """Attempt to create a new commit."""
  216. def __init__(self, amend, msg):
  217. ResetMode.__init__(self)
  218. self.amend = amend
  219. self.msg = core.encode(msg)
  220. self.old_commitmsg = self.model.commitmsg
  221. self.new_commitmsg = ''
  222. def do(self):
  223. tmpfile = utils.tmp_filename('commit-message')
  224. status, output = self.model.commit_with_msg(self.msg, tmpfile, amend=self.amend)
  225. if status == 0:
  226. ResetMode.do(self)
  227. self.model.set_commitmsg(self.new_commitmsg)
  228. title = 'Commit: '
  229. else:
  230. title = 'Commit failed: '
  231. _notifier.broadcast(signals.log_cmd, status, title+output)
  232. class Ignore(Command):
  233. """Add files to .gitignore"""
  234. def __init__(self, filenames):
  235. Command.__init__(self)
  236. self.filenames = filenames
  237. def do(self):
  238. new_additions = ''
  239. for fname in self.filenames:
  240. new_additions = new_additions + fname + '\n'
  241. for_status = new_additions
  242. if new_additions:
  243. if os.path.exists('.gitignore'):
  244. current_list = utils.slurp('.gitignore')
  245. new_additions = new_additions + current_list
  246. utils.write('.gitignore', new_additions)
  247. _notifier.broadcast(signals.log_cmd,
  248. 0,
  249. 'Added to .gitignore:\n%s' % for_status)
  250. self.model.update_file_status()
  251. class Delete(Command):
  252. """Simply delete files."""
  253. def __init__(self, filenames):
  254. Command.__init__(self)
  255. self.filenames = filenames
  256. # We could git-hash-object stuff and provide undo-ability
  257. # as an option. Heh.
  258. def do(self):
  259. rescan = False
  260. for filename in self.filenames:
  261. if filename:
  262. try:
  263. os.remove(filename)
  264. rescan=True
  265. except:
  266. _factory.prompt_user(signals.information,
  267. 'Error'
  268. 'Deleting "%s" failed.' % filename)
  269. if rescan:
  270. self.model.update_file_status()
  271. class DeleteBranch(Command):
  272. """Delete a git branch."""
  273. def __init__(self, branch):
  274. Command.__init__(self)
  275. self.branch = branch
  276. def do(self):
  277. status, output = self.model.delete_branch(self.branch)
  278. title = ''
  279. if output.startswith('error:'):
  280. output = 'E' + output[1:]
  281. else:
  282. title = 'Info: '
  283. _notifier.broadcast(signals.log_cmd, status, title + output)
  284. class Diff(Command):
  285. """Perform a diff and set the model's current text."""
  286. def __init__(self, filenames, cached=False):
  287. Command.__init__(self)
  288. # Guard against the list of files being empty
  289. if not filenames:
  290. return
  291. opts = {}
  292. if cached:
  293. opts['ref'] = self.model.head
  294. self.new_filename = filenames[0]
  295. self.old_filename = self.model.filename
  296. self.new_mode = self.model.mode_worktree
  297. self.new_diff_text = gitcmds.diff_helper(filename=self.new_filename,
  298. cached=cached, **opts)
  299. class Diffstat(Command):
  300. """Perform a diffstat and set the model's diff text."""
  301. def __init__(self):
  302. Command.__init__(self)
  303. diff = self.model.git.diff(self.model.head,
  304. unified=_config.get('diff.context', 3),
  305. no_color=True,
  306. M=True,
  307. stat=True)
  308. self.new_diff_text = core.decode(diff)
  309. self.new_mode = self.model.mode_worktree
  310. class DiffStaged(Diff):
  311. """Perform a staged diff on a file."""
  312. def __init__(self, filenames):
  313. Diff.__init__(self, filenames, cached=True)
  314. self.new_mode = self.model.mode_index
  315. class DiffStagedSummary(Command):
  316. def __init__(self):
  317. Command.__init__(self)
  318. diff = self.model.git.diff(self.model.head,
  319. cached=True,
  320. no_color=True,
  321. patch_with_stat=True,
  322. M=True)
  323. self.new_diff_text = core.decode(diff)
  324. self.new_mode = self.model.mode_index
  325. class Difftool(Command):
  326. """Run git-difftool limited by path."""
  327. def __init__(self, staged, filenames):
  328. Command.__init__(self)
  329. self.staged = staged
  330. self.filenames = filenames
  331. def do(self):
  332. if not self.filenames:
  333. return
  334. args = []
  335. if self.staged:
  336. args.append('--cached')
  337. if self.model.head != 'HEAD':
  338. args.append(self.model.head)
  339. args.append('--')
  340. args.extend(self.filenames)
  341. difftool.launch(args)
  342. class Edit(Command):
  343. """Edit a file using the configured gui.editor."""
  344. def __init__(self, filenames, line_number=None):
  345. Command.__init__(self)
  346. self.filenames = filenames
  347. self.line_number = line_number
  348. def do(self):
  349. filename = self.filenames[0]
  350. if not os.path.exists(filename):
  351. return
  352. editor = self.model.editor()
  353. opts = []
  354. if self.line_number is None:
  355. opts = self.filenames
  356. else:
  357. # Single-file w/ line-numbers (likely from grep)
  358. editor_opts = {
  359. '*vim*': ['+'+self.line_number, filename],
  360. '*emacs*': ['+'+self.line_number, filename],
  361. '*textpad*': ['%s(%s,0)' % (filename, self.line_number)],
  362. '*notepad++*': ['-n'+self.line_number, filename],
  363. }
  364. opts = self.filenames
  365. for pattern, opt in editor_opts.items():
  366. if fnmatch(editor, pattern):
  367. opts = opt
  368. break
  369. utils.fork(utils.shell_split(editor) + opts)
  370. class FormatPatch(Command):
  371. """Output a patch series given all revisions and a selected subset."""
  372. def __init__(self, to_export, revs):
  373. Command.__init__(self)
  374. self.to_export = to_export
  375. self.revs = revs
  376. def do(self):
  377. status, output = gitcmds.format_patchsets(self.to_export, self.revs)
  378. _notifier.broadcast(signals.log_cmd, status, output)
  379. class LoadCommitMessage(Command):
  380. """Loads a commit message from a path."""
  381. def __init__(self, path):
  382. Command.__init__(self)
  383. self.undoable = True
  384. self.path = path
  385. self.old_commitmsg = self.model.commitmsg
  386. self.old_directory = self.model.directory
  387. def do(self):
  388. path = self.path
  389. if not path or not os.path.isfile(path):
  390. raise errors.UsageError('Error: cannot find commit template',
  391. '%s: No such file or directory.' % path)
  392. self.model.set_directory(os.path.dirname(path))
  393. self.model.set_commitmsg(utils.slurp(path))
  394. def undo(self):
  395. self.model.set_commitmsg(self.old_commitmsg)
  396. self.model.set_directory(self.old_directory)
  397. class LoadCommitTemplate(LoadCommitMessage):
  398. """Loads the commit message template specified by commit.template."""
  399. def __init__(self):
  400. LoadCommitMessage.__init__(self, _config.get('commit.template'))
  401. def do(self):
  402. if self.path is None:
  403. raise errors.UsageError('Error: unconfigured commit template',
  404. 'A commit template has not been configured.\n'
  405. 'Use "git config" to define "commit.template"\n'
  406. 'so that it points to a commit template.')
  407. return LoadCommitMessage.do(self)
  408. class LoadPreviousMessage(Command):
  409. """Try to amend a commit."""
  410. def __init__(self, sha1):
  411. Command.__init__(self)
  412. self.sha1 = sha1
  413. self.old_commitmsg = self.model.commitmsg
  414. self.new_commitmsg = self.model.prev_commitmsg(sha1)
  415. self.undoable = True
  416. def do(self):
  417. self.model.set_commitmsg(self.new_commitmsg)
  418. def undo(self):
  419. self.model.set_commitmsg(self.old_commitmsg)
  420. class Mergetool(Command):
  421. """Launch git-mergetool on a list of paths."""
  422. def __init__(self, paths):
  423. Command.__init__(self)
  424. self.paths = paths
  425. def do(self):
  426. if not self.paths:
  427. return
  428. utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
  429. class OpenRepo(Command):
  430. """Launches git-cola on a repo."""
  431. def __init__(self, dirname):
  432. Command.__init__(self)
  433. self.new_directory = utils.quote_repopath(dirname)
  434. def do(self):
  435. self.model.set_directory(self.new_directory)
  436. utils.fork([sys.executable, sys.argv[0], '--repo', self.new_directory])
  437. class Clone(Command):
  438. """Clones a repository and optionally spawns a new cola session."""
  439. def __init__(self, url, destdir, spawn=True):
  440. Command.__init__(self)
  441. self.url = url
  442. self.new_directory = utils.quote_repopath(destdir)
  443. self.spawn = spawn
  444. def do(self):
  445. self.model.git.clone(self.url, self.new_directory,
  446. with_stderr=True, with_status=True)
  447. if self.spawn:
  448. utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
  449. rescan = 'rescan'
  450. class Rescan(Command):
  451. """Rescans for changes."""
  452. def do(self):
  453. self.model.update_status()
  454. rescan_and_refresh = 'rescan_and_refresh'
  455. class RescanAndRefresh(Command):
  456. """Rescans for changes."""
  457. def do(self):
  458. self.model.update_status(update_index=True)
  459. class RunConfigAction(Command):
  460. """Run a user-configured action, typically from the "Tools" menu"""
  461. def __init__(self, name):
  462. Command.__init__(self)
  463. self.name = name
  464. self.model = cola.model()
  465. def do(self):
  466. for env in ('FILENAME', 'REVISION', 'ARGS'):
  467. try:
  468. del os.environ[env]
  469. except KeyError:
  470. pass
  471. rev = None
  472. args = None
  473. opts = _config.get_guitool_opts(self.name)
  474. cmd = opts.get('cmd')
  475. if 'title' not in opts:
  476. opts['title'] = cmd
  477. if 'prompt' not in opts or opts.get('prompt') is True:
  478. prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
  479. opts['prompt'] = prompt
  480. if opts.get('needsfile'):
  481. filename = selection.filename()
  482. if not filename:
  483. _factory.prompt_user(signals.information,
  484. 'Please select a file',
  485. '"%s" requires a selected file' % cmd)
  486. return
  487. os.environ['FILENAME'] = filename
  488. if opts.get('revprompt') or opts.get('argprompt'):
  489. while True:
  490. ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
  491. if not ok:
  492. return
  493. rev = opts.get('revision')
  494. args = opts.get('args')
  495. if opts.get('revprompt') and not rev:
  496. title = 'Invalid Revision'
  497. msg = 'The revision expression cannot be empty.'
  498. _factory.prompt_user(signals.critical, title, msg)
  499. continue
  500. break
  501. elif opts.get('confirm'):
  502. title = os.path.expandvars(opts.get('title'))
  503. prompt = os.path.expandvars(opts.get('prompt'))
  504. if not _factory.prompt_user(signals.question, title, prompt):
  505. return
  506. if rev:
  507. os.environ['REVISION'] = rev
  508. if args:
  509. os.environ['ARGS'] = args
  510. title = os.path.expandvars(cmd)
  511. _notifier.broadcast(signals.log_cmd, 0, 'running: ' + title)
  512. cmd = ['sh', '-c', cmd]
  513. if opts.get('noconsole'):
  514. status, out, err = utils.run_command(cmd, flag_error=False)
  515. else:
  516. status, out, err = _factory.prompt_user(signals.run_command,
  517. title, cmd)
  518. _notifier.broadcast(signals.log_cmd, status,
  519. 'stdout: %s\nstatus: %s\nstderr: %s' %
  520. (out.rstrip(), status, err.rstrip()))
  521. if not opts.get('norescan'):
  522. self.model.update_status()
  523. return status
  524. class SetDiffText(Command):
  525. def __init__(self, text):
  526. Command.__init__(self)
  527. self.undoable = True
  528. self.new_diff_text = text
  529. class ShowUntracked(Command):
  530. """Show an untracked file."""
  531. # We don't actually do anything other than set the mode right now.
  532. # TODO check the mimetype for the file and handle things
  533. # generically.
  534. def __init__(self, filenames):
  535. Command.__init__(self)
  536. self.new_mode = self.model.mode_untracked
  537. # TODO new_diff_text = utils.file_preview(filenames[0])
  538. class SignOff(Command):
  539. def __init__(self):
  540. Command.__init__(self)
  541. self.undoable = True
  542. self.old_commitmsg = self.model.commitmsg
  543. def do(self):
  544. signoff = self.signoff()
  545. if signoff in self.model.commitmsg:
  546. return
  547. self.model.set_commitmsg(self.model.commitmsg + '\n' + signoff)
  548. def undo(self):
  549. self.model.set_commitmsg(self.old_commitmsg)
  550. def signoff(self):
  551. try:
  552. import pwd
  553. user = pwd.getpwuid(os.getuid()).pw_name
  554. except ImportError:
  555. user = os.getenv('USER', 'unknown')
  556. name = _config.get('user.name', user)
  557. email = _config.get('user.email', '%s@%s' % (user, platform.node()))
  558. return '\nSigned-off-by: %s <%s>' % (name, email)
  559. class Stage(Command):
  560. """Stage a set of paths."""
  561. def __init__(self, paths):
  562. Command.__init__(self)
  563. self.paths = paths
  564. def do(self):
  565. msg = 'Staging: %s' % (', '.join(self.paths))
  566. _notifier.broadcast(signals.log_cmd, 0, msg)
  567. self.model.stage_paths(self.paths)
  568. class StageModified(Stage):
  569. """Stage all modified files."""
  570. def __init__(self):
  571. Stage.__init__(self, None)
  572. self.paths = self.model.modified
  573. class StageUnmerged(Stage):
  574. """Stage all modified files."""
  575. def __init__(self):
  576. Stage.__init__(self, None)
  577. self.paths = self.model.unmerged
  578. class StageUntracked(Stage):
  579. """Stage all untracked files."""
  580. def __init__(self):
  581. Stage.__init__(self, None)
  582. self.paths = self.model.untracked
  583. class Tag(Command):
  584. """Create a tag object."""
  585. def __init__(self, name, revision, sign=False, message=''):
  586. Command.__init__(self)
  587. self._name = name
  588. self._message = core.encode(message)
  589. self._revision = revision
  590. self._sign = sign
  591. def do(self):
  592. log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
  593. opts = {}
  594. if self._message:
  595. opts['F'] = utils.tmp_filename('tag-message')
  596. utils.write(opts['F'], self._message)
  597. if self._sign:
  598. log_msg += ', GPG-signed'
  599. opts['s'] = True
  600. status, output = self.model.git.tag(self._name,
  601. self._revision,
  602. with_status=True,
  603. with_stderr=True,
  604. **opts)
  605. else:
  606. opts['a'] = bool(self._message)
  607. status, output = self.model.git.tag(self._name,
  608. self._revision,
  609. with_status=True,
  610. with_stderr=True,
  611. **opts)
  612. if 'F' in opts:
  613. os.unlink(opts['F'])
  614. if output:
  615. log_msg += '\nOutput:\n%s' % output
  616. _notifier.broadcast(signals.log_cmd, status, log_msg)
  617. if status == 0:
  618. self.model.update_status()
  619. class Unstage(Command):
  620. """Unstage a set of paths."""
  621. def __init__(self, paths):
  622. Command.__init__(self)
  623. self.paths = paths
  624. def do(self):
  625. msg = 'Unstaging: %s' % (', '.join(self.paths))
  626. _notifier.broadcast(signals.log_cmd, 0, msg)
  627. self.model.unstage_paths(self.paths)
  628. class UnstageAll(Command):
  629. """Unstage all files; resets the index."""
  630. def do(self):
  631. self.model.unstage_all()
  632. class UnstageSelected(Unstage):
  633. """Unstage selected files."""
  634. def __init__(self):
  635. Unstage.__init__(self, cola.selection_model().staged)
  636. class Untrack(Command):
  637. """Unstage a set of paths."""
  638. def __init__(self, paths):
  639. Command.__init__(self)
  640. self.paths = paths
  641. def do(self):
  642. msg = 'Untracking: %s' % (', '.join(self.paths))
  643. _notifier.broadcast(signals.log_cmd, 0, msg)
  644. status, out = self.model.untrack_paths(self.paths)
  645. _notifier.broadcast(signals.log_cmd, status, out)
  646. class UntrackedSummary(Command):
  647. """List possible .gitignore rules as the diff text."""
  648. def __init__(self):
  649. Command.__init__(self)
  650. untracked = self.model.untracked
  651. suffix = len(untracked) > 1 and 's' or ''
  652. io = StringIO()
  653. io.write('# %s untracked file%s\n' % (len(untracked), suffix))
  654. if untracked:
  655. io.write('# possible .gitignore rule%s:\n' % suffix)
  656. for u in untracked:
  657. io.write('/'+core.encode(u))
  658. self.new_diff_text = core.decode(io.getvalue())
  659. self.new_mode = self.model.mode_untracked
  660. class UpdateFileStatus(Command):
  661. """Rescans for changes."""
  662. def do(self):
  663. self.model.update_file_status()
  664. class VisualizeAll(Command):
  665. """Visualize all branches."""
  666. def do(self):
  667. browser = utils.shell_split(self.model.history_browser())
  668. utils.fork(browser + ['--all'])
  669. class VisualizeCurrent(Command):
  670. """Visualize all branches."""
  671. def do(self):
  672. browser = utils.shell_split(self.model.history_browser())
  673. utils.fork(browser + [self.model.currentbranch])
  674. class VisualizePaths(Command):
  675. """Path-limited visualization."""
  676. def __init__(self, paths):
  677. Command.__init__(self)
  678. browser = utils.shell_split(self.model.history_browser())
  679. if paths:
  680. self.argv = browser + paths
  681. else:
  682. self.argv = browser
  683. def do(self):
  684. utils.fork(self.argv)
  685. visualize_revision = 'visualize_revision'
  686. class VisualizeRevision(Command):
  687. """Visualize a specific revision."""
  688. def __init__(self, revision, paths=None):
  689. Command.__init__(self)
  690. self.revision = revision
  691. self.paths = paths
  692. def do(self):
  693. argv = utils.shell_split(self.model.history_browser())
  694. if self.revision:
  695. argv.append(self.revision)
  696. if self.paths:
  697. argv.append('--')
  698. argv.extend(self.paths)
  699. utils.fork(argv)
  700. def register():
  701. """
  702. Register signal mappings with the factory.
  703. These commands are automatically created and run when
  704. their corresponding signal is broadcast by the notifier.
  705. """
  706. signal_to_command_map = {
  707. signals.amend_mode: AmendMode,
  708. signals.apply_diff_selection: ApplyDiffSelection,
  709. signals.apply_patches: ApplyPatches,
  710. signals.clone: Clone,
  711. signals.checkout: Checkout,
  712. signals.checkout_branch: CheckoutBranch,
  713. signals.cherry_pick: CherryPick,
  714. signals.commit: Commit,
  715. signals.delete: Delete,
  716. signals.delete_branch: DeleteBranch,
  717. signals.diff: Diff,
  718. signals.diff_staged: DiffStaged,
  719. signals.diffstat: Diffstat,
  720. signals.difftool: Difftool,
  721. signals.edit: Edit,
  722. signals.format_patch: FormatPatch,
  723. signals.ignore: Ignore,
  724. signals.load_commit_message: LoadCommitMessage,
  725. signals.load_commit_template: LoadCommitTemplate,
  726. signals.load_previous_message: LoadPreviousMessage,
  727. signals.modified_summary: Diffstat,
  728. signals.mergetool: Mergetool,
  729. signals.open_repo: OpenRepo,
  730. signals.rescan: Rescan,
  731. signals.rescan_and_refresh: RescanAndRefresh,
  732. signals.reset_mode: ResetMode,
  733. signals.run_config_action: RunConfigAction,
  734. signals.set_diff_text: SetDiffText,
  735. signals.show_untracked: ShowUntracked,
  736. signals.signoff: SignOff,
  737. signals.stage: Stage,
  738. signals.stage_modified: StageModified,
  739. signals.stage_unmerged: StageUnmerged,
  740. signals.stage_untracked: StageUntracked,
  741. signals.staged_summary: DiffStagedSummary,
  742. signals.tag: Tag,
  743. signals.untrack: Untrack,
  744. signals.unstage: Unstage,
  745. signals.unstage_all: UnstageAll,
  746. signals.unstage_selected: UnstageSelected,
  747. signals.untracked_summary: UntrackedSummary,
  748. signals.update_file_status: UpdateFileStatus,
  749. signals.visualize_all: VisualizeAll,
  750. signals.visualize_current: VisualizeCurrent,
  751. signals.visualize_paths: VisualizePaths,
  752. signals.visualize_revision: VisualizeRevision,
  753. }
  754. for signal, cmd in signal_to_command_map.iteritems():
  755. _factory.add_global_command(signal, cmd)