Skip to content

Commit a6673dc

Browse files
committed
implement worktree command
1 parent 62ee27d commit a6673dc

File tree

14 files changed

+494
-48
lines changed

14 files changed

+494
-48
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ jobs:
1919
sudo apt-get update
2020
sudo apt-get install -y fish rsync jq
2121
# Install yq
22-
sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq
22+
sudo wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq
2323
sudo chmod +x /usr/bin/yq
2424
# Install just
25-
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin
25+
curl --proto '=https' --tlsv1.2 -sSf --retry 5 --retry-connrefused --retry-delay 5 https://just.systems/install.sh | bash -s -- --to ~/bin
2626
echo "$HOME/bin" >> $GITHUB_PATH
2727
2828
- name: Verify installation

Crossfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ cross sync
1111
# cross exec git remote -v | grep fetch | column -t
1212
# cross exec git worktree list
1313

14+
cross use khue https://github.com/khuedoan/homelab
15+
cross patch khue:master:/metal deploy/metal

Justfile.cross

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ CROSSDIR := ".git/cross"
77
METADATA := "$CROSSDIR/metadata.json"
88
JUST_DIR := env("JUST_DIR", source_dir())
99
REPO_DIR := env ("REPO_DIR", "$(git rev-parse --show-toplevel)")
10+
SHELL := env("SHELL")
11+
dry := ""
1012

1113

1214
# Auto-setup environment on first use
@@ -440,6 +442,50 @@ list: check-deps
440442
just cross _log info "No patches found. Run 'just cross patch <remote> <path>' to start."
441443
end
442444

445+
# wt wrapper
446+
[no-cd]
447+
worktree path="":
448+
just cross wt "{{path}}" dry="{{dry}}"
449+
450+
[no-cd]
451+
wt path="":
452+
#!/usr/bin/env fish
453+
454+
# if not initialized, exit
455+
just cross check-initialized | grep "No patches" && exit 1;
456+
457+
# try to resolve context (uses "{{path}}" if provided, else CWD)
458+
just cross _resolve_context2 (string trim -r -c / {{path}}) 2>/dev/null | source || true;
459+
460+
# if "worktree" is not provided and we are not in a patch, use fzf or just list
461+
if test -z "$worktree"
462+
if not command -v fzf >/dev/null
463+
just cross list
464+
exit 0
465+
end
466+
467+
# if dry is set, skip this section interaction with fzf is not tested
468+
if test -z "{{dry}}"
469+
echo ""
470+
# set -l selection (just cross list | tail -n +3 | fzf --select-1 --header "Select patch worktree" --height 40% 2>/dev/null)
471+
# set -l selected_path (echo "$selection" | awk '{print $NF}')
472+
# if test -n "$selected_path"
473+
# # Call just cross to re-dispatch to this recipe with the path
474+
# just cross dry="{{dry}}" wt "$selected_path"
475+
# end
476+
# exit 0
477+
else
478+
just cross list
479+
exit 0
480+
end
481+
end
482+
483+
just cross _log info "Entering worktree at $worktree, use 'CTRL+D' to return..."
484+
test -d "{{REPO_DIR}}/$worktree" > /dev/null || exit 1
485+
{{dry}} cd "{{REPO_DIR}}/$worktree"
486+
{{dry}} exec {{SHELL}}
487+
488+
443489
# 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)
444490
# Show status of all patches
445491
[no-cd]

TODO.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
- [x] Implement "cross" command in Golang.
3030
- [x] Update AGENTS.md, specs/ and .specify/ to reflect new implementations.
3131

32+
## Future Enhancements / Backlog
33+
34+
- [ ] Re-implement `wt` (worktree) command in Go and Rust with full test coverage (align logic with Justfile).
35+
- [ ] Improve interactive `fzf` selection in native implementations.
36+
3237
## Known Issues (To FIX)
3338

3439
- [x] If remote_spec contains "khue:master:/metal" the first slash shall be auto-removed

src-go/go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.25.5
55
require (
66
github.com/fatih/color v1.18.0
77
github.com/gogs/git-module v1.8.5
8-
github.com/mattn/go-shellwords v1.0.12
98
github.com/olekukonko/tablewriter v1.1.2
109
github.com/spf13/cobra v1.10.2
1110
github.com/zloylos/grsync v1.7.0

src-go/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
2020
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
2121
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
2222
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
23-
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
24-
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
2523
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk=
2624
github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
2725
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=

src-go/main.go

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,34 @@ import (
1818
)
1919

2020
const (
21-
MetadataPath = ".git/cross/metadata.json"
22-
CrossfilePath = "Crossfile"
21+
MetadataRelPath = ".git/cross/metadata.json"
22+
CrossfileRelPath = "Crossfile"
2323
)
2424

25+
func getRepoRoot() (string, error) {
26+
out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
27+
if err != nil {
28+
return "", err
29+
}
30+
return strings.TrimSpace(string(out)), nil
31+
}
32+
33+
func getMetadataPath() (string, error) {
34+
root, err := getRepoRoot()
35+
if err != nil {
36+
return "", err
37+
}
38+
return filepath.Join(root, MetadataRelPath), nil
39+
}
40+
41+
func getCrossfilePath() (string, error) {
42+
root, err := getRepoRoot()
43+
if err != nil {
44+
return "", err
45+
}
46+
return filepath.Join(root, CrossfileRelPath), nil
47+
}
48+
2549
type Patch struct {
2650
Remote string `json:"remote"`
2751
RemotePath string `json:"remote_path"`
@@ -110,7 +134,11 @@ func logError(msg string) {
110134

111135
func loadMetadata() (Metadata, error) {
112136
var meta Metadata
113-
data, err := os.ReadFile(MetadataPath)
137+
path, err := getMetadataPath()
138+
if err != nil {
139+
return meta, err
140+
}
141+
data, err := os.ReadFile(path)
114142
if err != nil {
115143
if os.IsNotExist(err) {
116144
return meta, nil
@@ -122,20 +150,28 @@ func loadMetadata() (Metadata, error) {
122150
}
123151

124152
func saveMetadata(meta Metadata) error {
125-
dir := filepath.Dir(MetadataPath)
153+
path, err := getMetadataPath()
154+
if err != nil {
155+
return err
156+
}
157+
dir := filepath.Dir(path)
126158
if err := os.MkdirAll(dir, 0o755); err != nil {
127159
return err
128160
}
129161
data, err := json.MarshalIndent(meta, "", " ")
130162
if err != nil {
131163
return err
132164
}
133-
return os.WriteFile(MetadataPath, data, 0o644)
165+
return os.WriteFile(path, data, 0o644)
134166
}
135167

136168
func updateCrossfile(line string) error {
169+
path, err := getCrossfilePath()
170+
if err != nil {
171+
return err
172+
}
137173
line = strings.TrimSpace(line)
138-
data, err := os.ReadFile(CrossfilePath)
174+
data, err := os.ReadFile(path)
139175
if err != nil && !os.IsNotExist(err) {
140176
return err
141177
}
@@ -145,7 +181,7 @@ func updateCrossfile(line string) error {
145181
return nil
146182
}
147183

148-
f, err := os.OpenFile(CrossfilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
184+
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
149185
if err != nil {
150186
return err
151187
}
@@ -212,8 +248,57 @@ func detectDefaultBranch(url string) (string, error) {
212248
return "main", nil
213249
}
214250

251+
func repoRelativePath() (string, error) {
252+
out, err := git.NewCommand("rev-parse", "--show-toplevel").RunInDir(".")
253+
if err != nil {
254+
return "", err
255+
}
256+
root := strings.TrimSpace(string(out))
257+
cwd, err := os.Getwd()
258+
if err != nil {
259+
return "", err
260+
}
261+
rel, err := filepath.Rel(root, cwd)
262+
if err != nil {
263+
return "", err
264+
}
265+
rel = filepath.ToSlash(rel)
266+
if rel == "." {
267+
return "", nil
268+
}
269+
return strings.Trim(rel, "/"), nil
270+
}
271+
272+
func findPatchForPath(meta Metadata, rel string) *Patch {
273+
rel = strings.Trim(strings.TrimSpace(rel), "/")
274+
if rel == "" {
275+
return nil
276+
}
277+
278+
var selected *Patch
279+
longest := -1
280+
for i := range meta.Patches {
281+
lp := strings.Trim(meta.Patches[i].LocalPath, "/")
282+
if lp == "" {
283+
continue
284+
}
285+
if rel == lp || strings.HasPrefix(rel, lp+"/") {
286+
if len(lp) > longest {
287+
longest = len(lp)
288+
selected = &meta.Patches[i]
289+
}
290+
}
291+
}
292+
293+
return selected
294+
}
295+
296+
// selectPatchWithFZF is removed as it was only used by wt command.
297+
215298
func main() {
299+
var dry string
216300
rootCmd := &cobra.Command{Use: "git-cross"}
301+
rootCmd.PersistentFlags().StringVar(&dry, "dry", "", "Dry run command (e.g. echo)")
217302

218303
useCmd := &cobra.Command{
219304
Use: "use [name] [url]",
@@ -505,12 +590,16 @@ func main() {
505590
Short: "Re-execute all Crossfile commands",
506591
RunE: func(cmd *cobra.Command, args []string) error {
507592
logInfo("Replaying Crossfile...")
508-
_, err := os.ReadFile(CrossfilePath)
593+
path, err := getCrossfilePath()
594+
if err != nil {
595+
return err
596+
}
597+
_, err = os.ReadFile(path)
509598
if err != nil {
510599
return err
511600
}
512601
currExe, _ := os.Executable()
513-
script := fmt.Sprintf(`cross() { "%s" "$@"; }; source "%s"`, currExe, CrossfilePath)
602+
script := fmt.Sprintf(`cross() { "%s" "$@"; }; source "%s"`, currExe, path)
514603
c := exec.Command("bash", "-c", script)
515604
c.Stdout = os.Stdout
516605
c.Stderr = os.Stderr
@@ -639,11 +728,12 @@ func main() {
639728
Use: "init",
640729
Short: "Initialize a new project with Crossfile",
641730
RunE: func(cmd *cobra.Command, args []string) error {
642-
if _, err := os.Stat(CrossfilePath); err == nil {
731+
path := "Crossfile"
732+
if _, err := os.Stat(path); err == nil {
643733
logInfo("Crossfile already exists.")
644734
return nil
645735
}
646-
err := os.WriteFile(CrossfilePath, []byte("# git-cross configuration\n"), 0o644)
736+
err := os.WriteFile(path, []byte("# git-cross configuration\n"), 0o644)
647737
if err != nil {
648738
return err
649739
}

0 commit comments

Comments
 (0)