repo.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. package gig
  2. import (
  3. "bufio"
  4. "bytes"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "os"
  9. "os/exec"
  10. "path"
  11. "path/filepath"
  12. "strings"
  13. )
  14. //Repository represents an on disk git repository.
  15. type Repository struct {
  16. Path string
  17. }
  18. //InitBareRepository creates a bare git repository at path.
  19. func InitBareRepository(path string) (*Repository, error) {
  20. path, err := filepath.Abs(path)
  21. if err != nil {
  22. return nil, fmt.Errorf("Could not determine absolute path: %v", err)
  23. }
  24. cmd := exec.Command("git", "init", "--bare", path)
  25. err = cmd.Run()
  26. if err != nil {
  27. return nil, err
  28. }
  29. return &Repository{Path: path}, nil
  30. }
  31. //IsBareRepository checks if path is a bare git repository.
  32. func IsBareRepository(path string) bool {
  33. cmd := exec.Command("git", fmt.Sprintf("--git-dir=%s", path), "rev-parse", "--is-bare-repository")
  34. body, err := cmd.Output()
  35. if err != nil {
  36. return false
  37. }
  38. status := strings.Trim(string(body), "\n ")
  39. return status == "true"
  40. }
  41. //OpenRepository opens the repository at path. Currently
  42. //verifies that it is a (bare) repository and returns an
  43. //error if the check fails.
  44. func OpenRepository(path string) (*Repository, error) {
  45. path, err := filepath.Abs(path)
  46. if err != nil {
  47. return nil, fmt.Errorf("git: could not determine absolute path")
  48. }
  49. if !IsBareRepository(path) {
  50. return nil, fmt.Errorf("git: not a bare repository")
  51. }
  52. return &Repository{Path: path}, nil
  53. }
  54. //DiscoverRepository returns the git repository that contains the
  55. //current working directory, or and error if the current working
  56. //dir does not lie inside one.
  57. func DiscoverRepository() (*Repository, error) {
  58. cmd := exec.Command("git", "rev-parse", "--git-dir")
  59. data, err := cmd.Output()
  60. if err != nil {
  61. return nil, err
  62. }
  63. path := strings.Trim(string(data), "\n ")
  64. return &Repository{Path: path}, nil
  65. }
  66. //ReadDescription returns the contents of the description file.
  67. func (repo *Repository) ReadDescription() string {
  68. path := filepath.Join(repo.Path, "description")
  69. dat, err := ioutil.ReadFile(path)
  70. if err != nil {
  71. return ""
  72. }
  73. return string(dat)
  74. }
  75. //WriteDescription writes the contents of the description file.
  76. func (repo *Repository) WriteDescription(description string) error {
  77. path := filepath.Join(repo.Path, "description")
  78. // not atomic, fine for now
  79. return ioutil.WriteFile(path, []byte(description), 0666)
  80. }
  81. // DeleteCollaborator removes a collaborator file from the repositories sharing folder.
  82. func (repo *Repository) DeleteCollaborator(username string) error {
  83. filePath := filepath.Join(repo.Path, "gin", "sharing", username)
  84. return os.Remove(filePath)
  85. }
  86. //OpenObject returns the git object for a give id (SHA1).
  87. func (repo *Repository) OpenObject(id SHA1) (Object, error) {
  88. obj, err := repo.openRawObject(id)
  89. if err != nil {
  90. return nil, err
  91. }
  92. if IsStandardObject(obj.otype) {
  93. return parseObject(obj)
  94. }
  95. //not a standard object, *must* be a delta object,
  96. // we know of no other types
  97. if !IsDeltaObject(obj.otype) {
  98. return nil, fmt.Errorf("git: unsupported object")
  99. }
  100. delta, err := parseDelta(obj)
  101. if err != nil {
  102. return nil, err
  103. }
  104. chain, err := buildDeltaChain(delta, repo)
  105. if err != nil {
  106. return nil, err
  107. }
  108. //TODO: check depth, and especially expected memory usage
  109. // beofre actually patching it
  110. return chain.resolve()
  111. }
  112. func (repo *Repository) openRawObject(id SHA1) (gitObject, error) {
  113. idstr := id.String()
  114. opath := filepath.Join(repo.Path, "objects", idstr[:2], idstr[2:])
  115. obj, err := openRawObject(opath)
  116. if err == nil {
  117. return obj, nil
  118. } else if err != nil && !os.IsNotExist(err) {
  119. return obj, err
  120. }
  121. indicies := repo.loadPackIndices()
  122. for _, f := range indicies {
  123. idx, err := PackIndexOpen(f)
  124. if err != nil {
  125. continue
  126. }
  127. //TODO: we should leave index files open,
  128. defer idx.Close()
  129. off, err := idx.FindOffset(id)
  130. if err != nil {
  131. continue
  132. }
  133. pf, err := idx.OpenPackFile()
  134. if err != nil {
  135. return gitObject{}, err
  136. }
  137. obj, err := pf.readRawObject(off)
  138. if err != nil {
  139. return gitObject{}, err
  140. }
  141. return obj, nil
  142. }
  143. // from inspecting the os.isNotExist source it
  144. // seems that if we have "not found" in the message
  145. // os.IsNotExist() report true, which is what we want
  146. return gitObject{}, fmt.Errorf("git: object not found")
  147. }
  148. func (repo *Repository) loadPackIndices() []string {
  149. target := filepath.Join(repo.Path, "objects", "pack", "*.idx")
  150. files, err := filepath.Glob(target)
  151. if err != nil {
  152. panic(err)
  153. }
  154. return files
  155. }
  156. //OpenRef returns the Ref with the given name or an error
  157. //if either no maching could be found or in case the match
  158. //was not unique.
  159. func (repo *Repository) OpenRef(name string) (Ref, error) {
  160. if name == "HEAD" {
  161. return repo.parseRef("HEAD")
  162. }
  163. matches := repo.listRefWithName(name)
  164. //first search in local heads
  165. var locals []Ref
  166. for _, v := range matches {
  167. if IsBranchRef(v) {
  168. if name == v.Fullname() {
  169. return v, nil
  170. }
  171. locals = append(locals, v)
  172. }
  173. }
  174. // if we find a single local match
  175. // we return it directly
  176. if len(locals) == 1 {
  177. return locals[0], nil
  178. }
  179. switch len(matches) {
  180. case 0:
  181. return nil, fmt.Errorf("git: ref matching %q not found", name)
  182. case 1:
  183. return matches[0], nil
  184. }
  185. return nil, fmt.Errorf("git: ambiguous ref name, multiple matches")
  186. }
  187. //Readlink returns the destination of a symbilc link blob object
  188. func (repo *Repository) Readlink(id SHA1) (string, error) {
  189. b, err := repo.OpenObject(id)
  190. if err != nil {
  191. return "", err
  192. }
  193. if b.Type() != ObjBlob {
  194. return "", fmt.Errorf("id must point to a blob")
  195. }
  196. blob := b.(*Blob)
  197. //TODO: check size and don't read unreasonable large blobs
  198. data, err := ioutil.ReadAll(blob)
  199. if err != nil {
  200. return "", err
  201. }
  202. return string(data), nil
  203. }
  204. //ObjectForPath will resolve the path to an object
  205. //for the file tree starting in the node root.
  206. //The root object can be either a Commit, Tree or Tag.
  207. func (repo *Repository) ObjectForPath(root Object, pathstr string) (Object, error) {
  208. var node Object
  209. var err error
  210. switch o := root.(type) {
  211. case *Tree:
  212. node = root
  213. case *Commit:
  214. node, err = repo.OpenObject(o.Tree)
  215. case *Tag:
  216. node, err = repo.OpenObject(o.Object)
  217. default:
  218. return nil, fmt.Errorf("unsupported root object type")
  219. }
  220. if err != nil {
  221. return nil, fmt.Errorf("could not root tree object: %v", err)
  222. }
  223. cleaned := path.Clean(strings.Trim(pathstr, " /"))
  224. comps := strings.Split(cleaned, "/")
  225. var i int
  226. for i = 0; i < len(comps); i++ {
  227. tree, ok := node.(*Tree)
  228. if !ok {
  229. cwd := strings.Join(comps[:i+1], "/")
  230. err := &os.PathError{
  231. Op: "convert git.Object to git.Tree",
  232. Path: cwd,
  233. Err: fmt.Errorf("expected tree object, got %s", node.Type()),
  234. }
  235. return nil, err
  236. }
  237. //Since we call path.Clean(), this should really
  238. //only happen at the root, but it is safe to
  239. //have here anyway
  240. if comps[i] == "." || comps[i] == "/" {
  241. continue
  242. }
  243. var id *SHA1
  244. for tree.Next() {
  245. entry := tree.Entry()
  246. if entry.Name == comps[i] {
  247. id = &entry.ID
  248. break
  249. }
  250. }
  251. if err = tree.Err(); err != nil {
  252. cwd := strings.Join(comps[:i+1], "/")
  253. return nil, &os.PathError{
  254. Op: "find object",
  255. Path: cwd,
  256. Err: err}
  257. } else if id == nil {
  258. cwd := strings.Join(comps[:i+1], "/")
  259. return nil, &os.PathError{
  260. Op: "find object",
  261. Path: cwd,
  262. Err: os.ErrNotExist}
  263. }
  264. node, err = repo.OpenObject(*id)
  265. if err != nil {
  266. cwd := strings.Join(comps[:i+1], "/")
  267. return nil, &os.PathError{
  268. Op: "open object",
  269. Path: cwd,
  270. Err: err,
  271. }
  272. }
  273. }
  274. return node, nil
  275. }
  276. // usefmt is the option string used by CommitsForRef to return a formatted git commit log.
  277. const usefmt = `--pretty=format:
  278. Commit:=%H%n
  279. Committer:=%cn%n
  280. Author:=%an%n
  281. Date-iso:=%ai%n
  282. Date-rel:=%ar%n
  283. Subject:=%s%n
  284. Changes:=`
  285. // CommitSummary represents a subset of information from a git commit.
  286. type CommitSummary struct {
  287. Commit string
  288. Committer string
  289. Author string
  290. DateIso string
  291. DateRelative string
  292. Subject string
  293. Changes []string
  294. }
  295. // CommitsForRef executes a custom git log command for the specified ref of the
  296. // associated git repository and returns the resulting byte array.
  297. func (repo *Repository) CommitsForRef(ref string) ([]CommitSummary, error) {
  298. raw, err := commitsForRef(repo.Path, ref, usefmt)
  299. if err != nil {
  300. return nil, err
  301. }
  302. sep := ":="
  303. var comList []CommitSummary
  304. r := bytes.NewReader(raw)
  305. br := bufio.NewReader(r)
  306. var changesFlag bool
  307. for {
  308. // Consume line until newline character
  309. l, err := br.ReadString('\n')
  310. if strings.Contains(l, sep) {
  311. splitList := strings.SplitN(l, sep, 2)
  312. key := splitList[0]
  313. val := splitList[1]
  314. switch key {
  315. case "Commit":
  316. // reset non key line flags
  317. changesFlag = false
  318. newCommit := CommitSummary{Commit: val}
  319. comList = append(comList, newCommit)
  320. case "Committer":
  321. comList[len(comList)-1].Committer = val
  322. case "Author":
  323. comList[len(comList)-1].Author = val
  324. case "Date-iso":
  325. comList[len(comList)-1].DateIso = val
  326. case "Date-rel":
  327. comList[len(comList)-1].DateRelative = val
  328. case "Subject":
  329. comList[len(comList)-1].Subject = val
  330. case "Changes":
  331. // Setting changes flag so we know, that the next lines are probably file change notification lines.
  332. changesFlag = true
  333. default:
  334. fmt.Printf("[W] commits: unexpected key %q, value %q\n", key, strings.Trim(val, "\n"))
  335. }
  336. } else if changesFlag && strings.Contains(l, "\t") {
  337. comList[len(comList)-1].Changes = append(comList[len(comList)-1].Changes, l)
  338. }
  339. // Breaks at the latest when EOF err is raised
  340. if err != nil {
  341. break
  342. }
  343. }
  344. if err != io.EOF && err != nil {
  345. return nil, err
  346. }
  347. return comList, nil
  348. }
  349. // commitsForRef executes a custom git log command for the specified ref of the
  350. // given git repository with the specified log format string and returns the resulting byte array.
  351. // Function is kept private to force handling of the []byte inside the package.
  352. func commitsForRef(repoPath, ref, usefmt string) ([]byte, error) {
  353. gdir := fmt.Sprintf("--git-dir=%s", repoPath)
  354. cmd := exec.Command("git", gdir, "log", ref, usefmt, "--name-status")
  355. body, err := cmd.Output()
  356. if err != nil {
  357. return nil, fmt.Errorf("failed running git log: %s\n", err.Error())
  358. }
  359. return body, nil
  360. }
  361. // BranchExists runs the "git branch <branchname> --list" command.
  362. // It will return an error, if the command fails, true, if the result is not empty and false otherwise.
  363. func (repo *Repository) BranchExists(branch string) (bool, error) {
  364. gdir := fmt.Sprintf("--git-dir=%s", repo.Path)
  365. cmd := exec.Command("git", gdir, "branch", branch, "--list")
  366. body, err := cmd.Output()
  367. if err != nil {
  368. return false, err
  369. } else if len(body) == 0 {
  370. return false, nil
  371. }
  372. return true, nil
  373. }