initial commit

This commit is contained in:
Bruno Perdigão
2023-04-22 16:07:30 -03:00
committed by Dion Moult
parent 2bcfe93dd2
commit bf945e36bd
7 changed files with 1046 additions and 0 deletions
@@ -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@postle.net>, Bruno Perdigão <brunoperdigao@tutanota.com>,
# Massimo Fabbro <maxfb87@yahoo.it>
# 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
@@ -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")
@@ -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"}
@@ -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,
)
@@ -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)))
+62
View File
@@ -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)
+356
View File
@@ -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(
"^(?!\.| |-|/)((?!\.\.)(?!.*/\.)(/\*|/\*/)*(?!@\{)[^\~\:\^\\\ \?*\[])+(?<!\.|/)(?<!\.lock)$",
string,
)
@classmethod
def load_project(cls, path_ifc):
"""Clear and load an ifc project"""
IfcStore.purge()
# delete any IfcProject/* collections
for collection in bpy.data.collections:
if re.match("^IfcProject/", collection.name):
IfcGit.delete_collection(collection)
# delete any Ifc* objects not in IfcProject/ heirarchy
for obj in bpy.data.objects:
if re.match("^Ifc", obj.name):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.orphans_purge(do_recursive=True)
bpy.ops.bim.load_project(filepath=path_ifc)
bpy.ops.ifcgit.refresh()
@classmethod
def branches_by_hexsha(cls, repo):
"""reverse lookup for branches"""
result = {}
for branch in repo.branches:
if branch.commit.hexsha in result:
result[branch.commit.hexsha].append(branch)
else:
result[branch.commit.hexsha] = [branch]
return result
@classmethod
def tags_by_hexsha(cls, repo):
"""reverse lookup for tags"""
result = {}
for tag in repo.tags:
if tag.commit.hexsha in result:
result[tag.commit.hexsha].append(tag)
else:
result[tag.commit.hexsha] = [tag]
return result
@classmethod
def ifc_diff_ids(cls, repo, hash_a, hash_b, path_ifc):
"""Given two revision hashes and a filename, retrieve"""
"""step-ids of modified, added and removed entities"""
# NOTE this is calling the git binary in a subprocess
if not hash_a:
diff_lines = repo.git.diff(hash_b, path_ifc).split("\n")
else:
diff_lines = repo.git.diff(hash_a, hash_b, path_ifc).split("\n")
inserted = set()
deleted = set()
for line in diff_lines:
re_search = re.search(r"^\+#([0-9]+)=", line)
if re_search:
inserted.add(int(re_search.group(1)))
continue
re_search = re.search(r"^-#([0-9]+)=", line)
if re_search:
deleted.add(int(re_search.group(1)))
modified = inserted.intersection(deleted)
return {
"modified": modified,
"added": inserted.difference(modified),
"removed": deleted.difference(modified),
}
@classmethod
def get_revisions_step_ids(cls):
path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file
props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
selected_revision = repo.commit(rev=item.hexsha)
current_revision = repo.commit()
if selected_revision == current_revision:
area = next(
area for area in bpy.context.screen.areas if area.type == "VIEW_3D"
)
area.spaces[0].shading.color_type = "MATERIAL"
return
if current_revision.committed_date > 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