Plugins/VcsPlugins/vcsGit/git.py

changeset 6020
baf6da1ae288
child 6026
4773c9469880
equal deleted inserted replaced
6019:58ecdaf0b789 6020:baf6da1ae288
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2014 - 2017 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the version control systems interface to Git.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode
13 except NameError:
14 pass
15
16 import os
17 import shutil
18 import re
19
20 from PyQt5.QtCore import QProcess, pyqtSignal, QFileInfo
21 from PyQt5.QtWidgets import QApplication, QDialog, QInputDialog, QLineEdit
22
23 from E5Gui.E5Application import e5App
24 from E5Gui import E5MessageBox, E5FileDialog
25
26 from Globals.E5ConfigParser import E5ConfigParser
27
28 from QScintilla.MiniEditor import MiniEditor
29
30 from VCS.VersionControl import VersionControl
31 from VCS.RepositoryInfoDialog import VcsRepositoryInfoDialog
32
33 from .GitDialog import GitDialog
34
35 import Utilities
36 import Preferences
37
38
39 class Git(VersionControl):
40 """
41 Class implementing the version control systems interface to Git.
42
43 @signal committed() emitted after the commit action has completed
44 """
45 committed = pyqtSignal()
46
47 IgnoreFileName = ".gitignore"
48
49 def __init__(self, plugin, parent=None, name=None):
50 """
51 Constructor
52
53 @param plugin reference to the plugin object
54 @param parent parent widget (QWidget)
55 @param name name of this object (string)
56 """
57 VersionControl.__init__(self, parent, name)
58 self.defaultOptions = {
59 'global': [''],
60 'commit': [''],
61 'checkout': [''],
62 'update': [''],
63 'add': [''],
64 'remove': [''],
65 'diff': [''],
66 'log': [''],
67 'history': [''],
68 'status': [''],
69 'tag': [''],
70 'export': ['']
71 }
72
73 self.__plugin = plugin
74 self.__ui = parent
75
76 self.options = self.defaultOptions
77
78 self.tagTypeList = [
79 'tags',
80 'branches',
81 ]
82
83 self.commandHistory = []
84
85 self.adminDir = '.git'
86
87 self.log = None
88 self.logBrowser = None
89 self.reflogBrowser = None
90 self.diff = None
91 self.sbsDiff = None
92 self.tagbranchList = None
93 self.status = None
94 self.remotesDialog = None
95 self.describeDialog = None
96 self.blame = None
97 self.stashBrowser = None
98 self.repoEditor = None
99 self.userEditor = None
100 self.bisectlogBrowser = None
101 self.bisectReplayEditor = None
102 self.patchStatisticsDialog = None
103 self.submoduleStatusDialog = None
104
105 self.__lastBundlePath = None
106 self.__lastReplayPath = None
107
108 self.statusCache = {}
109
110 self.__commitData = {}
111 self.__commitDialog = None
112
113 self.__patchCheckData = None
114
115 def getPlugin(self):
116 """
117 Public method to get a reference to the plugin object.
118
119 @return reference to the plugin object (VcsGitPlugin)
120 """
121 return self.__plugin
122
123 def vcsShutdown(self):
124 """
125 Public method used to shutdown the Git interface.
126 """
127 if self.log is not None:
128 self.log.close()
129 if self.logBrowser is not None:
130 self.logBrowser.close()
131 if self.reflogBrowser is not None:
132 self.reflogBrowser.close()
133 if self.diff is not None:
134 self.diff.close()
135 if self.sbsDiff is not None:
136 self.sbsDiff.close()
137 if self.tagbranchList is not None:
138 self.tagbranchList.close()
139 if self.status is not None:
140 self.status.close()
141 if self.remotesDialog is not None:
142 self.remotesDialog.close()
143 if self.describeDialog is not None:
144 self.describeDialog.close()
145 if self.blame is not None:
146 self.blame.close()
147 if self.stashBrowser is not None:
148 self.stashBrowser.close()
149 if self.bisectlogBrowser is not None:
150 self.bisectlogBrowser.close()
151 if self.bisectReplayEditor is not None:
152 self.bisectReplayEditor.close()
153 if self.repoEditor is not None:
154 self.repoEditor.close()
155 if self.userEditor is not None:
156 self.userEditor.close()
157 if self.patchStatisticsDialog is not None:
158 self.patchStatisticsDialog.close()
159 if self.submoduleStatusDialog is not None:
160 self.submoduleStatusDialog.close()
161
162 # shut down the project helpers
163 self.__projectHelper.shutdown()
164
165 def initCommand(self, command):
166 """
167 Public method to initialize a command arguments list.
168
169 @param command command name (string)
170 @return list of command options (list of string)
171 """
172 args = [command]
173 return args
174
175 def vcsExists(self):
176 """
177 Public method used to test for the presence of the git executable.
178
179 @return flag indicating the existance (boolean) and an error message
180 (string)
181 """
182 self.versionStr = ''
183 errMsg = ""
184 ioEncoding = Preferences.getSystem("IOEncoding")
185
186 args = self.initCommand("version")
187 process = QProcess()
188 process.start('git', args)
189 procStarted = process.waitForStarted(5000)
190 if procStarted:
191 finished = process.waitForFinished(30000)
192 if finished and process.exitCode() == 0:
193 output = str(process.readAllStandardOutput(),
194 ioEncoding, 'replace')
195 self.versionStr = output.splitlines()[0].split()[-1]
196 v = list(re.match(r'.*?(\d+)\.(\d+)\.?(\d+)?\.?(\d+)?',
197 self.versionStr).groups())
198 for i in range(4):
199 try:
200 v[i] = int(v[i])
201 except TypeError:
202 v[i] = 0
203 except IndexError:
204 v.append(0)
205 self.version = tuple(v)
206 return True, errMsg
207 else:
208 if finished:
209 errMsg = self.tr(
210 "The git process finished with the exit code {0}")\
211 .format(process.exitCode())
212 else:
213 errMsg = self.tr(
214 "The git process did not finish within 30s.")
215 else:
216 errMsg = self.tr("Could not start the git executable.")
217
218 return False, errMsg
219
220 def vcsInit(self, vcsDir, noDialog=False):
221 """
222 Public method used to initialize the Git repository.
223
224 The initialization is done, when a project is converted into a
225 Git controlled project. Therefore we always return TRUE without
226 doing anything.
227
228 @param vcsDir name of the VCS directory (string)
229 @param noDialog flag indicating quiet operations (boolean)
230 @return always TRUE
231 """
232 return True
233
234 def vcsConvertProject(self, vcsDataDict, project):
235 """
236 Public method to convert an uncontrolled project to a version
237 controlled project.
238
239 @param vcsDataDict dictionary of data required for the conversion
240 @param project reference to the project object
241 """
242 success = self.vcsImport(vcsDataDict, project.ppath)[0]
243 if not success:
244 E5MessageBox.critical(
245 self.__ui,
246 self.tr("Create project repository"),
247 self.tr(
248 """The project repository could not be created."""))
249 else:
250 pfn = project.pfile
251 if not os.path.isfile(pfn):
252 pfn += "z"
253 project.closeProject()
254 project.openProject(pfn)
255
256 def vcsImport(self, vcsDataDict, projectDir, noDialog=False):
257 """
258 Public method used to import the project into the Git repository.
259
260 @param vcsDataDict dictionary of data required for the import
261 @param projectDir project directory (string)
262 @param noDialog flag indicating quiet operations
263 @return flag indicating an execution without errors (boolean)
264 and a flag indicating the version controll status (boolean)
265 """
266 msg = vcsDataDict["message"]
267 if not msg:
268 msg = '***'
269
270 args = self.initCommand("init")
271 args.append(projectDir)
272 dia = GitDialog(self.tr('Creating Git repository'), self)
273 res = dia.startProcess(args)
274 if res:
275 dia.exec_()
276 status = dia.normalExit()
277
278 if status:
279 ignoreName = os.path.join(projectDir, Git.IgnoreFileName)
280 if not os.path.exists(ignoreName):
281 status = self.gitCreateIgnoreFile(projectDir)
282
283 if status:
284 args = self.initCommand("add")
285 args.append("-v")
286 args.append(".")
287 dia = GitDialog(
288 self.tr('Adding files to Git repository'),
289 self)
290 res = dia.startProcess(args, projectDir)
291 if res:
292 dia.exec_()
293 status = dia.normalExit()
294
295 if status:
296 args = self.initCommand("commit")
297 args.append('--message={0}'.format(msg))
298 dia = GitDialog(
299 self.tr('Initial commit to Git repository'),
300 self)
301 res = dia.startProcess(args, projectDir)
302 if res:
303 dia.exec_()
304 status = dia.normalExit()
305
306 return status, False
307
308 def vcsCheckout(self, vcsDataDict, projectDir, noDialog=False):
309 """
310 Public method used to check the project out of a Git repository
311 (clone).
312
313 @param vcsDataDict dictionary of data required for the checkout
314 @param projectDir project directory to create (string)
315 @param noDialog flag indicating quiet operations
316 @return flag indicating an execution without errors (boolean)
317 """
318 noDialog = False
319 vcsUrl = self.gitNormalizeURL(vcsDataDict["url"])
320
321 args = self.initCommand("clone")
322 args.append(vcsUrl)
323 args.append(projectDir)
324
325 if noDialog:
326 return self.startSynchronizedProcess(QProcess(), 'git', args)
327 else:
328 dia = GitDialog(
329 self.tr('Cloning project from a Git repository'),
330 self)
331 res = dia.startProcess(args)
332 if res:
333 dia.exec_()
334 return dia.normalExit()
335
336 def vcsExport(self, vcsDataDict, projectDir):
337 """
338 Public method used to export a directory from the Git repository.
339
340 @param vcsDataDict dictionary of data required for the checkout
341 @param projectDir project directory to create (string)
342 @return flag indicating an execution without errors (boolean)
343 """
344 status = self.vcsCheckout(vcsDataDict, projectDir)
345 shutil.rmtree(os.path.join(projectDir, self.adminDir), True)
346 if os.path.exists(os.path.join(projectDir, Git.IgnoreFileName)):
347 os.remove(os.path.join(projectDir, Git.IgnoreFileName))
348 return status
349
350 def vcsCommit(self, name, message="", noDialog=False, commitAll=True,
351 amend=False):
352 """
353 Public method used to make the change of a file/directory permanent
354 in the Git repository.
355
356 @param name file/directory name to be committed (string or list of
357 strings)
358 @param message message for this operation (string)
359 @param noDialog flag indicating quiet operations (boolean)
360 @param commitAll flag indicating to commit all local changes (boolean)
361 @param amend flag indicating to amend the HEAD commit (boolean)
362 """
363 if not noDialog:
364 # call CommitDialog and get message from there
365 if self.__commitDialog is None:
366 from .GitCommitDialog import GitCommitDialog
367 self.__commitDialog = GitCommitDialog(self, message, amend,
368 commitAll, self.__ui)
369 self.__commitDialog.accepted.connect(self.__vcsCommit_Step2)
370 self.__commitDialog.show()
371 self.__commitDialog.raise_()
372 self.__commitDialog.activateWindow()
373
374 self.__commitData["name"] = name
375 self.__commitData["msg"] = message
376 self.__commitData["noDialog"] = noDialog
377 self.__commitData["all"] = commitAll
378
379 if noDialog:
380 self.__vcsCommit_Step2()
381
382 def __vcsCommit_Step2(self):
383 """
384 Private slot performing the second step of the commit action.
385 """
386 name = self.__commitData["name"]
387 msg = self.__commitData["msg"]
388 noDialog = self.__commitData["noDialog"]
389 commitAll = self.__commitData["all"]
390
391 if not noDialog:
392 # check, if there are unsaved changes, that should be committed
393 if isinstance(name, list):
394 nameList = name
395 else:
396 nameList = [name]
397 ok = True
398 for nam in nameList:
399 # check for commit of the project
400 if os.path.isdir(nam):
401 project = e5App().getObject("Project")
402 if nam == project.getProjectPath():
403 ok &= \
404 project.checkAllScriptsDirty(
405 reportSyntaxErrors=True) and \
406 project.checkDirty()
407 continue
408 elif os.path.isfile(nam):
409 editor = \
410 e5App().getObject("ViewManager").getOpenEditor(nam)
411 if editor:
412 ok &= editor.checkDirty()
413 if not ok:
414 break
415
416 if not ok:
417 res = E5MessageBox.yesNo(
418 self.__ui,
419 self.tr("Commit Changes"),
420 self.tr(
421 """The commit affects files, that have unsaved"""
422 """ changes. Shall the commit be continued?"""),
423 icon=E5MessageBox.Warning)
424 if not res:
425 return
426
427 if self.__commitDialog is not None:
428 msg = self.__commitDialog.logMessage()
429 amend = self.__commitDialog.amend()
430 resetAuthor = self.__commitDialog.resetAuthor()
431 commitAll = not self.__commitDialog.stagedOnly()
432 self.__commitDialog.deleteLater()
433 self.__commitDialog = None
434 else:
435 amend = False
436 resetAuthor = False
437
438 if not msg and not amend:
439 msg = '***'
440
441 args = self.initCommand("commit")
442 if amend:
443 args.append("--amend")
444 if resetAuthor:
445 args.append("--reset-author")
446 if msg:
447 args.append("--message")
448 args.append(msg)
449
450 if isinstance(name, list):
451 dname, fnames = self.splitPathList(name)
452 else:
453 dname, fname = self.splitPath(name)
454
455 # find the root of the repo
456 repodir = dname
457 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
458 repodir = os.path.dirname(repodir)
459 if os.path.splitdrive(repodir)[1] == os.sep:
460 return
461
462 if isinstance(name, list):
463 args.append("--")
464 self.addArguments(args, fnames)
465 else:
466 if dname != repodir or fname != ".":
467 args.append("--")
468 args.append(fname)
469 else:
470 if commitAll:
471 args.append("--all")
472
473 if noDialog:
474 self.startSynchronizedProcess(QProcess(), "git", args, dname)
475 else:
476 dia = GitDialog(
477 self.tr('Committing changes to Git repository'),
478 self)
479 res = dia.startProcess(args, dname)
480 if res:
481 dia.exec_()
482 self.committed.emit()
483 self.checkVCSStatus()
484
485 def vcsUpdate(self, name, noDialog=False, revision=None):
486 """
487 Public method used to update a file/directory with the Git
488 repository.
489
490 @param name file/directory name to be updated (string or list of
491 strings)
492 @param noDialog flag indicating quiet operations (boolean)
493 @keyparam revision revision to update to (string)
494 @return flag indicating, that the update contained an add
495 or delete (boolean)
496 """
497 args = self.initCommand("checkout")
498 if revision:
499 res = E5MessageBox.yesNo(
500 None,
501 self.tr("Switch"),
502 self.tr("""<p>Do you really want to switch to <b>{0}</b>?"""
503 """</p>""").format(revision),
504 yesDefault=True)
505 if not res:
506 return False
507 args.append(revision)
508
509 if isinstance(name, list):
510 args.append("--")
511 dname, fnames = self.splitPathList(name)
512 self.addArguments(args, fnames)
513 else:
514 dname, fname = self.splitPath(name)
515 if fname != ".":
516 args.append("--")
517 args.append(fname)
518
519 # find the root of the repo
520 repodir = dname
521 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
522 repodir = os.path.dirname(repodir)
523 if os.path.splitdrive(repodir)[1] == os.sep:
524 return False
525
526 if noDialog:
527 self.startSynchronizedProcess(QProcess(), 'git', args, repodir)
528 res = False
529 else:
530 dia = GitDialog(self.tr(
531 'Synchronizing with the Git repository'),
532 self)
533 res = dia.startProcess(args, repodir)
534 if res:
535 dia.exec_()
536 res = dia.hasAddOrDelete()
537 self.checkVCSStatus()
538 return res
539
540 def vcsAdd(self, name, isDir=False, noDialog=False):
541 """
542 Public method used to add a file/directory to the Git repository.
543
544 @param name file/directory name to be added (string)
545 @param isDir flag indicating name is a directory (boolean)
546 @param noDialog flag indicating quiet operations
547 """
548 args = self.initCommand("add")
549 args.append("-v")
550
551 if isinstance(name, list):
552 if isDir:
553 dname, fname = os.path.split(name[0])
554 else:
555 dname, fnames = self.splitPathList(name)
556 else:
557 if isDir:
558 dname, fname = os.path.split(name)
559 else:
560 dname, fname = self.splitPath(name)
561
562 # find the root of the repo
563 repodir = dname
564 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
565 repodir = os.path.dirname(repodir)
566 if os.path.splitdrive(repodir)[1] == os.sep:
567 return
568
569 if isinstance(name, list):
570 self.addArguments(args, name)
571 else:
572 args.append(name)
573
574 if noDialog:
575 self.startSynchronizedProcess(QProcess(), 'git', args, repodir)
576 else:
577 dia = GitDialog(
578 self.tr(
579 'Adding files/directories to the Git repository'),
580 self)
581 res = dia.startProcess(args, repodir)
582 if res:
583 dia.exec_()
584
585 def vcsAddBinary(self, name, isDir=False):
586 """
587 Public method used to add a file/directory in binary mode to the
588 Git repository.
589
590 @param name file/directory name to be added (string)
591 @param isDir flag indicating name is a directory (boolean)
592 """
593 self.vcsAdd(name, isDir)
594
595 def vcsAddTree(self, path):
596 """
597 Public method to add a directory tree rooted at path to the Git
598 repository.
599
600 @param path root directory of the tree to be added (string or list of
601 strings))
602 """
603 self.vcsAdd(path, isDir=False)
604
605 def vcsRemove(self, name, project=False, noDialog=False, stageOnly=False):
606 """
607 Public method used to remove a file/directory from the Git
608 repository.
609
610 The default operation is to remove the local copy as well.
611
612 @param name file/directory name to be removed (string or list of
613 strings))
614 @param project flag indicating deletion of a project tree (boolean)
615 (not needed)
616 @param noDialog flag indicating quiet operations (boolean)
617 @param stageOnly flag indicating to just remove the file from the
618 staging area (boolean)
619 @return flag indicating successful operation (boolean)
620 """
621 args = self.initCommand("rm")
622 if noDialog and '--force' not in args:
623 args.append('--force')
624 if stageOnly:
625 args.append('--cached')
626
627 if isinstance(name, list):
628 if os.path.isdir(name[0]):
629 args.append("-r")
630 dname, fnames = self.splitPathList(name)
631 args.append("--")
632 self.addArguments(args, name)
633 else:
634 if os.path.isdir(name):
635 args.append("-r")
636 dname, fname = self.splitPath(name)
637 args.append("--")
638 args.append(name)
639
640 # find the root of the repo
641 repodir = dname
642 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
643 repodir = os.path.dirname(repodir)
644 if os.path.splitdrive(repodir)[1] == os.sep:
645 return False
646
647 if noDialog:
648 res = self.startSynchronizedProcess(
649 QProcess(), 'git', args, repodir)
650 else:
651 dia = GitDialog(
652 self.tr(
653 'Removing files/directories from the Git'
654 ' repository'),
655 self)
656 res = dia.startProcess(args, repodir)
657 if res:
658 dia.exec_()
659 res = dia.normalExitWithoutErrors()
660
661 return res
662
663 def vcsMove(self, name, project, target=None, noDialog=False):
664 """
665 Public method used to move a file/directory.
666
667 @param name file/directory name to be moved (string)
668 @param project reference to the project object
669 @param target new name of the file/directory (string)
670 @param noDialog flag indicating quiet operations
671 @return flag indicating successful operation (boolean)
672 """
673 isDir = os.path.isdir(name)
674
675 res = False
676 if noDialog:
677 if target is None:
678 return False
679 force = True
680 accepted = True
681 else:
682 from .GitCopyDialog import GitCopyDialog
683 dlg = GitCopyDialog(name, None, True)
684 accepted = dlg.exec_() == QDialog.Accepted
685 if accepted:
686 target, force = dlg.getData()
687
688 if accepted:
689 args = self.initCommand("mv")
690 args.append("-v")
691 if force:
692 args.append('--force')
693 args.append(name)
694 args.append(target)
695
696 dname, fname = self.splitPath(name)
697 # find the root of the repo
698 repodir = dname
699 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
700 repodir = os.path.dirname(repodir)
701 if os.path.splitdrive(repodir)[1] == os.sep:
702 return False
703
704 if noDialog:
705 res = self.startSynchronizedProcess(
706 QProcess(), 'git', args, repodir)
707 else:
708 dia = GitDialog(self.tr('Renaming {0}').format(name), self)
709 res = dia.startProcess(args, repodir)
710 if res:
711 dia.exec_()
712 res = dia.normalExit()
713 if res:
714 if target.startswith(project.getProjectPath()):
715 if isDir:
716 project.moveDirectory(name, target)
717 else:
718 project.renameFileInPdata(name, target)
719 else:
720 if isDir:
721 project.removeDirectory(name)
722 else:
723 project.removeFile(name)
724 return res
725
726 def vcsLogBrowser(self, name, isFile=False):
727 """
728 Public method used to browse the log of a file/directory from the
729 Git repository.
730
731 @param name file/directory name to show the log of (string)
732 @keyparam isFile flag indicating log for a file is to be shown
733 (boolean)
734 """
735 if self.logBrowser is None:
736 from .GitLogBrowserDialog import GitLogBrowserDialog
737 self.logBrowser = GitLogBrowserDialog(self)
738 self.logBrowser.show()
739 self.logBrowser.raise_()
740 self.logBrowser.start(name, isFile=isFile)
741
742 def gitReflogBrowser(self, projectDir):
743 """
744 Public method used to browse the reflog of the project.
745
746 @param projectDir name of the project directory (string)
747 """
748 if self.reflogBrowser is None:
749 from .GitReflogBrowserDialog import GitReflogBrowserDialog
750 self.reflogBrowser = GitReflogBrowserDialog(self)
751 self.reflogBrowser.show()
752 self.reflogBrowser.raise_()
753 self.reflogBrowser.start(projectDir)
754
755 def vcsDiff(self, name):
756 """
757 Public method used to view the difference of a file/directory to the
758 Git repository.
759
760 If name is a directory and is the project directory, all project files
761 are saved first. If name is a file (or list of files), which is/are
762 being edited and has unsaved modification, they can be saved or the
763 operation may be aborted.
764
765 @param name file/directory name to be diffed (string)
766 """
767 if isinstance(name, list):
768 names = name[:]
769 else:
770 names = [name]
771 for nam in names:
772 if os.path.isfile(nam):
773 editor = e5App().getObject("ViewManager").getOpenEditor(nam)
774 if editor and not editor.checkDirty():
775 return
776 else:
777 project = e5App().getObject("Project")
778 if nam == project.ppath and not project.saveAllScripts():
779 return
780 if self.diff is None:
781 from .GitDiffDialog import GitDiffDialog
782 self.diff = GitDiffDialog(self)
783 self.diff.show()
784 self.diff.raise_()
785 QApplication.processEvents()
786 self.diff.start(name, diffMode="work2stage2repo", refreshable=True)
787
788 def vcsStatus(self, name):
789 """
790 Public method used to view the status of files/directories in the
791 Git repository.
792
793 @param name file/directory name(s) to show the status of
794 (string or list of strings)
795 """
796 if self.status is None:
797 from .GitStatusDialog import GitStatusDialog
798 self.status = GitStatusDialog(self)
799 self.status.show()
800 self.status.raise_()
801 self.status.start(name)
802
803 def gitUnstage(self, name):
804 """
805 Public method used to unstage a file/directory.
806
807 @param name file/directory name to be reverted (string)
808 @return flag indicating, that the update contained an add
809 or delete (boolean)
810 """
811 if isinstance(name, list):
812 dname, fnames = self.splitPathList(name)
813 else:
814 dname, fname = self.splitPath(name)
815
816 # find the root of the repo
817 repodir = dname
818 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
819 repodir = os.path.dirname(repodir)
820 if os.path.splitdrive(repodir)[1] == os.sep:
821 return
822
823 args = self.initCommand("reset")
824 args.append("HEAD")
825 args.append("--")
826 if isinstance(name, list):
827 self.addArguments(args, name)
828 else:
829 args.append(name)
830
831 dia = GitDialog(
832 self.tr('Unstage files/directories'),
833 self)
834 res = dia.startProcess(args, repodir)
835 if res:
836 dia.exec_()
837 res = dia.hasAddOrDelete()
838 self.checkVCSStatus()
839
840 return res
841
842 def gitRevert(self, name):
843 """
844 Public method used to revert changes made to a file/directory.
845
846 @param name file/directory name to be reverted (string)
847 @return flag indicating, that the update contained an add
848 or delete (boolean)
849 """
850 args = self.initCommand("checkout")
851 args.append("--")
852 if isinstance(name, list):
853 dname, fnames = self.splitPathList(name)
854 self.addArguments(args, name)
855 names = name[:]
856 else:
857 dname, fname = self.splitPath(name)
858 args.append(name)
859 names = [name]
860
861 # find the root of the repo
862 repodir = dname
863 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
864 repodir = os.path.dirname(repodir)
865 if os.path.splitdrive(repodir)[1] == os.sep:
866 return False
867
868 project = e5App().getObject("Project")
869 names = [project.getRelativePath(nam) for nam in names]
870 if names[0]:
871 from UI.DeleteFilesConfirmationDialog import \
872 DeleteFilesConfirmationDialog
873 dlg = DeleteFilesConfirmationDialog(
874 self.parent(),
875 self.tr("Revert changes"),
876 self.tr(
877 "Do you really want to revert all changes to these files"
878 " or directories?"),
879 names)
880 yes = dlg.exec_() == QDialog.Accepted
881 else:
882 yes = E5MessageBox.yesNo(
883 None,
884 self.tr("Revert changes"),
885 self.tr("""Do you really want to revert all changes of"""
886 """ the project?"""))
887 if yes:
888 dia = GitDialog(self.tr('Reverting changes'), self)
889 res = dia.startProcess(args, repodir)
890 if res:
891 dia.exec_()
892 res = dia.hasAddOrDelete()
893 self.checkVCSStatus()
894 else:
895 res = False
896
897 return res
898
899 def vcsMerge(self, name):
900 """
901 Public method used to merge a URL/revision into the local project.
902
903 @param name file/directory name to be merged (string)
904 """
905 dname, fname = self.splitPath(name)
906
907 # find the root of the repo
908 repodir = dname
909 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
910 repodir = os.path.dirname(repodir)
911 if os.path.splitdrive(repodir)[1] == os.sep:
912 return
913
914 from .GitMergeDialog import GitMergeDialog
915 dlg = GitMergeDialog(self.gitGetTagsList(repodir),
916 self.gitGetBranchesList(repodir, withMaster=True),
917 self.gitGetCurrentBranch(repodir),
918 self.gitGetBranchesList(repodir, remotes=True))
919 if dlg.exec_() == QDialog.Accepted:
920 commit, doCommit, commitMessage, addLog, diffStat = \
921 dlg.getParameters()
922 args = self.initCommand("merge")
923 if doCommit:
924 args.append("--commit")
925 args.append("-m")
926 args.append(commitMessage)
927 if addLog:
928 args.append("--log")
929 else:
930 args.append("--no-log")
931 else:
932 args.append("--no-commit")
933 if diffStat:
934 args.append("--stat")
935 else:
936 args.append("--no-stat")
937 if commit:
938 args.append(commit)
939
940 dia = GitDialog(self.tr('Merging').format(name), self)
941 res = dia.startProcess(args, repodir)
942 if res:
943 dia.exec_()
944 self.checkVCSStatus()
945
946 def vcsSwitch(self, name):
947 """
948 Public method used to switch a working directory to a different
949 revision.
950
951 @param name directory name to be switched (string)
952 @return flag indicating, that the switch contained an add
953 or delete (boolean)
954 """
955 dname, fname = self.splitPath(name)
956
957 # find the root of the repo
958 repodir = dname
959 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
960 repodir = os.path.dirname(repodir)
961 if os.path.splitdrive(repodir)[1] == os.sep:
962 return False
963
964 from .GitRevisionSelectionDialog import GitRevisionSelectionDialog
965 dlg = GitRevisionSelectionDialog(
966 self.gitGetTagsList(repodir),
967 self.gitGetBranchesList(repodir),
968 trackingBranchesList=self.gitGetBranchesList(
969 repodir, remotes=True),
970 noneLabel=self.tr("Master branch head"))
971 if dlg.exec_() == QDialog.Accepted:
972 rev = dlg.getRevision()
973 return self.vcsUpdate(name, revision=rev)
974
975 return False
976
977 def vcsRegisteredState(self, name):
978 """
979 Public method used to get the registered state of a file in the vcs.
980
981 @param name filename to check (string)
982 @return a combination of canBeCommited and canBeAdded
983 """
984 if name.endswith(os.sep):
985 name = name[:-1]
986 name = os.path.normcase(name)
987 dname, fname = self.splitPath(name)
988
989 if fname == '.' and os.path.isdir(os.path.join(dname, self.adminDir)):
990 return self.canBeCommitted
991
992 if name in self.statusCache:
993 return self.statusCache[name]
994
995 # find the root of the repo
996 repodir = dname
997 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
998 repodir = os.path.dirname(repodir)
999 if os.path.splitdrive(repodir)[1] == os.sep:
1000 return 0
1001
1002 args = self.initCommand("status")
1003 args.append('--porcelain')
1004 args.append(name)
1005
1006 ioEncoding = Preferences.getSystem("IOEncoding")
1007 output = ""
1008 process = QProcess()
1009 process.setWorkingDirectory(repodir)
1010 process.start('git', args)
1011 procStarted = process.waitForStarted(5000)
1012 if procStarted:
1013 finished = process.waitForFinished(30000)
1014 if finished and process.exitCode() == 0:
1015 output = str(process.readAllStandardOutput(),
1016 ioEncoding, 'replace')
1017
1018 if output:
1019 for line in output.splitlines():
1020 if line and line[0] in " MADRCU!?":
1021 flag = line[1]
1022 path = line[3:].split(" -> ")[-1]
1023 absname = os.path.join(repodir, os.path.normcase(path))
1024 if absname.endswith(("/", "\\")):
1025 absname = absname[:-1]
1026 if flag not in "?!":
1027 if fname == '.':
1028 if absname.startswith(dname + os.path.sep):
1029 return self.canBeCommitted
1030 if absname == dname:
1031 return self.canBeCommitted
1032 else:
1033 if absname == name:
1034 return self.canBeCommitted
1035 else:
1036 return self.canBeCommitted
1037
1038 return self.canBeAdded
1039
1040 def vcsAllRegisteredStates(self, names, dname, shortcut=True):
1041 """
1042 Public method used to get the registered states of a number of files
1043 in the vcs.
1044
1045 <b>Note:</b> If a shortcut is to be taken, the code will only check,
1046 if the named directory has been scanned already. If so, it is assumed,
1047 that the states for all files have been populated by the previous run.
1048
1049 @param names dictionary with all filenames to be checked as keys
1050 @param dname directory to check in (string)
1051 @param shortcut flag indicating a shortcut should be taken (boolean)
1052 @return the received dictionary completed with a combination of
1053 canBeCommited and canBeAdded or None in order to signal an error
1054 """
1055 if dname.endswith(os.sep):
1056 dname = dname[:-1]
1057 dname = os.path.normcase(dname)
1058
1059 # revert the logic because git status doesn't show unchanged files
1060 for name in names:
1061 names[name] = self.canBeCommitted
1062
1063 found = False
1064 for name in self.statusCache:
1065 if name in names:
1066 found = True
1067 names[name] = self.statusCache[name]
1068
1069 if not found:
1070 # find the root of the repo
1071 repodir = dname
1072 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1073 repodir = os.path.dirname(repodir)
1074 if os.path.splitdrive(repodir)[1] == os.sep:
1075 return names
1076
1077 args = self.initCommand("status")
1078 args.append('--porcelain')
1079
1080 ioEncoding = Preferences.getSystem("IOEncoding")
1081 output = ""
1082 process = QProcess()
1083 process.setWorkingDirectory(dname)
1084 process.start('git', args)
1085 procStarted = process.waitForStarted(5000)
1086 if procStarted:
1087 finished = process.waitForFinished(30000)
1088 if finished and process.exitCode() == 0:
1089 output = str(process.readAllStandardOutput(),
1090 ioEncoding, 'replace')
1091
1092 if output:
1093 for line in output.splitlines():
1094 if line and line[0] in " MADRCU!?":
1095 flag = line[1]
1096 path = line[3:].split(" -> ")[-1]
1097 name = os.path.normcase(os.path.join(repodir, path))
1098 dirName = os.path.dirname(name)
1099 if name.startswith(dname):
1100 if flag in "?!":
1101 isDir = name.endswith(("/", "\\"))
1102 if isDir:
1103 name = name[:-1]
1104 if name in names:
1105 names[name] = self.canBeAdded
1106 if isDir:
1107 # it's a directory
1108 for nname in names:
1109 if nname.startswith(name):
1110 names[nname] = self.canBeAdded
1111 if flag not in "?!":
1112 self.statusCache[name] = self.canBeCommitted
1113 self.statusCache[dirName] = self.canBeCommitted
1114 else:
1115 self.statusCache[name] = self.canBeAdded
1116 if dirName not in self.statusCache:
1117 self.statusCache[dirName] = self.canBeAdded
1118
1119 return names
1120
1121 def clearStatusCache(self):
1122 """
1123 Public method to clear the status cache.
1124 """
1125 self.statusCache = {}
1126
1127 def vcsName(self):
1128 """
1129 Public method returning the name of the vcs.
1130
1131 @return always 'Git' (string)
1132 """
1133 return "Git"
1134
1135 def vcsInitConfig(self, project):
1136 """
1137 Public method to initialize the VCS configuration.
1138
1139 This method ensures, that an ignore file exists.
1140
1141 @param project reference to the project (Project)
1142 """
1143 ppath = project.getProjectPath()
1144 if ppath:
1145 ignoreName = os.path.join(ppath, Git.IgnoreFileName)
1146 if not os.path.exists(ignoreName):
1147 self.gitCreateIgnoreFile(project.getProjectPath(),
1148 autoAdd=True)
1149
1150 def vcsCleanup(self, name):
1151 """
1152 Public method used to cleanup the working directory.
1153
1154 @param name directory name to be cleaned up (string)
1155 """
1156 patterns = self.__plugin.getPreferences("CleanupPatterns").split()
1157
1158 entries = []
1159 for pat in patterns:
1160 entries.extend(Utilities.direntries(name, True, pat))
1161
1162 for entry in entries:
1163 try:
1164 os.remove(entry)
1165 except OSError:
1166 pass
1167
1168 def vcsCommandLine(self, name):
1169 """
1170 Public method used to execute arbitrary Git commands.
1171
1172 @param name directory name of the working directory (string)
1173 """
1174 from .GitCommandDialog import GitCommandDialog
1175 dlg = GitCommandDialog(self.commandHistory, name)
1176 if dlg.exec_() == QDialog.Accepted:
1177 command = dlg.getData()
1178 commandList = Utilities.parseOptionString(command)
1179
1180 # This moves any previous occurrence of these arguments to the head
1181 # of the list.
1182 if command in self.commandHistory:
1183 self.commandHistory.remove(command)
1184 self.commandHistory.insert(0, command)
1185
1186 args = []
1187 self.addArguments(args, commandList)
1188
1189 # find the root of the repo
1190 repodir = name
1191 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1192 repodir = os.path.dirname(repodir)
1193 if os.path.splitdrive(repodir)[1] == os.sep:
1194 return
1195
1196 dia = GitDialog(self.tr('Git Command'), self)
1197 res = dia.startProcess(args, repodir)
1198 if res:
1199 dia.exec_()
1200
1201 def vcsOptionsDialog(self, project, archive, editable=False, parent=None):
1202 """
1203 Public method to get a dialog to enter repository info.
1204
1205 @param project reference to the project object
1206 @param archive name of the project in the repository (string)
1207 @param editable flag indicating that the project name is editable
1208 (boolean)
1209 @param parent parent widget (QWidget)
1210 @return reference to the instantiated options dialog (GitOptionsDialog)
1211 """
1212 from .GitOptionsDialog import GitOptionsDialog
1213 return GitOptionsDialog(self, project, parent)
1214
1215 def vcsNewProjectOptionsDialog(self, parent=None):
1216 """
1217 Public method to get a dialog to enter repository info for getting a
1218 new project.
1219
1220 @param parent parent widget (QWidget)
1221 @return reference to the instantiated options dialog
1222 (GitNewProjectOptionsDialog)
1223 """
1224 from .GitNewProjectOptionsDialog import GitNewProjectOptionsDialog
1225 return GitNewProjectOptionsDialog(self, parent)
1226
1227 def vcsRepositoryInfos(self, ppath):
1228 """
1229 Public method to retrieve information about the repository.
1230
1231 @param ppath local path to get the repository infos (string)
1232 @return string with ready formated info for display (string)
1233 """
1234 formatTemplate = (
1235 'format:'
1236 '%h%n'
1237 '%p%n'
1238 '%an%n'
1239 '%ae%n'
1240 '%ai%n'
1241 '%cn%n'
1242 '%ce%n'
1243 '%ci%n'
1244 '%d%n'
1245 '%s')
1246
1247 args = self.initCommand("show")
1248 args.append("--abbrev-commit")
1249 args.append("--abbrev={0}".format(
1250 self.__plugin.getPreferences("CommitIdLength")))
1251 args.append("--format={0}".format(formatTemplate))
1252 args.append("--no-patch")
1253 args.append("HEAD")
1254
1255 output = ""
1256 process = QProcess()
1257 process.setWorkingDirectory(ppath)
1258 process.start('git', args)
1259 procStarted = process.waitForStarted(5000)
1260 if procStarted:
1261 finished = process.waitForFinished(30000)
1262 if finished and process.exitCode() == 0:
1263 output = str(process.readAllStandardOutput(),
1264 Preferences.getSystem("IOEncoding"),
1265 'replace')
1266
1267 if output:
1268 info = []
1269 (commit, parents, author, authorMail, authorDate,
1270 committer, committerMail, committerDate, refs, subject) = \
1271 output.splitlines()
1272 tags = []
1273 branches = []
1274 for name in refs.strip()[1:-1].split(","):
1275 name = name.strip()
1276 if name:
1277 if name.startswith("tag: "):
1278 tags.append(name.split()[1])
1279 elif name != "HEAD":
1280 branches.append(name)
1281
1282 info.append(self.tr(
1283 "<tr><td><b>Commit</b></td><td>{0}</td></tr>").format(
1284 commit))
1285 if parents:
1286 info.append(self.tr(
1287 "<tr><td><b>Parents</b></td><td>{0}</td></tr>").format(
1288 '<br/>'.join(parents.strip().split())))
1289 if tags:
1290 info.append(self.tr(
1291 "<tr><td><b>Tags</b></td><td>{0}</td></tr>").format(
1292 '<br/>'.join(tags)))
1293 if branches:
1294 info.append(self.tr(
1295 "<tr><td><b>Branches</b></td><td>{0}</td></tr>").format(
1296 '<br/>'.join(branches)))
1297 info.append(self.tr(
1298 "<tr><td><b>Author</b></td><td>{0} &lt;{1}&gt;</td></tr>")
1299 .format(author, authorMail))
1300 info.append(self.tr(
1301 "<tr><td><b>Date</b></td><td>{0}</td></tr>").format(
1302 authorDate.rsplit(":", 1)[0]))
1303 info.append(self.tr(
1304 "<tr><td><b>Committer</b></td><td>{0} &lt;{1}&gt;</td></tr>")
1305 .format(committer, committerMail))
1306 info.append(self.tr(
1307 "<tr><td><b>Committed Date</b></td><td>{0}</td></tr>").format(
1308 committerDate.rsplit(":", 1)[0]))
1309 info.append(self.tr(
1310 "<tr><td><b>Subject</b></td><td>{0}</td></tr>").format(
1311 subject))
1312 infoStr = "\n".join(info)
1313 else:
1314 infoStr = ""
1315
1316 return self.tr(
1317 """<h3>Repository information</h3>\n"""
1318 """<p><table>\n"""
1319 """<tr><td><b>Git V.</b></td><td>{0}</td></tr>\n"""
1320 """<tr></tr>\n"""
1321 """{1}"""
1322 """</table></p>\n"""
1323 ).format(self.versionStr, infoStr)
1324
1325 def vcsSupportCommandOptions(self):
1326 """
1327 Public method to signal the support of user settable command options.
1328
1329 @return flag indicating the support of user settable command options
1330 (boolean)
1331 """
1332 return False
1333
1334 ###########################################################################
1335 ## Git specific methods are below.
1336 ###########################################################################
1337
1338 def gitNormalizeURL(self, url):
1339 """
1340 Public method to normalize a url for Git.
1341
1342 @param url url string (string)
1343 @return properly normalized url for git (string)
1344 """
1345 url = url.replace('\\', '/')
1346 if url.endswith('/'):
1347 url = url[:-1]
1348 urll = url.split('//')
1349 return "{0}//{1}".format(urll[0], '/'.join(urll[1:]))
1350
1351 def gitCreateIgnoreFile(self, name, autoAdd=False):
1352 """
1353 Public method to create the ignore file.
1354
1355 @param name directory name to create the ignore file in (string)
1356 @param autoAdd flag indicating to add it automatically (boolean)
1357 @return flag indicating success
1358 """
1359 status = False
1360 ignorePatterns = [
1361 ".eric6project/",
1362 "_eric6project/",
1363 ".eric5project/",
1364 "_eric5project/",
1365 ".eric4project/",
1366 "_eric4project/",
1367 ".ropeproject/",
1368 "_ropeproject/",
1369 ".directory/",
1370 "*.pyc",
1371 "*.pyo",
1372 "*.orig",
1373 "*.bak",
1374 "*.rej",
1375 "*~",
1376 "cur/",
1377 "tmp/",
1378 "__pycache__/",
1379 "*.DS_Store",
1380 ]
1381
1382 ignoreName = os.path.join(name, Git.IgnoreFileName)
1383 if os.path.exists(ignoreName):
1384 res = E5MessageBox.yesNo(
1385 self.__ui,
1386 self.tr("Create {0} file").format(ignoreName),
1387 self.tr("""<p>The file <b>{0}</b> exists already."""
1388 """ Overwrite it?</p>""").format(ignoreName),
1389 icon=E5MessageBox.Warning)
1390 else:
1391 res = True
1392 if res:
1393 try:
1394 # create a .gitignore file
1395 ignore = open(ignoreName, "w")
1396 ignore.write("\n".join(ignorePatterns))
1397 ignore.write("\n")
1398 ignore.close()
1399 status = True
1400 except IOError:
1401 status = False
1402
1403 if status and autoAdd:
1404 self.vcsAdd(ignoreName, noDialog=True)
1405 project = e5App().getObject("Project")
1406 project.appendFile(ignoreName)
1407
1408 return status
1409
1410 def gitCopy(self, name, project):
1411 """
1412 Public method used to copy a file/directory.
1413
1414 @param name file/directory name to be copied (string)
1415 @param project reference to the project object
1416 @return flag indicating successful operation (boolean)
1417 """
1418 from .GitCopyDialog import GitCopyDialog
1419 dlg = GitCopyDialog(name)
1420 if dlg.exec_() == QDialog.Accepted:
1421 target, force = dlg.getData()
1422
1423 # step 1: copy the file/directory:
1424 if os.path.isdir(name):
1425 try:
1426 shutil.copytree(name, target)
1427 except (OSError, shutil.Error) as why:
1428 E5MessageBox.critical(
1429 self,
1430 self.tr("Git Copy"),
1431 self.tr("""<p>Copying the directory <b>{0}</b>"""
1432 """ failed.</p><p>Reason: {1}</p>""").format(
1433 name, str(why)))
1434 return False
1435 self.vcsAdd(target, isDir=True)
1436 if target.startswith(project.getProjectPath()):
1437 project.copyDirectory(name, target)
1438
1439 else:
1440 try:
1441 shutil.copy2(name, target)
1442 except (OSError, shutil.Error) as why:
1443 E5MessageBox.critical(
1444 self,
1445 self.tr("Git Copy"),
1446 self.tr("""<p>Copying the file <b>{0}</b>"""
1447 """ failed.</p><p>Reason: {1}</p>""").format(
1448 name, str(why)))
1449 return False
1450 self.vcsAdd(target, isDir=False)
1451 if target.startswith(project.getProjectPath()):
1452 project.appendFile(target)
1453 self.checkVCSStatus()
1454 return True
1455
1456 def gitBlame(self, name):
1457 """
1458 Public method to show the output of the git blame command.
1459
1460 @param name file name to show the annotations for (string)
1461 """
1462 if self.blame is None:
1463 from .GitBlameDialog import GitBlameDialog
1464 self.blame = GitBlameDialog(self)
1465 self.blame.show()
1466 self.blame.raise_()
1467 self.blame.start(name)
1468
1469 def gitExtendedDiff(self, name):
1470 """
1471 Public method used to view the difference of a file/directory to the
1472 Git repository.
1473
1474 If name is a directory and is the project directory, all project files
1475 are saved first. If name is a file (or list of files), which is/are
1476 being edited and has unsaved modification, they can be saved or the
1477 operation may be aborted.
1478
1479 This method gives the chance to enter the revisions to be compared.
1480
1481 @param name file/directory name to be diffed (string)
1482 """
1483 if isinstance(name, list):
1484 dname, fnames = self.splitPathList(name)
1485 names = name[:]
1486 else:
1487 dname, fname = self.splitPath(name)
1488 names = [name]
1489 for nam in names:
1490 if os.path.isfile(nam):
1491 editor = e5App().getObject("ViewManager").getOpenEditor(nam)
1492 if editor and not editor.checkDirty():
1493 return
1494 else:
1495 project = e5App().getObject("Project")
1496 if nam == project.ppath and not project.saveAllScripts():
1497 return
1498
1499 # find the root of the repo
1500 repodir = dname
1501 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1502 repodir = os.path.dirname(repodir)
1503 if os.path.splitdrive(repodir)[1] == os.sep:
1504 return
1505
1506 from .GitRevisionsSelectionDialog import GitRevisionsSelectionDialog
1507 dlg = GitRevisionsSelectionDialog(self.gitGetTagsList(repodir),
1508 self.gitGetBranchesList(repodir))
1509 if dlg.exec_() == QDialog.Accepted:
1510 revisions = dlg.getRevisions()
1511 if self.diff is None:
1512 from .GitDiffDialog import GitDiffDialog
1513 self.diff = GitDiffDialog(self)
1514 self.diff.show()
1515 self.diff.raise_()
1516 self.diff.start(name, revisions)
1517
1518 def __gitGetFileForRevision(self, name, rev=""):
1519 """
1520 Private method to get a file for a specific revision from the
1521 repository.
1522
1523 @param name file name to get from the repository (string)
1524 @keyparam rev revision to retrieve (string)
1525 @return contents of the file (string) and an error message (string)
1526 """
1527 # find the root of the repo
1528 repodir = self.splitPath(name)[0]
1529 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1530 repodir = os.path.dirname(repodir)
1531 if os.path.splitdrive(repodir)[1] == os.sep:
1532 return
1533
1534 args = self.initCommand("cat-file")
1535 args.append("blob")
1536 args.append("{0}:{1}".format(rev, name.replace(repodir + os.sep, "")))
1537
1538 output = ""
1539 error = ""
1540
1541 process = QProcess()
1542 process.setWorkingDirectory(repodir)
1543 process.start('git', args)
1544 procStarted = process.waitForStarted(5000)
1545 if procStarted:
1546 finished = process.waitForFinished(30000)
1547 if finished:
1548 if process.exitCode() == 0:
1549 output = str(process.readAllStandardOutput(),
1550 Preferences.getSystem("IOEncoding"),
1551 'replace')
1552 else:
1553 error = str(process.readAllStandardError(),
1554 Preferences.getSystem("IOEncoding"),
1555 'replace')
1556 else:
1557 error = self.tr(
1558 "The git process did not finish within 30s.")
1559 else:
1560 error = self.tr(
1561 'The process {0} could not be started. '
1562 'Ensure, that it is in the search path.').format('git')
1563
1564 # return file contents with 'universal newlines'
1565 return output.replace('\r\n', '\n').replace('\r', '\n'), error
1566
1567 def gitSbsDiff(self, name, extended=False, revisions=None):
1568 """
1569 Public method used to view the difference of a file to the Git
1570 repository side-by-side.
1571
1572 @param name file name to be diffed (string)
1573 @keyparam extended flag indicating the extended variant (boolean)
1574 @keyparam revisions tuple of two revisions (tuple of strings)
1575 @exception ValueError raised to indicate an invalid name parameter
1576 """
1577 if isinstance(name, list):
1578 raise ValueError("Wrong parameter type")
1579
1580 if extended:
1581 # find the root of the repo
1582 repodir = self.splitPath(name)[0]
1583 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1584 repodir = os.path.dirname(repodir)
1585 if os.path.splitdrive(repodir)[1] == os.sep:
1586 return
1587
1588 from .GitRevisionsSelectionDialog import \
1589 GitRevisionsSelectionDialog
1590 dlg = GitRevisionsSelectionDialog(self.gitGetTagsList(repodir),
1591 self.gitGetBranchesList(repodir))
1592 if dlg.exec_() == QDialog.Accepted:
1593 rev1, rev2 = dlg.getRevisions()
1594 elif revisions:
1595 rev1, rev2 = revisions[0], revisions[1]
1596 else:
1597 rev1, rev2 = "", ""
1598
1599 output1, error = self.__gitGetFileForRevision(name, rev=rev1)
1600 if error:
1601 E5MessageBox.critical(
1602 self.__ui,
1603 self.tr("Git Side-by-Side Difference"),
1604 error)
1605 return
1606 name1 = "{0} (rev. {1})".format(name, rev1 and rev1 or "Stage")
1607
1608 if rev2:
1609 if rev2 == "Stage":
1610 rev2 = ""
1611 output2, error = self.__gitGetFileForRevision(name, rev=rev2)
1612 if error:
1613 E5MessageBox.critical(
1614 self.__ui,
1615 self.tr("Git Side-by-Side Difference"),
1616 error)
1617 return
1618 name2 = "{0} (rev. {1})".format(name, rev2)
1619 else:
1620 try:
1621 f1 = open(name, "r", encoding="utf-8")
1622 output2 = f1.read()
1623 f1.close()
1624 name2 = "{0} (Work)".format(name)
1625 except IOError:
1626 E5MessageBox.critical(
1627 self.__ui,
1628 self.tr("Git Side-by-Side Difference"),
1629 self.tr(
1630 """<p>The file <b>{0}</b> could not be read.</p>""")
1631 .format(name))
1632 return
1633
1634 if self.sbsDiff is None:
1635 from UI.CompareDialog import CompareDialog
1636 self.sbsDiff = CompareDialog()
1637 self.sbsDiff.show()
1638 self.sbsDiff.raise_()
1639 self.sbsDiff.compare(output1, output2, name1, name2)
1640
1641 def gitFetch(self, name):
1642 """
1643 Public method to fetch changes from a remote repository.
1644
1645 @param name directory name (string)
1646 """
1647 # find the root of the repo
1648 repodir = self.splitPath(name)[0]
1649 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1650 repodir = os.path.dirname(repodir)
1651 if os.path.splitdrive(repodir)[1] == os.sep:
1652 return
1653
1654 from .GitFetchDialog import GitFetchDialog
1655 dlg = GitFetchDialog(self, repodir)
1656 if dlg.exec_() == QDialog.Accepted:
1657 (remote, url, remoteBranches, localBranch, fetchAll, prune,
1658 includeTags) = dlg.getData()
1659
1660 args = self.initCommand("fetch")
1661 args.append('--verbose')
1662 if prune:
1663 args.append('--prune')
1664 if includeTags:
1665 args.append("--tags")
1666 if fetchAll:
1667 args.append('--all')
1668 else:
1669 args.append(remote) if remote else args.append(url)
1670 if len(remoteBranches) == 1 and localBranch:
1671 args.append(remoteBranches[0] + ":" + localBranch)
1672 else:
1673 args.extend(remoteBranches)
1674
1675 dia = GitDialog(self.tr('Fetching from a remote Git repository'),
1676 self)
1677 res = dia.startProcess(args, repodir)
1678 if res:
1679 dia.exec_()
1680 self.checkVCSStatus()
1681
1682 def gitPull(self, name):
1683 """
1684 Public method used to pull changes from a remote Git repository.
1685
1686 @param name directory name of the project to be pulled to (string)
1687 @return flag indicating, that the update contained an add
1688 or delete (boolean)
1689 """
1690 # find the root of the repo
1691 repodir = self.splitPath(name)[0]
1692 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1693 repodir = os.path.dirname(repodir)
1694 if os.path.splitdrive(repodir)[1] == os.sep:
1695 return False
1696
1697 from .GitPullDialog import GitPullDialog
1698 dlg = GitPullDialog(self, repodir)
1699 if dlg.exec_() == QDialog.Accepted:
1700 remote, url, branches, pullAll, prune = dlg.getData()
1701
1702 args = self.initCommand('pull')
1703 args.append('--no-commit')
1704 args.append('--verbose')
1705 if prune:
1706 args.append('--prune')
1707 if pullAll:
1708 args.append('--all')
1709 else:
1710 args.append(remote) if remote else args.append(url)
1711 args.extend(branches)
1712
1713 dia = GitDialog(self.tr('Pulling from a remote Git repository'),
1714 self)
1715 res = dia.startProcess(args, repodir)
1716 if res:
1717 dia.exec_()
1718 res = dia.hasAddOrDelete()
1719 self.checkVCSStatus()
1720 return res
1721 else:
1722 return False
1723
1724 def gitPush(self, name):
1725 """
1726 Public method used to push changes to a remote Git repository.
1727
1728 @param name directory name of the project to be pushed from (string)
1729 """
1730 # find the root of the repo
1731 repodir = self.splitPath(name)[0]
1732 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1733 repodir = os.path.dirname(repodir)
1734 if os.path.splitdrive(repodir)[1] == os.sep:
1735 return
1736
1737 from .GitPushDialog import GitPushDialog
1738 dlg = GitPushDialog(self, repodir)
1739 if dlg.exec_() == QDialog.Accepted:
1740 remote, refspecs, tags, tracking, submodule = dlg.getData()
1741
1742 args = self.initCommand("push")
1743 args.append('--verbose')
1744 args.append('--porcelain')
1745 if tags:
1746 args.append("--tags")
1747 if tracking:
1748 args.append("--set-upstream")
1749 if submodule:
1750 args.append("--recurse-submodules={0}".format(submodule))
1751 args.append(remote)
1752 args.extend(refspecs)
1753
1754 dia = GitDialog(
1755 self.tr('Pushing to a remote Git repository'), self)
1756 res = dia.startProcess(args, repodir)
1757 if res:
1758 dia.exec_()
1759 self.checkVCSStatus()
1760
1761 def gitCommitMerge(self, name):
1762 """
1763 Public method to commit a failed merge.
1764
1765 @param name file/directory name (string)
1766 """
1767 dname, fname = self.splitPath(name)
1768
1769 # find the root of the repo
1770 repodir = dname
1771 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1772 repodir = os.path.dirname(repodir)
1773 if os.path.splitdrive(repodir)[1] == os.sep:
1774 return
1775
1776 import sys
1777 editor = sys.argv[0].replace(".py", "_editor.py")
1778 env = {"GIT_EDITOR": "{0} {1}".format(sys.executable, editor)}
1779
1780 args = self.initCommand("commit")
1781
1782 dia = GitDialog(self.tr('Committing failed merge'), self)
1783 res = dia.startProcess(args, repodir, environment=env)
1784 if res:
1785 dia.exec_()
1786 self.committed.emit()
1787 self.checkVCSStatus()
1788
1789 def gitCancelMerge(self, name):
1790 """
1791 Public method to cancel an uncommitted or failed merge.
1792
1793 @param name file/directory name (string)
1794 @return flag indicating, that the cancellation contained an add
1795 or delete (boolean)
1796 """
1797 dname, fname = self.splitPath(name)
1798
1799 # find the root of the repo
1800 repodir = dname
1801 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1802 repodir = os.path.dirname(repodir)
1803 if os.path.splitdrive(repodir)[1] == os.sep:
1804 return
1805
1806 args = self.initCommand("merge")
1807 args.append("--abort")
1808
1809 dia = GitDialog(
1810 self.tr('Aborting uncommitted/failed merge'),
1811 self)
1812 res = dia.startProcess(args, repodir, False)
1813 if res:
1814 dia.exec_()
1815 res = dia.hasAddOrDelete()
1816 self.checkVCSStatus()
1817 return res
1818
1819 def gitApply(self, repodir, patchFile, cached=False, reverse=False,
1820 noDialog=False):
1821 """
1822 Public method to apply a patch stored in a given file.
1823
1824 @param repodir directory name of the repository (string)
1825 @param patchFile name of the patch file (string)
1826 @param cached flag indicating to apply the patch to the staging area
1827 (boolean)
1828 @param reverse flag indicating to apply the patch in reverse (boolean)
1829 @param noDialog flag indicating quiet operations (boolean)
1830 """
1831 args = self.initCommand("apply")
1832 if cached:
1833 args.append("--index")
1834 args.append("--cached")
1835 if reverse:
1836 args.append("--reverse")
1837 args.append(patchFile)
1838
1839 if noDialog:
1840 self.startSynchronizedProcess(QProcess(), "git", args, repodir)
1841 else:
1842 dia = GitDialog(
1843 self.tr('Applying patch'),
1844 self)
1845 res = dia.startProcess(args, repodir)
1846 if res:
1847 dia.exec_()
1848
1849 def gitApplyCheckPatches(self, projectDir, check=False):
1850 """
1851 Public method to apply a list of patch files or check, if they would
1852 apply cleanly.
1853
1854 @param projectDir directory name of the project (string)
1855 @param check flag indicating to perform a check operation (boolean)
1856 """
1857 # find the root of the repo
1858 repodir = projectDir
1859 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1860 repodir = os.path.dirname(repodir)
1861 if os.path.splitdrive(repodir)[1] == os.sep:
1862 return
1863
1864 from .GitPatchFilesDialog import GitPatchFilesDialog
1865 dlg = GitPatchFilesDialog(repodir, self.__patchCheckData)
1866 if dlg.exec_() == QDialog.Accepted:
1867 patchFilesList, stripCount, inaccurateEof, recount = dlg.getData()
1868 if patchFilesList:
1869 args = self.initCommand("apply")
1870 if check:
1871 args.append("--check")
1872 self.__patchCheckData = (
1873 patchFilesList, stripCount, inaccurateEof, recount)
1874 title = self.tr('Check patch files')
1875 else:
1876 self.__patchCheckData = None
1877 title = self.tr('Apply patch files')
1878 if inaccurateEof:
1879 args.append("--inaccurate-eof")
1880 if recount:
1881 args.append("--recount")
1882 args.append("-p{0}".format(stripCount))
1883 args.extend(patchFilesList)
1884
1885 dia = GitDialog(
1886 title,
1887 self)
1888 res = dia.startProcess(args, repodir)
1889 if res:
1890 dia.exec_()
1891 if not check:
1892 self.checkVCSStatus()
1893
1894 def gitShowPatchesStatistics(self, projectDir):
1895 """
1896 Public method to show statistics for a set of patch files.
1897
1898 @param projectDir directory name of the project (string)
1899 """
1900 if self.patchStatisticsDialog is None:
1901 from .GitPatchStatisticsDialog import GitPatchStatisticsDialog
1902 self.patchStatisticsDialog = GitPatchStatisticsDialog(self)
1903 self.patchStatisticsDialog.show()
1904 self.patchStatisticsDialog.raise_()
1905 self.patchStatisticsDialog.start(projectDir, self.__patchCheckData)
1906 self.__patchCheckData = self.patchStatisticsDialog.getData()
1907
1908 ###########################################################################
1909 ## Methods for tag handling.
1910 ###########################################################################
1911
1912 def vcsTag(self, name, revision=None, tagName=None):
1913 """
1914 Public method used to set/remove a tag in the Git repository.
1915
1916 @param name file/directory name to determine the repo root from
1917 (string)
1918 @param revision revision to set tag for (string)
1919 @param tagName name of the tag (string)
1920 @return flag indicating a performed tag action (boolean)
1921 """
1922 dname, fname = self.splitPath(name)
1923
1924 # find the root of the repo
1925 repodir = dname
1926 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1927 repodir = os.path.dirname(repodir)
1928 if os.path.splitdrive(repodir)[1] == os.sep:
1929 return
1930
1931 from .GitTagDialog import GitTagDialog
1932 dlg = GitTagDialog(self.gitGetTagsList(repodir), revision, tagName)
1933 if dlg.exec_() == QDialog.Accepted:
1934 tag, revision, tagOp, tagType, force = dlg.getParameters()
1935 else:
1936 return False
1937
1938 args = self.initCommand("tag")
1939 if tagOp == GitTagDialog.CreateTag:
1940 msg = "Created tag <{0}>.".format(tag)
1941 if tagType == GitTagDialog.AnnotatedTag:
1942 args.append("--annotate")
1943 args.append("--message={0}".format(msg))
1944 elif tagType == GitTagDialog.SignedTag:
1945 args.append("--sign")
1946 args.append("--message={0}".format(msg))
1947 if force:
1948 args.append("--force")
1949 elif tagOp == GitTagDialog.DeleteTag:
1950 args.append("--delete")
1951 elif tagOp == GitTagDialog.VerifyTag:
1952 args.append("--verify")
1953 else:
1954 return False
1955 args.append(tag)
1956 if tagOp == GitTagDialog.CreateTag and revision:
1957 args.append(revision)
1958
1959 dia = GitDialog(self.tr('Tagging in the Git repository'),
1960 self)
1961 res = dia.startProcess(args, repodir)
1962 if res:
1963 dia.exec_()
1964
1965 return True
1966
1967 def gitGetTagsList(self, repodir, withType=False):
1968 """
1969 Public method to get the list of tags.
1970
1971 @param repodir directory name of the repository (string)
1972 @param withType flag indicating to get the tag type as well (boolean)
1973 @return list of tags (list of string) or list of tuples of
1974 tag name and flag indicating a local tag (list of tuple of string
1975 and boolean), if withType is True
1976 """
1977 args = self.initCommand("tag")
1978 args.append('--list')
1979
1980 output = ""
1981 process = QProcess()
1982 process.setWorkingDirectory(repodir)
1983 process.start('git', args)
1984 procStarted = process.waitForStarted(5000)
1985 if procStarted:
1986 finished = process.waitForFinished(30000)
1987 if finished and process.exitCode() == 0:
1988 output = str(process.readAllStandardOutput(),
1989 Preferences.getSystem("IOEncoding"),
1990 'replace')
1991
1992 tagsList = []
1993 if output:
1994 for line in output.splitlines():
1995 name = line.strip()
1996 tagsList.append(name)
1997
1998 return tagsList
1999
2000 def gitListTagBranch(self, path, tags=True, listAll=True, merged=True):
2001 """
2002 Public method used to list the available tags or branches.
2003
2004 @param path directory name of the project (string)
2005 @param tags flag indicating listing of branches or tags
2006 (False = branches, True = tags)
2007 @param listAll flag indicating to show all tags or branches (boolean)
2008 @param merged flag indicating to show only merged or non-merged
2009 branches (boolean)
2010 """
2011 if self.tagbranchList is None:
2012 from .GitTagBranchListDialog import GitTagBranchListDialog
2013 self.tagbranchList = GitTagBranchListDialog(self)
2014 self.tagbranchList.show()
2015 self.tagbranchList.raise_()
2016 if tags:
2017 self.tagbranchList.start(path, tags)
2018 else:
2019 self.tagbranchList.start(path, tags, listAll, merged)
2020
2021 ###########################################################################
2022 ## Methods for branch handling.
2023 ###########################################################################
2024
2025 def gitGetBranchesList(self, repodir, withMaster=False, allBranches=False,
2026 remotes=False):
2027 """
2028 Public method to get the list of branches.
2029
2030 @param repodir directory name of the repository (string)
2031 @param withMaster flag indicating to get 'master' as well (boolean)
2032 @param allBranches flag indicating to return all branches (boolean)
2033 @param remotes flag indicating to return remote branches only (boolean)
2034 @return list of branches (list of string)
2035 """
2036 args = self.initCommand("branch")
2037 args.append('--list')
2038 if allBranches:
2039 args.append("--all")
2040 elif remotes:
2041 args.append("--remotes")
2042
2043 output = ""
2044 process = QProcess()
2045 process.setWorkingDirectory(repodir)
2046 process.start('git', args)
2047 procStarted = process.waitForStarted(5000)
2048 if procStarted:
2049 finished = process.waitForFinished(30000)
2050 if finished and process.exitCode() == 0:
2051 output = str(process.readAllStandardOutput(),
2052 Preferences.getSystem("IOEncoding"),
2053 'replace')
2054
2055 branchesList = []
2056 if output:
2057 for line in output.splitlines():
2058 name = line[2:].strip()
2059 if (
2060 (name != "master" or withMaster) and
2061 "->" not in name and
2062 not name.startswith("(") and
2063 not name.endswith(")")
2064 ):
2065 branchesList.append(name)
2066
2067 return branchesList
2068
2069 def gitGetCurrentBranch(self, repodir):
2070 """
2071 Public method used to show the current branch of the working directory.
2072
2073 @param repodir directory name of the repository (string)
2074 @return name of the current branch (string)
2075 """
2076 args = self.initCommand("branch")
2077 args.append('--list')
2078
2079 branchName = ""
2080 output = ""
2081 process = QProcess()
2082 process.setWorkingDirectory(repodir)
2083 process.start('git', args)
2084 procStarted = process.waitForStarted(5000)
2085 if procStarted:
2086 finished = process.waitForFinished(30000)
2087 if finished and process.exitCode() == 0:
2088 output = str(process.readAllStandardOutput(),
2089 Preferences.getSystem("IOEncoding"),
2090 'replace')
2091
2092 if output:
2093 for line in output.splitlines():
2094 if line.startswith("* "):
2095 branchName = line[2:].strip()
2096 if branchName.startswith("(") and branchName.endswith(")"):
2097 # not a valid branch name, probably detached head
2098 branchName = ""
2099 break
2100
2101 return branchName
2102
2103 def gitBranch(self, name, revision=None, branchName=None, branchOp=None):
2104 """
2105 Public method used to create, delete or move a branch in the Git
2106 repository.
2107
2108 @param name file/directory name to be branched (string)
2109 @param revision revision to set tag for (string)
2110 @param branchName name of the branch (string)
2111 @param branchOp desired branch operation (integer)
2112 @return flag indicating a performed branch action (boolean) and
2113 a flag indicating, that the branch operation contained an add
2114 or delete (boolean)
2115 """
2116 dname, fname = self.splitPath(name)
2117
2118 # find the root of the repo
2119 repodir = dname
2120 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2121 repodir = os.path.dirname(repodir)
2122 if os.path.splitdrive(repodir)[1] == os.sep:
2123 return False, False
2124
2125 from .GitBranchDialog import GitBranchDialog
2126 dlg = GitBranchDialog(
2127 self.gitGetBranchesList(repodir, allBranches=True),
2128 revision, branchName, branchOp)
2129 if dlg.exec_() == QDialog.Accepted:
2130 branchOp, branch, revision, newBranch, remoteBranch, force = \
2131 dlg.getParameters()
2132 else:
2133 return False, False
2134
2135 if branchOp in [GitBranchDialog.CreateBranch,
2136 GitBranchDialog.DeleteBranch,
2137 GitBranchDialog.RenameBranch,
2138 GitBranchDialog.SetTrackingBranch,
2139 GitBranchDialog.UnsetTrackingBranch]:
2140 args = self.initCommand("branch")
2141 if branchOp == GitBranchDialog.CreateBranch:
2142 if force:
2143 args.append("--force")
2144 args.append(branch)
2145 if revision:
2146 args.append(revision)
2147 elif branchOp == GitBranchDialog.DeleteBranch:
2148 if force:
2149 args.append("-D")
2150 else:
2151 args.append("-d")
2152 args.append(branch)
2153 elif branchOp == GitBranchDialog.RenameBranch:
2154 if force:
2155 args.append("-M")
2156 else:
2157 args.append("-m")
2158 args.append(branch)
2159 args.append(newBranch)
2160 elif branchOp == GitBranchDialog.SetTrackingBranch:
2161 args.append("--set-upstream-to={0}".format(remoteBranch))
2162 if branch:
2163 args.append(branch)
2164 elif branchOp == GitBranchDialog.UnsetTrackingBranch:
2165 args.append("--unset-upstream")
2166 if branch:
2167 args.append(branch)
2168 elif branchOp in [GitBranchDialog.CreateSwitchBranch,
2169 GitBranchDialog.CreateTrackingBranch]:
2170 args = self.initCommand("checkout")
2171 if branchOp == GitBranchDialog.CreateSwitchBranch:
2172 if force:
2173 args.append("-B")
2174 else:
2175 args.append("-b")
2176 args.append(branch)
2177 if revision:
2178 args.append(revision)
2179 elif branchOp == GitBranchDialog.CreateTrackingBranch:
2180 args.append("--track")
2181 args.append(branch)
2182 else:
2183 return False, False
2184
2185 dia = GitDialog(self.tr('Branching in the Git repository'),
2186 self)
2187 res = dia.startProcess(args, repodir)
2188 if res:
2189 dia.exec_()
2190 if branchOp in [GitBranchDialog.CreateSwitchBranch,
2191 GitBranchDialog.CreateTrackingBranch]:
2192 update = dia.hasAddOrDelete()
2193 self.checkVCSStatus()
2194 else:
2195 update = False
2196
2197 return True, update
2198
2199 def gitDeleteRemoteBranch(self, name):
2200 """
2201 Public method to delete a branch from a remote repository.
2202
2203 @param name file/directory name (string)
2204 """
2205 dname, fname = self.splitPath(name)
2206
2207 # find the root of the repo
2208 repodir = dname
2209 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2210 repodir = os.path.dirname(repodir)
2211 if os.path.splitdrive(repodir)[1] == os.sep:
2212 return
2213
2214 from .GitBranchPushDialog import GitBranchPushDialog
2215 dlg = GitBranchPushDialog(self.gitGetBranchesList(repodir),
2216 self.gitGetRemotesList(repodir),
2217 delete=True)
2218 if dlg.exec_() == QDialog.Accepted:
2219 branchName, remoteName = dlg.getData()[:2]
2220
2221 args = self.initCommand("push")
2222 args.append(remoteName)
2223 args.append(":{0}".format(branchName))
2224
2225 dia = GitDialog(self.tr('Delete Remote Branch'), self)
2226 res = dia.startProcess(args, repodir)
2227 if res:
2228 dia.exec_()
2229
2230 def gitShowBranch(self, name):
2231 """
2232 Public method used to show the current branch of the working directory.
2233
2234 @param name file/directory name (string)
2235 """
2236 dname, fname = self.splitPath(name)
2237
2238 # find the root of the repo
2239 repodir = dname
2240 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2241 repodir = os.path.dirname(repodir)
2242 if os.path.splitdrive(repodir)[1] == os.sep:
2243 return
2244
2245 branchName = self.gitGetCurrentBranch(repodir)
2246 E5MessageBox.information(
2247 None,
2248 self.tr("Current Branch"),
2249 self.tr("""<p>The current branch is <b>{0}</b>."""
2250 """</p>""").format(branchName))
2251
2252 ###########################################################################
2253 ## Methods for bundles handling.
2254 ###########################################################################
2255
2256 def gitBundle(self, projectDir):
2257 """
2258 Public method to create a bundle file.
2259
2260 @param projectDir name of the project directory (string)
2261 """
2262 # find the root of the repo
2263 repodir = projectDir
2264 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2265 repodir = os.path.dirname(repodir)
2266 if os.path.splitdrive(repodir)[1] == os.sep:
2267 return
2268
2269 from .GitBundleDialog import GitBundleDialog
2270 dlg = GitBundleDialog(self.gitGetTagsList(repodir),
2271 self.gitGetBranchesList(repodir))
2272 if dlg.exec_() == QDialog.Accepted:
2273 revs = dlg.getData()
2274
2275 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
2276 None,
2277 self.tr("Create Bundle"),
2278 self.__lastBundlePath or repodir,
2279 self.tr("Git Bundle Files (*.bundle)"),
2280 None,
2281 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
2282
2283 if not fname:
2284 return # user aborted
2285
2286 ext = QFileInfo(fname).suffix()
2287 if not ext:
2288 ex = selectedFilter.split("(*")[1].split(")")[0]
2289 if ex:
2290 fname += ex
2291 if QFileInfo(fname).exists():
2292 res = E5MessageBox.yesNo(
2293 self.__ui,
2294 self.tr("Create Bundle"),
2295 self.tr("<p>The Git bundle file <b>{0}</b> "
2296 "already exists. Overwrite it?</p>")
2297 .format(fname),
2298 icon=E5MessageBox.Warning)
2299 if not res:
2300 return
2301 fname = Utilities.toNativeSeparators(fname)
2302 self.__lastBundlePath = os.path.dirname(fname)
2303
2304 args = self.initCommand("bundle")
2305 args.append("create")
2306 args.append(fname)
2307 for rev in revs:
2308 args.append(rev)
2309
2310 dia = GitDialog(self.tr('Create Bundle'), self)
2311 res = dia.startProcess(args, repodir)
2312 if res:
2313 dia.exec_()
2314
2315 def gitVerifyBundle(self, projectDir):
2316 """
2317 Public method to verify a bundle file.
2318
2319 @param projectDir name of the project directory (string)
2320 """
2321 # find the root of the repo
2322 repodir = projectDir
2323 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2324 repodir = os.path.dirname(repodir)
2325 if os.path.splitdrive(repodir)[1] == os.sep:
2326 return
2327
2328 fname = E5FileDialog.getOpenFileName(
2329 None,
2330 self.tr("Verify Bundle"),
2331 self.__lastBundlePath or repodir,
2332 self.tr("Git Bundle Files (*.bundle);;All Files (*)"))
2333 if fname:
2334 self.__lastBundlePath = os.path.dirname(fname)
2335
2336 args = self.initCommand("bundle")
2337 args.append("verify")
2338 args.append(fname)
2339
2340 dia = GitDialog(self.tr('Verify Bundle'), self)
2341 res = dia.startProcess(args, repodir)
2342 if res:
2343 dia.exec_()
2344
2345 def gitBundleListHeads(self, projectDir):
2346 """
2347 Public method to list the heads contained in a bundle file.
2348
2349 @param projectDir name of the project directory (string)
2350 """
2351 # find the root of the repo
2352 repodir = projectDir
2353 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2354 repodir = os.path.dirname(repodir)
2355 if os.path.splitdrive(repodir)[1] == os.sep:
2356 return
2357
2358 fname = E5FileDialog.getOpenFileName(
2359 None,
2360 self.tr("List Bundle Heads"),
2361 self.__lastBundlePath or repodir,
2362 self.tr("Git Bundle Files (*.bundle);;All Files (*)"))
2363 if fname:
2364 self.__lastBundlePath = os.path.dirname(fname)
2365
2366 args = self.initCommand("bundle")
2367 args.append("list-heads")
2368 args.append(fname)
2369
2370 dia = GitDialog(self.tr('List Bundle Heads'), self)
2371 res = dia.startProcess(args, repodir)
2372 if res:
2373 dia.exec_()
2374
2375 def gitGetBundleHeads(self, repodir, bundleFile):
2376 """
2377 Public method to get a list of heads contained in a bundle file.
2378
2379 @param repodir directory name of the repository (string)
2380 @param bundleFile file name of a git bundle file (string)
2381 @return list of heads (list of strings)
2382 """
2383 args = self.initCommand("bundle")
2384 args.append("list-heads")
2385 args.append(bundleFile)
2386
2387 output = ""
2388 process = QProcess()
2389 process.setWorkingDirectory(repodir)
2390 process.start('git', args)
2391 procStarted = process.waitForStarted(5000)
2392 if procStarted:
2393 finished = process.waitForFinished(30000)
2394 if finished and process.exitCode() == 0:
2395 output = str(process.readAllStandardOutput(),
2396 Preferences.getSystem("IOEncoding"),
2397 'replace')
2398
2399 heads = []
2400 if output:
2401 for line in output.splitlines():
2402 head = line.strip().split(None, 1)[1] # commit id, head
2403 heads.append(head.replace("refs/heads/", ""))
2404
2405 return heads
2406
2407 def gitBundleFetch(self, projectDir):
2408 """
2409 Public method to fetch a head of a bundle file into the local
2410 repository.
2411
2412 @param projectDir name of the project directory (string)
2413 """
2414 # find the root of the repo
2415 repodir = projectDir
2416 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2417 repodir = os.path.dirname(repodir)
2418 if os.path.splitdrive(repodir)[1] == os.sep:
2419 return
2420
2421 fname = E5FileDialog.getOpenFileName(
2422 None,
2423 self.tr("Apply Bundle"),
2424 self.__lastBundlePath or repodir,
2425 self.tr("Git Bundle Files (*.bundle);;All Files (*)"))
2426 if fname:
2427 self.__lastBundlePath = os.path.dirname(fname)
2428
2429 from .GitApplyBundleDataDialog import GitApplyBundleDataDialog
2430 dlg = GitApplyBundleDataDialog(
2431 self.gitGetBundleHeads(repodir, fname),
2432 self.gitGetBranchesList(repodir))
2433 if dlg.exec_() == QDialog.Accepted:
2434 bundleHead, branch = dlg.getData()
2435
2436 args = self.initCommand("fetch")
2437 args.append('--verbose')
2438 args.append(fname)
2439 if branch:
2440 args.append(bundleHead + ":" + branch)
2441 else:
2442 args.append(bundleHead)
2443
2444 dia = GitDialog(self.tr('Applying a bundle file (fetch)'),
2445 self)
2446 res = dia.startProcess(args, repodir)
2447 if res:
2448 dia.exec_()
2449 res = dia.hasAddOrDelete()
2450 self.checkVCSStatus()
2451
2452 def gitBundlePull(self, projectDir):
2453 """
2454 Public method to pull a head of a bundle file into the local
2455 repository and working area.
2456
2457 @param projectDir name of the project directory (string)
2458 @return flag indicating, that the update contained an add
2459 or delete (boolean)
2460 """
2461 # find the root of the repo
2462 repodir = projectDir
2463 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2464 repodir = os.path.dirname(repodir)
2465 if os.path.splitdrive(repodir)[1] == os.sep:
2466 return False
2467
2468 res = False
2469 fname = E5FileDialog.getOpenFileName(
2470 None,
2471 self.tr("Apply Bundle"),
2472 self.__lastBundlePath or repodir,
2473 self.tr("Git Bundle Files (*.bundle);;All Files (*)"))
2474 if fname:
2475 self.__lastBundlePath = os.path.dirname(fname)
2476
2477 from .GitApplyBundleDataDialog import GitApplyBundleDataDialog
2478 dlg = GitApplyBundleDataDialog(
2479 self.gitGetBundleHeads(repodir, fname),
2480 self.gitGetBranchesList(repodir))
2481 if dlg.exec_() == QDialog.Accepted:
2482 bundleHead, branch = dlg.getData()
2483
2484 args = self.initCommand("pull")
2485 args.append('--verbose')
2486 args.append(fname)
2487 if branch:
2488 args.append(bundleHead + ":" + branch)
2489 else:
2490 args.append(bundleHead)
2491
2492 dia = GitDialog(self.tr('Applying a bundle file (fetch)'),
2493 self)
2494 res = dia.startProcess(args, repodir)
2495 if res:
2496 dia.exec_()
2497 res = dia.hasAddOrDelete()
2498 self.checkVCSStatus()
2499
2500 return res
2501
2502 ###########################################################################
2503 ## Methods for bisect handling.
2504 ###########################################################################
2505
2506 def gitBisect(self, projectDir, subcommand):
2507 """
2508 Public method to perform bisect commands.
2509
2510 @param projectDir name of the project directory (string)
2511 @param subcommand name of the subcommand (string, one of 'start',
2512 'start_extended', 'good', 'bad', 'skip' or 'reset')
2513 @return flag indicating, that the update contained an add
2514 or delete (boolean)
2515 @exception ValueError raised to indicate an invalid bisect subcommand
2516 """
2517 if subcommand not in ("start", "start_extended", "good", "bad",
2518 "skip", "reset"):
2519 raise ValueError(
2520 self.tr("Bisect subcommand ({0}) invalid.")
2521 .format(subcommand))
2522
2523 # find the root of the repo
2524 repodir = projectDir
2525 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2526 repodir = os.path.dirname(repodir)
2527 if os.path.splitdrive(repodir)[1] == os.sep:
2528 return False
2529
2530 res = False
2531 rev = ""
2532 if subcommand in ("good", "bad", "skip", "reset"):
2533 showBranches = subcommand == "reset"
2534 showHead = subcommand == "reset"
2535 from .GitRevisionSelectionDialog import GitRevisionSelectionDialog
2536 dlg = GitRevisionSelectionDialog(self.gitGetTagsList(repodir),
2537 self.gitGetBranchesList(repodir),
2538 showBranches=showBranches,
2539 showHead=showHead)
2540 if dlg.exec_() == QDialog.Accepted:
2541 rev = dlg.getRevision()
2542 else:
2543 return False
2544
2545 args = self.initCommand("bisect")
2546 if subcommand == "start_extended":
2547 from .GitBisectStartDialog import GitBisectStartDialog
2548 dlg = GitBisectStartDialog()
2549 if dlg.exec_() == QDialog.Accepted:
2550 bad, good, noCheckout = dlg.getData()
2551 args.append("start")
2552 if noCheckout:
2553 args.append("--no-checkout")
2554 args.append(bad)
2555 args.extend(good)
2556 args.append("--")
2557 else:
2558 return False
2559 else:
2560 args.append(subcommand)
2561 if rev:
2562 args.extend(rev.split())
2563 # treat rev as a list separated by whitespace
2564
2565 dia = GitDialog(
2566 self.tr('Git Bisect ({0})').format(subcommand), self)
2567 res = dia.startProcess(args, repodir)
2568 if res:
2569 dia.exec_()
2570 res = dia.hasAddOrDelete()
2571 self.checkVCSStatus()
2572
2573 return res
2574
2575 def gitBisectLogBrowser(self, projectDir):
2576 """
2577 Public method used to browse the bisect log of the project.
2578
2579 @param projectDir name of the project directory (string)
2580 """
2581 if self.bisectlogBrowser is None:
2582 from .GitBisectLogBrowserDialog import GitBisectLogBrowserDialog
2583 self.bisectlogBrowser = GitBisectLogBrowserDialog(self)
2584 self.bisectlogBrowser.show()
2585 self.bisectlogBrowser.raise_()
2586 self.bisectlogBrowser.start(projectDir)
2587
2588 def gitBisectCreateReplayFile(self, projectDir):
2589 """
2590 Public method used to create a bisect replay file for the project.
2591
2592 @param projectDir name of the project directory (string)
2593 """
2594 # find the root of the repo
2595 repodir = projectDir
2596 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2597 repodir = os.path.dirname(repodir)
2598 if os.path.splitdrive(repodir)[1] == os.sep:
2599 return
2600
2601 args = self.initCommand("bisect")
2602 args.append("log")
2603
2604 output = ""
2605 process = QProcess()
2606 process.setWorkingDirectory(repodir)
2607 process.start('git', args)
2608 procStarted = process.waitForStarted(5000)
2609 if procStarted:
2610 finished = process.waitForFinished(30000)
2611 if finished and process.exitCode() == 0:
2612 output = str(process.readAllStandardOutput(),
2613 Preferences.getSystem("IOEncoding"),
2614 'replace')
2615 else:
2616 E5MessageBox.critical(
2617 self,
2618 self.tr('Process Generation Error'),
2619 self.tr(
2620 'The process {0} could not be started. '
2621 'Ensure, that it is in the search path.'
2622 ).format('git'))
2623 return
2624
2625 if output:
2626 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
2627 None,
2628 self.tr("Create Bisect Replay File"),
2629 self.__lastBundlePath or repodir,
2630 self.tr("Git Bisect Replay Files (*.replay)"),
2631 None,
2632 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
2633
2634 if not fname:
2635 return # user aborted
2636
2637 ext = QFileInfo(fname).suffix()
2638 if not ext:
2639 ex = selectedFilter.split("(*")[1].split(")")[0]
2640 if ex:
2641 fname += ex
2642 if QFileInfo(fname).exists():
2643 res = E5MessageBox.yesNo(
2644 self.__ui,
2645 self.tr("Create Bisect Replay File"),
2646 self.tr("<p>The Git bisect replay file <b>{0}</b> "
2647 "already exists. Overwrite it?</p>")
2648 .format(fname),
2649 icon=E5MessageBox.Warning)
2650 if not res:
2651 return
2652 fname = Utilities.toNativeSeparators(fname)
2653 self.__lastReplayPath = os.path.dirname(fname)
2654
2655 try:
2656 f = open(fname, "w")
2657 f.write(output)
2658 f.close()
2659 except (OSError, IOError) as err:
2660 E5MessageBox.critical(
2661 self.__ui,
2662 self.tr("Create Bisect Replay File"),
2663 self.tr(
2664 """<p>The file <b>{0}</b> could not be written.</p>"""
2665 """<p>Reason: {1}</p>""")
2666 .format(fname, str(err)))
2667
2668 def gitBisectEditReplayFile(self, projectDir):
2669 """
2670 Public method used to edit a bisect replay file.
2671
2672 @param projectDir name of the project directory (string)
2673 """
2674 # find the root of the repo
2675 repodir = projectDir
2676 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2677 repodir = os.path.dirname(repodir)
2678 if os.path.splitdrive(repodir)[1] == os.sep:
2679 return
2680
2681 fname = E5FileDialog.getOpenFileName(
2682 None,
2683 self.tr("Edit Bisect Replay File"),
2684 self.__lastReplayPath or repodir,
2685 self.tr("Git Bisect Replay Files (*.replay);;All Files (*)"))
2686 if fname:
2687 self.__lastReplayPath = os.path.dirname(fname)
2688
2689 self.bisectReplayEditor = MiniEditor(fname)
2690 self.bisectReplayEditor.show()
2691
2692 def gitBisectReplay(self, projectDir):
2693 """
2694 Public method to replay a bisect session.
2695
2696 @param projectDir name of the project directory (string)
2697 @return flag indicating, that the update contained an add
2698 or delete (boolean)
2699 """
2700 # find the root of the repo
2701 repodir = projectDir
2702 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2703 repodir = os.path.dirname(repodir)
2704 if os.path.splitdrive(repodir)[1] == os.sep:
2705 return False
2706
2707 res = False
2708 fname = E5FileDialog.getOpenFileName(
2709 None,
2710 self.tr("Bisect Replay"),
2711 self.__lastReplayPath or repodir,
2712 self.tr("Git Bisect Replay Files (*.replay);;All Files (*)"))
2713 if fname:
2714 self.__lastReplayPath = os.path.dirname(fname)
2715
2716 args = self.initCommand("bisect")
2717 args.append("replay")
2718 args.append(fname)
2719
2720 dia = GitDialog(
2721 self.tr('Git Bisect ({0})').format("replay"), self)
2722 res = dia.startProcess(args, repodir)
2723 if res:
2724 dia.exec_()
2725 res = dia.hasAddOrDelete()
2726 self.checkVCSStatus()
2727
2728 return res
2729
2730 ###########################################################################
2731 ## Methods for remotes handling.
2732 ###########################################################################
2733
2734 def gitGetRemotesList(self, repodir):
2735 """
2736 Public method to get the list of remote repos.
2737
2738 @param repodir directory name of the repository (string)
2739 @return list of remote repos (list of string)
2740 """
2741 args = self.initCommand("remote")
2742
2743 output = ""
2744 process = QProcess()
2745 process.setWorkingDirectory(repodir)
2746 process.start('git', args)
2747 procStarted = process.waitForStarted(5000)
2748 if procStarted:
2749 finished = process.waitForFinished(30000)
2750 if finished and process.exitCode() == 0:
2751 output = str(process.readAllStandardOutput(),
2752 Preferences.getSystem("IOEncoding"),
2753 'replace')
2754
2755 remotesList = []
2756 if output:
2757 for line in output.splitlines():
2758 name = line.strip()
2759 remotesList.append(name)
2760
2761 return remotesList
2762
2763 def gitGetRemoteUrlsList(self, repodir, forFetch=True):
2764 """
2765 Public method to get the list of remote repos and their URLs.
2766
2767 @param repodir directory name of the repository (string)
2768 @param forFetch flag indicating to get Fetch info (string)
2769 @return list of tuples of remote repo name and repo URL (list of
2770 tuple of two strings)
2771 """
2772 args = self.initCommand("remote")
2773 args.append("--verbose")
2774
2775 output = ""
2776 process = QProcess()
2777 process.setWorkingDirectory(repodir)
2778 process.start('git', args)
2779 procStarted = process.waitForStarted(5000)
2780 if procStarted:
2781 finished = process.waitForFinished(30000)
2782 if finished and process.exitCode() == 0:
2783 output = str(process.readAllStandardOutput(),
2784 Preferences.getSystem("IOEncoding"),
2785 'replace')
2786
2787 remotesList = []
2788 if output:
2789 for line in output.splitlines():
2790 name, urlmode = line.strip().split(None, 1)
2791 url, mode = urlmode.rsplit(None, 1)
2792 if forFetch and mode == "(fetch)" or \
2793 (not forFetch) and mode == "(push)":
2794 remotesList.append((name, url))
2795
2796 return remotesList
2797
2798 def gitGetRemoteBranchesList(self, repodir, remote):
2799 """
2800 Public method to get the list of a remote repository branches.
2801
2802 @param repodir directory name of the repository (string)
2803 @param remote remote repository name (string)
2804 @return list of remote repository branches (list of string)
2805 """
2806 args = self.initCommand("ls-remote")
2807 args.append("--heads")
2808 args.append(remote)
2809
2810 output = ""
2811 process = QProcess()
2812 process.setWorkingDirectory(repodir)
2813 process.start('git', args)
2814 procStarted = process.waitForStarted(5000)
2815 if procStarted:
2816 finished = process.waitForFinished(30000)
2817 if finished and process.exitCode() == 0:
2818 output = str(process.readAllStandardOutput(),
2819 Preferences.getSystem("IOEncoding"),
2820 'replace')
2821
2822 remoteBranches = []
2823 if output:
2824 for line in output.splitlines():
2825 branch = line.strip().split()[-1].split("/")[-1]
2826 remoteBranches.append(branch)
2827
2828 return remoteBranches
2829
2830 def gitShowRemote(self, projectDir, remoteName):
2831 """
2832 Public method to show information about a remote repository.
2833
2834 @param projectDir name of the project directory (string)
2835 @param remoteName name of the remote repository (string)
2836 """
2837 # find the root of the repo
2838 repodir = projectDir
2839 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2840 repodir = os.path.dirname(repodir)
2841 if os.path.splitdrive(repodir)[1] == os.sep:
2842 return
2843
2844 args = self.initCommand("remote")
2845 args.append("show")
2846 args.append(remoteName)
2847
2848 dia = GitDialog(self.tr('Show Remote Info'), self)
2849 res = dia.startProcess(args, repodir, showArgs=False)
2850 if res:
2851 dia.exec_()
2852
2853 def gitShowRemotes(self, projectDir):
2854 """
2855 Public method to show available remote repositories.
2856
2857 @param projectDir name of the project directory (string)
2858 """
2859 if self.remotesDialog is None:
2860 from .GitRemoteRepositoriesDialog import \
2861 GitRemoteRepositoriesDialog
2862 self.remotesDialog = GitRemoteRepositoriesDialog(self)
2863 self.remotesDialog.show()
2864 self.remotesDialog.raise_()
2865 self.remotesDialog.start(projectDir)
2866
2867 def gitAddRemote(self, projectDir):
2868 """
2869 Public method to add a remote repository.
2870
2871 @param projectDir name of the project directory (string)
2872 """
2873 # find the root of the repo
2874 repodir = projectDir
2875 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2876 repodir = os.path.dirname(repodir)
2877 if os.path.splitdrive(repodir)[1] == os.sep:
2878 return
2879
2880 from .GitAddRemoteDialog import GitAddRemoteDialog
2881 dlg = GitAddRemoteDialog()
2882 if dlg.exec_() == QDialog.Accepted:
2883 name, url = dlg.getData()
2884 args = self.initCommand("remote")
2885 args.append("add")
2886 args.append(name)
2887 args.append(url)
2888
2889 self.startSynchronizedProcess(QProcess(), "git", args,
2890 workingDir=repodir)
2891
2892 def gitRenameRemote(self, projectDir, remoteName):
2893 """
2894 Public method to rename a remote repository.
2895
2896 @param projectDir name of the project directory (string)
2897 @param remoteName name of the remote repository (string)
2898 """
2899 # find the root of the repo
2900 repodir = projectDir
2901 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2902 repodir = os.path.dirname(repodir)
2903 if os.path.splitdrive(repodir)[1] == os.sep:
2904 return
2905
2906 newName, ok = QInputDialog.getText(
2907 None,
2908 self.tr("Rename Remote Repository"),
2909 self.tr("Enter new name for remote repository:"),
2910 QLineEdit.Normal)
2911 if ok and newName and newName != remoteName:
2912 args = self.initCommand("remote")
2913 args.append("rename")
2914 args.append(remoteName)
2915 args.append(newName)
2916
2917 self.startSynchronizedProcess(QProcess(), "git", args,
2918 workingDir=repodir)
2919
2920 def gitRemoveRemote(self, projectDir, remoteName):
2921 """
2922 Public method to remove a remote repository.
2923
2924 @param projectDir name of the project directory (string)
2925 @param remoteName name of the remote repository (string)
2926 """
2927 # find the root of the repo
2928 repodir = projectDir
2929 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2930 repodir = os.path.dirname(repodir)
2931 if os.path.splitdrive(repodir)[1] == os.sep:
2932 return
2933
2934 args = self.initCommand("remote")
2935 args.append("remove")
2936 args.append(remoteName)
2937
2938 self.startSynchronizedProcess(QProcess(), "git", args,
2939 workingDir=repodir)
2940
2941 def gitPruneRemote(self, projectDir, remoteName):
2942 """
2943 Public method to prune stale remote-tracking branches.
2944
2945 @param projectDir name of the project directory (string)
2946 @param remoteName name of the remote repository (string)
2947 """
2948 # find the root of the repo
2949 repodir = projectDir
2950 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2951 repodir = os.path.dirname(repodir)
2952 if os.path.splitdrive(repodir)[1] == os.sep:
2953 return
2954
2955 args = self.initCommand("remote")
2956 args.append("prune")
2957 args.append(remoteName)
2958
2959 dia = GitDialog(self.tr('Show Remote Info'), self)
2960 res = dia.startProcess(args, repodir)
2961 if res:
2962 dia.exec_()
2963
2964 def gitShortlog(self, projectDir, commit):
2965 """
2966 Public method to show a short log suitable for inclusion in release
2967 announcements.
2968
2969 @param projectDir name of the project directory (string)
2970 @param commit commit to start the log at (strings)
2971 """
2972 # find the root of the repo
2973 repodir = projectDir
2974 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2975 repodir = os.path.dirname(repodir)
2976 if os.path.splitdrive(repodir)[1] == os.sep:
2977 return
2978
2979 args = self.initCommand("shortlog")
2980 args.append("-w")
2981 args.append(commit)
2982
2983 dia = GitDialog(self.tr('Show Shortlog'), self)
2984 res = dia.startProcess(args, repodir, showArgs=False)
2985 if res:
2986 dia.exec_()
2987
2988 def gitDescribe(self, projectDir, commits):
2989 """
2990 Public method to find the most recent tag reachable from each commit.
2991
2992 @param projectDir name of the project directory (string)
2993 @param commits list of commits to start the search from
2994 (list of strings)
2995 """
2996 if self.describeDialog is None:
2997 from .GitDescribeDialog import GitDescribeDialog
2998 self.describeDialog = GitDescribeDialog(self)
2999 self.describeDialog.show()
3000 self.describeDialog.raise_()
3001 self.describeDialog.start(projectDir, commits)
3002
3003 ###########################################################################
3004 ## Methods for cherry-pick handling.
3005 ###########################################################################
3006
3007 def gitCherryPick(self, projectDir, commits=None):
3008 """
3009 Public method to cherry pick commits and apply them to the current
3010 branch.
3011
3012 @param projectDir name of the project directory (string)
3013 @param commits list of commits to be applied (list of strings)
3014 @return flag indicating that the project should be reread (boolean)
3015 """
3016 # find the root of the repo
3017 repodir = projectDir
3018 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3019 repodir = os.path.dirname(repodir)
3020 if os.path.splitdrive(repodir)[1] == os.sep:
3021 return False
3022
3023 res = False
3024
3025 from .GitCherryPickDialog import GitCherryPickDialog
3026 dlg = GitCherryPickDialog(commits)
3027 if dlg.exec_() == QDialog.Accepted:
3028 commits, cherrypickInfo, signoff, nocommit = \
3029 dlg.getData()
3030
3031 args = self.initCommand("cherry-pick")
3032 args.append("-Xpatience")
3033 if cherrypickInfo:
3034 args.append("-x")
3035 if signoff:
3036 args.append("--signoff")
3037 if nocommit:
3038 args.append("--no-commit")
3039 args.extend(commits)
3040
3041 dia = GitDialog(self.tr('Cherry-pick'), self)
3042 res = dia.startProcess(args, repodir)
3043 if res:
3044 dia.exec_()
3045 res = dia.hasAddOrDelete()
3046 self.checkVCSStatus()
3047 return res
3048
3049 def gitCherryPickContinue(self, projectDir):
3050 """
3051 Public method to continue the last copying session after conflicts
3052 were resolved.
3053
3054 @param projectDir name of the project directory (string)
3055 @return flag indicating that the project should be reread (boolean)
3056 """
3057 # find the root of the repo
3058 repodir = projectDir
3059 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3060 repodir = os.path.dirname(repodir)
3061 if os.path.splitdrive(repodir)[1] == os.sep:
3062 return False
3063
3064 import sys
3065 editor = sys.argv[0].replace(".py", "_editor.py")
3066 env = {"GIT_EDITOR": "{0} {1}".format(sys.executable, editor)}
3067
3068 args = self.initCommand("cherry-pick")
3069 args.append("--continue")
3070
3071 dia = GitDialog(self.tr('Copy Changesets (Continue)'), self)
3072 res = dia.startProcess(args, repodir, environment=env)
3073 if res:
3074 dia.exec_()
3075 res = dia.hasAddOrDelete()
3076 self.checkVCSStatus()
3077 return res
3078
3079 def gitCherryPickQuit(self, projectDir):
3080 """
3081 Public method to quit the current copying operation.
3082
3083 @param projectDir name of the project directory (string)
3084 @return flag indicating that the project should be reread (boolean)
3085 """
3086 # find the root of the repo
3087 repodir = projectDir
3088 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3089 repodir = os.path.dirname(repodir)
3090 if os.path.splitdrive(repodir)[1] == os.sep:
3091 return False
3092
3093 args = self.initCommand("cherry-pick")
3094 args.append("--quit")
3095
3096 dia = GitDialog(self.tr('Copy Changesets (Quit)'), self)
3097 res = dia.startProcess(args, repodir)
3098 if res:
3099 dia.exec_()
3100 res = dia.hasAddOrDelete()
3101 self.checkVCSStatus()
3102 return res
3103
3104 def gitCherryPickAbort(self, projectDir):
3105 """
3106 Public method to cancel the last copying session and return to
3107 the previous state.
3108
3109 @param projectDir name of the project directory (string)
3110 @return flag indicating that the project should be reread (boolean)
3111 """
3112 # find the root of the repo
3113 repodir = projectDir
3114 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3115 repodir = os.path.dirname(repodir)
3116 if os.path.splitdrive(repodir)[1] == os.sep:
3117 return False
3118
3119 args = self.initCommand("cherry-pick")
3120 args.append("--abort")
3121
3122 dia = GitDialog(self.tr('Copy Changesets (Cancel)'), self)
3123 res = dia.startProcess(args, repodir)
3124 if res:
3125 dia.exec_()
3126 res = dia.hasAddOrDelete()
3127 self.checkVCSStatus()
3128 return res
3129
3130 ###########################################################################
3131 ## Methods for stash handling.
3132 ###########################################################################
3133
3134 def __gitGetStashesList(self, projectDir):
3135 """
3136 Private method to get a list of stash names.
3137
3138 @param projectDir name of the project directory (string)
3139 @return list of available stashes (list of string)
3140 """
3141 # find the root of the repo
3142 repodir = projectDir
3143 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3144 repodir = os.path.dirname(repodir)
3145 if os.path.splitdrive(repodir)[1] == os.sep:
3146 return []
3147
3148 args = self.initCommand("stash")
3149 args.append("list")
3150 args.append("--format=format:%gd")
3151
3152 stashesList = []
3153 output = ""
3154 process = QProcess()
3155 process.setWorkingDirectory(repodir)
3156 process.start('git', args)
3157 procStarted = process.waitForStarted(5000)
3158 if procStarted:
3159 finished = process.waitForFinished(30000)
3160 if finished and process.exitCode() == 0:
3161 output = str(process.readAllStandardOutput(),
3162 Preferences.getSystem("IOEncoding"),
3163 'replace')
3164
3165 if output:
3166 stashesList = output.strip().splitlines()
3167
3168 return stashesList
3169
3170 def gitStashSave(self, projectDir):
3171 """
3172 Public method to save the current changes to a new stash.
3173
3174 @param projectDir name of the project directory (string)
3175 @return flag indicating, that the save contained an add
3176 or delete (boolean)
3177 """
3178 # find the root of the repo
3179 repodir = projectDir
3180 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3181 repodir = os.path.dirname(repodir)
3182 if os.path.splitdrive(repodir)[1] == os.sep:
3183 return False
3184
3185 res = False
3186 from .GitStashDataDialog import GitStashDataDialog
3187 dlg = GitStashDataDialog()
3188 if dlg.exec_() == QDialog.Accepted:
3189 message, keepIndex, untracked = dlg.getData()
3190 args = self.initCommand("stash")
3191 args.append("save")
3192 if keepIndex:
3193 args.append("--keep-index")
3194 if untracked == GitStashDataDialog.UntrackedOnly:
3195 args.append("--include-untracked")
3196 elif untracked == GitStashDataDialog.UntrackedAndIgnored:
3197 args.append("--all")
3198 if message:
3199 args.append(message)
3200
3201 dia = GitDialog(self.tr('Saving stash'), self)
3202 res = dia.startProcess(args, repodir)
3203 if res:
3204 dia.exec_()
3205 res = dia.hasAddOrDelete()
3206 self.checkVCSStatus()
3207 return res
3208
3209 def gitStashBrowser(self, projectDir):
3210 """
3211 Public method used to browse the stashed changes.
3212
3213 @param projectDir name of the project directory (string)
3214 """
3215 if self.stashBrowser is None:
3216 from .GitStashBrowserDialog import GitStashBrowserDialog
3217 self.stashBrowser = GitStashBrowserDialog(self)
3218 self.stashBrowser.show()
3219 self.stashBrowser.raise_()
3220 self.stashBrowser.start(projectDir)
3221
3222 def gitStashShowPatch(self, projectDir, stashName=""):
3223 """
3224 Public method to show the contents of a stash.
3225
3226 @param projectDir name of the project directory (string)
3227 @param stashName name of a stash (string)
3228 """
3229 # find the root of the repo
3230 repodir = projectDir
3231 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3232 repodir = os.path.dirname(repodir)
3233 if os.path.splitdrive(repodir)[1] == os.sep:
3234 return
3235
3236 if not stashName:
3237 availableStashes = self.__gitGetStashesList(repodir)
3238 stashName, ok = QInputDialog.getItem(
3239 None,
3240 self.tr("Show Stash"),
3241 self.tr("Select a stash (empty for latest stash):"),
3242 [""] + availableStashes,
3243 0, False)
3244 if not ok:
3245 return
3246
3247 if self.diff is None:
3248 from .GitDiffDialog import GitDiffDialog
3249 self.diff = GitDiffDialog(self)
3250 self.diff.show()
3251 self.diff.raise_()
3252 self.diff.start(repodir, diffMode="stash", stashName=stashName)
3253
3254 def gitStashApply(self, projectDir, stashName=""):
3255 """
3256 Public method to apply a stash but keep it.
3257
3258 @param projectDir name of the project directory (string)
3259 @param stashName name of a stash (string)
3260 @return flag indicating, that the restore contained an add
3261 or delete (boolean)
3262 """
3263 # find the root of the repo
3264 repodir = projectDir
3265 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3266 repodir = os.path.dirname(repodir)
3267 if os.path.splitdrive(repodir)[1] == os.sep:
3268 return False
3269
3270 if not stashName:
3271 availableStashes = self.__gitGetStashesList(repodir)
3272 stashName, ok = QInputDialog.getItem(
3273 None,
3274 self.tr("Restore Stash"),
3275 self.tr("Select a stash (empty for latest stash):"),
3276 [""] + availableStashes,
3277 0, False)
3278 if not ok:
3279 return False
3280
3281 args = self.initCommand("stash")
3282 args.append("apply")
3283 if stashName:
3284 args.append(stashName)
3285
3286 dia = GitDialog(self.tr('Restoring stash'), self)
3287 res = dia.startProcess(args, repodir)
3288 if res:
3289 dia.exec_()
3290 res = dia.hasAddOrDelete()
3291 self.checkVCSStatus()
3292 return res
3293
3294 def gitStashPop(self, projectDir, stashName=""):
3295 """
3296 Public method to apply a stash and delete it.
3297
3298 @param projectDir name of the project directory (string)
3299 @param stashName name of a stash (string)
3300 @return flag indicating, that the restore contained an add
3301 or delete (boolean)
3302 """
3303 # find the root of the repo
3304 repodir = projectDir
3305 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3306 repodir = os.path.dirname(repodir)
3307 if os.path.splitdrive(repodir)[1] == os.sep:
3308 return False
3309
3310 if not stashName:
3311 availableStashes = self.__gitGetStashesList(repodir)
3312 stashName, ok = QInputDialog.getItem(
3313 None,
3314 self.tr("Restore Stash"),
3315 self.tr("Select a stash (empty for latest stash):"),
3316 [""] + availableStashes,
3317 0, False)
3318 if not ok:
3319 return False
3320
3321 args = self.initCommand("stash")
3322 args.append("pop")
3323 if stashName:
3324 args.append(stashName)
3325
3326 dia = GitDialog(self.tr('Restoring stash'), self)
3327 res = dia.startProcess(args, repodir)
3328 if res:
3329 dia.exec_()
3330 res = dia.hasAddOrDelete()
3331 self.checkVCSStatus()
3332 return res
3333
3334 def gitStashBranch(self, projectDir, stashName=""):
3335 """
3336 Public method to create a branch from a stash.
3337
3338 @param projectDir name of the project directory (string)
3339 @param stashName name of a stash (string)
3340 @return flag indicating, that the restore contained an add
3341 or delete (boolean)
3342 """
3343 # find the root of the repo
3344 repodir = projectDir
3345 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3346 repodir = os.path.dirname(repodir)
3347 if os.path.splitdrive(repodir)[1] == os.sep:
3348 return False
3349
3350 branchName, ok = QInputDialog.getText(
3351 None,
3352 self.tr("Create Branch"),
3353 self.tr("Enter a branch name to restore a stash to:"),
3354 QLineEdit.Normal)
3355 if not ok or branchName == "":
3356 return False
3357
3358 if not stashName:
3359 availableStashes = self.__gitGetStashesList(repodir)
3360 stashName, ok = QInputDialog.getItem(
3361 None,
3362 self.tr("Create Branch"),
3363 self.tr("Select a stash (empty for latest stash):"),
3364 [""] + availableStashes,
3365 0, False)
3366 if not ok:
3367 return False
3368
3369 args = self.initCommand("stash")
3370 args.append("branch")
3371 args.append(branchName)
3372 if stashName:
3373 args.append(stashName)
3374
3375 dia = GitDialog(self.tr('Creating branch'), self)
3376 res = dia.startProcess(args, repodir)
3377 if res:
3378 dia.exec_()
3379 res = dia.hasAddOrDelete()
3380 self.checkVCSStatus()
3381 return res
3382
3383 def gitStashDrop(self, projectDir, stashName=""):
3384 """
3385 Public method to delete a stash.
3386
3387 @param projectDir name of the project directory (string)
3388 @param stashName name of a stash (string)
3389 @return flag indicating a successful deletion (boolean)
3390 """
3391 # find the root of the repo
3392 repodir = projectDir
3393 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3394 repodir = os.path.dirname(repodir)
3395 if os.path.splitdrive(repodir)[1] == os.sep:
3396 return False
3397
3398 if not stashName:
3399 availableStashes = self.__gitGetStashesList(repodir)
3400 stashName, ok = QInputDialog.getItem(
3401 None,
3402 self.tr("Show Stash"),
3403 self.tr("Select a stash (empty for latest stash):"),
3404 [""] + availableStashes,
3405 0, False)
3406 if not ok:
3407 return False
3408
3409 res = E5MessageBox.yesNo(
3410 None,
3411 self.tr("Delete Stash"),
3412 self.tr("""Do you really want to delete the stash <b>{0}</b>?""")
3413 .format(stashName))
3414 if res:
3415 args = self.initCommand("stash")
3416 args.append("drop")
3417 if stashName:
3418 args.append(stashName)
3419
3420 dia = GitDialog(self.tr('Deleting stash'), self)
3421 res = dia.startProcess(args, repodir)
3422 if res:
3423 dia.exec_()
3424 return res
3425
3426 def gitStashClear(self, projectDir):
3427 """
3428 Public method to delete all stashes.
3429
3430 @param projectDir name of the project directory (string)
3431 @return flag indicating a successful deletion (boolean)
3432 """
3433 # find the root of the repo
3434 repodir = projectDir
3435 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3436 repodir = os.path.dirname(repodir)
3437 if os.path.splitdrive(repodir)[1] == os.sep:
3438 return False
3439
3440 res = E5MessageBox.yesNo(
3441 None,
3442 self.tr("Delete All Stashes"),
3443 self.tr("""Do you really want to delete all stashes?"""))
3444 if res:
3445 args = self.initCommand("stash")
3446 args.append("clear")
3447
3448 dia = GitDialog(self.tr('Deleting all stashes'), self)
3449 res = dia.startProcess(args, repodir)
3450 if res:
3451 dia.exec_()
3452 return res
3453
3454 def gitEditConfig(self, projectDir):
3455 """
3456 Public method used to edit the repository configuration file.
3457
3458 @param projectDir name of the project directory (string)
3459 """
3460 # find the root of the repo
3461 repodir = projectDir
3462 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3463 repodir = os.path.dirname(repodir)
3464 if os.path.splitdrive(repodir)[1] == os.sep:
3465 return
3466
3467 cfgFile = os.path.join(repodir, self.adminDir, "config")
3468 if not os.path.exists(cfgFile):
3469 # create an empty one
3470 try:
3471 cfg = open(cfgFile, "w")
3472 cfg.close()
3473 except IOError:
3474 pass
3475 self.repoEditor = MiniEditor(cfgFile, "Properties")
3476 self.repoEditor.show()
3477
3478 def gitEditUserConfig(self):
3479 """
3480 Public method used to edit the user configuration file.
3481 """
3482 from .GitUtilities import getConfigPath
3483 cfgFile = getConfigPath()
3484 if not os.path.exists(cfgFile):
3485 from .GitUserConfigDataDialog import GitUserConfigDataDialog
3486 dlg = GitUserConfigDataDialog()
3487 if dlg.exec_() == QDialog.Accepted:
3488 firstName, lastName, email = dlg.getData()
3489 else:
3490 firstName, lastName, email = (
3491 "Firstname", "Lastname", "email_address")
3492 try:
3493 f = open(cfgFile, "w")
3494 f.write("[user]\n")
3495 f.write(" name = {0} {1}\n".format(firstName, lastName))
3496 f.write(" email = {0}\n".format(email))
3497 f.close()
3498 except (IOError, OSError):
3499 # ignore these
3500 pass
3501 self.userEditor = MiniEditor(cfgFile, "Properties")
3502 self.userEditor.show()
3503
3504 def gitShowConfig(self, projectDir):
3505 """
3506 Public method to show the combined configuration.
3507
3508 @param projectDir name of the project directory (string)
3509 """
3510 # find the root of the repo
3511 repodir = projectDir
3512 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3513 repodir = os.path.dirname(repodir)
3514 if os.path.splitdrive(repodir)[1] == os.sep:
3515 return
3516
3517 args = self.initCommand("config")
3518 args.append("--list")
3519
3520 dia = GitDialog(
3521 self.tr('Showing the combined configuration settings'),
3522 self)
3523 res = dia.startProcess(args, repodir, False)
3524 if res:
3525 dia.exec_()
3526
3527 def gitVerify(self, projectDir):
3528 """
3529 Public method to verify the connectivity and validity of objects
3530 of the database.
3531
3532 @param projectDir name of the project directory (string)
3533 """
3534 # find the root of the repo
3535 repodir = projectDir
3536 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3537 repodir = os.path.dirname(repodir)
3538 if os.path.splitdrive(repodir)[1] == os.sep:
3539 return
3540
3541 args = self.initCommand("fsck")
3542 args.append("--strict")
3543 args.append("--full")
3544 args.append("--cache")
3545
3546 dia = GitDialog(
3547 self.tr('Verifying the integrity of the Git repository'),
3548 self)
3549 res = dia.startProcess(args, repodir, False)
3550 if res:
3551 dia.exec_()
3552
3553 def gitHouseKeeping(self, projectDir):
3554 """
3555 Public method to cleanup and optimize the local repository.
3556
3557 @param projectDir name of the project directory (string)
3558 """
3559 # find the root of the repo
3560 repodir = projectDir
3561 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3562 repodir = os.path.dirname(repodir)
3563 if os.path.splitdrive(repodir)[1] == os.sep:
3564 return
3565
3566 args = self.initCommand("gc")
3567 args.append("--prune")
3568 if self.__plugin.getPreferences("AggressiveGC"):
3569 args.append("--aggressive")
3570
3571 dia = GitDialog(
3572 self.tr('Performing Repository Housekeeping'),
3573 self)
3574 res = dia.startProcess(args, repodir)
3575 if res:
3576 dia.exec_()
3577
3578 def gitStatistics(self, projectDir):
3579 """
3580 Public method to show some statistics of the local repository.
3581
3582 @param projectDir name of the project directory (string)
3583 """
3584 # find the root of the repo
3585 repodir = projectDir
3586 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3587 repodir = os.path.dirname(repodir)
3588 if os.path.splitdrive(repodir)[1] == os.sep:
3589 return
3590
3591 args = self.initCommand("count-objects")
3592 args.append("-v")
3593
3594 output = ""
3595 process = QProcess()
3596 process.setWorkingDirectory(repodir)
3597 process.start('git', args)
3598 procStarted = process.waitForStarted(5000)
3599 if procStarted:
3600 finished = process.waitForFinished(30000)
3601 if finished and process.exitCode() == 0:
3602 output = str(process.readAllStandardOutput(),
3603 Preferences.getSystem("IOEncoding"),
3604 'replace')
3605
3606 info = []
3607 if output:
3608 statistics = {}
3609 for line in output.splitlines():
3610 key, value = line.strip().split(": ", 1)
3611 statistics[key] = value
3612
3613 info.append("""<p><table>""")
3614 info.append(self.tr("""<tr><td><b>Statistics</b></td></tr>"""))
3615 info.append(
3616 self.tr("""<tr><td>Number of loose objects: </td>"""
3617 """<td>{0}</td></tr>""")
3618 .format(statistics["count"]))
3619 info.append(
3620 self.tr("""<tr><td>Disk space used by loose objects: </td>"""
3621 """<td>{0} KiB</td></tr>""")
3622 .format(statistics["size"]))
3623 info.append(
3624 self.tr("""<tr><td>Number of packed objects: </td>"""
3625 """<td>{0}</td></tr>""")
3626 .format(statistics["in-pack"]))
3627 info.append(
3628 self.tr("""<tr><td>Number of packs: </td>"""
3629 """<td>{0}</td></tr>""")
3630 .format(statistics["packs"]))
3631 info.append(
3632 self.tr("""<tr><td>Disk space used by packed objects: </td>"""
3633 """<td>{0} KiB</td></tr>""")
3634 .format(statistics["size-pack"]))
3635 info.append(
3636 self.tr("""<tr><td>Packed objects waiting for pruning: </td>"""
3637 """<td>{0}</td></tr>""")
3638 .format(statistics["prune-packable"]))
3639 info.append(
3640 self.tr("""<tr><td>Garbage files: </td>"""
3641 """<td>{0}</td></tr>""")
3642 .format(statistics["garbage"]))
3643 info.append(
3644 self.tr("""<tr><td>Disk space used by garbage files: </td>"""
3645 """<td>{0} KiB</td></tr>""")
3646 .format(statistics["size-garbage"]))
3647 info.append("""</table></p>""")
3648 else:
3649 info.append(self.tr("<p><b>No statistics available.</b></p>"))
3650 dlg = VcsRepositoryInfoDialog(None, "\n".join(info))
3651 dlg.exec_()
3652
3653 def gitGetArchiveFormats(self, repodir):
3654 """
3655 Public method to get a list of supported archive formats.
3656
3657 @param repodir directory name of the repository (string)
3658 @return list of supported archive formats (list of strings)
3659 """
3660 args = self.initCommand("archive")
3661 args.append("--list")
3662
3663 output = ""
3664 process = QProcess()
3665 process.setWorkingDirectory(repodir)
3666 process.start('git', args)
3667 procStarted = process.waitForStarted(5000)
3668 if procStarted:
3669 finished = process.waitForFinished(30000)
3670 if finished and process.exitCode() == 0:
3671 output = str(process.readAllStandardOutput(),
3672 Preferences.getSystem("IOEncoding"),
3673 'replace')
3674
3675 archiveFormats = []
3676 if output:
3677 for line in output.splitlines():
3678 archiveFormat = line.strip()
3679 archiveFormats.append(archiveFormat)
3680
3681 return archiveFormats
3682
3683 def gitCreateArchive(self, projectDir):
3684 """
3685 Public method to show some statistics of the local repository.
3686
3687 @param projectDir name of the project directory (string)
3688 """
3689 # find the root of the repo
3690 repodir = projectDir
3691 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3692 repodir = os.path.dirname(repodir)
3693 if os.path.splitdrive(repodir)[1] == os.sep:
3694 return
3695
3696 from .GitArchiveDataDialog import GitArchiveDataDialog
3697 dlg = GitArchiveDataDialog(
3698 self.gitGetTagsList(repodir),
3699 self.gitGetBranchesList(repodir, withMaster=True),
3700 self.gitGetArchiveFormats(repodir)
3701 )
3702 if dlg.exec_() == QDialog.Accepted:
3703 commit, archiveFormat, fileName, prefix = dlg.getData()
3704 args = self.initCommand("archive")
3705 args.append("--format={0}".format(archiveFormat))
3706 args.append("--output={0}".format(fileName))
3707 if prefix:
3708 prefix = Utilities.fromNativeSeparators(prefix)
3709 if not prefix.endswith("/"):
3710 prefix += "/"
3711 args.append("--prefix={0}".format(prefix))
3712 args.append(commit)
3713
3714 dia = GitDialog(
3715 self.tr('Creating Archive'),
3716 self)
3717 res = dia.startProcess(args, repodir)
3718 if res:
3719 dia.exec_()
3720
3721 ###########################################################################
3722 ## Methods related to submodules.
3723 ###########################################################################
3724
3725 def gitSubmoduleAdd(self, projectDir):
3726 """
3727 Public method to add a submodule to the project.
3728
3729 @param projectDir name of the project directory
3730 @type str
3731 """
3732 # find the root of the repo
3733 repodir = projectDir
3734 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3735 repodir = os.path.dirname(repodir)
3736 if os.path.splitdrive(repodir)[1] == os.sep:
3737 return
3738
3739 from .GitSubmoduleAddDialog import GitSubmoduleAddDialog
3740 dlg = GitSubmoduleAddDialog(self, repodir)
3741 if dlg.exec_() == QDialog.Accepted:
3742 repo, branch, name, path, force = dlg.getData()
3743 args = self.initCommand("submodule")
3744 args.append("add")
3745 if branch:
3746 args.append("--branch")
3747 args.append(branch)
3748 if force:
3749 args.append("--force")
3750 if name:
3751 args.append("--name")
3752 args.append(name)
3753 args.append(repo)
3754 if path:
3755 args.append(path)
3756
3757 dia = GitDialog(
3758 self.tr("Add Submodule"),
3759 self)
3760 res = dia.startProcess(args, repodir)
3761 if res:
3762 dia.exec_()
3763
3764 def __gitSubmodulesList(self, repodir):
3765 """
3766 Private method to get the data of defined submodules.
3767
3768 @param repodir name of the directory containing the repo subdirectory
3769 @type str
3770 @return list of dictionaries with submodule name, path, URL and branch
3771 @rtype list of dict
3772 """
3773 submodulesFile = os.path.join(repodir, ".gitmodules")
3774 if not os.path.exists(submodulesFile):
3775 return []
3776
3777 configParser = E5ConfigParser()
3778 if configParser.read(submodulesFile):
3779 submodules = []
3780 for submodule in configParser:
3781 if submodule.startswith("submodule"):
3782 submoduleDict = {"name": submodule.split(None, 1)[1][1:-1]}
3783 section = configParser[submodule]
3784 submoduleDict["path"] = section["path"]
3785 submoduleDict["url"] = section["url"]
3786 if "branch" in section:
3787 submoduleDict["branch"] = section["branch"]
3788 else:
3789 submoduleDict["branch"] = ""
3790 submodules.append(submoduleDict)
3791 return submodules
3792
3793 return []
3794
3795 def gitSubmoduleList(self, projectDir):
3796 """
3797 Public method to show a list of all submodules of the project.
3798
3799 @param projectDir name of the project directory
3800 @type str
3801 """
3802 # find the root of the repo
3803 repodir = projectDir
3804 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3805 repodir = os.path.dirname(repodir)
3806 if os.path.splitdrive(repodir)[1] == os.sep:
3807 return
3808
3809 submodulesList = self.__gitSubmodulesList(repodir)
3810 if submodulesList:
3811 from .GitSubmodulesListDialog import GitSubmodulesListDialog
3812 dlg = GitSubmodulesListDialog(submodulesList)
3813 dlg.exec_()
3814 else:
3815 E5MessageBox.information(
3816 None,
3817 self.tr("List Submodules"),
3818 self.tr("""No submodules defined for the project."""))
3819
3820 def __selectSubmodulePath(self, repodir):
3821 """
3822 Private method to select a submodule path.
3823
3824 @param repodir name of the directory containing the repo subdirectory
3825 @type str
3826 @return tuple of selected submodule path and flag indicating
3827 a cancellation
3828 @rtype tuple of (str, bool)
3829 """
3830 allEntry = self.tr("All")
3831 paths = [submodule["path"]
3832 for submodule in self.__gitSubmodulesList(repodir)]
3833 submodulePath, ok = QInputDialog.getItem(
3834 None,
3835 self.tr("Submodule Path"),
3836 self.tr("Select a submodule path:"),
3837 [allEntry] + sorted(paths),
3838 0, False)
3839 if submodulePath == allEntry:
3840 submodulePath = ""
3841
3842 return submodulePath, ok
3843
3844 def __selectSubmodulePaths(self, repodir):
3845 """
3846 Private method to select a list of submodule paths.
3847
3848 @param repodir name of the directory containing the repo subdirectory
3849 @type str
3850 @return tuple of selected submodule paths and flag indicating
3851 a cancellation
3852 @rtype tuple of (list of str, bool)
3853 """
3854 paths = [submodule["path"]
3855 for submodule in self.__gitSubmodulesList(repodir)]
3856
3857 from .GitListDialog import GitListDialog
3858 dlg = GitListDialog(sorted(paths))
3859 if dlg.exec_() == QDialog.Accepted:
3860 selectedPaths = dlg.getSelection()
3861 return selectedPaths, True
3862 else:
3863 return [], False
3864
3865 def gitSubmoduleInit(self, projectDir):
3866 """
3867 Public method to initialize one or all submodules.
3868
3869 @param projectDir name of the project directory
3870 @type str
3871 """
3872 # find the root of the repo
3873 repodir = projectDir
3874 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3875 repodir = os.path.dirname(repodir)
3876 if os.path.splitdrive(repodir)[1] == os.sep:
3877 return
3878
3879 submodulePaths, ok = self.__selectSubmodulePaths(repodir)
3880 if ok:
3881 args = self.initCommand("submodule")
3882 args.append("init")
3883 args.extend(submodulePaths)
3884
3885 dia = GitDialog(
3886 self.tr("Initialize Submodules"),
3887 self)
3888 res = dia.startProcess(args, repodir)
3889 if res:
3890 dia.exec_()
3891
3892 def gitSubmoduleDeinit(self, projectDir):
3893 """
3894 Public method to unregister submodules.
3895
3896 @param projectDir name of the project directory
3897 @type str
3898 """
3899 # find the root of the repo
3900 repodir = projectDir
3901 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3902 repodir = os.path.dirname(repodir)
3903 if os.path.splitdrive(repodir)[1] == os.sep:
3904 return
3905
3906 paths = [submodule["path"]
3907 for submodule in self.__gitSubmodulesList(repodir)]
3908
3909 from .GitSubmodulesDeinitDialog import GitSubmodulesDeinitDialog
3910 dlg = GitSubmodulesDeinitDialog(paths)
3911 if dlg.exec_() == QDialog.Accepted:
3912 all, submodulePaths, force = dlg.getData()
3913 args = self.initCommand("submodule")
3914 args.append("deinit")
3915 if all:
3916 args.append("--all")
3917 else:
3918 args.extend(submodulePaths)
3919 if force:
3920 args.append("--force")
3921
3922 dia = GitDialog(
3923 self.tr("Unregister Submodules"),
3924 self)
3925 res = dia.startProcess(args, repodir)
3926 if res:
3927 dia.exec_()
3928
3929 def gitSubmoduleUpdate(self, projectDir, initialize=False, remote=False):
3930 """
3931 Public method to update submodules.
3932
3933 @param projectDir name of the project directory
3934 @type str
3935 @param initialize flag indicating an initialize and update operation
3936 @type bool
3937 @param remote flag indicating a fetch and update operation
3938 @type bool
3939 """
3940 # find the root of the repo
3941 repodir = projectDir
3942 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3943 repodir = os.path.dirname(repodir)
3944 if os.path.splitdrive(repodir)[1] == os.sep:
3945 return
3946
3947 submodulePaths, ok = self.__selectSubmodulePaths(repodir)
3948 if ok:
3949 args = self.initCommand("submodule")
3950 args.append("update")
3951 if initialize:
3952 args.append("--init")
3953 if remote:
3954 args.append("--remote")
3955 args.extend(submodulePaths)
3956
3957 dia = GitDialog(
3958 self.tr("Update Submodules"),
3959 self)
3960 res = dia.startProcess(args, repodir)
3961 if res:
3962 dia.exec_()
3963
3964 def gitSubmoduleUpdateWithOptions(self, projectDir):
3965 """
3966 Public method to update submodules offering a dialog to select the
3967 update options.
3968
3969 @param projectDir name of the project directory
3970 @type str
3971 """
3972 # find the root of the repo
3973 repodir = projectDir
3974 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
3975 repodir = os.path.dirname(repodir)
3976 if os.path.splitdrive(repodir)[1] == os.sep:
3977 return
3978
3979 paths = [submodule["path"]
3980 for submodule in self.__gitSubmodulesList(repodir)]
3981
3982 from .GitSubmodulesUpdateOptionsDialog import \
3983 GitSubmodulesUpdateOptionsDialog
3984 dlg = GitSubmodulesUpdateOptionsDialog(paths)
3985 if dlg.exec_() == QDialog.Accepted:
3986 procedure, init, remote, noFetch, force, submodulePaths = \
3987 dlg.getData()
3988
3989 args = self.initCommand("submodule")
3990 args.append("update")
3991 args.append(procedure)
3992 if init:
3993 args.append("--init")
3994 if remote:
3995 args.append("--remote")
3996 if noFetch:
3997 args.append("--no-fetch")
3998 if force:
3999 args.append("--force")
4000 args.extend(submodulePaths)
4001
4002 dia = GitDialog(
4003 self.tr("Update Submodules"),
4004 self)
4005 res = dia.startProcess(args, repodir)
4006 if res:
4007 dia.exec_()
4008
4009 def gitSubmoduleSync(self, projectDir):
4010 """
4011 Public method to synchronize submodules.
4012
4013 @param projectDir name of the project directory
4014 @type str
4015 """
4016 # find the root of the repo
4017 repodir = projectDir
4018 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
4019 repodir = os.path.dirname(repodir)
4020 if os.path.splitdrive(repodir)[1] == os.sep:
4021 return
4022
4023 paths = [submodule["path"]
4024 for submodule in self.__gitSubmodulesList(repodir)]
4025
4026 from .GitSubmodulesSyncDialog import GitSubmodulesSyncDialog
4027 dlg = GitSubmodulesSyncDialog(paths)
4028 if dlg.exec_() == QDialog.Accepted:
4029 submodulePaths, recursive = dlg.getData()
4030 args = self.initCommand("submodule")
4031 args.append("sync")
4032 if recursive:
4033 args.append("--recursive")
4034 args.extend(submodulePaths)
4035
4036 dia = GitDialog(
4037 self.tr("Synchronize Submodules"),
4038 self)
4039 res = dia.startProcess(args, repodir)
4040 if res:
4041 dia.exec_()
4042
4043 def gitSubmoduleStatus(self, projectDir):
4044 """
4045 Public method to show the status of the submodules.
4046
4047 @param projectDir name of the project directory
4048 @type str
4049 """
4050 if self.submoduleStatusDialog is None:
4051 from .GitSubmodulesStatusDialog import GitSubmodulesStatusDialog
4052 self.submoduleStatusDialog = GitSubmodulesStatusDialog(self)
4053 self.submoduleStatusDialog.show()
4054 self.submoduleStatusDialog.raise_()
4055 self.submoduleStatusDialog.start(projectDir)
4056
4057 def gitSubmoduleSummary(self, projectDir):
4058 """
4059 Public method to show the status of the submodules.
4060
4061 @param projectDir name of the project directory
4062 @type str
4063 """
4064 # find the root of the repo
4065 repodir = projectDir
4066 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
4067 repodir = os.path.dirname(repodir)
4068 if os.path.splitdrive(repodir)[1] == os.sep:
4069 return
4070
4071 paths = [submodule["path"]
4072 for submodule in self.__gitSubmodulesList(repodir)]
4073
4074 from .GitSubmodulesSummaryOptionsDialog import \
4075 GitSubmodulesSummaryOptionsDialog
4076 dlg = GitSubmodulesSummaryOptionsDialog(paths)
4077 if dlg.exec_() == QDialog.Accepted:
4078 submodulePaths, superProject, index, commit, limit = dlg.getData()
4079 args = self.initCommand("submodule")
4080 args.append("summary")
4081 if superProject:
4082 args.append("--files")
4083 if index:
4084 args.append("--cached")
4085 if limit > -1:
4086 args.append("--summary-limit")
4087 args.append(str(limit))
4088 if commit:
4089 args.append(commit)
4090 if submodulePaths:
4091 args.append("--")
4092 args.extend(submodulePaths)
4093
4094 dia = GitDialog(
4095 self.tr("Submodules Summary"),
4096 self)
4097 res = dia.startProcess(args, repodir)
4098 if res:
4099 dia.exec_()
4100
4101 ###########################################################################
4102 ## Methods to get the helper objects are below.
4103 ###########################################################################
4104
4105 def vcsGetProjectBrowserHelper(self, browser, project,
4106 isTranslationsBrowser=False):
4107 """
4108 Public method to instantiate a helper object for the different
4109 project browsers.
4110
4111 @param browser reference to the project browser object
4112 @param project reference to the project object
4113 @param isTranslationsBrowser flag indicating, the helper is requested
4114 for the translations browser (this needs some special treatment)
4115 @return the project browser helper object
4116 """
4117 from .ProjectBrowserHelper import GitProjectBrowserHelper
4118 return GitProjectBrowserHelper(self, browser, project,
4119 isTranslationsBrowser)
4120
4121 def vcsGetProjectHelper(self, project):
4122 """
4123 Public method to instantiate a helper object for the project.
4124
4125 @param project reference to the project object
4126 @return the project helper object
4127 """
4128 self.__projectHelper = self.__plugin.getProjectHelper()
4129 self.__projectHelper.setObjects(self, project)
4130 return self.__projectHelper
4131
4132 ###########################################################################
4133 ## Status Monitor Thread methods
4134 ###########################################################################
4135
4136 def _createStatusMonitorThread(self, interval, project):
4137 """
4138 Protected method to create an instance of the VCS status monitor
4139 thread.
4140
4141 @param interval check interval for the monitor thread in seconds
4142 (integer)
4143 @param project reference to the project object (Project)
4144 @return reference to the monitor thread (QThread)
4145 """
4146 from .GitStatusMonitorThread import GitStatusMonitorThread
4147 return GitStatusMonitorThread(interval, project, self)

eric ide

mercurial