eric6/Plugins/VcsPlugins/vcsGit/git.py

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

eric ide

mercurial