66import json
77import logging
88import os
9+ import re
910import threading
1011import time
1112import uuid
1819from blinker import signal
1920from flask_login import current_user
2021from pygeodiff import GeoDiff
22+ from functools import cached_property
2123from sqlalchemy import text , null , desc , nullslast , tuple_
24+ from sqlalchemy .orm import contains_eager , joinedload , load_only
2225from sqlalchemy .dialects .postgresql import ARRAY , BIGINT , UUID , JSONB , ENUM , insert
2326from sqlalchemy .types import String
2427from sqlalchemy .ext .hybrid import hybrid_property
@@ -137,6 +140,12 @@ def workspace(self):
137140 project_workspace = current_app .ws_handler .get (self .workspace_id )
138141 return project_workspace
139142
143+ @cached_property
144+ def _has_conflict (self ) -> bool :
145+ """True if any current project file matches a known conflict-copy pattern."""
146+ pattern = r"(\.gpkg|\.qgs|.qgz)(.*conflict.*)|( \(.*conflict.*)"
147+ return any (re .search (pattern , f .path ) for f in self .files )
148+
140149 def get_latest_files_cache (self ) -> List [int ]:
141150 """Get latest file history ids either from cached table or calculate them on the fly"""
142151 if self .latest_project_files .file_history_ids is not None :
@@ -669,7 +678,7 @@ def __init__(
669678 def path (self ) -> str :
670679 return self .file .path
671680
672- @property
681+ @cached_property
673682 def diff (self ) -> Optional [FileDiff ]:
674683 """Diff file pushed with UPDATE_DIFF change type.
675684
@@ -724,9 +733,37 @@ def changes(
724733 if not (is_versioned_file (file ) and since is not None and to is not None ):
725734 return []
726735
727- history = []
736+ # when since=1 the range spans the entire project history; narrow it to
737+ # the most recent CREATE/DELETE so we don't load records from previous
738+ # file lifecycles that the Python break would discard anyway
739+ if since == 1 :
740+ boundary = (
741+ FileHistory .query .join (ProjectFilePath )
742+ .filter (
743+ ProjectFilePath .project_id == project_id ,
744+ ProjectFilePath .path == file ,
745+ FileHistory .project_version_name <= to ,
746+ FileHistory .change .in_ (
747+ [PushChangeType .CREATE .value , PushChangeType .DELETE .value ]
748+ ),
749+ )
750+ .order_by (desc (FileHistory .project_version_name ))
751+ .with_entities (FileHistory .project_version_name )
752+ .first ()
753+ )
754+ since = boundary [0 ] if boundary else since
755+
728756 full_history = (
729757 FileHistory .query .join (ProjectFilePath )
758+ .join (FileHistory .version )
759+ .join (ProjectVersion .project )
760+ .options (
761+ contains_eager (FileHistory .file ).load_only (ProjectFilePath .path ),
762+ contains_eager (FileHistory .version )
763+ .load_only (ProjectVersion .name , ProjectVersion .project_id )
764+ .contains_eager (ProjectVersion .project )
765+ .load_only (Project .storage_params ),
766+ )
730767 .filter (
731768 ProjectFilePath .project_id == project_id ,
732769 FileHistory .project_version_name <= to ,
@@ -737,6 +774,7 @@ def changes(
737774 .all ()
738775 )
739776
777+ history = []
740778 for item in full_history :
741779 history .append (item )
742780
@@ -1792,11 +1830,15 @@ def diff_summary(self):
17921830
17931831 def changes_count (self ) -> Dict :
17941832 """Return number of changes by type"""
1795- query = f "SELECT change, COUNT(change) FROM file_history WHERE version_id = :version_id GROUP BY change;"
1833+ query = "SELECT change, COUNT(change) FROM file_history WHERE version_id = :version_id GROUP BY change;"
17961834 params = {"version_id" : self .id }
17971835 result = db .session .execute (text (query ), params ).fetchall ()
17981836 return {row [0 ]: row [1 ] for row in result }
17991837
1838+ @cached_property
1839+ def _changes_count (self ) -> Dict :
1840+ return self .changes_count ()
1841+
18001842 @property
18011843 def zip_path (self ):
18021844 return os .path .join (
0 commit comments