mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
initial commit
This commit is contained in:
committed by
Dion Moult
parent
2bcfe93dd2
commit
bf945e36bd
@@ -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)))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user