A vibe coded tangled fork which supports pijul.
at 5bf28708dcf8972c724fb0c33fcab1281cbc3f27 306 lines 7.0 kB view raw
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}