A vibe coded tangled fork which supports pijul.
1package pijul
2
3import (
4 "context"
5 "fmt"
6 "io/fs"
7 "os"
8 "path"
9 "path/filepath"
10 "strings"
11
12 securejoin "github.com/cyphar/filepath-securejoin"
13 "tangled.org/core/types"
14)
15
16// TreeEntry represents a file or directory in the repository tree
17type TreeEntry struct {
18 Name string `json:"name"`
19 Mode fs.FileMode `json:"mode"`
20 Size int64 `json:"size"`
21 IsDir bool `json:"is_dir"`
22}
23
24// FileTree returns the file tree at the given path
25// For Pijul, we read directly from the working directory
26func (p *PijulRepo) FileTree(ctx context.Context, treePath string) ([]types.NiceTree, error) {
27 fullPath, err := securejoin.SecureJoin(p.path, treePath)
28 if err != nil {
29 return nil, fmt.Errorf("invalid path: %w", err)
30 }
31
32 info, err := os.Stat(fullPath)
33 if err != nil {
34 if os.IsNotExist(err) {
35 return nil, ErrPathNotFound
36 }
37 return nil, err
38 }
39
40 // If it's a file, return empty (no tree for files)
41 if !info.IsDir() {
42 return []types.NiceTree{}, nil
43 }
44
45 entries, err := os.ReadDir(fullPath)
46 if err != nil {
47 return nil, err
48 }
49
50 trees := make([]types.NiceTree, 0, len(entries))
51
52 for _, entry := range entries {
53 // Skip .pijul directory
54 if entry.Name() == ".pijul" {
55 continue
56 }
57
58 info, err := entry.Info()
59 if err != nil {
60 continue
61 }
62
63 trees = append(trees, types.NiceTree{
64 Name: entry.Name(),
65 Mode: fileModeToString(info.Mode()),
66 Size: info.Size(),
67 // LastCommit would require additional work to implement
68 // For now, we leave it nil
69 })
70 }
71
72 return trees, nil
73}
74
75// fileModeToString converts fs.FileMode to octal string representation
76func fileModeToString(mode fs.FileMode) string {
77 // Convert to git-style mode representation
78 if mode.IsDir() {
79 return "040000"
80 }
81 if mode&fs.ModeSymlink != 0 {
82 return "120000"
83 }
84 if mode&0111 != 0 {
85 return "100755"
86 }
87 return "100644"
88}
89
90// Walk callback type
91type WalkCallback func(path string, info fs.FileInfo, isDir bool) error
92
93// Walk traverses the file tree
94func (p *PijulRepo) Walk(ctx context.Context, root string, cb WalkCallback) error {
95 startPath, err := securejoin.SecureJoin(p.path, root)
96 if err != nil {
97 return fmt.Errorf("invalid path: %w", err)
98 }
99
100 return filepath.WalkDir(startPath, func(walkPath string, d fs.DirEntry, err error) error {
101 if err != nil {
102 return err
103 }
104
105 // Check context
106 select {
107 case <-ctx.Done():
108 return ctx.Err()
109 default:
110 }
111
112 // Skip .pijul directory
113 if d.IsDir() && filepath.Base(walkPath) == ".pijul" {
114 return filepath.SkipDir
115 }
116
117 // Get relative path
118 relPath, err := filepath.Rel(p.path, walkPath)
119 if err != nil {
120 return err
121 }
122
123 if relPath == "." {
124 return nil
125 }
126
127 info, err := d.Info()
128 if err != nil {
129 return err
130 }
131
132 return cb(relPath, info, d.IsDir())
133 })
134}
135
136// ListFiles returns all tracked files in the repository
137func (p *PijulRepo) ListFiles() ([]string, error) {
138 output, err := p.runPijulCmd("ls")
139 if err != nil {
140 return nil, err
141 }
142
143 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
144 if len(lines) == 1 && lines[0] == "" {
145 return []string{}, nil
146 }
147
148 return lines, nil
149}
150
151// IsTracked checks if a file is tracked by Pijul
152func (p *PijulRepo) IsTracked(filePath string) (bool, error) {
153 files, err := p.ListFiles()
154 if err != nil {
155 return false, err
156 }
157
158 for _, f := range files {
159 if f == filePath {
160 return true, nil
161 }
162 }
163
164 return false, nil
165}
166
167// FileExists checks if a file exists in the working directory
168func (p *PijulRepo) FileExists(filePath string) bool {
169 fullPath, err := securejoin.SecureJoin(p.path, filePath)
170 if err != nil {
171 return false
172 }
173 _, err = os.Stat(fullPath)
174 return err == nil
175}
176
177// IsDir checks if a path is a directory
178func (p *PijulRepo) IsDir(treePath string) (bool, error) {
179 fullPath, err := securejoin.SecureJoin(p.path, treePath)
180 if err != nil {
181 return false, fmt.Errorf("invalid path: %w", err)
182 }
183 info, err := os.Stat(fullPath)
184 if err != nil {
185 return false, err
186 }
187 return info.IsDir(), nil
188}
189
190// MakeNiceTree creates a NiceTree from file info
191func MakeNiceTree(name string, info fs.FileInfo) types.NiceTree {
192 return types.NiceTree{
193 Name: path.Base(name),
194 Mode: fileModeToString(info.Mode()),
195 Size: info.Size(),
196 }
197}