-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJustfile.cross
More file actions
930 lines (810 loc) · 38.1 KB
/
Justfile.cross
File metadata and controls
930 lines (810 loc) · 38.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
# settings
set export := true
set positional-arguments
CROSSFILE := "Crossfile"
CROSSDIR := ".git/cross"
METADATA := "$CROSSDIR/metadata.json"
JUST_DIR := env("JUST_DIR", source_dir())
REPO_DIR := env ("REPO_DIR", "$(git rev-parse --show-toplevel)")
SHELL := env("SHELL")
dry := ""
# Auto-setup environment on first use
# AICONTEXT: Optional target to help user to ensure correct setup for tool usage. Taks: Call direnv, loading .envrc and from that .env file. Ideally short implementation.
setup:
#!/usr/bin/env fish
# Check if direnv is available
if command -v direnv >/dev/null; and test -f .envrc
if not direnv status | grep -q "Found RC allowed true"
direnv allow .
end
end
# AICONTEXT: Implemented through just -l option. Use comment strings on individual targets to document ussage.
# Show this help message
help:
just -l
# AICONTEXT: This is for AI tools to execute tests in sanbox environment. Do not change implementation!
# Run test suite in clean environment (optional args: [test_id]: 01, 02, etc)
test *ARGS:
#!/usr/bin/env bash
# PRESERVE THESE ENVS
THOME="$HOME"
TUSER="$USER"
TPATH="$PATH"
TTERM="$TERM"
BASH="$(command -v bash)"
if [[ "${1:-}" == "shell" ]]; then
shift || true
just --justfile "{{JUST_DIR}}/Justfile.cross" test-shell {{ARGS}}
exit 0
fi
# Run in clean environment with minimal PATH
#TARG="${ARGS:+--test $ARGS}"
env -i \
TARG="$TARG" \
HOME="$THOME" \
USER="$TUSER" \
PATH="$TPATH" \
SHELL="$BASH" \
TERM="${TTERM:-xterm}" \
GIT_CONFIG_NOSYSTEM=1 \
GIT_CONFIG_GLOBAL=/dev/null \
"$BASH" --norc --noprofile -c 'cd "{{JUST_DIR}}"; ./test/run-all.sh {{ARGS}}';
STATUS=$?
echo "Test exited with $STATUS";
exit $STATUS
# AICONTEXT: This is for AI tools to execute tests in sanbox environment. Do not change implementation!
# Run "test" shell environment or commands passed as ARGS inside "test shell environment"
test-shell *ARGS="bash":
#!/usr/bin/env bash
set -euo pipefail
THOME="$HOME"
TUSER="$USER"
TPATH="$PATH"
TTERM="$TERM"
BASH="$(command -v bash)"
if [[ -t 0 && -t 1 ]]; then
echo 'Launching git-cross test shell (type CTRL+D to leave)...'
env -i \
HOME="$THOME" \
USER="$TUSER" \
PATH="$TPATH" \
SHELL="$BASH" \
TERM="${TTERM:-xterm}" \
"$BASH" --norc --noprofile -i -c "{{ARGS}}"
fi
# AICONTEXT: This methond validate user workspace has installed all required dependencies. Dont install them, but prints how-to.
# Check for required dependencies
check-deps:
#!/usr/bin/env fish
set -l missing
for cmd in fish rsync git jq
if not command -v $cmd > /dev/null
set missing $missing $cmd
end
end
if test (count $missing) -gt 0
just cross _log error "Missing: $missing"
just cross _log info "Install with: brew install $missing"
exit 1
end
# AICONTEXT: This is new and better implementation of resolve_context
# Internal: resolve patch metadata from path argument or CWD
[no-cd] # to be able to set CWD as $path if not provided
_resolve_context2 path="": check-initialized
#!/usr/bin/env fish
# Resolve git repo relative path of target
if test -z "$path"
set -x path "$(git rev-parse --show-prefix | sed 's,\/$,,')" # cwd, relative to git repo
end
if test -z "$path"
just cross _log error "Provide path to 'patch' or change directory into it."
exit 1
end
# Query metadata.json and export matching key as env variables
# Find patch where local_path matches rel_target or is a parent of rel_target
jq -r --arg path "{{path}}" '
.patches
| map(. as $patch | select($patch.local_path | startswith($path)))
| map(. + {mlen:(.local_path|length)})
| max_by(.mlen)
| to_entries | map("set -x \(.key) \(.value|@sh)") | .[]
' "{{REPO_DIR}}/{{METADATA}}"
# AICONTEXT: This method updates Crossfile, an configuration file for git-cross. Do not change implementation!
# Internal: append command to Crossfile (avoiding duplicates)
update_crossfile +cmd:
grep -qF "{{cmd}}" "{{CROSSFILE}}" 2>/dev/null || echo "{{cmd}}" >> "{{CROSSFILE}}"; exit 0
# Internal: Log message with color
_log level +message:
#!/usr/bin/env fish
set color_reset (set_color normal)
switch {{level}}
case info
set color (set_color blue)
echo "$color==> {{message}}$color_reset"
case success
set color (set_color green)
echo "$color==> {{message}}$color_reset"
case warn
set color (set_color yellow)
echo "$color==> WARNING: {{message}}$color_reset"
case error
set color (set_color red)
echo "$color==> ERROR: {{message}}$color_reset" >&2
case '*'
echo "==> {{message}}"
end
# Execute arbitrary shell command
exec +CMD:
{{CMD}}
# Initialize a new project with Crossfile
[no-cd]
init:
#!/usr/bin/env fish
if test -f "{{CROSSFILE}}"
just cross _log info "Crossfile already exists."
else
echo "# git-cross configuration" > "{{CROSSFILE}}"
just cross _log success "Crossfile initialized."
end
# AICONTEXT: "use" register remote git repository and update Crossfile with "use" command. Do not change implementation!
# Add a remote repository
[no-cd]
use name url: check-deps
#!/usr/bin/env fish
cd "{{REPO_DIR}}" &&\
if not git remote show {{name}} >/dev/null 2>&1
git remote add {{name}} {{url}}
# Detect default branch
git ls-remote --heads {{url}} 2>/dev/null \
&& just cross update_crossfile "cross use {{name}} {{url}}"
end
# Remove a patch and its worktree
[no-cd]
remove path: check-deps
#!/usr/bin/env fish
set l_path "{{path}}"
pushd "{{REPO_DIR}}"
if not test -f {{METADATA}}
just cross _log error "No metadata found."
exit 1
end
set entry (jq -r --arg lp "$l_path" '.patches[] | select(.local_path == $lp)' {{METADATA}})
if test -z "$entry"
just cross _log error "Patch not found for path: $l_path"
exit 1
end
set wt (echo "$entry" | jq -r '.worktree')
just cross _log info "Removing patch at $l_path..."
# 1. Remove worktree
if test -d "$wt"
just cross _log info "Removing git worktree at $wt..."
git worktree remove --force "$wt"
end
# 2. Remove from Crossfile
just cross _log info "Removing from Crossfile..."
if test -f "{{CROSSFILE}}"
set tmp (mktemp)
grep -v "patch" "{{CROSSFILE}}" > "$tmp"
grep "patch" "{{CROSSFILE}}" | grep -v "$l_path" >> "$tmp"
mv "$tmp" "{{CROSSFILE}}"
end
# 3. Update metadata
just cross _log info "Updating metadata..."
set tmp_meta (mktemp)
jq --arg lp "$l_path" '.patches |= map(select(.local_path != $lp))' {{METADATA}} > "$tmp_meta"
mv "$tmp_meta" {{METADATA}}
# 4. Remove local directory
just cross _log info "Deleting local directory $l_path..."
rm -rf "$l_path"
just cross _log success "Patch removed successfully."
popd
# Prune unused remotes and worktrees, or remove all patches for a specific remote
prune remote_name="": check-deps
#!/usr/bin/env fish
set remote "{{remote_name}}"
pushd "{{REPO_DIR}}"
if not test -f {{METADATA}}
just cross _log error "No metadata found."
exit 1
end
if test -n "$remote"
# Prune specific remote: remove all its patches
just cross _log info "Pruning all patches for remote: $remote..."
# Get all patches for this remote
set patches (jq -r --arg remote "$remote" '.patches[] | select(.remote == $remote) | .local_path' {{METADATA}})
if test -z "$patches"
just cross _log warn "No patches found for remote: $remote"
else
# Remove each patch
for patch_path in $patches
just cross _log info "Removing patch: $patch_path"
just cross remove "$patch_path"
end
end
# Remove the remote itself
if git remote | grep -q "^$remote\$"
just cross _log info "Removing git remote: $remote"
git remote remove "$remote"
end
just cross _log success "Remote $remote and all its patches pruned successfully."
else
# Prune all unused remotes (no active patches)
just cross _log info "Finding unused remotes..."
# Get all remotes used by patches
set used_remotes (jq -r '.patches[].remote' {{METADATA}} | sort -u)
# Get all git remotes (except origin and git-cross)
set all_remotes (git remote | grep -v "^origin\$" | grep -v "^git-cross\$")
# Find unused remotes
set unused_remotes
for remote in $all_remotes
if not contains $remote $used_remotes
set unused_remotes $unused_remotes $remote
end
end
if test -z "$unused_remotes"
just cross _log info "No unused remotes found."
else
just cross _log info "Unused remotes: $unused_remotes"
read -P "Remove these remotes? [y/N]: " confirm
if test "$confirm" = "y"; or test "$confirm" = "Y"
for remote in $unused_remotes
just cross _log info "Removing remote: $remote"
git remote remove "$remote"
end
just cross _log success "Unused remotes removed."
else
just cross _log info "Pruning cancelled."
end
end
# Always prune stale worktrees
just cross _log info "Pruning stale worktrees..."
git worktree prune --verbose
just cross _log success "Worktree pruning complete."
end
popd
# AICONTEXT: "patch" will do sparse checkout of specified branch of remote repository into local path. "remote_spec" is in format "remote_name:branch", where "branch" is optional. local_path is the same are remote_path if not provided. Command shall firs use `git ls-remote --heads ` to identify whether remote is having main or master as default branch if not provided - no more evaluation needed, simply grep "refs/heads" with regexp. Tool shall configure sparse checkout and use `git worktree add` and use `".git/cross/worktrees/$remote"_"$hash"` as working directory. Hash shall be short, but shall be created from path and branch name and humans shall ideally read it. checkout either only need maximum of 1 git history (last commmit version). When the checkout is done, "sync" target is called, to sync just this specific "patch" git worktree into main repository to local_path. Then a placeholder is required for post_sync_hook function to run. Finally call `git add` on local_path in top level repo.
# AICONTEXT: for implementation, use "fish" keep it simple, shell comamnds shall be readable. Ideally keep bellow 30 lines. User interaction shall be kept minimal. Debug statements are not needed. Document in comments major logical blocks.
# Patch a directory from a remote into a local path
patch remote_spec local_path="": check-deps
#!/usr/bin/env fish
set parts (string split : {{remote_spec}})
switch (count $parts)
case 3
set remote $parts[1]
set remote_branch $parts[2]
set remote_path $parts[3]
case 2
set remote $parts[1]
set remote_path $parts[2]
case '*'
just cross _log error "Error: Invalid format Use remote[:branch]:remote_path [local_path]"
exit 1
end
# update vars
set r_path "$remote_path"
set l_path "{{local_path}}"
if test -z "$l_path"
set l_path "$r_path"
end
pushd "{{REPO_DIR}}"
# validate remote
if not git remote show $remote |grep -vq "^$remote\$"
just cross _log error "Error: Remote $remote not found. Run: just use $remote <url>"
exit 1
end
# validate paths
if test -z "$l_path"; or test "$l_path" = "/";
just cross _log error "Error: Invalid format Use remote[:branch]:remote_path [local_path]"
exit 1
end
# validate target branch
if test -z "$remote_branch"
set remote_branch "master"
if git ls-remote --heads $remote | grep -q "refs/heads/main"
set remote_branch "main"
end
end
# calculate hash/id
set hash (echo $l_path | md5sum | cut -d' ' -f1 | cut -c1-8)
set wt ".git/cross/worktrees/$remote"_"$hash"
# setup worktree
just cross _log info "Setting up worktree at $wt..."
if not test -d $wt
mkdir -p (dirname $wt)
git fetch $remote $remote_branch
git worktree add --no-checkout -B "cross/$remote/$remote_branch/$hash" $wt "$remote/$remote_branch" >/dev/null 2>&1
# Sparse checkout
git -C $wt sparse-checkout init --no-cone
git -C $wt sparse-checkout set $r_path
git -C $wt checkout
end
# sync to local_path
just cross _log info "Syncing files to $l_path..."
mkdir -p $l_path
rsync -av --delete --exclude .git $wt/$r_path/ $l_path/
# Add local_path to git
git add $l_path
# update Crossfile
just cross _log info "Update Crossfile"
just cross update_crossfile "cross patch $remote:$remote_branch:$r_path $l_path"
# Initialize metadata.json
if not test -f {{METADATA}}
mkdir -p (dirname {{METADATA}})
echo '{"patches": []}' > {{METADATA}}
end
# Update metadata.json
set new_entry "{\"id\": \"$hash\", \"remote\": \"$remote\", \"remote_path\": \"$r_path\", \"local_path\": \"$l_path\", \"worktree\": \"$wt\", \"branch\": \"$remote_branch\"}"
# 1. Delete existing entry with same id (if any)
# 2. Append new entry
set tmp_file (mktemp)
## AICONTEXT: use direct update with jq instead the temp file
jq ".patches |= map(select(.id != \"$hash\")) + [$new_entry]" "{{METADATA}}" > "$tmp_file"
mv "$tmp_file" "{{METADATA}}"
popd
[no-cd]
@check-initialized:
cd {{REPO_DIR}} && test -d {{CROSSDIR}}/worktrees && test -f {{METADATA}} \
|| { just cross _log warn "No patches to sync"; exit 0; }
# AICONTEXT: "sync" will sync all or the provided local_path with upstream. Workflow: 1. Stash uncommitted changes in local_path. 2. Rsync git-tracked files from local_path to worktree. 3. Commit changes in worktree. 4. Pull rebase from upstream. 5. If conflicts, exit and ask user to resolve. 6. Rsync worktree back to local_path. 7. Restore stashed changes. 8. Check for conflicts in restored changes.
# Sync all patches from upstream
[no-cd]
sync *path="": check-initialized
#!/usr/bin/env fish
# Query metadata.json
just cross _resolve_context2 {{path}} | source \
|| { just cross _log error "Error: Could not resolve metadata for $path."; exit 1; }
pushd "{{REPO_DIR}}"
just cross _log info "Syncing $local_path with $worktree..."
# 0. Ensure local_path exists
mkdir -p $local_path
# 0.5. Check for uncommitted changes in local_path (detect but don't stash yet)
set stashed false
set has_changes (git status --porcelain $local_path 2>/dev/null | wc -l | tr -d ' ')
if test "$has_changes" -gt 0
just cross _log info "Detected uncommitted changes in $local_path..."
set stashed true
end
# 1. Rsync current state (including uncommitted) from local_path to worktree
# AICONTEXT: the rsync need to sync only git tracked files in $local_path to $worktree/$remote_path
just cross _log info "Syncing local changes to worktree..."
if test -d $local_path
pushd $local_path
# Get tracked files (includes files with uncommitted changes)
set tracked (git ls-files .)
if test -n "$tracked"
# Rsync tracked files (with their current content, including uncommitted changes)
git ls-files . -z | rsync -0 --files-from=- -av --relative --exclude .git {{REPO_DIR}}/$local_path {{REPO_DIR}}/$worktree/$remote_path
end
popd
end
# 1.5. NOW stash uncommitted changes (after copying them to worktree)
if test "$stashed" = "true"
just cross _log info "Stashing uncommitted changes in $local_path..."
# Stash including untracked files, only in local_path
git stash push --include-untracked -m "cross-sync-auto-stash: $local_path" -- $local_path
end
# 2. Commit local changes in worktree
set dirty (git -C $worktree status --porcelain)
if test -n "$dirty"
just cross _log info "Committing local changes in $worktree..."
git -C $worktree add .
git -C $worktree commit -m "Sync local changes"
end
# 2.5. Check if worktree is in a good state (not detached HEAD, not mid-rebase)
pushd $worktree
# Check for ongoing rebase/merge FIRST (before anything else)
# For worktrees, rebase-merge can be in multiple locations
set worktree_name (basename $worktree)
set rebase_dirs .git/rebase-merge .git/rebase-apply {{REPO_DIR}}/.git/worktrees/$worktree_name/rebase-merge {{REPO_DIR}}/.git/worktrees/$worktree_name/rebase-apply
set needs_cleanup false
for dir in $rebase_dirs
if test -d $dir
set needs_cleanup true
break
end
end
if test "$needs_cleanup" = "true"
just cross _log warn "Worktree has an in-progress operation. Cleaning up..."
git rebase --abort 2>/dev/null || true
git merge --abort 2>/dev/null || true
# Force remove all possible rebase dirs
for dir in $rebase_dirs
rm -rf $dir 2>/dev/null || true
end
end
# Check for detached HEAD
if not git symbolic-ref -q HEAD >/dev/null 2>&1
just cross _log warn "Worktree is in detached HEAD state. Attempting to recover..."
# Find the worktree's branch name
set branch_name (git for-each-ref --format='%(refname:short)' refs/heads/ | grep -E 'cross/' | head -1)
if test -n "$branch_name"
just cross _log info "Checking out branch: $branch_name"
git checkout -B $branch_name 2>/dev/null || true
# Reset to clean state
git fetch $remote 2>/dev/null || true
git reset --hard $remote/$remote_branch 2>/dev/null || true
end
end
popd
# 3. Pull rebase from upstream
just cross _log info "Pulling from upstream..."
if not git -C $worktree pull --rebase
just cross _log error "Conflict detected in $worktree. Please resolve manually."
just cross _log info "cd $worktree"
if test "$stashed" = "true"
just cross _log warn "Note: Local changes are stashed. Run 'git stash pop' in $local_path after resolving."
end
exit 1
end
# 4. Sync back to local - ensure directory exists
just cross _log info "Syncing back to $local_path..."
mkdir -p $local_path
# 4.1. Remove files from local_path that were deleted upstream
# Only delete files that are:
# - Tracked in the main repo (in local_path)
# - No longer exist in the worktree
pushd $worktree/$remote_path
# Get list of tracked files in worktree
set worktree_files (git ls-files .)
# Create temp file with worktree files list
set temp_list (mktemp)
echo "$worktree_files" > $temp_list
# Get list of tracked files in local_path (from main repo)
pushd {{REPO_DIR}}
set local_tracked (git ls-files $local_path 2>/dev/null || echo "")
popd
# Only check tracked files for deletion
if test -n "$local_tracked"
for tracked_file in $local_tracked
# Get just the filename relative to local_path
set rel_file (string replace -r "^$local_path/" "" $tracked_file)
# Check if this tracked file no longer exists in worktree
if not grep -qF "$rel_file" $temp_list 2>/dev/null
just cross _log info "Removing deleted file: $rel_file"
rm -f {{REPO_DIR}}/$tracked_file
end
end
end
rm -f $temp_list
# 4.2. Rsync tracked files from worktree to local
git ls-files . -z | rsync -0 --files-from=- -av --relative --exclude .git {{REPO_DIR}}/$worktree/$remote_path {{REPO_DIR}}/$local_path
popd
# 5. Restore stashed changes if they exist
if test "$stashed" = "true"
just cross _log info "Restoring stashed local changes..."
# First, add any new files that came from worktree sync
git add $local_path 2>/dev/null || true
# Now try to pop the stash (might have conflicts if same files were modified upstream)
if git stash pop
# Success - check for conflicts
set conflicts (git diff --name-only --diff-filter=U -- $local_path 2>/dev/null | wc -l | tr -d ' ')
if test "$conflicts" -gt 0
just cross _log error "Conflicts detected after restoring local changes in $local_path:"
git diff --name-only --diff-filter=U -- $local_path
just cross _log info "Resolve conflicts, then run 'git add' and continue."
end
else
# Stash pop failed - likely due to conflicts
just cross _log warn "Could not automatically restore stashed changes."
just cross _log info "Your changes are preserved in the stash."
just cross _log info "Run 'git stash list' to see them, 'git stash show' to view, 'git stash pop' to retry."
just cross _log info "Or run 'git stash drop' if you want to discard them."
end
end
just cross _log success "Sync completed for $local_path"
popd
# AICONTEXT: "diff" will diff local vs .git/cross/worktrees/$remote"_"$hash. It will take all "git tracked" files from local_path and rsync them to .git/cross/worktrees/$remote"_"$hash/$remote_path. This function shall be used by "sync" target to manipulate with files between local_path and WT. When on WT, regular `git diff` can be run. (though then CWD shal return to where user was before the execution, which probably just ensure anyway)
# Diff local vs upstream
[no-cd]
diff path="": check-initialized
#!/usr/bin/env fish
set resolved_path "{{path}}"
# If path provided, resolve to repo-relative
if test -n "$resolved_path"
pushd "{{REPO_DIR}}" >/dev/null
# Check if path is a directory or file
if test -e "$resolved_path"
# cd to the path (or its directory) and get repo-relative location
if test -d "$resolved_path"
set resolved_path (cd "$resolved_path" && git rev-parse --show-prefix | sed 's,/$,,')
else
# Handle file path - get directory
set dir (dirname "$resolved_path")
set resolved_path (cd "$dir" && git rev-parse --show-prefix | sed 's,/$,,')
end
else
just cross _log error "Error: Path does not exist: $resolved_path"
exit 1
end
popd >/dev/null
end
# Query metadata.json
just cross _resolve_context2 "$resolved_path" | source \
|| { just cross _log error "Error: Could not resolve metadata for '$resolved_path'."; exit 1; }
pushd "{{REPO_DIR}}"
if test -d $worktree
git diff --no-index $worktree/$remote_path $local_path || true
else
just cross _log error "Error: Worktree not found $worktree"
exit 1
end
popd
# AICONTEXT: "push" works on .git/cross/worktrees/$remote"_"$hash/$remote_path. Input argument is local_path (or understand user stand in the under git-cross'ed local_path and call this comand). The "sync" shall be run first, then regular git push to remote (as pull request, merge request shall happen to remote), the branch to push MR shall be autocalculated ie: "gituser_$hash" used in WT. User shall be give the optino to force-push, as he can repeate. (feature on git-cross)
# Push changes back to upstream
[no-cd]
push path="" branch="" force="false" yes="false" message="": check-initialized
#!/usr/bin/env fish
# Query metadata.json
just cross _resolve_context2 "{{path}}" | source \
|| { just cross _log error "Error: Could not resolve metadata for '$path'."; exit 1; }
pushd "{{REPO_DIR}}"
just cross _log warn "The 'push' command is currently WORK IN PROGRESS."
if not test -d $worktree
just cross _log error "Error: Worktree not found. Run 'just patch' first."
exit 1
end
just cross _log info "Syncing changes from $local_path back to $worktree..."
rsync -av --delete --exclude .git $local_path/ $worktree/$remote_path/
just cross _log info "---------------------------------------------------"
just cross _log info "Worktree updated. Status:"
git -C $worktree status
just cross _log info "---------------------------------------------------"
while true
if test "{{yes}}" = "true"
set choice "r"
else
read -P "Run (r), Manual (m), Cancel (c)? " choice
end
switch $choice
case r R
just cross _log info "Preparing commit..."
pushd $worktree
git add .
# Determine commit message
set msg "{{message}}"
if test -z "$msg"; and test "{{yes}}" = "false"
read -P "Commit message (empty to auto-generate from local log): " user_msg
if test -n "$user_msg"
set msg "$user_msg"
end
end
if test -z "$msg"
# Auto-generate from local git log of the path
# We need to find the last commit that touched the local_path in the main repo
# Note: REPO_DIR is the main repo root
set msg (git -C "{{REPO_DIR}}" log -n 1 --pretty=format:%s -- "$local_path")
if test -z "$msg"
set msg "Sync updates from $local_path"
end
just cross _log info "Auto-generated message: $msg"
end
git commit -m "$msg"
# Determine push target
if test -z "{{branch}}"
set upstream (git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}')
set remote (string split -m1 / $upstream)[1]
set target_branch (string split -m1 / $upstream)[2]
else
set remote "$remote"
set target_branch "{{branch}}"
end
set push_args "$remote" "HEAD:$target_branch"
if test "{{force}}" = "true"
set push_args $push_args "--force"
end
just cross _log info "Pushing to $push_args..."
git push $push_args
popd >/dev/null
break
case m M
just cross _log info "Spawning subshell in $worktree..."
just cross _log info "Type 'exit' to return."
pushd $worktree
fish
popd >/dev/null
just cross _log info "Returned from manual mode."
case c C
just cross _log warn "Cancelled."
exit 0
case '*'
just cross _log error "Invalid choice."
end
end
popd
# AICONTEXT: "list" lists all patches in local repo, by reading Crossfile and `git worktree list` under .git/cross/worktrees. The listing shall be combined with status of each WT and git status of local_path. The logic can be take from "status" implementation.
# List all patches
[no-cd]
list: check-deps
#!/usr/bin/env fish
pushd "{{REPO_DIR}}" >/dev/null
if test -f .git/cross/metadata.json
# Get unique remotes used by patches
set used_remotes (jq -r '.patches[].remote' .git/cross/metadata.json | sort -u)
if test (count $used_remotes) -gt 0
just cross _log info "Configured Remotes:"
printf "%-20s %s\n" "NAME" "URL"
printf "%s\n" (string repeat -n 70 "-")
# Build grep pattern for used remotes
set pattern (string join "|" $used_remotes)
# Get remotes, filter by used, deduplicate fetch/push
git remote -v | grep -E "^($pattern)\s" | awk '
{
name=$1; url=$2; type=$3
if (!(name in seen)) {
seen[name] = url
types[name] = type
} else if (seen[name] != url) {
# Different fetch/push URLs
printf "%-20s %s\n", name, seen[name] " " types[name]
printf "%-20s %s\n", name, url " " type
delete seen[name]
}
}
END {
for (name in seen) {
printf "%-20s %s\n", name, seen[name]
}
}
'
echo ""
end
end
if not test -f Crossfile
just cross _log warn "No patches found (Crossfile missing)."
popd >/dev/null
exit 0
end
just cross _log info "Configured Patches:"
printf "%-20s %-30s %-20s\n" "REMOTE" "REMOTE PATH" "LOCAL PATH"
printf "%s\n" (string repeat -n 70 "-")
if test -f .git/cross/metadata.json
jq -r '.patches[] | "\(.remote) \(.remote_path) \(.local_path)"' .git/cross/metadata.json | while read -l remote rpath lpath
printf "%-20s %-30s %-20s\n" $remote $rpath $lpath
end
else
just cross _log info "No patches found. Run 'just cross patch <remote> <path>' to start."
end
popd >/dev/null
# wt wrapper
[no-cd]
worktree path="":
just cross cd "{{path}}" dry="{{dry}}"
# Internal: Copy text to clipboard (cross-platform)
[no-cd]
_copy_to_clipboard text:
#!/usr/bin/env bash
if command -v pbcopy >/dev/null 2>&1; then
# macOS
echo "{{text}}" | pbcopy
elif command -v xclip >/dev/null 2>&1; then
# Linux with xclip
echo "{{text}}" | xclip -selection clipboard
elif command -v xsel >/dev/null 2>&1; then
# Linux with xsel
echo "{{text}}" | xsel --clipboard --input
else
echo "Error: No clipboard tool found (pbcopy/xclip/xsel)"
exit 1
fi
# Internal: Open shell in target directory
# target: "local_path" or "worktree"
# path: must be provided (non-empty)
[no-cd]
_open_shell target path:
#!/usr/bin/env fish
just cross _resolve_context2 "{{path}}" | source || exit 1
set -l dir (test "{{target}}" = "local_path" && echo $local_path || echo $worktree)
cd {{REPO_DIR}}/$dir && exec $SHELL
# Go to worktree directory (for working with git history)
# With path: opens subshell
# Without path: uses fzf selection and copies to clipboard
[no-cd]
wt path="":
#!/usr/bin/env fish
if test -n "{{path}}"
just cross _open_shell worktree "{{path}}"
else
set -l selected (just cross list | tail -n +3 | fzf --height 40% 2>/dev/null | awk '{print $NF}')
test -z "$selected" && exit 0
just cross _resolve_context2 "$selected" | source || exit 1
set -l rel_dir (realpath -m --relative-to=$PWD {{REPO_DIR}}/$worktree)
just cross _copy_to_clipboard $rel_dir
just cross _log success "Path copied: $rel_dir"
end
# Go to local_path directory (for editing patched files)
# With path: opens subshell
# Without path: uses fzf selection and copies to clipboard
[no-cd]
cd path="":
#!/usr/bin/env fish
if test -n "{{path}}"
just cross _open_shell local_path "{{path}}"
else
set -l selected (just cross list | tail -n +3 | fzf --height 40% 2>/dev/null | awk '{print $NF}')
test -z "$selected" && exit 0
just cross _resolve_context2 "$selected" | source || exit 1
set -l rel_dir (realpath -m --relative-to=$PWD {{REPO_DIR}}/$local_path)
just cross _copy_to_clipboard $rel_dir
just cross _log success "Path copied: $rel_dir"
end
# AICONTEXT: "status" shows status of current local_path patch vs. WT, and WT upstream. Input argument is local_path (or understand user stand in the under git-cross'ed local_path and call this comand). It shall be called to get status of local_path from "list" command. Either the status of remote WT upstream (as resource consuming, shall be optional or only ocasional)
# Show status of all patches
[no-cd]
status: check-deps
#!/usr/bin/env fish
if not test -f Crossfile
just cross _log warn "No patches found."
exit 0
end
printf "%-20s %-15s %-15s %-15s\n" "LOCAL PATH" "DIFF" "UPSTREAM" "CONFLICTS"
printf "%s\n" (string repeat -n 70 "-")
if test -f .git/cross/metadata.json
jq -r '.patches[] | "\(.remote) \(.remote_path) \(.local_path) \(.worktree)"' .git/cross/metadata.json | while read -l remote rpath local_path wt
set diff_stat "Clean"
set upstream_stat "Synced"
set conflict_stat "No"
if test -d $wt
# Check diffs
if not git diff --no-index --quiet $wt/$rpath $local_path 2>/dev/null
set diff_stat "Modified"
end
# Check upstream divergence
set behind (git -C $wt rev-list --count HEAD..@{upstream} 2>/dev/null)
set ahead (git -C $wt rev-list --count @{upstream}..HEAD 2>/dev/null)
if test "$behind" -gt 0
set upstream_stat "$behind behind"
else if test "$ahead" -gt 0
set upstream_stat "$ahead ahead"
end
# Check conflicts in worktree
if git -C $wt ls-files -u | grep -q .
set conflict_stat "YES"
end
# Also check conflicts in local path (from failed stash restore)
if git ls-files -u $local_path | grep -q .
set conflict_stat "YES"
end
else
set diff_stat "Missing WT"
end
printf "%-20s %-15s %-15s %-15s\n" $local_path $diff_stat $upstream_stat $conflict_stat
end
else
just cross _log info "No patches found."
end
# Replay commands from Crossfile
replay: check-deps
/usr/bin/env bash -c "cross() { just --justfile '{{JUST_DIR}}/Justfile' cross \"\$@\"; }; git() { if [ \"\${1-}\" = \"cross\" ]; then shift; cross \"\$@\"; else command git \"\$@\"; fi; }; source '{{REPO_DIR}}/Crossfile'"
# Install git alias for git-cross (impl: go, shell, rust)
install impl="go":
#!/usr/bin/env bash
set -e
export PATH=$HOME/homebrew/bin:$PATH
case "{{impl}}" in
go)
echo "==> Building Go implementation..."
(cd "{{JUST_DIR}}/src-go" && go build -o git-cross-go main.go)
EXE="{{JUST_DIR}}/src-go/git-cross-go"
echo "==> Setting up git alias 'cross' -> $EXE"
git config --global alias.cross "!$EXE"
echo "Success: Git alias 'cross' installed."
;;
shell|just)
CROSS_PATH="{{JUST_DIR}}/Justfile"
echo "==> Setting up git alias 'cross' -> just --justfile $CROSS_PATH cross"
git config --global alias.cross "!just --justfile $CROSS_PATH cross"
echo "Success: Git alias 'cross' installed."
;;
rust)
echo "==> Building Rust implementation..."
(cd "{{JUST_DIR}}/src-rust" && cargo build)
EXE="{{JUST_DIR}}/src-rust/target/debug/git-cross-rust"
echo "==> Setting up git alias 'cross' -> $EXE"
git config --global alias.cross-rust "!$EXE"
echo "Success: Git alias 'cross' installed."
;;
*)
echo "ERROR: Unknown implementation '{{impl}}'. Use: go, shell, or rust."
exit 1
;;
esac