src/eric7/Plugins/VcsPlugins/vcsGit/git.py

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

eric ide

mercurial