From bf945e36bdf259f9abaae09bd45b94b43715b8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sat, 22 Apr 2023 16:07:30 -0300 Subject: [PATCH] initial commit --- .../blenderbim/bim/module/ifcgit/__init__.py | 59 +++ .../blenderbim/bim/module/ifcgit/data.py | 119 ++++++ .../blenderbim/bim/module/ifcgit/operator.py | 199 ++++++++++ .../blenderbim/bim/module/ifcgit/prop.py | 76 ++++ .../blenderbim/bim/module/ifcgit/ui.py | 175 +++++++++ src/blenderbim/blenderbim/core/ifcgit.py | 62 +++ src/blenderbim/blenderbim/tool/ifcgit.py | 356 ++++++++++++++++++ 7 files changed, 1046 insertions(+) create mode 100644 src/blenderbim/blenderbim/bim/module/ifcgit/__init__.py create mode 100644 src/blenderbim/blenderbim/bim/module/ifcgit/data.py create mode 100644 src/blenderbim/blenderbim/bim/module/ifcgit/operator.py create mode 100644 src/blenderbim/blenderbim/bim/module/ifcgit/prop.py create mode 100644 src/blenderbim/blenderbim/bim/module/ifcgit/ui.py create mode 100644 src/blenderbim/blenderbim/core/ifcgit.py create mode 100644 src/blenderbim/blenderbim/tool/ifcgit.py diff --git a/src/blenderbim/blenderbim/bim/module/ifcgit/__init__.py b/src/blenderbim/blenderbim/bim/module/ifcgit/__init__.py new file mode 100644 index 0000000000..d90dbee43a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/ifcgit/__init__.py @@ -0,0 +1,59 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# 2023 Bruno Postle , Bruno Perdigão , +# Massimo Fabbro + +# import os +# import sys +import bpy +from . import ui, prop, operator + +# sys.path.insert(0, "/home/bruno/src/ifc-git") +# sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# bl_info = { +# "name": "IFC Git", +# "author": "Bruno Postle", +# "location": "Scene > IFC Git", +# "description": "Manage IFC files in Git repositories", +# "blender": (2, 80, 0), +# "category": "Import-Export", +# } + +classes = ( + operators.AddFileToRepo, + operators.CommitChanges, + operators.CreateRepo, + operators.DiscardUncommitted, + operators.DisplayRevision, + operators.DisplayUncommitted, + operators.Merge, + operators.RefreshGit, + operators.SwitchRevision, + prop.IfcGitListItem, + prop.IfcGitProperties, + ui.IFCGIT_PT_panel, + ui.COMMIT_UL_List, + ) + + + + +def register(): + bpy.types.Scene.IfcGitProperties = bpy.props.PointerProperty(type=prop.IfcGitProperties) + + +def unregister(): + del bpy.types.Scene.IfcGitProperties diff --git a/src/blenderbim/blenderbim/bim/module/ifcgit/data.py b/src/blenderbim/blenderbim/bim/module/ifcgit/data.py new file mode 100644 index 0000000000..73857c2e8a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/ifcgit/data.py @@ -0,0 +1,119 @@ +import bpy +import os +import shutil +import tool +import blenderbim.tool as btool + + +def refresh(): + IfcGitData.is_loaded = False + + +class IfcGitData: + + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "repo": cls.repo(), + "branch_names": cls.branch_names(), + "path_ifc": cls.path_ifc(), + "branches_by_hexsha": cls.branches_by_hexsha(), + "tags_by_hexsha": cls.tags_by_hexsha(), + "name_ifc": cls.name_ifc(), + "dir_name": cls.dir_name(), + "base_name": cls.base_name(), + "is_dirty": cls.is_dirty(), + "commit": cls.commit(), + "current_revision": cls.current_revision(), + "git_exe": cls.git_exe(), + "ifcmerge_exe": cls.ifcmerge_exe(), + } + cls.is_loaded = True + + @classmethod + def repo(cls): + if bool(btool.Ifc.get()): + path_ifc = btool.Ifc.get_path() + if not os.path.isfile(path_ifc): + return None + return tool.IfcGit.repo_from_path(path_ifc) + else: + return tool.IfcGitRepo.repo + + @classmethod + def branch_names(cls): + return [] + + @classmethod + def path_ifc(cls): + return btool.Ifc.get_path() + + @classmethod + def branches_by_hexsha(cls): + try: + if tool.IfcGitRepo.repo.branches: + return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo) + except: + return {} + + @classmethod + def tags_by_hexsha(cls): + if tool.IfcGitRepo.repo: + return tool.IfcGit.tags_by_hexsha(tool.IfcGitRepo.repo) + return {} + + @classmethod + def name_ifc(cls): + if bool(btool.Ifc.get()): + path_ifc = btool.Ifc.get_path() + if tool.IfcGitRepo.repo: + working_dir = tool.IfcGitRepo.repo.working_dir + return os.path.relpath(path_ifc, working_dir) + return None + + @classmethod + def dir_name(cls): + if bool(btool.Ifc.get()): + path_ifc = btool.Ifc.get_path() + if not os.path.isfile(path_ifc): + return None + return os.path.dirname(path_ifc) + return None + + @classmethod + def base_name(cls): + if bool(btool.Ifc.get()): + path_ifc = btool.Ifc.get_path() + return os.path.basename(path_ifc) + return None + + @classmethod + def is_dirty(cls): + if tool.IfcGitRepo.repo and cls.git_exe(): + path_ifc = btool.Ifc.get_path() + return tool.IfcGitRepo.repo.is_dirty(path=path_ifc) + return False + + @classmethod + def commit(cls): + props = bpy.context.scene.IfcGitProperties + if len(props.ifcgit_commits) > 0: + item = props.ifcgit_commits[props.commit_index] + return tool.IfcGitRepo.repo.commit(rev=item.hexsha) + + @classmethod + def current_revision(cls): + props = bpy.context.scene.IfcGitProperties + if len(props.ifcgit_commits) > 0: + return tool.IfcGitRepo.repo.commit() + + @classmethod + def git_exe(cls): + return shutil.which("git") + + @classmethod + def ifcmerge_exe(cls): + return shutil.which("ifcmerge") diff --git a/src/blenderbim/blenderbim/bim/module/ifcgit/operator.py b/src/blenderbim/blenderbim/bim/module/ifcgit/operator.py new file mode 100644 index 0000000000..628f1e01ba --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/ifcgit/operator.py @@ -0,0 +1,199 @@ +import os +import re +import bpy + +import core +from tool import IfcGit as tool +from data import IfcGitData + +# TODO Remove all handler elements once in the correct folder structure +import handler + +import blenderbim.tool as btool + + +class CreateRepo(bpy.types.Operator): + """Initialise a Git repository""" + + bl_label = "Create Git repository" + bl_idname = "ifcgit.createrepo" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + path_ifc = IfcGitData.data["path_ifc"] + if not os.path.isfile(path_ifc): + return False + if IfcGitData.data["repo"]: + # repo already exists + return False + if re.match("^/home/[^/]+/?$", os.path.dirname(path_ifc)): + # don't make ${HOME} a repo + return False + return True + + def execute(self, context): + + core.create_repo(tool, btool.Ifc) + handler.refresh_ui_data() + return {"FINISHED"} + + +class AddFileToRepo(bpy.types.Operator): + """Add a file to a repository""" + + bl_label = "Add file to repository" + bl_idname = "ifcgit.addfile" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + path_ifc = IfcGitData.data["path_ifc"] + if not os.path.isfile(path_ifc): + return False + if not IfcGitData.data["repo"]: + # repo doesn't exist + return False + return True + + def execute(self, context): + + core.add_file(tool, btool.Ifc) + handler.refresh_ui_data() + return {"FINISHED"} + + +class DiscardUncommitted(bpy.types.Operator): + """Discard saved changes and update to HEAD""" + + bl_label = "Discard uncommitted changes" + bl_idname = "ifcgit.discard" + bl_options = {"REGISTER"} + + def execute(self, context): + + core.discard_uncomitted(tool, btool.Ifc) + handler.refresh_ui_data() + return {"FINISHED"} + + +class CommitChanges(bpy.types.Operator): + """Commit current saved changes""" + + bl_label = "Commit changes" + bl_idname = "ifcgit.commit_changes" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + props = context.scene.IfcGitProperties + repo = IfcGitData.data["repo"] + if props.commit_message == "": + return False + if ( + repo + and repo.head.is_detached + and ( + not tool.is_valid_ref_format(props.new_branch_name) + or props.new_branch_name in [branch.name for branch in repo.branches] + ) + ): + cls.poll_message_set( + "The new branch name is invalid, please insert a valid branch name (eg. with no spaces, ...)" + ) + return False + return True + + def execute(self, context): + + repo = IfcGitData.data["repo"] + core.commit_changes(tool, btool.Ifc, repo, context) + handler.refresh_ui_data() + return {"FINISHED"} + + +class RefreshGit(bpy.types.Operator): + """Refresh revision list""" + + bl_label = "" + bl_idname = "ifcgit.refresh" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + repo = IfcGitData.data["repo"] + if repo != None and repo.heads: + return True + return False + + def execute(self, context): + + repo = IfcGitData.data["repo"] + core.refresh_revision_list(tool, repo, btool.Ifc) + handler.refresh_ui_data() + return {"FINISHED"} + + +class DisplayRevision(bpy.types.Operator): + """Colourise objects by selected revision""" + + bl_label = "" + bl_idname = "ifcgit.display_revision" + bl_options = {"REGISTER"} + + def execute(self, context): + + core.colourise_revision(tool, context) + handler.refresh_ui_data() + return {"FINISHED"} + + +class DisplayUncommitted(bpy.types.Operator): + """Colourise uncommitted objects""" + + bl_label = "Show uncommitted changes" + bl_idname = "ifcgit.display_uncommitted" + bl_options = {"REGISTER"} + + def execute(self, context): + + repo = IfcGitData.data["repo"] + core.colourise_uncommitted(tool, btool.Ifc, repo) + handler.refresh_ui_data() + return {"FINISHED"} + + +class SwitchRevision(bpy.types.Operator): + """Switches the repository to the selected revision and reloads the IFC file""" + + bl_label = "" + bl_idname = "ifcgit.switch_revision" + bl_options = {"REGISTER"} + + def execute(self, context): + + core.switch_revision(tool, btool.Ifc) + handler.refresh_ui_data() + return {"FINISHED"} + + +class Merge(bpy.types.Operator): + """Merges the selected branch into working branch""" + + bl_label = "Merge this branch" + bl_idname = "ifcgit.merge" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + if IfcGitData.data["ifcmerge_exe"]: + return True + return False + + def execute(self, context): + + if core.merge_branch(tool, btool.Ifc, self): + handler.refresh_ui_data() + return {"FINISHED"} + else: + return {"CANCELLED"} diff --git a/src/blenderbim/blenderbim/bim/module/ifcgit/prop.py b/src/blenderbim/blenderbim/bim/module/ifcgit/prop.py new file mode 100644 index 0000000000..8e312d7062 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/ifcgit/prop.py @@ -0,0 +1,76 @@ +import bpy +from bpy.types import PropertyGroup + +from bpy.props import ( + StringProperty, + BoolProperty, + CollectionProperty, + IntProperty, + EnumProperty, +) + +from data import IfcGitData + + +def git_branches(self, context): + """branches enum""" + + # NOTE "Python must keep a reference to the strings returned by + # the callback or Blender will misbehave or even crash" + IfcGitData.data["branch_names"] = sorted( + [branch.name for branch in IfcGitData.data["repo"].heads] + ) + + if "main" in IfcGitData.data["branch_names"]: + IfcGitData.data["branch_names"].remove("main") + IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"] + + return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]] + + +def update_revlist(self, context): + """wrapper to trigger update of the revision list""" + + bpy.ops.ifcgit.refresh() + props = context.scene.IfcGitProperties + props.commit_index = 0 + + +class IfcGitListItem(PropertyGroup): + """Group of properties representing an item in the list.""" + + hexsha: StringProperty( + name="Git hash", + description="checksum for this commit", + default="Uncommitted data!", + ) + relevant: BoolProperty( + name="Is relevant", + description="does this commit reference our ifc file", + default=False, + ) + + +class IfcGitProperties(PropertyGroup): + + ifcgit_commits: CollectionProperty(type=IfcGitListItem, name="List of git items") + commit_index: IntProperty(name="Index for my_list", default=0) + commit_message: StringProperty( + name="Commit message", + description="A human readable description of these changes", + default="", + ) + new_branch_name: StringProperty( + name="New branch name", + description="A short name used to refer to this branch", + default="", + ) + display_branch: EnumProperty(items=git_branches, update=update_revlist) + ifcgit_filter: EnumProperty( + items=[ + ("all", "All", "All revisions"), + ("tagged", "Tagged", "Tagged revisions"), + ("relevant", "Relevant", "Revisions for this project"), + ], + update=update_revlist, + ) diff --git a/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py b/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py new file mode 100644 index 0000000000..c2f0adafee --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py @@ -0,0 +1,175 @@ + +import bpy +import time +from data import IfcGitData + + +class IFCGIT_PT_panel(bpy.types.Panel): + """Scene Properties panel to interact with IFC repository data""" + + bl_label = "IFC Git" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_project_info" + + def draw(self, context): + + if not IfcGitData.is_loaded: + IfcGitData.load() + + layout = self.layout + path_ifc = IfcGitData.data["path_ifc"] + + if not IfcGitData.data["git_exe"]: + row = layout.row() + row.label(text="Git is not installed", icon="ERROR") + return + + props = context.scene.IfcGitProperties + + # TODO if file isn't saved, offer to save to disk + + row = layout.row() + if path_ifc: + if IfcGitData.data["repo"]: + name_ifc = IfcGitData.data["name_ifc"] + row.label(text=IfcGitData.data["repo"].working_dir, icon="SYSTEM") + if name_ifc in IfcGitData.data["repo"].untracked_files: + row.operator( + "ifcgit.addfile", + text="Add '" + name_ifc + "' to repository", + icon="FILE", + ) + else: + row.label(text=name_ifc, icon="FILE") + else: + row.operator( + "ifcgit.createrepo", + text="Create '" + IfcGitData.data["dir_name"] + "' repository", + icon="SYSTEM", + ) + row.label(text=IfcGitData.data["base_name"], icon="FILE") + return + else: + row.label(text="No Git repository found", icon="SYSTEM") + row.label(text="No IFC project saved", icon="FILE") + return + + is_dirty = IfcGitData.data["is_dirty"] + + if is_dirty: + row = layout.row() + row.label(text="Saved changes have not been committed", icon="ERROR") + + row = layout.row() + row.operator("ifcgit.display_uncommitted", icon="SELECT_DIFFERENCE") + row.operator("ifcgit.discard", icon="TRASH") + + row = layout.row() + row.prop(props, "commit_message") + + if IfcGitData.data["repo"].head.is_detached: + row = layout.row() + row.label( + text="HEAD is detached, commit will create a branch", icon="ERROR" + ) + row.prop(props, "new_branch_name") + + row = layout.row() + row.operator("ifcgit.commit_changes", icon="GREASEPENCIL") + + row = layout.row() + if IfcGitData.data["repo"].head.is_detached: + row.label(text="Working branch: Detached HEAD") + else: + row.label( + text="Working branch: " + IfcGitData.data["repo"].active_branch.name + ) + + grouped = layout.row() + column = grouped.column() + row = column.row() + row.prop(props, "display_branch", text="Browse branch") + row.prop(props, "ifcgit_filter", text="Filter revisions") + + row = column.row() + row.template_list( + "COMMIT_UL_List", + "The_List", + props, + "ifcgit_commits", + props, + "commit_index", + ) + column = grouped.column() + row = column.row() + row.operator("ifcgit.refresh", icon="FILE_REFRESH") + + if not is_dirty: + + row = column.row() + row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE") + + row = column.row() + row.operator("ifcgit.switch_revision", icon="CURRENT_FILE") + + # TODO operator to tag selected + + row = column.row() + row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="") + + if not props.ifcgit_commits: + return + + item = props.ifcgit_commits[props.commit_index] + commit = IfcGitData.data["commit"] + if not item.relevant: + row = layout.row() + row.label(text="Revision unrelated to current IFC project", icon="ERROR") + + box = layout.box() + column = box.column(align=True) + row = column.row() + row.label(text=commit.hexsha) + row = column.row() + row.label(text=commit.author.name + " <" + commit.author.email + ">") + row = column.row() + row.label(text=commit.message) + + +class COMMIT_UL_List(bpy.types.UIList): + """List of Git commits""" + + def draw_item( + self, context, layout, data, item, icon, active_data, active_propname, index + ): + + props = context.scene.IfcGitProperties + + current_revision = IfcGitData.data["current_revision"] + + # TODO Figure how this "item" can be acesse in "data.py" + # so it's possible to move the ".commit" + commit = IfcGitData.data["repo"].commit(rev=item.hexsha) + + lookup = IfcGitData.data["branches_by_hexsha"] + refs = "" + if item.hexsha in lookup: + for branch in lookup[item.hexsha]: + if branch.name == props.display_branch: + refs = "[" + branch.name + "] " + + lookup = IfcGitData.data["tags_by_hexsha"] + if item.hexsha in lookup: + for tag in lookup[item.hexsha]: + refs += "{" + tag.name + "} " + + if commit == current_revision: + layout.label( + text="[HEAD] " + refs + commit.message, icon="DECORATE_KEYFRAME" + ) + else: + layout.label(text=refs + commit.message, icon="DECORATE_ANIMATE") + layout.label(text=time.strftime("%c", time.localtime(commit.committed_date))) diff --git a/src/blenderbim/blenderbim/core/ifcgit.py b/src/blenderbim/blenderbim/core/ifcgit.py new file mode 100644 index 0000000000..ff6a17178c --- /dev/null +++ b/src/blenderbim/blenderbim/core/ifcgit.py @@ -0,0 +1,62 @@ +def create_repo(ifcgit, ifc): + path_ifc = ifc.get_path() + path_dir = ifcgit.get_path_dir(path_ifc) + ifcgit.init_repo(path_dir) + + +def add_file(ifcgit, ifc): + path_ifc = ifc.get_path() + repo = ifcgit.repo_from_path(path_ifc) + ifcgit.add_file_to_repo(repo, path_ifc) + + +def discard_uncomitted(ifcgit, ifc): + path_ifc = ifc.get_path() + # NOTE this is calling the git binary in a subprocess + ifcgit.git_checkout(path_ifc) + ifcgit.load_project(path_ifc) + + +def commit_changes(ifcgit, ifc, repo, context): + path_ifc = ifc.get_path() + ifcgit.git_commit(path_ifc) + + if repo.head.is_detached: + ifcgit.create_new_branch() + + +def refresh_revision_list(ifcgit, repo, ifc): + ifcgit.clear_commits_list() + path_ifc = ifc.get_path() + lookup = ifcgit.tags_by_hexsha(repo) + ifcgit.get_commits_list(path_ifc, lookup) + + +def colourise_revision(ifcgit, context): + + step_ids = ifcgit.get_revisions_step_ids() + if not step_ids: + return + modified_shape_object_step_ids = ifcgit.get_modified_shape_object_step_ids(step_ids) + final_step_ids = ifcgit.update_step_ids(step_ids, modified_shape_object_step_ids) + ifcgit.colourise(final_step_ids) + + +def colourise_uncommitted(ifcgit, ifc, repo): + path_ifc = ifc.get_path() + step_ids = ifcgit.ifc_diff_ids(repo, None, "HEAD", path_ifc) + ifcgit.colourise(step_ids) + + +def switch_revision(ifcgit, ifc): + # FIXME bad things happen when switching to a revision that predates current project + + path_ifc = ifc.get_path() + ifcgit.switch_to_revision_item() + ifcgit.load_project(path_ifc) + + +def merge_branch(ifcgit, ifc, operator): + path_ifc = ifc.get_path() + ifcgit.config_ifcmerge() + ifcgit.execute_merge(path_ifc, operator) diff --git a/src/blenderbim/blenderbim/tool/ifcgit.py b/src/blenderbim/blenderbim/tool/ifcgit.py new file mode 100644 index 0000000000..416cc05e52 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/ifcgit.py @@ -0,0 +1,356 @@ +import os +import re + +# allows git import even if git executable isn't found +os.environ["GIT_PYTHON_REFRESH"] = "quiet" +import git +import bpy +from blenderbim.bim.ifc import IfcStore +import blenderbim.tool as tool + + +class IfcGit: + @classmethod + def init_repo(cls, path_dir): + IfcGitRepo.repo = git.Repo.init(path_dir) + + @classmethod + def get_path_dir(cls, path_ifc): + return os.path.abspath(os.path.dirname(path_ifc)) + + @classmethod + def repo_from_path(cls, path): + """Returns a Git repository object or None""" + + if os.path.isdir(path): + path_dir = os.path.abspath(path) + elif os.path.isfile(path): + path_dir = os.path.abspath(os.path.dirname(path)) + else: + return None + + if IfcGitRepo.repo != None and IfcGitRepo.repo.working_dir == path_dir: + return IfcGitRepo.repo + + try: + repo = git.Repo(path_dir) + except: + parentdir_path = os.path.abspath(os.path.join(path_dir, os.pardir)) + if parentdir_path == path_dir: + # root folder + return None + return IfcGit.repo_from_path(parentdir_path) + if repo: + IfcGitRepo.repo = repo + return repo + + @classmethod + def add_file_to_repo(cls, repo, path_file): + repo.index.add(path_file) + repo.index.commit( + message="Added " + os.path.relpath(path_file, repo.working_dir) + ) + bpy.ops.ifcgit.refresh() + + @classmethod + def git_checkout(cls, path_file): + IfcGitRepo.repo.git.checkout(path_file) + + @classmethod + def git_commit(cls, path_file): + props = bpy.context.scene.IfcGitProperties + repo = IfcGitRepo.repo + repo.index.add(path_file) + repo.index.commit(message=props.commit_message) + props.commit_message = "" + return repo + + @classmethod + def create_new_branch(cls): + props = bpy.context.scene.IfcGitProperties + repo = IfcGitRepo.repo + new_branch = repo.create_head(props.new_branch_name) + new_branch.checkout() + props.display_branch = props.new_branch_name + props.new_branch_name = "" + + bpy.ops.ifcgit.refresh() + + @classmethod + def clear_commits_list(cls): + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].shading.color_type = "MATERIAL" + props = bpy.context.scene.IfcGitProperties + + # ifcgit_commits is registered list widget + props.ifcgit_commits.clear() + + @classmethod + def get_commits_list(cls, path_ifc, lookup): + + props = bpy.context.scene.IfcGitProperties + commits = list( + git.objects.commit.Commit.iter_items( + repo=IfcGitRepo.repo, + rev=[props.display_branch], + ) + ) + commits_relevant = list( + git.objects.commit.Commit.iter_items( + repo=IfcGitRepo.repo, + rev=[props.display_branch], + paths=[path_ifc], + ) + ) + + for commit in commits: + + if props.ifcgit_filter == "tagged" and not commit.hexsha in lookup: + continue + elif props.ifcgit_filter == "relevant" and not commit in commits_relevant: + continue + + props.ifcgit_commits.add() + props.ifcgit_commits[-1].hexsha = commit.hexsha + if commit in commits_relevant: + props.ifcgit_commits[-1].relevant = True + + @classmethod + def is_valid_ref_format(cls, string): + """Check a bare branch or tag name is valid""" + + return re.match( + "^(?!\.| |-|/)((?!\.\.)(?!.*/\.)(/\*|/\*/)*(?!@\{)[^\~\:\^\\\ \?*\[])+(? selected_revision.committed_date: + step_ids = IfcGit.ifc_diff_ids( + repo, + selected_revision.hexsha, + current_revision.hexsha, + path_ifc, + ) + else: + step_ids = IfcGit.ifc_diff_ids( + repo, + current_revision.hexsha, + selected_revision.hexsha, + path_ifc, + ) + return step_ids + + @classmethod + def get_modified_shape_object_step_ids(cls, step_ids): + model = tool.Ifc.get() + modified_shape_object_step_ids = {"modified": []} + + for step_id in step_ids["modified"]: + if model.by_id(step_id).is_a() == "IfcProductDefinitionShape": + product = model.by_id(step_id).ShapeOfProduct[0] + modified_shape_object_step_ids["modified"].append(product.id()) + + return modified_shape_object_step_ids + + @classmethod + def update_step_ids(cls, step_ids, modified_shape_object_step_ids): + + final_step_ids = {} + final_step_ids["added"] = step_ids["added"] + final_step_ids["removed"] = step_ids["removed"] + final_step_ids["modified"] = step_ids["modified"].union( + modified_shape_object_step_ids["modified"] + ) + return final_step_ids + + @classmethod + def colourise(cls, step_ids): + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].shading.color_type = "OBJECT" + + for obj in bpy.context.visible_objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + step_id = obj.BIMObjectProperties.ifc_definition_id + if step_id in step_ids["modified"]: + obj.color = (0.3, 0.3, 1.0, 1) + elif step_id in step_ids["added"]: + obj.color = (0.2, 0.8, 0.2, 1) + elif step_id in step_ids["removed"]: + obj.color = (1.0, 0.2, 0.2, 1) + else: + obj.color = (1.0, 1.0, 1.0, 0.5) + + @classmethod + def switch_to_revision_item(cls): + props = bpy.context.scene.IfcGitProperties + repo = IfcGitRepo.repo + item = props.ifcgit_commits[props.commit_index] + + lookup = IfcGit.branches_by_hexsha(repo) + if item.hexsha in lookup: + for branch in lookup[item.hexsha]: + if branch.name == props.display_branch: + branch.checkout() + else: + # NOTE this is calling the git binary in a subprocess + repo.git.checkout(item.hexsha) + + @classmethod + def delete_collection(cls, blender_collection): + for obj in blender_collection.objects: + bpy.data.objects.remove(obj, do_unlink=True) + bpy.data.collections.remove(blender_collection) + for collection in bpy.data.collections: + if not collection.users: + bpy.data.collections.remove(collection) + + @classmethod + def is_valid_branch_name(cls, new_branch_name): + """Check if a branch name is valid and doesn't conflict with existing branches""" + if not IfcGit.is_valid_ref_format(new_branch_name): + return False + if new_branch_name in [branch.name for branch in IfcGitRepo.repo.branches]: + return False + return True + + @classmethod + def config_ifcmerge(cls): + config_reader = IfcGitRepo.repo.config_reader() + section = 'mergetool "ifcmerge"' + if not config_reader.has_section(section): + config_writer = IfcGitRepo.repo.config_writer() + config_writer.set_value( + section, "cmd", "ifcmerge $BASE $LOCAL $REMOTE $MERGED" + ) + config_writer.set_value(section, "trustExitCode", True) + + @classmethod + def execute_merge(cls, path_ifc, operator): + props = bpy.context.scene.IfcGitProperties + repo = IfcGitRepo.repo + item = props.ifcgit_commits[props.commit_index] + lookup = IfcGit.branches_by_hexsha(repo) + if item.hexsha in lookup: + for branch in lookup[item.hexsha]: + if branch.name == props.display_branch: + # this is a branch! + try: + # NOTE this is calling the git binary in a subprocess + repo.git.merge(branch) + except git.exc.GitCommandError: + # merge is expected to fail, run ifcmerge + try: + repo.git.mergetool(tool="ifcmerge") + except: + # ifcmerge failed, rollback + repo.git.merge(abort=True) + # FIXME need to report errors somehow + + operator.report({"ERROR"}, "IFC Merge failed") + return False + except: + + operator.report({"ERROR"}, "Unknown IFC Merge failure") + return False + + repo.index.add(path_ifc) + props.commit_message = "Merged branch: " + props.display_branch + props.display_branch = repo.active_branch.name + + IfcGit.load_project(path_ifc) + + +class IfcGitRepo: + repo = None