# -*- coding: utf-8 -*- # *************************************************************************** # * * # * Copyright (c) 2022 FreeCAD Project Association * # * * # * This library is free software; you can redistribute it and/or * # * modify it under the terms of the GNU Lesser General Public * # * License as published by the Free Software Foundation; either * # * version 2.1 of the License, or (at your option) any later version. * # * * # * This library is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with this library; if not, write to the Free Software * # * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * # * 02110-1301 USA * # * * # *************************************************************************** import os import FreeCAD import FreeCADGui from PySide2 import QtWidgets, QtCore translate = FreeCAD.Qt.translate try: import git NO_GIT = False except Exception: NO_GIT = True class ChangeBranchDialog(QtWidgets.QWidget): branch_changed = QtCore.Signal(str) def __init__(self, path: os.PathLike, parent=None): super().__init__(parent) self.ui = FreeCADGui.PySideUic.loadUi( os.path.join(os.path.dirname(__file__), "change_branch.ui") ) self.ui.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) self.item_filter = ChangeBranchDialogFilter() self.ui.tableView.setModel(self.item_filter) self.item_model = ChangeBranchDialogModel(path, self) self.item_filter.setSourceModel(self.item_model) self.ui.tableView.sortByColumn( 4, QtCore.Qt.DescendingOrder ) # Default to sorting by remote last-changed date # Figure out what row gets selected: row = 0 current_ref = self.item_model.repo.head.ref selection_model = self.ui.tableView.selectionModel() for ref in self.item_model.refs: if ref == current_ref: index = self.item_filter.mapFromSource(self.item_model.index(row, 0)) selection_model.select(index, QtCore.QItemSelectionModel.ClearAndSelect) selection_model.select( index.siblingAtColumn(1), QtCore.QItemSelectionModel.Select ) selection_model.select( index.siblingAtColumn(2), QtCore.QItemSelectionModel.Select ) selection_model.select( index.siblingAtColumn(3), QtCore.QItemSelectionModel.Select ) selection_model.select( index.siblingAtColumn(4), QtCore.QItemSelectionModel.Select ) break row += 1 # Make sure the column widths are OK: header = self.ui.tableView.horizontalHeader() header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) def exec(self): if self.ui.exec() == QtWidgets.QDialog.Accepted: selection = self.ui.tableView.selectedIndexes() index = self.item_filter.mapToSource(selection[0]) ref = self.item_model.data(index, ChangeBranchDialogModel.RefAccessRole) if ref == self.item_model.repo.head.ref: # This is the one we are already on... just return return result = QtWidgets.QMessageBox.critical( self, translate("AddonsInstaller", "DANGER: Developer feature"), translate( "AddonsInstaller", "DANGER: Switching branches is intended for developers and beta testers, and may result in broken, non-backwards compatible documents, instability, crashes, and/or the premature heat death of the universe. Are you sure you want to continue?", ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel, ) if result == QtWidgets.QMessageBox.Cancel: return if self.item_model.repo.is_dirty(): result = QtWidgets.QMessageBox.critical( self, translate("AddonsInstaller", "There are local changes"), translate( "AddonsInstaller", "WARNING: This repo has uncommitted local changes. Are you sure you want to change branches (bringing the changes with you)?", ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel, ) if result == QtWidgets.QMessageBox.Cancel: return if isinstance(ref, git.TagReference): # Detach the head self.item_model.repo.head.reference = ref self.item_model.repo.head.reset(index=True, working_tree=True) elif isinstance(ref, git.RemoteReference): # Set up a local tracking branch slash_index = ref.name.find("/") if slash_index != -1: local_name = ref.name[slash_index + 1 :] else: local_name = ref.name self.item_model.repo.create_head(local_name, ref) self.item_model.repo.heads[local_name].set_tracking_branch(ref) self.item_model.repo.heads[local_name].checkout() else: # It's already a local branch, just check it out ref.checkout() self.branch_changed.emit(ref.name) class ChangeBranchDialogModel(QtCore.QAbstractTableModel): refs = [] display_data = [] DataSortRole = QtCore.Qt.UserRole RefAccessRole = QtCore.Qt.UserRole + 1 def __init__(self, path: os.PathLike, parent=None) -> None: super().__init__(parent) self.repo = git.Repo(path) self.refs = [] tracking_refs = [] for ref in self.repo.refs: row = ["", None, None, None, None] if "HEAD" in ref.name: continue if isinstance(ref, git.RemoteReference): if ref.name in tracking_refs: # Already seen, it's the remote part of a remote tracking branch continue else: # Just a remote branch, not tracking: row[0] = translate("AddonsInstaller", "Branch", "git terminology") row[2] = ref.name if hasattr(ref, "commit") and hasattr(ref.commit, "committed_date"): row[4] = ref.commit.committed_date else: row[4] = ref.log_entry(0).time[0] elif isinstance(ref, git.TagReference): # Tags are simple, there is no tracking to worry about row[0] = translate("AddonsInstaller", "Tag", "git terminology") row[1] = ref.name row[3] = ref.commit.committed_date elif isinstance(ref, git.Head): if hasattr(ref, "tracking_branch") and ref.tracking_branch(): # This local branch tracks a remote: we have all five pieces of data... row[0] = translate("AddonsInstaller", "Branch", "git terminology") row[1] = ref.name row[2] = ref.tracking_branch().name row[3] = ref.commit.committed_date row[4] = ref.tracking_branch().commit.committed_date tracking_refs.append(ref.tracking_branch().name) else: # Just a local branch, no remote tracking: row[0] = translate("AddonsInstaller", "Branch", "git terminology") row[1] = ref.name if hasattr(ref, "commit") and hasattr(ref.commit, "committed_date"): row[3] = ref.commit.committed_date else: row[3] = ref.log_entry(0).time[0] else: continue self.display_data.append(row.copy()) self.refs.append(ref) def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: if parent.isValid(): return 0 return len(self.refs) def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int: if parent.isValid(): return 0 return 5 # Type, local name, tracking name, local update, and remote update def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole): if not index.isValid(): return None row = index.row() column = index.column() if role == QtCore.Qt.ToolTipRole: tooltip = "" # TODO: What should the tooltip be for these items? Last commit message? return tooltip elif role == QtCore.Qt.DisplayRole: dd = self.display_data[row] if column == 3 or column == 4: if dd[column] is not None: qdate = QtCore.QDateTime.fromTime_t(dd[column]) return QtCore.QLocale().toString(qdate, QtCore.QLocale.ShortFormat) elif column < len(dd): return dd[column] else: return None elif role == ChangeBranchDialogModel.DataSortRole: if column == 0: if self.refs[row] in self.repo.heads: return 0 else: return 1 elif column < len(self.display_data[row]): return self.display_data[row][column] else: return None elif role == ChangeBranchDialogModel.RefAccessRole: return self.refs[row] def headerData( self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole, ): if orientation == QtCore.Qt.Vertical: return None if role != QtCore.Qt.DisplayRole: return None if section == 0: return translate( "AddonsInstaller", "Kind", "Table header for git ref type (e.g. either Tag or Branch)", ) elif section == 1: return translate( "AddonsInstaller", "Local name", "Table header for git ref name" ) elif section == 2: return translate( "AddonsInstaller", "Tracking", "Table header for git remote tracking branch name name", ) elif section == 3: return translate( "AddonsInstaller", "Local updated", "Table header for git update time of local branch", ) elif section == 4: return translate( "AddonsInstaller", "Remote updated", "Table header for git update time of remote branch", ) else: return None class ChangeBranchDialogFilter(QtCore.QSortFilterProxyModel): def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex): leftData = self.sourceModel().data(left, ChangeBranchDialogModel.DataSortRole) rightData = self.sourceModel().data(right, ChangeBranchDialogModel.DataSortRole) if leftData is None or rightData is None: if rightData is not None: return True else: return False return leftData < rightData