unityflow 0.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
unityflow/git_utils.py ADDED
@@ -0,0 +1,307 @@
1
+ """Git utilities for incremental normalization.
2
+
3
+ Provides functions to detect changed Unity files based on git status or commit history.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+
12
+ # Unity YAML file extensions that can be normalized
13
+ # Core assets
14
+ UNITY_CORE_EXTENSIONS = {
15
+ ".prefab", # Prefab files
16
+ ".unity", # Scene files
17
+ ".asset", # ScriptableObject and generic assets
18
+ }
19
+
20
+ # Animation & control
21
+ UNITY_ANIMATION_EXTENSIONS = {
22
+ ".anim", # Animation clips
23
+ ".controller", # Animator Controller
24
+ ".overrideController", # Animator Override Controller
25
+ ".playable", # Playable assets (Timeline, etc.)
26
+ ".mask", # Avatar masks
27
+ ".signal", # Timeline Signal assets
28
+ }
29
+
30
+ # Materials & rendering
31
+ UNITY_RENDERING_EXTENSIONS = {
32
+ ".mat", # Materials
33
+ ".renderTexture", # Render Textures
34
+ ".flare", # Lens flare assets
35
+ ".shadervariants", # Shader variant collections
36
+ ".spriteatlas", # Sprite atlases
37
+ ".cubemap", # Cubemap assets
38
+ }
39
+
40
+ # Physics
41
+ UNITY_PHYSICS_EXTENSIONS = {
42
+ ".physicMaterial", # 3D Physics materials
43
+ ".physicsMaterial2D", # 2D Physics materials
44
+ }
45
+
46
+ # Terrain
47
+ UNITY_TERRAIN_EXTENSIONS = {
48
+ ".terrainlayer", # Terrain layer assets
49
+ ".brush", # Terrain brush assets
50
+ }
51
+
52
+ # Audio
53
+ UNITY_AUDIO_EXTENSIONS = {
54
+ ".mixer", # Audio Mixer assets
55
+ }
56
+
57
+ # UI & Editor
58
+ UNITY_UI_EXTENSIONS = {
59
+ ".guiskin", # GUI Skin assets
60
+ ".fontsettings", # Font settings
61
+ ".preset", # Presets
62
+ ".giparams", # Global Illumination parameters
63
+ }
64
+
65
+ # All Unity YAML extensions combined
66
+ UNITY_EXTENSIONS = (
67
+ UNITY_CORE_EXTENSIONS
68
+ | UNITY_ANIMATION_EXTENSIONS
69
+ | UNITY_RENDERING_EXTENSIONS
70
+ | UNITY_PHYSICS_EXTENSIONS
71
+ | UNITY_TERRAIN_EXTENSIONS
72
+ | UNITY_AUDIO_EXTENSIONS
73
+ | UNITY_UI_EXTENSIONS
74
+ )
75
+
76
+
77
+ def get_repo_root(path: Path | None = None) -> Path | None:
78
+ """Get the root directory of the git repository.
79
+
80
+ Args:
81
+ path: Starting path to search from (default: current directory)
82
+
83
+ Returns:
84
+ Path to repository root, or None if not in a git repository
85
+ """
86
+ try:
87
+ result = subprocess.run(
88
+ ["git", "rev-parse", "--show-toplevel"],
89
+ cwd=path or Path.cwd(),
90
+ capture_output=True,
91
+ text=True,
92
+ check=True,
93
+ )
94
+ return Path(result.stdout.strip())
95
+ except subprocess.CalledProcessError:
96
+ return None
97
+
98
+
99
+ def is_git_repository(path: Path | None = None) -> bool:
100
+ """Check if the given path is inside a git repository.
101
+
102
+ Args:
103
+ path: Path to check (default: current directory)
104
+
105
+ Returns:
106
+ True if inside a git repository
107
+ """
108
+ return get_repo_root(path) is not None
109
+
110
+
111
+ def get_changed_files(
112
+ extensions: Sequence[str] | None = None,
113
+ staged_only: bool = False,
114
+ include_untracked: bool = True,
115
+ cwd: Path | None = None,
116
+ ) -> list[Path]:
117
+ """Get list of changed files from git status.
118
+
119
+ Args:
120
+ extensions: File extensions to filter (default: Unity extensions)
121
+ staged_only: Only include staged files
122
+ include_untracked: Include untracked files
123
+ cwd: Working directory (default: current directory)
124
+
125
+ Returns:
126
+ List of paths to changed files
127
+ """
128
+ if extensions is None:
129
+ extensions = list(UNITY_EXTENSIONS)
130
+
131
+ repo_root = get_repo_root(cwd)
132
+ if repo_root is None:
133
+ return []
134
+
135
+ changed_files: list[Path] = []
136
+
137
+ # Get staged and unstaged changes
138
+ # --porcelain=v1 gives stable, parseable output
139
+ # -uall shows individual files in untracked directories (not just directory names)
140
+ cmd = ["git", "status", "--porcelain=v1"]
141
+ if include_untracked:
142
+ cmd.append("-uall")
143
+ else:
144
+ cmd.append("--untracked-files=no")
145
+
146
+ try:
147
+ result = subprocess.run(
148
+ cmd,
149
+ cwd=cwd or Path.cwd(),
150
+ capture_output=True,
151
+ text=True,
152
+ check=True,
153
+ )
154
+ except subprocess.CalledProcessError:
155
+ return []
156
+
157
+ for line in result.stdout.split("\n"):
158
+ if not line or len(line) < 4:
159
+ continue
160
+
161
+ # Porcelain format: XY filename (XY = 2 chars, space, then filename)
162
+ # X = index status, Y = worktree status
163
+ status_index = line[0]
164
+ status_worktree = line[1]
165
+ # Skip the space at position 2, filename starts at position 3
166
+ filepath = line[3:]
167
+
168
+ # Handle renames: "R old -> new"
169
+ if " -> " in filepath:
170
+ filepath = filepath.split(" -> ")[1]
171
+
172
+ # Filter by staged_only
173
+ if staged_only:
174
+ # Only include if index has changes (X is not ' ' or '?')
175
+ if status_index in (" ", "?"):
176
+ continue
177
+ else:
178
+ # Include both staged and unstaged, but not deleted
179
+ if status_index == "D" or status_worktree == "D":
180
+ continue
181
+
182
+ file_path = repo_root / filepath
183
+
184
+ # Filter by extension
185
+ if file_path.suffix.lower() in extensions:
186
+ if file_path.exists():
187
+ changed_files.append(file_path)
188
+
189
+ return changed_files
190
+
191
+
192
+ def get_files_changed_since(
193
+ ref: str,
194
+ extensions: Sequence[str] | None = None,
195
+ cwd: Path | None = None,
196
+ ) -> list[Path]:
197
+ """Get list of files changed since a git reference (commit, tag, branch).
198
+
199
+ Args:
200
+ ref: Git reference (e.g., "HEAD~5", "main", "v1.0.0")
201
+ extensions: File extensions to filter (default: Unity extensions)
202
+ cwd: Working directory (default: current directory)
203
+
204
+ Returns:
205
+ List of paths to changed files
206
+ """
207
+ if extensions is None:
208
+ extensions = list(UNITY_EXTENSIONS)
209
+
210
+ repo_root = get_repo_root(cwd)
211
+ if repo_root is None:
212
+ return []
213
+
214
+ # Get files changed between ref and HEAD
215
+ try:
216
+ result = subprocess.run(
217
+ ["git", "diff", "--name-only", ref, "HEAD"],
218
+ cwd=cwd or Path.cwd(),
219
+ capture_output=True,
220
+ text=True,
221
+ check=True,
222
+ )
223
+ except subprocess.CalledProcessError:
224
+ return []
225
+
226
+ changed_files: list[Path] = []
227
+
228
+ for line in result.stdout.strip().split("\n"):
229
+ if not line:
230
+ continue
231
+
232
+ file_path = repo_root / line
233
+
234
+ # Filter by extension
235
+ if file_path.suffix.lower() in extensions:
236
+ if file_path.exists():
237
+ changed_files.append(file_path)
238
+
239
+ return changed_files
240
+
241
+
242
+ def get_files_in_commit(
243
+ commit: str,
244
+ extensions: Sequence[str] | None = None,
245
+ cwd: Path | None = None,
246
+ ) -> list[Path]:
247
+ """Get list of files changed in a specific commit.
248
+
249
+ Args:
250
+ commit: Git commit hash or reference
251
+ extensions: File extensions to filter (default: Unity extensions)
252
+ cwd: Working directory (default: current directory)
253
+
254
+ Returns:
255
+ List of paths to changed files
256
+ """
257
+ if extensions is None:
258
+ extensions = list(UNITY_EXTENSIONS)
259
+
260
+ repo_root = get_repo_root(cwd)
261
+ if repo_root is None:
262
+ return []
263
+
264
+ try:
265
+ result = subprocess.run(
266
+ ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit],
267
+ cwd=cwd or Path.cwd(),
268
+ capture_output=True,
269
+ text=True,
270
+ check=True,
271
+ )
272
+ except subprocess.CalledProcessError:
273
+ return []
274
+
275
+ changed_files: list[Path] = []
276
+
277
+ for line in result.stdout.strip().split("\n"):
278
+ if not line:
279
+ continue
280
+
281
+ file_path = repo_root / line
282
+
283
+ # Filter by extension
284
+ if file_path.suffix.lower() in extensions:
285
+ if file_path.exists():
286
+ changed_files.append(file_path)
287
+
288
+ return changed_files
289
+
290
+
291
+ def filter_unity_files(
292
+ paths: Sequence[Path],
293
+ extensions: Sequence[str] | None = None,
294
+ ) -> list[Path]:
295
+ """Filter paths to only include Unity YAML files.
296
+
297
+ Args:
298
+ paths: List of paths to filter
299
+ extensions: File extensions to include (default: Unity extensions)
300
+
301
+ Returns:
302
+ Filtered list of paths
303
+ """
304
+ if extensions is None:
305
+ extensions = list(UNITY_EXTENSIONS)
306
+
307
+ return [p for p in paths if p.suffix.lower() in extensions and p.exists()]