Files
create/src/Mod/AddonManager/change_branch.py
2022-03-11 20:07:20 -06:00

298 lines
12 KiB
Python

# -*- 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