Addon Manager: Complete migration away from GitPython
This commit is contained in:
committed by
Chris Hennes
parent
f301b763f6
commit
c622e6ca42
@@ -29,7 +29,7 @@ import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import List, Optional
|
||||
from typing import List, Dict, Optional
|
||||
import time
|
||||
|
||||
import addonmanager_utilities as utils
|
||||
@@ -46,6 +46,30 @@ class GitFailed(RuntimeError):
|
||||
"""The call to git returned an error of some kind"""
|
||||
|
||||
|
||||
def _ref_format_string() -> str:
|
||||
return (
|
||||
"--format=%(refname:lstrip=2)\t%(upstream:lstrip=2)\t%(authordate:rfc)\t%("
|
||||
"authorname)\t%(subject)"
|
||||
)
|
||||
|
||||
|
||||
def _parse_ref_table(text: str):
|
||||
rows = text.splitlines()
|
||||
result = []
|
||||
for row in rows:
|
||||
columns = row.split("\t")
|
||||
result.append(
|
||||
{
|
||||
"ref_name": columns[0],
|
||||
"upstream": columns[1],
|
||||
"date": columns[2],
|
||||
"author": columns[3],
|
||||
"subject": columns[4],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class GitManager:
|
||||
"""A class to manage access to git: mostly just provides a simple wrapper around
|
||||
the basic command-line calls. Provides optional asynchronous access to clone and
|
||||
@@ -82,6 +106,36 @@ class GitManager:
|
||||
self._synchronous_call_git(final_args)
|
||||
os.chdir(old_dir)
|
||||
|
||||
def dirty(self, local_path: str) -> bool:
|
||||
"""Check for local changes"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
result = False
|
||||
final_args = ["diff-index", "HEAD"]
|
||||
try:
|
||||
stdout = self._synchronous_call_git(final_args)
|
||||
if stdout:
|
||||
result = True
|
||||
except GitFailed:
|
||||
result = False
|
||||
os.chdir(old_dir)
|
||||
return result
|
||||
|
||||
def detached_head(self, local_path: str) -> bool:
|
||||
"""Check for detached head state"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
result = False
|
||||
final_args = ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "HEAD"]
|
||||
try:
|
||||
stdout = self._synchronous_call_git(final_args)
|
||||
if stdout == "HEAD":
|
||||
result = True
|
||||
except GitFailed:
|
||||
result = False
|
||||
os.chdir(old_dir)
|
||||
return result
|
||||
|
||||
def update(self, local_path):
|
||||
"""Fetches and pulls the local_path from its remote"""
|
||||
old_dir = os.getcwd()
|
||||
@@ -258,6 +312,30 @@ class GitManager:
|
||||
branches.append(branch)
|
||||
return branches
|
||||
|
||||
def get_branches_with_info(self, local_path) -> List[Dict[str, str]]:
|
||||
"""Get a list of branches, where each entry is a dictionary with status information about
|
||||
the branch."""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
try:
|
||||
stdout = self._synchronous_call_git(["branch", "-a", _ref_format_string()])
|
||||
return _parse_ref_table(stdout)
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
|
||||
def get_tags_with_info(self, local_path) -> List[Dict[str, str]]:
|
||||
"""Get a list of branches, where each entry is a dictionary with status information about
|
||||
the branch."""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
try:
|
||||
stdout = self._synchronous_call_git(["tag", "-l", _ref_format_string()])
|
||||
return _parse_ref_table(stdout)
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
|
||||
def get_last_committers(self, local_path, n=10):
|
||||
"""Examine the last n entries of the commit history, and return a list of all
|
||||
the committers, their email addresses, and how many commits each one is
|
||||
|
||||
@@ -25,24 +25,18 @@ import os
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from addonmanager_git import initialize_git
|
||||
|
||||
from PySide 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):
|
||||
def __init__(self, path: str, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.ui = FreeCADGui.PySideUic.loadUi(
|
||||
@@ -56,21 +50,20 @@ class ChangeBranchDialog(QtWidgets.QWidget):
|
||||
self.item_model = ChangeBranchDialogModel(path, self)
|
||||
self.item_filter.setSourceModel(self.item_model)
|
||||
self.ui.tableView.sortByColumn(
|
||||
4, QtCore.Qt.DescendingOrder
|
||||
2, QtCore.Qt.DescendingOrder
|
||||
) # Default to sorting by remote last-changed date
|
||||
|
||||
# Figure out what row gets selected:
|
||||
git_manager = initialize_git()
|
||||
row = 0
|
||||
current_ref = self.item_model.repo.head.ref
|
||||
current_ref = git_manager.current_branch(path)
|
||||
selection_model = self.ui.tableView.selectionModel()
|
||||
for ref in self.item_model.refs:
|
||||
if ref == current_ref:
|
||||
for ref in self.item_model.branches:
|
||||
if ref["ref_name"] == 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
|
||||
|
||||
@@ -85,7 +78,7 @@ class ChangeBranchDialog(QtWidgets.QWidget):
|
||||
index = self.item_filter.mapToSource(selection[0])
|
||||
ref = self.item_model.data(index, ChangeBranchDialogModel.RefAccessRole)
|
||||
|
||||
if ref == self.item_model.repo.head.ref:
|
||||
if ref["ref_name"] == self.item_model.current_branch:
|
||||
# This is the one we are already on... just return
|
||||
return
|
||||
|
||||
@@ -94,20 +87,24 @@ class ChangeBranchDialog(QtWidgets.QWidget):
|
||||
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?",
|
||||
"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():
|
||||
if self.item_model.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)?",
|
||||
"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,
|
||||
@@ -115,93 +112,41 @@ class ChangeBranchDialog(QtWidgets.QWidget):
|
||||
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()
|
||||
gm = initialize_git()
|
||||
remote_name = ref["ref_name"]
|
||||
_, _, local_name = ref["ref_name"].rpartition("/")
|
||||
if ref["upstream"]:
|
||||
gm.checkout(self.item_model.path, remote_name)
|
||||
else:
|
||||
# It's already a local branch, just check it out
|
||||
ref.checkout()
|
||||
|
||||
self.branch_changed.emit(ref.name)
|
||||
gm.checkout(self.item_model.path, remote_name, args=["-b", local_name])
|
||||
self.branch_changed.emit(local_name)
|
||||
|
||||
|
||||
class ChangeBranchDialogModel(QtCore.QAbstractTableModel):
|
||||
|
||||
refs = []
|
||||
display_data = []
|
||||
branches = []
|
||||
DataSortRole = QtCore.Qt.UserRole
|
||||
RefAccessRole = QtCore.Qt.UserRole + 1
|
||||
|
||||
def __init__(self, path: os.PathLike, parent=None) -> None:
|
||||
def __init__(self, path: str, 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)
|
||||
gm = initialize_git()
|
||||
self.path = path
|
||||
self.branches = gm.get_branches_with_info(path)
|
||||
self.current_branch = gm.current_branch(path)
|
||||
self.dirty = gm.dirty(path)
|
||||
self._remove_tracking_duplicates()
|
||||
|
||||
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
|
||||
if parent.isValid():
|
||||
return 0
|
||||
return len(self.refs)
|
||||
return len(self.branches)
|
||||
|
||||
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
|
||||
return 3 # Local name, remote name, date
|
||||
|
||||
def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
|
||||
if not index.isValid():
|
||||
@@ -209,31 +154,39 @@ class ChangeBranchDialogModel(QtCore.QAbstractTableModel):
|
||||
row = index.row()
|
||||
column = index.column()
|
||||
if role == QtCore.Qt.ToolTipRole:
|
||||
tooltip = ""
|
||||
# TODO: What should the tooltip be for these items? Last commit message?
|
||||
tooltip = self.branches[row]["author"] + ": " + self.branches[row]["subject"]
|
||||
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.fromSecsSinceEpoch(dd[column])
|
||||
return QtCore.QLocale().toString(qdate, QtCore.QLocale.ShortFormat)
|
||||
elif column < len(dd):
|
||||
return dd[column]
|
||||
dd = self.branches[row]
|
||||
if column == 2:
|
||||
if dd["date"] is not None:
|
||||
q_date = QtCore.QDateTime.fromString(
|
||||
dd["date"], QtCore.Qt.DateFormat.RFC2822Date
|
||||
)
|
||||
return QtCore.QLocale().toString(q_date, QtCore.QLocale.ShortFormat)
|
||||
return None
|
||||
elif column == 0:
|
||||
return dd["ref_name"]
|
||||
elif column == 1:
|
||||
return dd["upstream"]
|
||||
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]
|
||||
if column == 2:
|
||||
if self.branches[row]["date"] is not None:
|
||||
q_date = QtCore.QDateTime.fromString(
|
||||
self.branches[row]["date"], QtCore.Qt.DateFormat.RFC2822Date
|
||||
)
|
||||
return q_date
|
||||
return None
|
||||
elif column == 0:
|
||||
return self.branches[row]["ref_name"]
|
||||
elif column == 1:
|
||||
return self.branches[row]["upstream"]
|
||||
else:
|
||||
return None
|
||||
elif role == ChangeBranchDialogModel.RefAccessRole:
|
||||
return self.refs[row]
|
||||
return self.branches[row]
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
@@ -248,32 +201,38 @@ class ChangeBranchDialogModel(QtCore.QAbstractTableModel):
|
||||
if section == 0:
|
||||
return translate(
|
||||
"AddonsInstaller",
|
||||
"Kind",
|
||||
"Table header for git ref type (e.g. either Tag or Branch)",
|
||||
"Local",
|
||||
"Table header for local git ref name",
|
||||
)
|
||||
if section == 1:
|
||||
return translate(
|
||||
"AddonsInstaller",
|
||||
"Remote tracking",
|
||||
"Table header for git remote tracking branch name",
|
||||
)
|
||||
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",
|
||||
"Last Updated",
|
||||
"Table header for git update date",
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _remove_tracking_duplicates(self):
|
||||
remote_tracking_branches = []
|
||||
branches_to_keep = []
|
||||
for branch in self.branches:
|
||||
if branch["upstream"]:
|
||||
remote_tracking_branches.append(branch["upstream"])
|
||||
for branch in self.branches:
|
||||
if (
|
||||
"HEAD" not in branch["ref_name"]
|
||||
and branch["ref_name"] not in remote_tracking_branches
|
||||
):
|
||||
branches_to_keep.append(branch)
|
||||
self.branches = branches_to_keep
|
||||
|
||||
|
||||
class ChangeBranchDialogFilter(QtCore.QSortFilterProxyModel):
|
||||
def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex):
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Change to branch or tag:</string>
|
||||
<string>Change to branch:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
||||
@@ -34,19 +34,10 @@ import addonmanager_utilities as utils
|
||||
from addonmanager_metadata import Version, UrlType, get_first_supported_freecad_version
|
||||
from addonmanager_workers_startup import GetMacroDetailsWorker, CheckSingleUpdateWorker
|
||||
from addonmanager_readme_viewer import ReadmeViewer
|
||||
from addonmanager_git import GitManager, NoGitFound, GitFailed
|
||||
from Addon import Addon
|
||||
from change_branch import ChangeBranchDialog
|
||||
|
||||
have_git = False
|
||||
try:
|
||||
import git
|
||||
|
||||
if hasattr(git, "Repo"):
|
||||
have_git = True
|
||||
except ImportError:
|
||||
git = None
|
||||
|
||||
|
||||
translate = fci.translate
|
||||
|
||||
|
||||
@@ -70,6 +61,10 @@ class PackageDetails(QtWidgets.QWidget):
|
||||
self.worker = None
|
||||
self.repo = None
|
||||
self.status_update_thread = None
|
||||
try:
|
||||
self.git_manager = GitManager()
|
||||
except NoGitFound:
|
||||
self.git_manager = None
|
||||
|
||||
self.ui.buttonBack.clicked.connect(self.back.emit)
|
||||
self.ui.buttonExecute.clicked.connect(lambda: self.execute.emit(self.repo))
|
||||
@@ -183,14 +178,16 @@ class PackageDetails(QtWidgets.QWidget):
|
||||
elif status == Addon.Status.NO_UPDATE_AVAILABLE:
|
||||
detached_head = False
|
||||
branch = repo.branch
|
||||
if have_git and repo.repo_type != Addon.Kind.MACRO:
|
||||
if self.git_manager and repo.repo_type != Addon.Kind.MACRO:
|
||||
basedir = fci.getUserAppDataDir()
|
||||
moddir = os.path.join(basedir, "Mod", repo.name)
|
||||
if os.path.exists(os.path.join(moddir, ".git")):
|
||||
gitrepo = git.Repo(moddir)
|
||||
branch = gitrepo.head.ref.name
|
||||
detached_head = gitrepo.head.is_detached
|
||||
|
||||
repo_path = os.path.join(moddir, ".git")
|
||||
if os.path.exists(repo_path):
|
||||
branch = self.git_manager.current_branch(repo_path)
|
||||
if self.git_manager.detached_head(repo_path):
|
||||
tag = self.git_manager.current_tag(repo_path)
|
||||
branch = tag
|
||||
detached_head = True
|
||||
if detached_head:
|
||||
installed_version_string += (
|
||||
translate(
|
||||
@@ -356,7 +353,7 @@ class PackageDetails(QtWidgets.QWidget):
|
||||
return
|
||||
|
||||
# Can we actually switch branches? If not, return.
|
||||
if not have_git:
|
||||
if not self.git_manager:
|
||||
return
|
||||
|
||||
# Is there a .git subdirectory? If not, return.
|
||||
@@ -367,6 +364,7 @@ class PackageDetails(QtWidgets.QWidget):
|
||||
|
||||
# If all four above checks passed, then it's possible for us to switch
|
||||
# branches, if there are any besides the one we are on: show the button
|
||||
print("Showing the button")
|
||||
self.ui.buttonChangeBranch.show()
|
||||
|
||||
def set_disable_button_state(self):
|
||||
|
||||
Reference in New Issue
Block a user