A vibe coded tangled fork which supports pijul.
1package pijul
2
3import (
4 "archive/tar"
5 "bytes"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "os/exec"
12 "path/filepath"
13
14 securejoin "github.com/cyphar/filepath-securejoin"
15)
16
17var (
18 ErrBinaryFile = errors.New("binary file")
19 ErrNotBinaryFile = errors.New("not binary file")
20 ErrNoPijulRepo = errors.New("not a pijul repository")
21 ErrChannelNotFound = errors.New("channel not found")
22 ErrChangeNotFound = errors.New("change not found")
23 ErrPathNotFound = errors.New("path not found")
24)
25
26// PijulRepo represents a Pijul repository
27type PijulRepo struct {
28 path string
29 channelName string // current channel (empty means default)
30}
31
32// Open opens a Pijul repository at the given path with optional channel
33func Open(path string, channel string) (*PijulRepo, error) {
34 // Verify it's a pijul repository
35 pijulDir := filepath.Join(path, ".pijul")
36 if _, err := os.Stat(pijulDir); os.IsNotExist(err) {
37 return nil, fmt.Errorf("%w: %s", ErrNoPijulRepo, path)
38 }
39
40 p := &PijulRepo{
41 path: path,
42 channelName: channel,
43 }
44
45 // Verify channel exists if specified
46 if channel != "" {
47 channels, err := p.Channels()
48 if err != nil {
49 return nil, fmt.Errorf("listing channels: %w", err)
50 }
51 found := false
52 for _, ch := range channels {
53 if ch.Name == channel {
54 found = true
55 break
56 }
57 }
58 if !found {
59 return nil, fmt.Errorf("%w: %s", ErrChannelNotFound, channel)
60 }
61 }
62
63 return p, nil
64}
65
66// PlainOpen opens a Pijul repository without setting a specific channel
67func PlainOpen(path string) (*PijulRepo, error) {
68 // Verify it's a pijul repository
69 pijulDir := filepath.Join(path, ".pijul")
70 if _, err := os.Stat(pijulDir); os.IsNotExist(err) {
71 return nil, fmt.Errorf("%w: %s", ErrNoPijulRepo, path)
72 }
73
74 return &PijulRepo{path: path}, nil
75}
76
77// Path returns the repository path
78func (p *PijulRepo) Path() string {
79 return p.path
80}
81
82// CurrentChannel returns the current channel (or empty for default)
83func (p *PijulRepo) CurrentChannel() string {
84 return p.channelName
85}
86
87// FindDefaultChannel returns the default channel name
88func (p *PijulRepo) FindDefaultChannel() (string, error) {
89 channels, err := p.Channels()
90 if err != nil {
91 return "", err
92 }
93
94 // Look for 'main' first, then fall back to first channel
95 for _, ch := range channels {
96 if ch.Name == "main" {
97 return "main", nil
98 }
99 }
100
101 if len(channels) > 0 {
102 return channels[0].Name, nil
103 }
104
105 return "main", nil // default
106}
107
108// SetDefaultChannel changes which channel is considered default
109// In Pijul, this would typically be done by renaming channels
110func (p *PijulRepo) SetDefaultChannel(channel string) error {
111 // Pijul doesn't have a built-in default branch concept like git HEAD
112 // This is typically managed at application level
113 // For now, just verify the channel exists
114 channels, err := p.Channels()
115 if err != nil {
116 return err
117 }
118
119 for _, ch := range channels {
120 if ch.Name == channel {
121 return nil
122 }
123 }
124
125 return fmt.Errorf("%w: %s", ErrChannelNotFound, channel)
126}
127
128// FileContent reads a file from the working copy at a specific path
129// Note: Pijul doesn't have the concept of reading files at a specific revision
130// like git. We read from the working directory or need to use pijul credit.
131func (p *PijulRepo) FileContent(filePath string) ([]byte, error) {
132 fullPath, err := securejoin.SecureJoin(p.path, filePath)
133 if err != nil {
134 return nil, fmt.Errorf("invalid path: %w", err)
135 }
136
137 content, err := os.ReadFile(fullPath)
138 if err != nil {
139 if os.IsNotExist(err) {
140 return nil, fmt.Errorf("%w: %s", ErrPathNotFound, filePath)
141 }
142 return nil, err
143 }
144
145 return content, nil
146}
147
148// FileContentN reads up to cap bytes of a file
149func (p *PijulRepo) FileContentN(filePath string, cap int64) ([]byte, error) {
150 fullPath, err := securejoin.SecureJoin(p.path, filePath)
151 if err != nil {
152 return nil, fmt.Errorf("invalid path: %w", err)
153 }
154
155 f, err := os.Open(fullPath)
156 if err != nil {
157 if os.IsNotExist(err) {
158 return nil, fmt.Errorf("%w: %s", ErrPathNotFound, filePath)
159 }
160 return nil, err
161 }
162 defer f.Close()
163
164 // Check if binary
165 buf := make([]byte, 512)
166 n, err := f.Read(buf)
167 if err != nil && err != io.EOF {
168 return nil, err
169 }
170 if isBinary(buf[:n]) {
171 return nil, ErrBinaryFile
172 }
173
174 // Reset and read up to cap
175 if _, err := f.Seek(0, 0); err != nil {
176 return nil, err
177 }
178
179 content := make([]byte, cap)
180 n, err = f.Read(content)
181 if err != nil && err != io.EOF {
182 return nil, err
183 }
184
185 return content[:n], nil
186}
187
188// RawContent reads raw file content without binary check
189func (p *PijulRepo) RawContent(filePath string) ([]byte, error) {
190 fullPath, err := securejoin.SecureJoin(p.path, filePath)
191 if err != nil {
192 return nil, fmt.Errorf("invalid path: %w", err)
193 }
194 return os.ReadFile(fullPath)
195}
196
197// isBinary checks if data appears to be binary
198func isBinary(data []byte) bool {
199 for _, b := range data {
200 if b == 0 {
201 return true
202 }
203 }
204 return false
205}
206
207// WriteTar writes the repository contents to a tar archive
208func (p *PijulRepo) WriteTar(w io.Writer, prefix string) error {
209 tw := tar.NewWriter(w)
210 defer tw.Close()
211
212 return filepath.Walk(p.path, func(path string, info fs.FileInfo, err error) error {
213 if err != nil {
214 return err
215 }
216
217 // Skip .pijul directory
218 if info.IsDir() && filepath.Base(path) == ".pijul" {
219 return filepath.SkipDir
220 }
221
222 relPath, err := filepath.Rel(p.path, path)
223 if err != nil {
224 return err
225 }
226
227 if relPath == "." {
228 return nil
229 }
230
231 header, err := tar.FileInfoHeader(info, "")
232 if err != nil {
233 return err
234 }
235
236 header.Name = filepath.Join(prefix, relPath)
237
238 if err := tw.WriteHeader(header); err != nil {
239 return err
240 }
241
242 if !info.IsDir() {
243 if err := copyFileToTar(tw, path); err != nil {
244 return err
245 }
246 }
247
248 return nil
249 })
250}
251
252// copyFileToTar copies a single file into a tar writer, closing the file before returning.
253func copyFileToTar(tw *tar.Writer, path string) error {
254 f, err := os.Open(path)
255 if err != nil {
256 return err
257 }
258 defer f.Close()
259 _, err = io.Copy(tw, f)
260 return err
261}
262
263// InitRepo initializes a new Pijul repository
264func InitRepo(path string, bare bool) error {
265 if err := os.MkdirAll(path, 0755); err != nil {
266 return fmt.Errorf("creating directory: %w", err)
267 }
268
269 args := []string{"init"}
270 if bare {
271 // Pijul doesn't have explicit bare repos like git
272 // A "bare" repo is typically just a repo without a working directory
273 args = append(args, "--kind=bare")
274 }
275
276 cmd := exec.Command("pijul", args...)
277 cmd.Dir = path
278
279 var stderr bytes.Buffer
280 cmd.Stderr = &stderr
281
282 if err := cmd.Run(); err != nil {
283 return fmt.Errorf("pijul init: %w, stderr: %s", err, stderr.String())
284 }
285
286 return nil
287}
288
289// Clone clones a Pijul repository
290func Clone(url, destPath string, channel string) error {
291 args := []string{"clone", url, destPath}
292 if channel != "" {
293 args = append(args, "--channel", channel)
294 }
295
296 cmd := exec.Command("pijul", args...)
297
298 var stderr bytes.Buffer
299 cmd.Stderr = &stderr
300
301 if err := cmd.Run(); err != nil {
302 return fmt.Errorf("pijul clone: %w, stderr: %s", err, stderr.String())
303 }
304
305 return nil
306}