Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

reachability: Add basic reachability API

Bitmaps not supported yet

runxiyu.tngl.sh 1fa0d2bc 68ab022c

verified
+581
+385
reachability.go
··· 1 + package furgit 2 + 3 + import ( 4 + "fmt" 5 + "iter" 6 + ) 7 + 8 + // ReachabilityMode controls which object types are walked. 9 + type ReachabilityMode uint8 10 + 11 + const ( 12 + // ReachabilityCommitsOnly walks only commit objects. 13 + ReachabilityCommitsOnly ReachabilityMode = iota 14 + // ReachabilityAllObjects walks commits, trees, blobs, and tags reachable 15 + // from the commits in Wants. 16 + ReachabilityAllObjects 17 + ) 18 + 19 + // ReachabilityQuery describes a want/have reachability walk. 20 + // 21 + // ReachableObjects returns objects reachable from Wants. If Mode is 22 + // ReachabilityCommitsOnly, non-commit Wants are ignored except for tags, 23 + // which are peeled to their target. 24 + type ReachabilityQuery struct { 25 + Wants []Hash 26 + Haves []Hash 27 + Mode ReachabilityMode 28 + 29 + // StopAtHaves prunes traversal when an object is reachable from Haves. 30 + StopAtHaves bool 31 + } 32 + 33 + // ReachableObject reports a reachable object and whether it is also reachable 34 + // from the Have set. 35 + type ReachableObject struct { 36 + ID Hash 37 + Type ObjectType 38 + InHave bool 39 + } 40 + 41 + // ReachabilityWalk is a single-use reachability iterator. 42 + // After iterating, Err reports any error encountered during the walk. 43 + type ReachabilityWalk struct { 44 + repo *Repository 45 + query ReachabilityQuery 46 + err error 47 + 48 + haveInit bool 49 + haveErr error 50 + haveSet map[Hash]struct{} 51 + } 52 + 53 + // ReachableObjects returns a single-use iterator over objects reachable from 54 + // query.Wants. 55 + // 56 + // It yields ReachableObject values; InHave is true when the object is also 57 + // reachable from query.Haves. 58 + func (repo *Repository) ReachableObjects(query ReachabilityQuery) (*ReachabilityWalk, error) { 59 + if repo == nil { 60 + return nil, ErrInvalidObject 61 + } 62 + switch query.Mode { 63 + case ReachabilityCommitsOnly, ReachabilityAllObjects: 64 + default: 65 + return nil, ErrInvalidObject 66 + } 67 + for _, id := range query.Wants { 68 + if id.algo != repo.hashAlgo { 69 + return nil, fmt.Errorf("furgit: reachability: want hash algorithm mismatch") 70 + } 71 + } 72 + for _, id := range query.Haves { 73 + if id.algo != repo.hashAlgo { 74 + return nil, fmt.Errorf("furgit: reachability: have hash algorithm mismatch") 75 + } 76 + } 77 + return &ReachabilityWalk{ 78 + repo: repo, 79 + query: query, 80 + }, nil 81 + } 82 + 83 + // Seq returns the iterator. 84 + func (w *ReachabilityWalk) Seq() iter.Seq[ReachableObject] { 85 + return func(yield func(ReachableObject) bool) { 86 + if w == nil || w.repo == nil { 87 + w.err = ErrInvalidObject 88 + return 89 + } 90 + haveSet, err := w.ensureHaveSet() 91 + if err != nil { 92 + w.err = err 93 + return 94 + } 95 + 96 + wantWalk := reachabilityWalker{ 97 + repo: w.repo, 98 + mode: w.query.Mode, 99 + seenCommits: make(map[Hash]struct{}), 100 + seenObjects: make(map[Hash]struct{}), 101 + haveSet: haveSet, 102 + stopAtHaves: w.query.StopAtHaves, 103 + } 104 + if err := wantWalk.walkRoots(w.query.Wants, func(obj ReachableObject) bool { 105 + return yield(obj) 106 + }); err != nil { 107 + w.err = err 108 + return 109 + } 110 + } 111 + } 112 + 113 + // Err reports the first error encountered by the iterator. 114 + func (w *ReachabilityWalk) Err() error { 115 + if w == nil { 116 + return ErrInvalidObject 117 + } 118 + return w.err 119 + } 120 + 121 + // HaveContains reports whether id is reachable from Haves. 122 + func (w *ReachabilityWalk) HaveContains(id Hash) (bool, error) { 123 + if w == nil || w.repo == nil { 124 + return false, ErrInvalidObject 125 + } 126 + haveSet, err := w.ensureHaveSet() 127 + if err != nil { 128 + return false, err 129 + } 130 + _, ok := haveSet[id] 131 + return ok, nil 132 + } 133 + 134 + func (w *ReachabilityWalk) ensureHaveSet() (map[Hash]struct{}, error) { 135 + if w.haveInit { 136 + return w.haveSet, w.haveErr 137 + } 138 + w.haveInit = true 139 + w.haveSet = make(map[Hash]struct{}) 140 + if len(w.query.Haves) == 0 { 141 + return w.haveSet, nil 142 + } 143 + haveWalk := reachabilityWalker{ 144 + repo: w.repo, 145 + mode: w.query.Mode, 146 + seenCommits: make(map[Hash]struct{}), 147 + seenObjects: make(map[Hash]struct{}), 148 + recordHaveOnly: true, 149 + haveSet: w.haveSet, 150 + } 151 + if err := haveWalk.walkRoots(w.query.Haves, nil); err != nil { 152 + w.haveErr = err 153 + return nil, err 154 + } 155 + return w.haveSet, nil 156 + } 157 + 158 + type reachabilityWalker struct { 159 + repo *Repository 160 + mode ReachabilityMode 161 + 162 + seenCommits map[Hash]struct{} 163 + seenObjects map[Hash]struct{} 164 + 165 + haveSet map[Hash]struct{} 166 + recordHaveOnly bool 167 + stopAtHaves bool 168 + 169 + cg *commitGraph 170 + cgInit bool 171 + } 172 + 173 + func (rw *reachabilityWalker) initCommitGraph() { 174 + if rw.cgInit { 175 + return 176 + } 177 + rw.cgInit = true 178 + cg, err := rw.repo.CommitGraph() 179 + if err == nil { 180 + rw.cg = cg 181 + } 182 + } 183 + 184 + func (rw *reachabilityWalker) walkRoots(roots []Hash, emit func(ReachableObject) bool) error { 185 + for _, id := range roots { 186 + if err := rw.walkObject(id, emit); err != nil { 187 + return err 188 + } 189 + } 190 + return nil 191 + } 192 + 193 + func (rw *reachabilityWalker) walkObject(id Hash, emit func(ReachableObject) bool) error { 194 + if rw.stopAtHaves { 195 + if _, ok := rw.haveSet[id]; ok { 196 + return nil 197 + } 198 + } 199 + if rw.recordHaveOnly { 200 + if _, ok := rw.haveSet[id]; ok { 201 + return nil 202 + } 203 + } else { 204 + if _, ok := rw.seenObjects[id]; ok { 205 + return nil 206 + } 207 + } 208 + 209 + rw.initCommitGraph() 210 + if rw.cg != nil { 211 + if pos, ok := rw.cg.CommitPosition(id); ok { 212 + return rw.walkCommitByPos(pos, id, emit) 213 + } 214 + } 215 + 216 + ty, body, err := rw.repo.ReadObjectTypeRaw(id) 217 + if err != nil { 218 + return err 219 + } 220 + 221 + switch ty { 222 + case ObjectTypeCommit: 223 + return rw.walkCommitBody(id, body, emit) 224 + case ObjectTypeTree: 225 + if rw.mode != ReachabilityAllObjects { 226 + return nil 227 + } 228 + return rw.walkTreeBody(id, body, emit) 229 + case ObjectTypeBlob: 230 + if rw.mode != ReachabilityAllObjects { 231 + return nil 232 + } 233 + return rw.emitObject(id, ObjectTypeBlob, emit) 234 + case ObjectTypeTag: 235 + return rw.walkTagBody(id, body, emit) 236 + default: 237 + return ErrInvalidObject 238 + } 239 + } 240 + 241 + func (rw *reachabilityWalker) walkCommitByPos(pos uint32, id Hash, emit func(ReachableObject) bool) error { 242 + if _, ok := rw.seenCommits[id]; ok { 243 + return nil 244 + } 245 + rw.seenCommits[id] = struct{}{} 246 + 247 + cc, err := rw.cg.CommitAt(pos) 248 + if err != nil { 249 + return err 250 + } 251 + 252 + if err := rw.emitObject(id, ObjectTypeCommit, emit); err != nil { 253 + return err 254 + } 255 + 256 + if rw.mode == ReachabilityAllObjects { 257 + if err := rw.walkTreeByID(cc.Tree, emit); err != nil { 258 + return err 259 + } 260 + } 261 + 262 + for _, parentPos := range cc.Parents { 263 + parentID, err := rw.cg.OIDAt(parentPos) 264 + if err != nil { 265 + return err 266 + } 267 + if err := rw.walkObject(parentID, emit); err != nil { 268 + return err 269 + } 270 + } 271 + return nil 272 + } 273 + 274 + func (rw *reachabilityWalker) walkCommitBody(id Hash, body []byte, emit func(ReachableObject) bool) error { 275 + if _, ok := rw.seenCommits[id]; ok { 276 + return nil 277 + } 278 + rw.seenCommits[id] = struct{}{} 279 + 280 + commit, err := parseCommit(id, body, rw.repo) 281 + if err != nil { 282 + return err 283 + } 284 + if err := rw.emitObject(id, ObjectTypeCommit, emit); err != nil { 285 + return err 286 + } 287 + if rw.mode == ReachabilityAllObjects { 288 + if err := rw.walkTreeByID(commit.Tree, emit); err != nil { 289 + return err 290 + } 291 + } 292 + for _, parent := range commit.Parents { 293 + if err := rw.walkObject(parent, emit); err != nil { 294 + return err 295 + } 296 + } 297 + return nil 298 + } 299 + 300 + func (rw *reachabilityWalker) walkTagBody(id Hash, body []byte, emit func(ReachableObject) bool) error { 301 + tag, err := parseTag(id, body, rw.repo) 302 + if err != nil { 303 + return err 304 + } 305 + if rw.mode == ReachabilityAllObjects { 306 + if err := rw.emitObject(id, ObjectTypeTag, emit); err != nil { 307 + return err 308 + } 309 + } 310 + if tag.TargetType == ObjectTypeCommit { 311 + return rw.walkObject(tag.Target, emit) 312 + } 313 + if rw.mode == ReachabilityAllObjects { 314 + return rw.walkObject(tag.Target, emit) 315 + } 316 + return nil 317 + } 318 + 319 + func (rw *reachabilityWalker) walkTreeByID(id Hash, emit func(ReachableObject) bool) error { 320 + if rw.mode != ReachabilityAllObjects { 321 + return nil 322 + } 323 + if _, ok := rw.seenObjects[id]; ok && !rw.recordHaveOnly { 324 + return nil 325 + } 326 + ty, body, err := rw.repo.ReadObjectTypeRaw(id) 327 + if err != nil { 328 + return err 329 + } 330 + if ty != ObjectTypeTree { 331 + return ErrInvalidObject 332 + } 333 + return rw.walkTreeBody(id, body, emit) 334 + } 335 + 336 + func (rw *reachabilityWalker) walkTreeBody(id Hash, body []byte, emit func(ReachableObject) bool) error { 337 + if rw.mode != ReachabilityAllObjects { 338 + return nil 339 + } 340 + tree, err := parseTree(id, body, rw.repo) 341 + if err != nil { 342 + return err 343 + } 344 + if err := rw.emitObject(id, ObjectTypeTree, emit); err != nil { 345 + return err 346 + } 347 + for _, entry := range tree.Entries { 348 + switch entry.Mode { 349 + case FileModeDir: 350 + if err := rw.walkTreeByID(entry.ID, emit); err != nil { 351 + return err 352 + } 353 + case FileModeGitlink: 354 + // IIRC Gitlinks are references to external repositories 355 + // and do not imply reachability of the target commit... 356 + continue 357 + default: 358 + if err := rw.emitObject(entry.ID, ObjectTypeBlob, emit); err != nil { 359 + return err 360 + } 361 + } 362 + } 363 + return nil 364 + } 365 + 366 + func (rw *reachabilityWalker) emitObject(id Hash, ty ObjectType, emit func(ReachableObject) bool) error { 367 + if rw.recordHaveOnly { 368 + rw.haveSet[id] = struct{}{} 369 + return nil 370 + } 371 + if _, ok := rw.seenObjects[id]; ok { 372 + return nil 373 + } 374 + rw.seenObjects[id] = struct{}{} 375 + inHave := false 376 + if _, ok := rw.haveSet[id]; ok { 377 + inHave = true 378 + } 379 + if emit != nil { 380 + if !emit(ReachableObject{ID: id, Type: ty, InHave: inHave}) { 381 + return nil 382 + } 383 + } 384 + return nil 385 + }
+196
reachability_test.go
··· 1 + package furgit 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "strings" 7 + "testing" 8 + ) 9 + 10 + func TestReachabilityCommitsWantHave(t *testing.T) { 11 + repoPath, cleanup := setupTestRepo(t) 12 + defer cleanup() 13 + 14 + workDir, cleanupWork := setupWorkDir(t) 15 + defer cleanupWork() 16 + 17 + var commits []string 18 + for i := 0; i < 3; i++ { 19 + path := filepath.Join(workDir, "file.txt") 20 + if err := os.WriteFile(path, []byte{byte('a' + i), '\n'}, 0o644); err != nil { 21 + t.Fatalf("write file: %v", err) 22 + } 23 + gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 24 + gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit") 25 + commits = append(commits, gitCmd(t, repoPath, "rev-parse", "HEAD")) 26 + } 27 + 28 + repo, err := OpenRepository(repoPath) 29 + if err != nil { 30 + t.Fatalf("OpenRepository failed: %v", err) 31 + } 32 + defer func() { _ = repo.Close() }() 33 + 34 + wantID, _ := repo.ParseHash(commits[2]) 35 + haveID, _ := repo.ParseHash(commits[1]) 36 + walk, err := repo.ReachableObjects(ReachabilityQuery{ 37 + Wants: []Hash{wantID}, 38 + Haves: []Hash{haveID}, 39 + Mode: ReachabilityCommitsOnly, 40 + }) 41 + if err != nil { 42 + t.Fatalf("ReachableObjects failed: %v", err) 43 + } 44 + 45 + seen := make(map[Hash]ReachableObject) 46 + for obj := range walk.Seq() { 47 + seen[obj.ID] = obj 48 + if obj.Type != ObjectTypeCommit { 49 + t.Fatalf("unexpected object type: %v", obj.Type) 50 + } 51 + } 52 + if err := walk.Err(); err != nil { 53 + t.Fatalf("Reachability walk error: %v", err) 54 + } 55 + 56 + headID := wantID 57 + parentID, _ := repo.ParseHash(commits[1]) 58 + rootID, _ := repo.ParseHash(commits[0]) 59 + if _, ok := seen[headID]; !ok { 60 + t.Fatalf("missing head commit") 61 + } 62 + if _, ok := seen[parentID]; !ok { 63 + t.Fatalf("missing parent commit") 64 + } 65 + if _, ok := seen[rootID]; !ok { 66 + t.Fatalf("missing root commit") 67 + } 68 + if seen[headID].InHave { 69 + t.Fatalf("head commit incorrectly marked InHave") 70 + } 71 + if !seen[parentID].InHave || !seen[rootID].InHave { 72 + t.Fatalf("expected parent and root commits to be InHave") 73 + } 74 + 75 + inHave, err := walk.HaveContains(parentID) 76 + if err != nil { 77 + t.Fatalf("HaveContains failed: %v", err) 78 + } 79 + if !inHave { 80 + t.Fatalf("expected parent to be reachable from have") 81 + } 82 + } 83 + 84 + func TestReachabilityAllObjects(t *testing.T) { 85 + repoPath, cleanup := setupTestRepo(t) 86 + defer cleanup() 87 + 88 + workDir, cleanupWork := setupWorkDir(t) 89 + defer cleanupWork() 90 + 91 + if err := os.WriteFile(filepath.Join(workDir, "file1.txt"), []byte("one\n"), 0o644); err != nil { 92 + t.Fatalf("write file1: %v", err) 93 + } 94 + if err := os.Mkdir(filepath.Join(workDir, "dir"), 0o755); err != nil { 95 + t.Fatalf("mkdir dir: %v", err) 96 + } 97 + if err := os.WriteFile(filepath.Join(workDir, "dir", "file2.txt"), []byte("two\n"), 0o644); err != nil { 98 + t.Fatalf("write file2: %v", err) 99 + } 100 + gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 101 + gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit") 102 + 103 + repo, err := OpenRepository(repoPath) 104 + if err != nil { 105 + t.Fatalf("OpenRepository failed: %v", err) 106 + } 107 + defer func() { _ = repo.Close() }() 108 + 109 + head := gitCmd(t, repoPath, "rev-parse", "HEAD") 110 + wantID, _ := repo.ParseHash(head) 111 + walk, err := repo.ReachableObjects(ReachabilityQuery{ 112 + Wants: []Hash{wantID}, 113 + Mode: ReachabilityAllObjects, 114 + }) 115 + if err != nil { 116 + t.Fatalf("ReachableObjects failed: %v", err) 117 + } 118 + 119 + seen := make(map[Hash]ObjectType) 120 + for obj := range walk.Seq() { 121 + seen[obj.ID] = obj.Type 122 + } 123 + if err := walk.Err(); err != nil { 124 + t.Fatalf("Reachability walk error: %v", err) 125 + } 126 + 127 + treeStr := gitCmd(t, repoPath, "show", "-s", "--format=%T", head) 128 + treeID, _ := repo.ParseHash(treeStr) 129 + lsTree := gitCmd(t, repoPath, "ls-tree", "-r", treeStr) 130 + fields := strings.Fields(lsTree) 131 + if len(fields) < 3 { 132 + t.Fatalf("unexpected ls-tree output: %q", lsTree) 133 + } 134 + blobID, _ := repo.ParseHash(fields[2]) 135 + 136 + if seen[wantID] != ObjectTypeCommit { 137 + t.Fatalf("missing commit in reachability walk") 138 + } 139 + if seen[treeID] != ObjectTypeTree { 140 + t.Fatalf("missing tree in reachability walk") 141 + } 142 + if seen[blobID] != ObjectTypeBlob { 143 + t.Fatalf("missing blob in reachability walk") 144 + } 145 + } 146 + 147 + func TestReachabilityStopAtHaves(t *testing.T) { 148 + repoPath, cleanup := setupTestRepo(t) 149 + defer cleanup() 150 + 151 + workDir, cleanupWork := setupWorkDir(t) 152 + defer cleanupWork() 153 + 154 + var commits []string 155 + for i := 0; i < 3; i++ { 156 + path := filepath.Join(workDir, "file.txt") 157 + if err := os.WriteFile(path, []byte{byte('a' + i), '\n'}, 0o644); err != nil { 158 + t.Fatalf("write file: %v", err) 159 + } 160 + gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 161 + gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit") 162 + commits = append(commits, gitCmd(t, repoPath, "rev-parse", "HEAD")) 163 + } 164 + 165 + repo, err := OpenRepository(repoPath) 166 + if err != nil { 167 + t.Fatalf("OpenRepository failed: %v", err) 168 + } 169 + defer func() { _ = repo.Close() }() 170 + 171 + wantID, _ := repo.ParseHash(commits[2]) 172 + haveID, _ := repo.ParseHash(commits[1]) 173 + walk, err := repo.ReachableObjects(ReachabilityQuery{ 174 + Wants: []Hash{wantID}, 175 + Haves: []Hash{haveID}, 176 + Mode: ReachabilityCommitsOnly, 177 + StopAtHaves: true, 178 + }) 179 + if err != nil { 180 + t.Fatalf("ReachableObjects failed: %v", err) 181 + } 182 + 183 + var got []Hash 184 + for obj := range walk.Seq() { 185 + got = append(got, obj.ID) 186 + if obj.InHave { 187 + t.Fatalf("unexpected InHave object in send set") 188 + } 189 + } 190 + if err := walk.Err(); err != nil { 191 + t.Fatalf("Reachability walk error: %v", err) 192 + } 193 + if len(got) != 1 || got[0] != wantID { 194 + t.Fatalf("StopAtHaves mismatch: got %d objects", len(got)) 195 + } 196 + }