1 from __future__
import division
, absolute_import
, unicode_literals
6 from PyQt4
import QtCore
7 from PyQt4
import QtGui
8 from PyQt4
.QtCore
import SIGNAL
12 from cola
import difftool
13 from cola
import gitcmds
14 from cola
import icons
15 from cola
import qtutils
16 from cola
import utils
17 from cola
.git
import git
18 from cola
.i18n
import N_
19 from cola
.interaction
import Interaction
20 from cola
.models
import main
21 from cola
.widgets
import completion
22 from cola
.widgets
.browse
import BrowseDialog
23 from cola
.widgets
.selectcommits
import select_commits
24 from cola
.compat
import ustr
28 """Launch the 'Delete Branch' dialog."""
29 icon
= icons
.discard()
30 branch
= choose_branch(N_('Delete Branch'), N_('Delete'), icon
=icon
)
33 cmds
.do(cmds
.DeleteBranch
, branch
)
36 def delete_remote_branch():
37 """Launch the 'Delete Remote Branch' dialog."""
38 branch
= choose_remote_branch(N_('Delete Remote Branch'), N_('Delete'),
42 rgx
= re
.compile(r
'^(?P<remote>[^/]+)/(?P<branch>.+)$')
43 match
= rgx
.match(branch
)
45 remote
= match
.group('remote')
46 branch
= match
.group('branch')
47 cmds
.do(cmds
.DeleteRemoteBranch
, remote
, branch
)
51 """Launch the 'Browse Current Branch' dialog."""
52 branch
= gitcmds
.current_branch()
53 BrowseDialog
.browse(branch
)
57 """Prompt for a branch and inspect content at that point in time."""
58 # Prompt for a branch to browse
59 branch
= choose_ref(N_('Browse Commits...'), N_('Browse'))
62 BrowseDialog
.browse(branch
)
65 def checkout_branch():
66 """Launch the 'Checkout Branch' dialog."""
67 branch
= choose_branch(N_('Checkout Branch'), N_('Checkout'))
70 cmds
.do(cmds
.CheckoutBranch
, branch
)
74 """Launch the 'Cherry-Pick' dialog."""
75 revs
, summaries
= gitcmds
.log_helper(all
=True)
76 commits
= select_commits(N_('Cherry-Pick Commit'),
77 revs
, summaries
, multiselect
=False)
80 cmds
.do(cmds
.CherryPick
, commits
)
84 """Prompt for a new directory and create a new Git repository
86 :returns str: repository path or None if no repository was created.
89 dlg
= QtGui
.QFileDialog()
90 dlg
.setFileMode(QtGui
.QFileDialog
.Directory
)
91 dlg
.setOption(QtGui
.QFileDialog
.ShowDirsOnly
)
94 if dlg
.exec_() != QtGui
.QFileDialog
.Accepted
:
96 paths
= dlg
.selectedFiles()
102 # Avoid needlessly calling `git init`.
103 if git
.is_git_dir(path
):
104 # We could prompt here and confirm that they really didn't
105 # mean to open an existing repository, but I think
106 # treating it like an "Open" is a sensible DWIM answer.
109 status
, out
, err
= core
.run_command(['git', 'init', path
])
113 title
= N_('Error Creating Repository')
114 msg
= (N_('"%(command)s" returned exit status %(status)d') %
115 dict(command
='git init %s' % path
, status
=status
))
116 details
= N_('Output:\n%s') % out
119 details
+= N_('Errors: %s') % err
120 qtutils
.critical(title
, msg
, details
)
128 cmds
.do(cmds
.OpenRepo
, dirname
)
131 def prompt_for_clone():
133 Present a GUI for cloning a repository.
135 Returns the target directory and URL
138 url
, ok
= qtutils
.prompt(N_('Path or URL to clone (Env. $VARS okay)'))
139 url
= utils
.expandpath(url
)
140 if not ok
or not url
:
143 # Pick a suitable basename by parsing the URL
144 newurl
= url
.replace('\\', '/').rstrip('/')
145 default
= newurl
.rsplit('/', 1)[-1]
146 if default
== '.git':
147 # The end of the URL is /.git, so assume it's a file path
148 default
= os
.path
.basename(os
.path
.dirname(newurl
))
149 if default
.endswith('.git'):
150 # The URL points to a bare repo
151 default
= default
[:-4]
153 # The URL is the current repo
154 default
= os
.path
.basename(core
.getcwd())
158 Interaction
.information(
160 N_('Could not parse Git URL: "%s"') % url
)
161 Interaction
.log(N_('Could not parse Git URL: "%s"') % url
)
164 # Prompt the user for a directory to use as the parent directory
165 msg
= N_('Select a parent directory for the new clone')
166 dirname
= qtutils
.opendir_dialog(msg
, main
.model().getcwd())
170 destdir
= os
.path
.join(dirname
, default
)
172 if core
.exists(destdir
):
173 # An existing path can be specified
174 msg
= (N_('"%s" already exists, cola will create a new directory') %
176 Interaction
.information(N_('Directory Exists'), msg
)
178 # Make sure the new destdir doesn't exist
179 while core
.exists(destdir
):
180 destdir
= olddestdir
+ str(count
)
186 def export_patches():
187 """Run 'git format-patch' on a list of commits."""
188 revs
, summaries
= gitcmds
.log_helper()
189 to_export
= select_commits(N_('Export Patches'), revs
, summaries
)
192 cmds
.do(cmds
.FormatPatch
, reversed(to_export
), reversed(revs
))
195 def diff_expression():
196 """Diff using an arbitrary expression."""
197 tracked
= gitcmds
.tracked_branch()
198 current
= gitcmds
.current_branch()
199 if tracked
and current
:
200 ref
= tracked
+ '..' + current
202 ref
= 'origin/master..'
203 difftool
.diff_expression(qtutils
.active_window(), ref
)
207 dirname
= qtutils
.opendir_dialog(N_('Open Git Repository...'),
208 main
.model().getcwd())
211 cmds
.do(cmds
.OpenRepo
, dirname
)
214 def open_repo_in_new_window():
215 """Spawn a new cola session."""
216 dirname
= qtutils
.opendir_dialog(N_('Open Git Repository...'),
217 main
.model().getcwd())
220 cmds
.do(cmds
.OpenNewRepo
, dirname
)
223 def load_commitmsg():
224 """Load a commit message from a file."""
225 filename
= qtutils
.open_file(N_('Load Commit Message'),
226 directory
=main
.model().getcwd())
228 cmds
.do(cmds
.LoadCommitMessageFromFile
, filename
)
231 def choose_from_dialog(get
, title
, button_text
, default
, icon
=None):
232 parent
= qtutils
.active_window()
233 return get(title
, button_text
, parent
, default
=default
, icon
=icon
)
236 def choose_ref(title
, button_text
, default
=None):
237 return choose_from_dialog(completion
.GitRefDialog
.get
,
238 title
, button_text
, default
)
241 def choose_branch(title
, button_text
, default
=None, icon
=None):
242 return choose_from_dialog(completion
.GitBranchDialog
.get
,
243 title
, button_text
, default
, icon
=icon
)
246 def choose_remote_branch(title
, button_text
, default
=None, icon
=None):
247 return choose_from_dialog(completion
.GitRemoteBranchDialog
.get
,
248 title
, button_text
, default
, icon
=icon
)
252 """Diff against an arbitrary revision, branch, tag, etc."""
253 branch
= choose_ref(N_('Select Branch to Review'), N_('Review'))
256 merge_base
= gitcmds
.merge_base_parent(branch
)
257 difftool
.diff_commits(qtutils
.active_window(), merge_base
, branch
)
260 class TaskRunner(QtCore
.QObject
):
261 """Runs QRunnable instances and transfers control when they finish"""
263 def __init__(self
, parent
):
264 QtCore
.QObject
.__init
__(self
, parent
)
266 self
.task_details
= {}
267 self
.connect(self
, Task
.FINISHED
, self
.finish
)
269 def start(self
, task
, progress
=None, finish
=None):
270 """Start the task and register a callback"""
271 if progress
is not None:
273 # prevents garbage collection bugs in certain PyQt4 versions
274 self
.tasks
.append(task
)
276 self
.task_details
[task_id
] = (progress
, finish
)
277 QtCore
.QThreadPool
.globalInstance().start(task
)
279 def finish(self
, task
, *args
, **kwargs
):
282 self
.tasks
.remove(task
)
286 progress
, finish
= self
.task_details
[task_id
]
287 del self
.task_details
[task_id
]
289 finish
= progress
= None
291 if progress
is not None:
294 if finish
is not None:
295 finish(task
, *args
, **kwargs
)
298 class Task(QtCore
.QRunnable
):
299 """Base class for concrete tasks"""
301 FINISHED
= SIGNAL('finished')
303 def __init__(self
, sender
):
304 QtCore
.QRunnable
.__init
__(self
)
307 def finish(self
, *args
, **kwargs
):
308 self
.sender
.emit(self
.FINISHED
, self
, *args
, **kwargs
)
311 class CloneTask(Task
):
312 """Clones a Git repository"""
314 def __init__(self
, sender
, url
, destdir
, spawn
):
315 Task
.__init
__(self
, sender
)
317 self
.destdir
= destdir
322 """Runs the model action and captures the result"""
323 self
.cmd
= cmds
.do(cmds
.Clone
, self
.url
, self
.destdir
,
328 def clone_repo(task_runner
, progress
, finish
, spawn
):
329 """Clone a repostiory asynchronously with progress animation"""
330 result
= prompt_for_clone()
333 # Use a thread to update in the background
334 url
, destdir
= result
335 progress
.set_details(N_('Clone Repository'),
336 N_('Cloning repository at %s') % url
)
337 task
= CloneTask(task_runner
, url
, destdir
, spawn
)
338 task_runner
.start(task
,
343 def report_clone_repo_errors(task
):
344 """Report errors from the clone task if they exist"""
345 if task
.cmd
is None or task
.cmd
.ok
:
347 Interaction
.critical(task
.cmd
.error_message
,
348 message
=task
.cmd
.error_message
,
349 details
=task
.cmd
.error_details
)
352 """Launch the 'Rename Branch' dialogs."""
353 branch
= choose_branch(N_('Rename Existing Branch'), N_('Select'))
356 new_branch
= choose_branch(N_('Enter Branch New Name'), N_('Rename'))
359 cmds
.do(cmds
.RenameBranch
, branch
, new_branch
)