dotgit.go 25 KB

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