You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dotgit.go 25KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. // https://github.com/git/git/blob/master/Documentation/gitrepository-layout.txt
  2. package dotgit
  3. import (
  4. "bufio"
  5. "errors"
  6. "fmt"
  7. "io"
  8. stdioutil "io/ioutil"
  9. "os"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "github.com/go-git/go-billy/v5/osfs"
  14. "github.com/go-git/go-git/v5/plumbing"
  15. "github.com/go-git/go-git/v5/storage"
  16. "github.com/go-git/go-git/v5/utils/ioutil"
  17. "github.com/go-git/go-billy/v5"
  18. )
  19. const (
  20. suffix = ".git"
  21. packedRefsPath = "packed-refs"
  22. configPath = "config"
  23. indexPath = "index"
  24. shallowPath = "shallow"
  25. modulePath = "modules"
  26. objectsPath = "objects"
  27. packPath = "pack"
  28. refsPath = "refs"
  29. tmpPackedRefsPrefix = "._packed-refs"
  30. packPrefix = "pack-"
  31. packExt = ".pack"
  32. idxExt = ".idx"
  33. )
  34. var (
  35. // ErrNotFound is returned by New when the path is not found.
  36. ErrNotFound = errors.New("path not found")
  37. // ErrIdxNotFound is returned by Idxfile when the idx file is not found
  38. ErrIdxNotFound = errors.New("idx file not found")
  39. // ErrPackfileNotFound is returned by Packfile when the packfile is not found
  40. ErrPackfileNotFound = errors.New("packfile not found")
  41. // ErrConfigNotFound is returned by Config when the config is not found
  42. ErrConfigNotFound = errors.New("config file not found")
  43. // ErrPackedRefsDuplicatedRef is returned when a duplicated reference is
  44. // found in the packed-ref file. This is usually the case for corrupted git
  45. // repositories.
  46. ErrPackedRefsDuplicatedRef = errors.New("duplicated ref found in packed-ref file")
  47. // ErrPackedRefsBadFormat is returned when the packed-ref file corrupt.
  48. ErrPackedRefsBadFormat = errors.New("malformed packed-ref")
  49. // ErrSymRefTargetNotFound is returned when a symbolic reference is
  50. // targeting a non-existing object. This usually means the repository
  51. // is corrupt.
  52. ErrSymRefTargetNotFound = errors.New("symbolic reference target not found")
  53. // ErrIsDir is returned when a reference file is attempting to be read,
  54. // but the path specified is a directory.
  55. ErrIsDir = errors.New("reference path is a directory")
  56. )
  57. // Options holds configuration for the storage.
  58. type Options struct {
  59. // ExclusiveAccess means that the filesystem is not modified externally
  60. // while the repo is open.
  61. ExclusiveAccess bool
  62. // KeepDescriptors makes the file descriptors to be reused but they will
  63. // need to be manually closed calling Close().
  64. KeepDescriptors bool
  65. }
  66. // The DotGit type represents a local git repository on disk. This
  67. // type is not zero-value-safe, use the New function to initialize it.
  68. type DotGit struct {
  69. options Options
  70. fs billy.Filesystem
  71. // incoming object directory information
  72. incomingChecked bool
  73. incomingDirName string
  74. objectList []plumbing.Hash
  75. objectMap map[plumbing.Hash]struct{}
  76. packList []plumbing.Hash
  77. packMap map[plumbing.Hash]struct{}
  78. files map[plumbing.Hash]billy.File
  79. }
  80. // New returns a DotGit value ready to be used. The path argument must
  81. // be the absolute path of a git repository directory (e.g.
  82. // "/foo/bar/.git").
  83. func New(fs billy.Filesystem) *DotGit {
  84. return NewWithOptions(fs, Options{})
  85. }
  86. // NewWithOptions sets non default configuration options.
  87. // See New for complete help.
  88. func NewWithOptions(fs billy.Filesystem, o Options) *DotGit {
  89. return &DotGit{
  90. options: o,
  91. fs: fs,
  92. }
  93. }
  94. // Initialize creates all the folder scaffolding.
  95. func (d *DotGit) Initialize() error {
  96. mustExists := []string{
  97. d.fs.Join("objects", "info"),
  98. d.fs.Join("objects", "pack"),
  99. d.fs.Join("refs", "heads"),
  100. d.fs.Join("refs", "tags"),
  101. }
  102. for _, path := range mustExists {
  103. _, err := d.fs.Stat(path)
  104. if err == nil {
  105. continue
  106. }
  107. if !os.IsNotExist(err) {
  108. return err
  109. }
  110. if err := d.fs.MkdirAll(path, os.ModeDir|os.ModePerm); err != nil {
  111. return err
  112. }
  113. }
  114. return nil
  115. }
  116. // Close closes all opened files.
  117. func (d *DotGit) Close() error {
  118. var firstError error
  119. if d.files != nil {
  120. for _, f := range d.files {
  121. err := f.Close()
  122. if err != nil && firstError == nil {
  123. firstError = err
  124. continue
  125. }
  126. }
  127. d.files = nil
  128. }
  129. if firstError != nil {
  130. return firstError
  131. }
  132. return nil
  133. }
  134. // ConfigWriter returns a file pointer for write to the config file
  135. func (d *DotGit) ConfigWriter() (billy.File, error) {
  136. return d.fs.Create(configPath)
  137. }
  138. // Config returns a file pointer for read to the config file
  139. func (d *DotGit) Config() (billy.File, error) {
  140. return d.fs.Open(configPath)
  141. }
  142. // IndexWriter returns a file pointer for write to the index file
  143. func (d *DotGit) IndexWriter() (billy.File, error) {
  144. return d.fs.Create(indexPath)
  145. }
  146. // Index returns a file pointer for read to the index file
  147. func (d *DotGit) Index() (billy.File, error) {
  148. return d.fs.Open(indexPath)
  149. }
  150. // ShallowWriter returns a file pointer for write to the shallow file
  151. func (d *DotGit) ShallowWriter() (billy.File, error) {
  152. return d.fs.Create(shallowPath)
  153. }
  154. // Shallow returns a file pointer for read to the shallow file
  155. func (d *DotGit) Shallow() (billy.File, error) {
  156. f, err := d.fs.Open(shallowPath)
  157. if err != nil {
  158. if os.IsNotExist(err) {
  159. return nil, nil
  160. }
  161. return nil, err
  162. }
  163. return f, nil
  164. }
  165. // NewObjectPack return a writer for a new packfile, it saves the packfile to
  166. // disk and also generates and save the index for the given packfile.
  167. func (d *DotGit) NewObjectPack() (*PackWriter, error) {
  168. d.cleanPackList()
  169. return newPackWrite(d.fs)
  170. }
  171. // ObjectPacks returns the list of availables packfiles
  172. func (d *DotGit) ObjectPacks() ([]plumbing.Hash, error) {
  173. if !d.options.ExclusiveAccess {
  174. return d.objectPacks()
  175. }
  176. err := d.genPackList()
  177. if err != nil {
  178. return nil, err
  179. }
  180. return d.packList, nil
  181. }
  182. func (d *DotGit) objectPacks() ([]plumbing.Hash, error) {
  183. packDir := d.fs.Join(objectsPath, packPath)
  184. files, err := d.fs.ReadDir(packDir)
  185. if err != nil {
  186. if os.IsNotExist(err) {
  187. return nil, nil
  188. }
  189. return nil, err
  190. }
  191. var packs []plumbing.Hash
  192. for _, f := range files {
  193. n := f.Name()
  194. if !strings.HasSuffix(n, packExt) || !strings.HasPrefix(n, packPrefix) {
  195. continue
  196. }
  197. h := plumbing.NewHash(n[5 : len(n)-5]) //pack-(hash).pack
  198. if h.IsZero() {
  199. // Ignore files with badly-formatted names.
  200. continue
  201. }
  202. packs = append(packs, h)
  203. }
  204. return packs, nil
  205. }
  206. func (d *DotGit) objectPackPath(hash plumbing.Hash, extension string) string {
  207. return d.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s.%s", hash.String(), extension))
  208. }
  209. func (d *DotGit) objectPackOpen(hash plumbing.Hash, extension string) (billy.File, error) {
  210. if d.options.KeepDescriptors && extension == "pack" {
  211. if d.files == nil {
  212. d.files = make(map[plumbing.Hash]billy.File)
  213. }
  214. f, ok := d.files[hash]
  215. if ok {
  216. return f, nil
  217. }
  218. }
  219. err := d.hasPack(hash)
  220. if err != nil {
  221. return nil, err
  222. }
  223. path := d.objectPackPath(hash, extension)
  224. pack, err := d.fs.Open(path)
  225. if err != nil {
  226. if os.IsNotExist(err) {
  227. return nil, ErrPackfileNotFound
  228. }
  229. return nil, err
  230. }
  231. if d.options.KeepDescriptors && extension == "pack" {
  232. d.files[hash] = pack
  233. }
  234. return pack, nil
  235. }
  236. // ObjectPack returns a fs.File of the given packfile
  237. func (d *DotGit) ObjectPack(hash plumbing.Hash) (billy.File, error) {
  238. err := d.hasPack(hash)
  239. if err != nil {
  240. return nil, err
  241. }
  242. return d.objectPackOpen(hash, `pack`)
  243. }
  244. // ObjectPackIdx returns a fs.File of the index file for a given packfile
  245. func (d *DotGit) ObjectPackIdx(hash plumbing.Hash) (billy.File, error) {
  246. err := d.hasPack(hash)
  247. if err != nil {
  248. return nil, err
  249. }
  250. return d.objectPackOpen(hash, `idx`)
  251. }
  252. func (d *DotGit) DeleteOldObjectPackAndIndex(hash plumbing.Hash, t time.Time) error {
  253. d.cleanPackList()
  254. path := d.objectPackPath(hash, `pack`)
  255. if !t.IsZero() {
  256. fi, err := d.fs.Stat(path)
  257. if err != nil {
  258. return err
  259. }
  260. // too new, skip deletion.
  261. if !fi.ModTime().Before(t) {
  262. return nil
  263. }
  264. }
  265. err := d.fs.Remove(path)
  266. if err != nil {
  267. return err
  268. }
  269. return d.fs.Remove(d.objectPackPath(hash, `idx`))
  270. }
  271. // NewObject return a writer for a new object file.
  272. func (d *DotGit) NewObject() (*ObjectWriter, error) {
  273. d.cleanObjectList()
  274. return newObjectWriter(d.fs)
  275. }
  276. // Objects returns a slice with the hashes of objects found under the
  277. // .git/objects/ directory.
  278. func (d *DotGit) Objects() ([]plumbing.Hash, error) {
  279. if d.options.ExclusiveAccess {
  280. err := d.genObjectList()
  281. if err != nil {
  282. return nil, err
  283. }
  284. return d.objectList, nil
  285. }
  286. var objects []plumbing.Hash
  287. err := d.ForEachObjectHash(func(hash plumbing.Hash) error {
  288. objects = append(objects, hash)
  289. return nil
  290. })
  291. if err != nil {
  292. return nil, err
  293. }
  294. return objects, nil
  295. }
  296. // ForEachObjectHash iterates over the hashes of objects found under the
  297. // .git/objects/ directory and executes the provided function.
  298. func (d *DotGit) ForEachObjectHash(fun func(plumbing.Hash) error) error {
  299. if !d.options.ExclusiveAccess {
  300. return d.forEachObjectHash(fun)
  301. }
  302. err := d.genObjectList()
  303. if err != nil {
  304. return err
  305. }
  306. for _, h := range d.objectList {
  307. err := fun(h)
  308. if err != nil {
  309. return err
  310. }
  311. }
  312. return nil
  313. }
  314. func (d *DotGit) forEachObjectHash(fun func(plumbing.Hash) error) error {
  315. files, err := d.fs.ReadDir(objectsPath)
  316. if err != nil {
  317. if os.IsNotExist(err) {
  318. return nil
  319. }
  320. return err
  321. }
  322. for _, f := range files {
  323. if f.IsDir() && len(f.Name()) == 2 && isHex(f.Name()) {
  324. base := f.Name()
  325. d, err := d.fs.ReadDir(d.fs.Join(objectsPath, base))
  326. if err != nil {
  327. return err
  328. }
  329. for _, o := range d {
  330. h := plumbing.NewHash(base + o.Name())
  331. if h.IsZero() {
  332. // Ignore files with badly-formatted names.
  333. continue
  334. }
  335. err = fun(h)
  336. if err != nil {
  337. return err
  338. }
  339. }
  340. }
  341. }
  342. return nil
  343. }
  344. func (d *DotGit) cleanObjectList() {
  345. d.objectMap = nil
  346. d.objectList = nil
  347. }
  348. func (d *DotGit) genObjectList() error {
  349. if d.objectMap != nil {
  350. return nil
  351. }
  352. d.objectMap = make(map[plumbing.Hash]struct{})
  353. return d.forEachObjectHash(func(h plumbing.Hash) error {
  354. d.objectList = append(d.objectList, h)
  355. d.objectMap[h] = struct{}{}
  356. return nil
  357. })
  358. }
  359. func (d *DotGit) hasObject(h plumbing.Hash) error {
  360. if !d.options.ExclusiveAccess {
  361. return nil
  362. }
  363. err := d.genObjectList()
  364. if err != nil {
  365. return err
  366. }
  367. _, ok := d.objectMap[h]
  368. if !ok {
  369. return plumbing.ErrObjectNotFound
  370. }
  371. return nil
  372. }
  373. func (d *DotGit) cleanPackList() {
  374. d.packMap = nil
  375. d.packList = nil
  376. }
  377. func (d *DotGit) genPackList() error {
  378. if d.packMap != nil {
  379. return nil
  380. }
  381. op, err := d.objectPacks()
  382. if err != nil {
  383. return err
  384. }
  385. d.packMap = make(map[plumbing.Hash]struct{})
  386. d.packList = nil
  387. for _, h := range op {
  388. d.packList = append(d.packList, h)
  389. d.packMap[h] = struct{}{}
  390. }
  391. return nil
  392. }
  393. func (d *DotGit) hasPack(h plumbing.Hash) error {
  394. if !d.options.ExclusiveAccess {
  395. return nil
  396. }
  397. err := d.genPackList()
  398. if err != nil {
  399. return err
  400. }
  401. _, ok := d.packMap[h]
  402. if !ok {
  403. return ErrPackfileNotFound
  404. }
  405. return nil
  406. }
  407. func (d *DotGit) objectPath(h plumbing.Hash) string {
  408. hash := h.String()
  409. return d.fs.Join(objectsPath, hash[0:2], hash[2:40])
  410. }
  411. // incomingObjectPath is intended to add support for a git pre-receive hook
  412. // to be written it adds support for go-git to find objects in an "incoming"
  413. // directory, so that the library can be used to write a pre-receive hook
  414. // that deals with the incoming objects.
  415. //
  416. // More on git hooks found here : https://git-scm.com/docs/githooks
  417. // More on 'quarantine'/incoming directory here:
  418. // https://git-scm.com/docs/git-receive-pack
  419. func (d *DotGit) incomingObjectPath(h plumbing.Hash) string {
  420. hString := h.String()
  421. if d.incomingDirName == "" {
  422. return d.fs.Join(objectsPath, hString[0:2], hString[2:40])
  423. }
  424. return d.fs.Join(objectsPath, d.incomingDirName, hString[0:2], hString[2:40])
  425. }
  426. // hasIncomingObjects searches for an incoming directory and keeps its name
  427. // so it doesn't have to be found each time an object is accessed.
  428. func (d *DotGit) hasIncomingObjects() bool {
  429. if !d.incomingChecked {
  430. directoryContents, err := d.fs.ReadDir(objectsPath)
  431. if err == nil {
  432. for _, file := range directoryContents {
  433. if strings.HasPrefix(file.Name(), "incoming-") && file.IsDir() {
  434. d.incomingDirName = file.Name()
  435. }
  436. }
  437. }
  438. d.incomingChecked = true
  439. }
  440. return d.incomingDirName != ""
  441. }
  442. // Object returns a fs.File pointing the object file, if exists
  443. func (d *DotGit) Object(h plumbing.Hash) (billy.File, error) {
  444. err := d.hasObject(h)
  445. if err != nil {
  446. return nil, err
  447. }
  448. obj1, err1 := d.fs.Open(d.objectPath(h))
  449. if os.IsNotExist(err1) && d.hasIncomingObjects() {
  450. obj2, err2 := d.fs.Open(d.incomingObjectPath(h))
  451. if err2 != nil {
  452. return obj1, err1
  453. }
  454. return obj2, err2
  455. }
  456. return obj1, err1
  457. }
  458. // ObjectStat returns a os.FileInfo pointing the object file, if exists
  459. func (d *DotGit) ObjectStat(h plumbing.Hash) (os.FileInfo, error) {
  460. err := d.hasObject(h)
  461. if err != nil {
  462. return nil, err
  463. }
  464. obj1, err1 := d.fs.Stat(d.objectPath(h))
  465. if os.IsNotExist(err1) && d.hasIncomingObjects() {
  466. obj2, err2 := d.fs.Stat(d.incomingObjectPath(h))
  467. if err2 != nil {
  468. return obj1, err1
  469. }
  470. return obj2, err2
  471. }
  472. return obj1, err1
  473. }
  474. // ObjectDelete removes the object file, if exists
  475. func (d *DotGit) ObjectDelete(h plumbing.Hash) error {
  476. d.cleanObjectList()
  477. err1 := d.fs.Remove(d.objectPath(h))
  478. if os.IsNotExist(err1) && d.hasIncomingObjects() {
  479. err2 := d.fs.Remove(d.incomingObjectPath(h))
  480. if err2 != nil {
  481. return err1
  482. }
  483. return err2
  484. }
  485. return err1
  486. }
  487. func (d *DotGit) readReferenceFrom(rd io.Reader, name string) (ref *plumbing.Reference, err error) {
  488. b, err := stdioutil.ReadAll(rd)
  489. if err != nil {
  490. return nil, err
  491. }
  492. line := strings.TrimSpace(string(b))
  493. return plumbing.NewReferenceFromStrings(name, line), nil
  494. }
  495. func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference) error {
  496. if old == nil {
  497. return nil
  498. }
  499. ref, err := d.readReferenceFrom(f, old.Name().String())
  500. if err != nil {
  501. return err
  502. }
  503. if ref.Hash() != old.Hash() {
  504. return storage.ErrReferenceHasChanged
  505. }
  506. _, err = f.Seek(0, io.SeekStart)
  507. if err != nil {
  508. return err
  509. }
  510. return f.Truncate(0)
  511. }
  512. func (d *DotGit) SetRef(r, old *plumbing.Reference) error {
  513. var content string
  514. switch r.Type() {
  515. case plumbing.SymbolicReference:
  516. content = fmt.Sprintf("ref: %s\n", r.Target())
  517. case plumbing.HashReference:
  518. content = fmt.Sprintln(r.Hash().String())
  519. }
  520. fileName := r.Name().String()
  521. return d.setRef(fileName, content, old)
  522. }
  523. // Refs scans the git directory collecting references, which it returns.
  524. // Symbolic references are resolved and included in the output.
  525. func (d *DotGit) Refs() ([]*plumbing.Reference, error) {
  526. var refs []*plumbing.Reference
  527. var seen = make(map[plumbing.ReferenceName]bool)
  528. if err := d.addRefsFromRefDir(&refs, seen); err != nil {
  529. return nil, err
  530. }
  531. if err := d.addRefsFromPackedRefs(&refs, seen); err != nil {
  532. return nil, err
  533. }
  534. if err := d.addRefFromHEAD(&refs); err != nil {
  535. return nil, err
  536. }
  537. return refs, nil
  538. }
  539. // Ref returns the reference for a given reference name.
  540. func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) {
  541. ref, err := d.readReferenceFile(".", name.String())
  542. if err == nil {
  543. return ref, nil
  544. }
  545. return d.packedRef(name)
  546. }
  547. func (d *DotGit) findPackedRefsInFile(f billy.File) ([]*plumbing.Reference, error) {
  548. s := bufio.NewScanner(f)
  549. var refs []*plumbing.Reference
  550. for s.Scan() {
  551. ref, err := d.processLine(s.Text())
  552. if err != nil {
  553. return nil, err
  554. }
  555. if ref != nil {
  556. refs = append(refs, ref)
  557. }
  558. }
  559. return refs, s.Err()
  560. }
  561. func (d *DotGit) findPackedRefs() (r []*plumbing.Reference, err error) {
  562. f, err := d.fs.Open(packedRefsPath)
  563. if err != nil {
  564. if os.IsNotExist(err) {
  565. return nil, nil
  566. }
  567. return nil, err
  568. }
  569. defer ioutil.CheckClose(f, &err)
  570. return d.findPackedRefsInFile(f)
  571. }
  572. func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, error) {
  573. refs, err := d.findPackedRefs()
  574. if err != nil {
  575. return nil, err
  576. }
  577. for _, ref := range refs {
  578. if ref.Name() == name {
  579. return ref, nil
  580. }
  581. }
  582. return nil, plumbing.ErrReferenceNotFound
  583. }
  584. // RemoveRef removes a reference by name.
  585. func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error {
  586. path := d.fs.Join(".", name.String())
  587. _, err := d.fs.Stat(path)
  588. if err == nil {
  589. err = d.fs.Remove(path)
  590. // Drop down to remove it from the packed refs file, too.
  591. }
  592. if err != nil && !os.IsNotExist(err) {
  593. return err
  594. }
  595. return d.rewritePackedRefsWithoutRef(name)
  596. }
  597. func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference, seen map[plumbing.ReferenceName]bool) (err error) {
  598. packedRefs, err := d.findPackedRefs()
  599. if err != nil {
  600. return err
  601. }
  602. for _, ref := range packedRefs {
  603. if !seen[ref.Name()] {
  604. *refs = append(*refs, ref)
  605. seen[ref.Name()] = true
  606. }
  607. }
  608. return nil
  609. }
  610. func (d *DotGit) addRefsFromPackedRefsFile(refs *[]*plumbing.Reference, f billy.File, seen map[plumbing.ReferenceName]bool) (err error) {
  611. packedRefs, err := d.findPackedRefsInFile(f)
  612. if err != nil {
  613. return err
  614. }
  615. for _, ref := range packedRefs {
  616. if !seen[ref.Name()] {
  617. *refs = append(*refs, ref)
  618. seen[ref.Name()] = true
  619. }
  620. }
  621. return nil
  622. }
  623. func (d *DotGit) openAndLockPackedRefs(doCreate bool) (
  624. pr billy.File, err error) {
  625. var f billy.File
  626. defer func() {
  627. if err != nil && f != nil {
  628. ioutil.CheckClose(f, &err)
  629. }
  630. }()
  631. // File mode is retrieved from a constant defined in the target specific
  632. // files (dotgit_rewrite_packed_refs_*). Some modes are not available
  633. // in all filesystems.
  634. openFlags := d.openAndLockPackedRefsMode()
  635. if doCreate {
  636. openFlags |= os.O_CREATE
  637. }
  638. // Keep trying to open and lock the file until we're sure the file
  639. // didn't change between the open and the lock.
  640. for {
  641. f, err = d.fs.OpenFile(packedRefsPath, openFlags, 0600)
  642. if err != nil {
  643. if os.IsNotExist(err) && !doCreate {
  644. return nil, nil
  645. }
  646. return nil, err
  647. }
  648. fi, err := d.fs.Stat(packedRefsPath)
  649. if err != nil {
  650. return nil, err
  651. }
  652. mtime := fi.ModTime()
  653. err = f.Lock()
  654. if err != nil {
  655. return nil, err
  656. }
  657. fi, err = d.fs.Stat(packedRefsPath)
  658. if err != nil {
  659. return nil, err
  660. }
  661. if mtime.Equal(fi.ModTime()) {
  662. break
  663. }
  664. // The file has changed since we opened it. Close and retry.
  665. err = f.Close()
  666. if err != nil {
  667. return nil, err
  668. }
  669. }
  670. return f, nil
  671. }
  672. func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err error) {
  673. pr, err := d.openAndLockPackedRefs(false)
  674. if err != nil {
  675. return err
  676. }
  677. if pr == nil {
  678. return nil
  679. }
  680. defer ioutil.CheckClose(pr, &err)
  681. // Creating the temp file in the same directory as the target file
  682. // improves our chances for rename operation to be atomic.
  683. tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix)
  684. if err != nil {
  685. return err
  686. }
  687. tmpName := tmp.Name()
  688. defer func() {
  689. ioutil.CheckClose(tmp, &err)
  690. _ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
  691. }()
  692. s := bufio.NewScanner(pr)
  693. found := false
  694. for s.Scan() {
  695. line := s.Text()
  696. ref, err := d.processLine(line)
  697. if err != nil {
  698. return err
  699. }
  700. if ref != nil && ref.Name() == name {
  701. found = true
  702. continue
  703. }
  704. if _, err := fmt.Fprintln(tmp, line); err != nil {
  705. return err
  706. }
  707. }
  708. if err := s.Err(); err != nil {
  709. return err
  710. }
  711. if !found {
  712. return nil
  713. }
  714. return d.rewritePackedRefsWhileLocked(tmp, pr)
  715. }
  716. // process lines from a packed-refs file
  717. func (d *DotGit) processLine(line string) (*plumbing.Reference, error) {
  718. if len(line) == 0 {
  719. return nil, nil
  720. }
  721. switch line[0] {
  722. case '#': // comment - ignore
  723. return nil, nil
  724. case '^': // annotated tag commit of the previous line - ignore
  725. return nil, nil
  726. default:
  727. ws := strings.Split(line, " ") // hash then ref
  728. if len(ws) != 2 {
  729. return nil, ErrPackedRefsBadFormat
  730. }
  731. return plumbing.NewReferenceFromStrings(ws[1], ws[0]), nil
  732. }
  733. }
  734. func (d *DotGit) addRefsFromRefDir(refs *[]*plumbing.Reference, seen map[plumbing.ReferenceName]bool) error {
  735. return d.walkReferencesTree(refs, []string{refsPath}, seen)
  736. }
  737. func (d *DotGit) walkReferencesTree(refs *[]*plumbing.Reference, relPath []string, seen map[plumbing.ReferenceName]bool) error {
  738. files, err := d.fs.ReadDir(d.fs.Join(relPath...))
  739. if err != nil {
  740. if os.IsNotExist(err) {
  741. return nil
  742. }
  743. return err
  744. }
  745. for _, f := range files {
  746. newRelPath := append(append([]string(nil), relPath...), f.Name())
  747. if f.IsDir() {
  748. if err = d.walkReferencesTree(refs, newRelPath, seen); err != nil {
  749. return err
  750. }
  751. continue
  752. }
  753. ref, err := d.readReferenceFile(".", strings.Join(newRelPath, "/"))
  754. if err != nil {
  755. return err
  756. }
  757. if ref != nil && !seen[ref.Name()] {
  758. *refs = append(*refs, ref)
  759. seen[ref.Name()] = true
  760. }
  761. }
  762. return nil
  763. }
  764. func (d *DotGit) addRefFromHEAD(refs *[]*plumbing.Reference) error {
  765. ref, err := d.readReferenceFile(".", "HEAD")
  766. if err != nil {
  767. if os.IsNotExist(err) {
  768. return nil
  769. }
  770. return err
  771. }
  772. *refs = append(*refs, ref)
  773. return nil
  774. }
  775. func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference, err error) {
  776. path = d.fs.Join(path, d.fs.Join(strings.Split(name, "/")...))
  777. st, err := d.fs.Stat(path)
  778. if err != nil {
  779. return nil, err
  780. }
  781. if st.IsDir() {
  782. return nil, ErrIsDir
  783. }
  784. f, err := d.fs.Open(path)
  785. if err != nil {
  786. return nil, err
  787. }
  788. defer ioutil.CheckClose(f, &err)
  789. return d.readReferenceFrom(f, name)
  790. }
  791. func (d *DotGit) CountLooseRefs() (int, error) {
  792. var refs []*plumbing.Reference
  793. var seen = make(map[plumbing.ReferenceName]bool)
  794. if err := d.addRefsFromRefDir(&refs, seen); err != nil {
  795. return 0, err
  796. }
  797. return len(refs), nil
  798. }
  799. // PackRefs packs all loose refs into the packed-refs file.
  800. //
  801. // This implementation only works under the assumption that the view
  802. // of the file system won't be updated during this operation. This
  803. // strategy would not work on a general file system though, without
  804. // locking each loose reference and checking it again before deleting
  805. // the file, because otherwise an updated reference could sneak in and
  806. // then be deleted by the packed-refs process. Alternatively, every
  807. // ref update could also lock packed-refs, so only one lock is
  808. // required during ref-packing. But that would worsen performance in
  809. // the common case.
  810. //
  811. // TODO: add an "all" boolean like the `git pack-refs --all` flag.
  812. // When `all` is false, it would only pack refs that have already been
  813. // packed, plus all tags.
  814. func (d *DotGit) PackRefs() (err error) {
  815. // Lock packed-refs, and create it if it doesn't exist yet.
  816. f, err := d.openAndLockPackedRefs(true)
  817. if err != nil {
  818. return err
  819. }
  820. defer ioutil.CheckClose(f, &err)
  821. // Gather all refs using addRefsFromRefDir and addRefsFromPackedRefs.
  822. var refs []*plumbing.Reference
  823. seen := make(map[plumbing.ReferenceName]bool)
  824. if err = d.addRefsFromRefDir(&refs, seen); err != nil {
  825. return err
  826. }
  827. if len(refs) == 0 {
  828. // Nothing to do!
  829. return nil
  830. }
  831. numLooseRefs := len(refs)
  832. if err = d.addRefsFromPackedRefsFile(&refs, f, seen); err != nil {
  833. return err
  834. }
  835. // Write them all to a new temp packed-refs file.
  836. tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix)
  837. if err != nil {
  838. return err
  839. }
  840. tmpName := tmp.Name()
  841. defer func() {
  842. ioutil.CheckClose(tmp, &err)
  843. _ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
  844. }()
  845. w := bufio.NewWriter(tmp)
  846. for _, ref := range refs {
  847. _, err = w.WriteString(ref.String() + "\n")
  848. if err != nil {
  849. return err
  850. }
  851. }
  852. err = w.Flush()
  853. if err != nil {
  854. return err
  855. }
  856. // Rename the temp packed-refs file.
  857. err = d.rewritePackedRefsWhileLocked(tmp, f)
  858. if err != nil {
  859. return err
  860. }
  861. // Delete all the loose refs, while still holding the packed-refs
  862. // lock.
  863. for _, ref := range refs[:numLooseRefs] {
  864. path := d.fs.Join(".", ref.Name().String())
  865. err = d.fs.Remove(path)
  866. if err != nil && !os.IsNotExist(err) {
  867. return err
  868. }
  869. }
  870. return nil
  871. }
  872. // Module return a billy.Filesystem pointing to the module folder
  873. func (d *DotGit) Module(name string) (billy.Filesystem, error) {
  874. return d.fs.Chroot(d.fs.Join(modulePath, name))
  875. }
  876. // Alternates returns DotGit(s) based off paths in objects/info/alternates if
  877. // available. This can be used to checks if it's a shared repository.
  878. func (d *DotGit) Alternates() ([]*DotGit, error) {
  879. altpath := d.fs.Join("objects", "info", "alternates")
  880. f, err := d.fs.Open(altpath)
  881. if err != nil {
  882. return nil, err
  883. }
  884. defer f.Close()
  885. var alternates []*DotGit
  886. // Read alternate paths line-by-line and create DotGit objects.
  887. scanner := bufio.NewScanner(f)
  888. for scanner.Scan() {
  889. path := scanner.Text()
  890. if !filepath.IsAbs(path) {
  891. // For relative paths, we can perform an internal conversion to
  892. // slash so that they work cross-platform.
  893. slashPath := filepath.ToSlash(path)
  894. // If the path is not absolute, it must be relative to object
  895. // database (.git/objects/info).
  896. // https://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html
  897. // Hence, derive a path relative to DotGit's root.
  898. // "../../../reponame/.git/" -> "../../reponame/.git"
  899. // Remove the first ../
  900. relpath := filepath.Join(strings.Split(slashPath, "/")[1:]...)
  901. normalPath := filepath.FromSlash(relpath)
  902. path = filepath.Join(d.fs.Root(), normalPath)
  903. }
  904. fs := osfs.New(filepath.Dir(path))
  905. alternates = append(alternates, New(fs))
  906. }
  907. if err = scanner.Err(); err != nil {
  908. return nil, err
  909. }
  910. return alternates, nil
  911. }
  912. // Fs returns the underlying filesystem of the DotGit folder.
  913. func (d *DotGit) Fs() billy.Filesystem {
  914. return d.fs
  915. }
  916. func isHex(s string) bool {
  917. for _, b := range []byte(s) {
  918. if isNum(b) {
  919. continue
  920. }
  921. if isHexAlpha(b) {
  922. continue
  923. }
  924. return false
  925. }
  926. return true
  927. }
  928. func isNum(b byte) bool {
  929. return b >= '0' && b <= '9'
  930. }
  931. func isHexAlpha(b byte) bool {
  932. return b >= 'a' && b <= 'f' || b >= 'A' && b <= 'F'
  933. }