298 lines
12 KiB
Python
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
|