Skip to content

Commit 0fffc59

Browse files
committed
Merge branch 'feat/improvements'
2 parents d4500cb + cec5ab5 commit 0fffc59

8 files changed

Lines changed: 240 additions & 888 deletions

AGENTS.md

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
CLI tool that sorts sections of Xcode `project.pbxproj` files to reduce merge conflicts. Forked from WebKit's sort-Xcode-project-file with extended functionality.
66

7-
- **Script**: `sort-Xcode-project-file.py` (~428 lines, Python 3.9+, stdlib only)
8-
- **Legacy Perl version**: `sort-Xcode-project-file.pl` (maintained for reference; not actively developed)
7+
- **Script**: `sort-Xcode-project-file.py` (~503 lines, Python 3.9+, stdlib only)
98

109
**License:** MIT
1110

@@ -17,11 +16,13 @@ python3 sort-Xcode-project-file.py <path-to-project.pbxproj>
1716
python3 sort-Xcode-project-file.py <path-to-project.xcodeproj>
1817
python3 sort-Xcode-project-file.py --case-insensitive <path>
1918
python3 sort-Xcode-project-file.py --check <path> # CI mode: exit 0 if sorted
19+
python3 sort-Xcode-project-file.py -r <directory> # Recursive search
20+
cat project.pbxproj | python3 sort-Xcode-project-file.py - > sorted.pbxproj # Stdin/stdout
2021
```
2122

2223
### Tests
2324
```bash
24-
python3 -m unittest discover tests -v # Run all 54 tests
25+
python3 -m unittest discover tests -v # Run all 70 tests
2526
```
2627

2728
### Verify syntax
@@ -32,19 +33,20 @@ python3 -c "import py_compile; py_compile.compile('sort-Xcode-project-file.py',
3233
## Architecture
3334

3435
```
35-
sort-Xcode-project-file.py # Everything lives here (~428 lines)
36+
sort-Xcode-project-file.py # Everything lives here (~503 lines)
3637
├── Regex patterns # re.compile with re.VERBOSE (lines 31-76)
37-
├── natural_sort_key() # Natural (alphanumeric) sort key (line 98)
38-
├── extract_filename() # Regex-based filename extraction (line 117)
39-
├── is_directory() # Heuristic: no extension = directory (line 123)
40-
├── uniq() # Deduplicate preserving order (line 136)
41-
├── children_sort_key() # Sort key: dirs first, then natural sort (line 147)
42-
├── files_sort_key() # Sort key: natural sort only (line 153)
43-
├── read_array_entries() # Extract array contents between ( and ); (line 159)
44-
├── write_file() # Atomic write via tempfile + os.replace() (line 193)
45-
├── sort_project_file() # Core: read → parse → sort → write (line 217)
46-
├── build_parser() # argparse CLI definition (line 307)
47-
└── main() # Entry point: parse args, process files (line 366)
38+
├── natural_sort_key() # Natural (alphanumeric) sort key (line 115)
39+
├── extract_filename() # Regex-based filename extraction (line 134)
40+
├── is_directory() # Heuristic: no extension = directory (line 140)
41+
├── uniq() # Deduplicate preserving order (line 153)
42+
├── children_sort_key() # Sort key: dirs first, then natural sort (line 164)
43+
├── files_sort_key() # Sort key: natural sort only (line 170)
44+
├── read_array_entries() # Extract array contents between ( and ); (line 176)
45+
├── write_file() # Atomic write via tempfile + os.replace() (line 210)
46+
├── sort_project_content() # Core sorting logic (pure function) (line 234)
47+
├── sort_project_file() # Read → sort_project_content → write (line 315)
48+
├── build_parser() # argparse CLI definition (line 343)
49+
└── main() # Entry point: parse args, process files (line 409)
4850
```
4951

5052
### Test suite (`tests/`)
@@ -54,12 +56,11 @@ tests/
5456
├── __init__.py # Package marker
5557
├── _helpers.py # importlib-based import of hyphenated script name
5658
├── test_natural_sort.py # 15 tests for natural_sort_key()
57-
├── test_is_directory.py # 7 tests for is_directory()
59+
├── test_is_directory.py # 9 tests for is_directory()
5860
├── test_dedup.py # 7 tests for uniq()
5961
├── test_extract_filename.py # 5 tests for extract_filename()
60-
├── test_sort_project.py # 9 integration tests (sort, idempotency, check mode)
61-
├── test_cli.py # 12 CLI tests via subprocess (exit codes, flags)
62-
├── cross_validate.py # Standalone cross-validation script (Perl vs Python)
62+
├── test_sort_project.py # 16 integration tests (sort, idempotency, check mode, edge cases)
63+
├── test_cli.py # 19 CLI tests via subprocess (exit codes, flags, stdin, recursive)
6364
└── fixtures/
6465
├── basic_unsorted.pbxproj # Unsorted input fixture
6566
├── basic_sorted.pbxproj # Expected output (case-sensitive)
@@ -69,7 +70,15 @@ tests/
6970
├── with_duplicates.pbxproj # Duplicate entries fixture
7071
├── with_duplicates_sorted.pbxproj
7172
├── empty_arrays.pbxproj # Empty arrays fixture
72-
└── empty_arrays_sorted.pbxproj
73+
├── empty_arrays_sorted.pbxproj
74+
├── with_spm_packages.pbxproj # SPM packageProductDependencies/packageReferences
75+
├── with_spm_packages_sorted.pbxproj
76+
├── with_special_chars.pbxproj # Filenames with +, long names
77+
├── with_special_chars_sorted.pbxproj
78+
├── with_multi_fw_targets.pbxproj # Multiple FW build phases
79+
├── with_multi_fw_targets_sorted.pbxproj
80+
├── with_nested_groups.pbxproj # Nested children arrays, empty packageReferences
81+
└── with_nested_groups_sorted.pbxproj
7382
```
7483

7584
### Key Regex Patterns (compiled, module level)
@@ -155,17 +164,15 @@ def function_name(param: type) -> return_type:
155164

156165
| Flag | Effect |
157166
|---|---|
167+
| `-` | Read from stdin, write to stdout |
158168
| `--case-insensitive` | Case-insensitive natural sort |
159169
| `--case-sensitive` | Force case-sensitive (default) |
160170
| `--check` | Exit 0 if sorted, exit 1 if not (no modification) |
171+
| `-r` / `--recursive` | Recursively find and sort all `project.pbxproj` under directories |
161172
| `--version` | Show version and exit |
162173
| `-w` / `--no-warnings` | Suppress warning messages |
163174
| `-h` / `--help` | Show usage and exit |
164175

165-
## Known Limitations & Improvement Plan
166-
167-
See [`docs/improvement-plan.md`](docs/improvement-plan.md) for a prioritized list of improvements.
168-
169176
## Commit Style
170177

171178
Based on git history, use conventional commits:

README.md

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ Forked from [WebKit's sort-Xcode-project-file](https://github.com/WebKit/webkit/
66

77
## Features
88

9-
- Bypass `PBXFrameworksBuildPhase` section (preserve original order, important for some projects)
109
- Sort `children` arrays throughout the project (directories before files)
1110
- Sort `files` arrays in build phases
12-
- Sort `targets` list in project
13-
- Sort `packageProductDependencies` and `packageReferences` lists in project
14-
- Sort `buildConfigurations` list in each target
15-
- Case-sensitive sorting by default (preserves original behavior)
16-
- Option to enable case-insensitive sorting: `--case-insensitive`
11+
- Sort `targets`, `buildConfigurations`, `packageProductDependencies`, and `packageReferences` arrays
12+
- Preserve `PBXFrameworksBuildPhase` order (link order matters)
1713
- Remove duplicate references automatically
1814
- Natural (human) sorting for names/filenames (e.g., `file2` < `file10`)
15+
- Case-sensitive sorting by default (original WebKit behavior)
16+
- Option for case-insensitive sorting: `--case-insensitive`
1917
- `--check` mode for CI pipelines (exit 0 if sorted, exit 1 if unsorted)
18+
- Stdin/stdout pipeline support (`-`)
19+
- Recursive directory search (`-r` / `--recursive`)
2020
- Atomic file writes — never leaves a `.pbxproj` in a corrupted state
2121

2222
## Requirements
@@ -26,89 +26,88 @@ Forked from [WebKit's sort-Xcode-project-file](https://github.com/WebKit/webkit/
2626

2727
## Usage
2828

29-
**Please backup Xcode project file before using this script.** You can execute the following command to sort a project file:
30-
3129
```bash
32-
python3 sort-Xcode-project-file.py <path-to-xcodeproj-or-project.pbxproj>
30+
python3 sort-Xcode-project-file.py MyApp.xcodeproj
31+
python3 sort-Xcode-project-file.py path/to/project.pbxproj
3332
```
3433

34+
Both `.xcodeproj` directories and `project.pbxproj` files are accepted.
35+
3536
### Options
3637

3738
| Flag | Description |
3839
|------|-------------|
40+
| `-` | Read from stdin, write to stdout |
3941
| `--case-insensitive` | Enable case-insensitive sorting (default is case-sensitive) |
4042
| `--case-sensitive` | Explicit alias to force case-sensitive sorting |
4143
| `--check` | Check if file is already sorted (exit 0 = sorted, exit 1 = unsorted) |
44+
| `-r`, `--recursive` | Recursively find and sort all `project.pbxproj` under directories |
4245
| `--version` | Show version and exit |
4346
| `-w`, `--no-warnings` | Suppress warning messages |
4447
| `-h`, `--help` | Show help |
4548

46-
### CI Usage
47-
48-
Use `--check` in CI pipelines to enforce sorted project files:
49+
### Examples
4950

5051
```bash
52+
# Sort a single project
53+
python3 sort-Xcode-project-file.py MyApp.xcodeproj
54+
55+
# CI check — fails if unsorted
5156
python3 sort-Xcode-project-file.py --check MyApp.xcodeproj
52-
```
5357

54-
Exit code 0 means the file is already sorted; exit code 1 means it needs sorting.
58+
# Sort all projects in a monorepo
59+
python3 sort-Xcode-project-file.py -r .
5560

56-
## Pre-commit Hook Setup
61+
# Pipe through stdin/stdout
62+
cat project.pbxproj | python3 sort-Xcode-project-file.py - > sorted.pbxproj
5763

58-
You can use this tool to sort Xcode project files before committing to git. Sorting project files decreases merge conflict probability.
64+
# Check stdin without modifying anything
65+
cat project.pbxproj | python3 sort-Xcode-project-file.py --check -
66+
```
5967

60-
### 1.
68+
## Pre-commit Hook
6169

62-
Create a `Scripts` directory in project root directory, and put `sort-Xcode-project-file.py` into `Scripts` directory.
70+
Sort Xcode project files automatically before each commit to reduce merge conflicts.
6371

64-
### 2.
72+
**1.** Create a `Scripts` directory in your project root and copy `sort-Xcode-project-file.py` into it.
6573

66-
Put the following code into `.git/hooks/pre-commit` file.
74+
**2.** Create `.git/hooks/pre-commit` with the following content:
6775

6876
```bash
6977
#!/bin/sh
70-
#
71-
# Following script is to sort Xcode project files, and add them back to version control.
72-
# The reason to sort project file is that it can decrease project.pbxproj file merging conflict possibility.
73-
#
78+
7479
echo 'Sorting Xcode project files'
7580

7681
GIT_ROOT=$(git rev-parse --show-toplevel)
7782
sorter="$GIT_ROOT/Scripts/sort-Xcode-project-file.py"
78-
modifiedProjectFiles=( $(git diff --name-only --cached | grep "project.pbxproj") )
7983

80-
for filePath in ${modifiedProjectFiles[@]}; do
84+
git diff --name-only --cached | grep "project.pbxproj" | while IFS= read -r filePath; do
8185
fullFilePath="$GIT_ROOT/$filePath"
82-
python3 $sorter $fullFilePath
83-
git add $fullFilePath
86+
python3 "$sorter" "$fullFilePath"
87+
git add "$fullFilePath"
8488
done
8589

8690
echo 'Done sorting Xcode project files'
87-
88-
exit 0
8991
```
9092

91-
### 3.
92-
93-
Put following line into `.gitattributes` file then commit it.
93+
**3.** Make it executable:
9494

95-
```
96-
*.pbxproj merge=union
95+
```bash
96+
chmod +x .git/hooks/pre-commit
9797
```
9898

99-
## Running Tests
99+
**4.** *(Optional)* Add to `.gitattributes` to reduce merge conflicts further:
100100

101-
```bash
102-
python3 -m unittest discover tests # Run all tests (54 tests)
103-
python3 tests/cross_validate.py # Cross-validate Python vs Perl output
101+
```
102+
*.pbxproj merge=union
104103
```
105104

106-
## Legacy Perl Version
105+
> **Note:** `merge=union` tells Git to keep both sides of a conflict automatically. This works well for sorted `.pbxproj` files but can produce invalid results on non-sorted ones. Use this tool consistently to avoid issues.
107106
108-
The original Perl version (`sort-Xcode-project-file.pl`) is still available. The Python version produces byte-for-byte identical output.
107+
## Running Tests
109108

110109
```bash
111-
perl sort-Xcode-project-file.pl <path-to-xcodeproj-or-project.pbxproj>
110+
python3 -m unittest discover tests -v # Run all 70 tests
112111
```
113112

114113
## License

docs/improvement-plan.md

Lines changed: 0 additions & 107 deletions
This file was deleted.

0 commit comments

Comments
 (0)