|
@@ -0,0 +1,463 @@
|
|
|
+package gig
|
|
|
+
|
|
|
+import (
|
|
|
+ "bufio"
|
|
|
+ "bytes"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "io/ioutil"
|
|
|
+ "os"
|
|
|
+ "os/exec"
|
|
|
+ "path"
|
|
|
+ "path/filepath"
|
|
|
+ "strings"
|
|
|
+)
|
|
|
+
|
|
|
+//Repository represents an on disk git repository.
|
|
|
+type Repository struct {
|
|
|
+ Path string
|
|
|
+}
|
|
|
+
|
|
|
+//InitBareRepository creates a bare git repository at path.
|
|
|
+func InitBareRepository(path string) (*Repository, error) {
|
|
|
+
|
|
|
+ path, err := filepath.Abs(path)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("Could not determine absolute path: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ cmd := exec.Command("git", "init", "--bare", path)
|
|
|
+ err = cmd.Run()
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Repository{Path: path}, nil
|
|
|
+}
|
|
|
+
|
|
|
+//IsBareRepository checks if path is a bare git repository.
|
|
|
+func IsBareRepository(path string) bool {
|
|
|
+
|
|
|
+ cmd := exec.Command("git", fmt.Sprintf("--git-dir=%s", path), "rev-parse", "--is-bare-repository")
|
|
|
+ body, err := cmd.Output()
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ status := strings.Trim(string(body), "\n ")
|
|
|
+ return status == "true"
|
|
|
+}
|
|
|
+
|
|
|
+//OpenRepository opens the repository at path. Currently
|
|
|
+//verifies that it is a (bare) repository and returns an
|
|
|
+//error if the check fails.
|
|
|
+func OpenRepository(path string) (*Repository, error) {
|
|
|
+
|
|
|
+ path, err := filepath.Abs(path)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("git: could not determine absolute path")
|
|
|
+ }
|
|
|
+
|
|
|
+ if !IsBareRepository(path) {
|
|
|
+ return nil, fmt.Errorf("git: not a bare repository")
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Repository{Path: path}, nil
|
|
|
+}
|
|
|
+
|
|
|
+//DiscoverRepository returns the git repository that contains the
|
|
|
+//current working directory, or and error if the current working
|
|
|
+//dir does not lie inside one.
|
|
|
+func DiscoverRepository() (*Repository, error) {
|
|
|
+ cmd := exec.Command("git", "rev-parse", "--git-dir")
|
|
|
+ data, err := cmd.Output()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ path := strings.Trim(string(data), "\n ")
|
|
|
+ return &Repository{Path: path}, nil
|
|
|
+}
|
|
|
+
|
|
|
+//ReadDescription returns the contents of the description file.
|
|
|
+func (repo *Repository) ReadDescription() string {
|
|
|
+ path := filepath.Join(repo.Path, "description")
|
|
|
+
|
|
|
+ dat, err := ioutil.ReadFile(path)
|
|
|
+ if err != nil {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ return string(dat)
|
|
|
+}
|
|
|
+
|
|
|
+//WriteDescription writes the contents of the description file.
|
|
|
+func (repo *Repository) WriteDescription(description string) error {
|
|
|
+ path := filepath.Join(repo.Path, "description")
|
|
|
+
|
|
|
+ // not atomic, fine for now
|
|
|
+ return ioutil.WriteFile(path, []byte(description), 0666)
|
|
|
+}
|
|
|
+
|
|
|
+// DeleteCollaborator removes a collaborator file from the repositories sharing folder.
|
|
|
+func (repo *Repository) DeleteCollaborator(username string) error {
|
|
|
+ filePath := filepath.Join(repo.Path, "gin", "sharing", username)
|
|
|
+
|
|
|
+ return os.Remove(filePath)
|
|
|
+}
|
|
|
+
|
|
|
+//OpenObject returns the git object for a give id (SHA1).
|
|
|
+func (repo *Repository) OpenObject(id SHA1) (Object, error) {
|
|
|
+ obj, err := repo.openRawObject(id)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if IsStandardObject(obj.otype) {
|
|
|
+ return parseObject(obj)
|
|
|
+ }
|
|
|
+
|
|
|
+ //not a standard object, *must* be a delta object,
|
|
|
+ // we know of no other types
|
|
|
+ if !IsDeltaObject(obj.otype) {
|
|
|
+ return nil, fmt.Errorf("git: unsupported object")
|
|
|
+ }
|
|
|
+
|
|
|
+ delta, err := parseDelta(obj)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ chain, err := buildDeltaChain(delta, repo)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ //TODO: check depth, and especially expected memory usage
|
|
|
+ // beofre actually patching it
|
|
|
+
|
|
|
+ return chain.resolve()
|
|
|
+}
|
|
|
+
|
|
|
+func (repo *Repository) openRawObject(id SHA1) (gitObject, error) {
|
|
|
+ idstr := id.String()
|
|
|
+ opath := filepath.Join(repo.Path, "objects", idstr[:2], idstr[2:])
|
|
|
+
|
|
|
+ obj, err := openRawObject(opath)
|
|
|
+
|
|
|
+ if err == nil {
|
|
|
+ return obj, nil
|
|
|
+ } else if err != nil && !os.IsNotExist(err) {
|
|
|
+ return obj, err
|
|
|
+ }
|
|
|
+
|
|
|
+ indicies := repo.loadPackIndices()
|
|
|
+
|
|
|
+ for _, f := range indicies {
|
|
|
+
|
|
|
+ idx, err := PackIndexOpen(f)
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ //TODO: we should leave index files open,
|
|
|
+ defer idx.Close()
|
|
|
+
|
|
|
+ off, err := idx.FindOffset(id)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ pf, err := idx.OpenPackFile()
|
|
|
+ if err != nil {
|
|
|
+ return gitObject{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ obj, err := pf.readRawObject(off)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return gitObject{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return obj, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // from inspecting the os.isNotExist source it
|
|
|
+ // seems that if we have "not found" in the message
|
|
|
+ // os.IsNotExist() report true, which is what we want
|
|
|
+ return gitObject{}, fmt.Errorf("git: object not found")
|
|
|
+}
|
|
|
+
|
|
|
+func (repo *Repository) loadPackIndices() []string {
|
|
|
+ target := filepath.Join(repo.Path, "objects", "pack", "*.idx")
|
|
|
+ files, err := filepath.Glob(target)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return files
|
|
|
+}
|
|
|
+
|
|
|
+//OpenRef returns the Ref with the given name or an error
|
|
|
+//if either no maching could be found or in case the match
|
|
|
+//was not unique.
|
|
|
+func (repo *Repository) OpenRef(name string) (Ref, error) {
|
|
|
+
|
|
|
+ if name == "HEAD" {
|
|
|
+ return repo.parseRef("HEAD")
|
|
|
+ }
|
|
|
+
|
|
|
+ matches := repo.listRefWithName(name)
|
|
|
+
|
|
|
+ //first search in local heads
|
|
|
+ var locals []Ref
|
|
|
+ for _, v := range matches {
|
|
|
+ if IsBranchRef(v) {
|
|
|
+ if name == v.Fullname() {
|
|
|
+ return v, nil
|
|
|
+ }
|
|
|
+ locals = append(locals, v)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // if we find a single local match
|
|
|
+ // we return it directly
|
|
|
+ if len(locals) == 1 {
|
|
|
+ return locals[0], nil
|
|
|
+ }
|
|
|
+
|
|
|
+ switch len(matches) {
|
|
|
+ case 0:
|
|
|
+ return nil, fmt.Errorf("git: ref matching %q not found", name)
|
|
|
+ case 1:
|
|
|
+ return matches[0], nil
|
|
|
+ }
|
|
|
+ return nil, fmt.Errorf("git: ambiguous ref name, multiple matches")
|
|
|
+}
|
|
|
+
|
|
|
+//Readlink returns the destination of a symbilc link blob object
|
|
|
+func (repo *Repository) Readlink(id SHA1) (string, error) {
|
|
|
+
|
|
|
+ b, err := repo.OpenObject(id)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ if b.Type() != ObjBlob {
|
|
|
+ return "", fmt.Errorf("id must point to a blob")
|
|
|
+ }
|
|
|
+
|
|
|
+ blob := b.(*Blob)
|
|
|
+
|
|
|
+ //TODO: check size and don't read unreasonable large blobs
|
|
|
+ data, err := ioutil.ReadAll(blob)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ return string(data), nil
|
|
|
+}
|
|
|
+
|
|
|
+//ObjectForPath will resolve the path to an object
|
|
|
+//for the file tree starting in the node root.
|
|
|
+//The root object can be either a Commit, Tree or Tag.
|
|
|
+func (repo *Repository) ObjectForPath(root Object, pathstr string) (Object, error) {
|
|
|
+
|
|
|
+ var node Object
|
|
|
+ var err error
|
|
|
+
|
|
|
+ switch o := root.(type) {
|
|
|
+ case *Tree:
|
|
|
+ node = root
|
|
|
+ case *Commit:
|
|
|
+ node, err = repo.OpenObject(o.Tree)
|
|
|
+ case *Tag:
|
|
|
+ node, err = repo.OpenObject(o.Object)
|
|
|
+ default:
|
|
|
+ return nil, fmt.Errorf("unsupported root object type")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("could not root tree object: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ cleaned := path.Clean(strings.Trim(pathstr, " /"))
|
|
|
+ comps := strings.Split(cleaned, "/")
|
|
|
+
|
|
|
+ var i int
|
|
|
+ for i = 0; i < len(comps); i++ {
|
|
|
+
|
|
|
+ tree, ok := node.(*Tree)
|
|
|
+ if !ok {
|
|
|
+ cwd := strings.Join(comps[:i+1], "/")
|
|
|
+ err := &os.PathError{
|
|
|
+ Op: "convert git.Object to git.Tree",
|
|
|
+ Path: cwd,
|
|
|
+ Err: fmt.Errorf("expected tree object, got %s", node.Type()),
|
|
|
+ }
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ //Since we call path.Clean(), this should really
|
|
|
+ //only happen at the root, but it is safe to
|
|
|
+ //have here anyway
|
|
|
+ if comps[i] == "." || comps[i] == "/" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ var id *SHA1
|
|
|
+ for tree.Next() {
|
|
|
+ entry := tree.Entry()
|
|
|
+ if entry.Name == comps[i] {
|
|
|
+ id = &entry.ID
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if err = tree.Err(); err != nil {
|
|
|
+ cwd := strings.Join(comps[:i+1], "/")
|
|
|
+ return nil, &os.PathError{
|
|
|
+ Op: "find object",
|
|
|
+ Path: cwd,
|
|
|
+ Err: err}
|
|
|
+ } else if id == nil {
|
|
|
+ cwd := strings.Join(comps[:i+1], "/")
|
|
|
+ return nil, &os.PathError{
|
|
|
+ Op: "find object",
|
|
|
+ Path: cwd,
|
|
|
+ Err: os.ErrNotExist}
|
|
|
+ }
|
|
|
+
|
|
|
+ node, err = repo.OpenObject(*id)
|
|
|
+ if err != nil {
|
|
|
+ cwd := strings.Join(comps[:i+1], "/")
|
|
|
+ return nil, &os.PathError{
|
|
|
+ Op: "open object",
|
|
|
+ Path: cwd,
|
|
|
+ Err: err,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return node, nil
|
|
|
+}
|
|
|
+
|
|
|
+// usefmt is the option string used by CommitsForRef to return a formatted git commit log.
|
|
|
+const usefmt = `--pretty=format:
|
|
|
+Commit:=%H%n
|
|
|
+Committer:=%cn%n
|
|
|
+Author:=%an%n
|
|
|
+Date-iso:=%ai%n
|
|
|
+Date-rel:=%ar%n
|
|
|
+Subject:=%s%n
|
|
|
+Changes:=`
|
|
|
+
|
|
|
+// CommitSummary represents a subset of information from a git commit.
|
|
|
+type CommitSummary struct {
|
|
|
+ Commit string
|
|
|
+ Committer string
|
|
|
+ Author string
|
|
|
+ DateIso string
|
|
|
+ DateRelative string
|
|
|
+ Subject string
|
|
|
+ Changes []string
|
|
|
+}
|
|
|
+
|
|
|
+// CommitsForRef executes a custom git log command for the specified ref of the
|
|
|
+// associated git repository and returns the resulting byte array.
|
|
|
+func (repo *Repository) CommitsForRef(ref string) ([]CommitSummary, error) {
|
|
|
+
|
|
|
+ raw, err := commitsForRef(repo.Path, ref, usefmt)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ sep := ":="
|
|
|
+ var comList []CommitSummary
|
|
|
+ r := bytes.NewReader(raw)
|
|
|
+ br := bufio.NewReader(r)
|
|
|
+
|
|
|
+ var changesFlag bool
|
|
|
+ for {
|
|
|
+ // Consume line until newline character
|
|
|
+ l, err := br.ReadString('\n')
|
|
|
+
|
|
|
+ if strings.Contains(l, sep) {
|
|
|
+ splitList := strings.SplitN(l, sep, 2)
|
|
|
+
|
|
|
+ key := splitList[0]
|
|
|
+ val := splitList[1]
|
|
|
+ switch key {
|
|
|
+ case "Commit":
|
|
|
+ // reset non key line flags
|
|
|
+ changesFlag = false
|
|
|
+ newCommit := CommitSummary{Commit: val}
|
|
|
+ comList = append(comList, newCommit)
|
|
|
+ case "Committer":
|
|
|
+ comList[len(comList)-1].Committer = val
|
|
|
+ case "Author":
|
|
|
+ comList[len(comList)-1].Author = val
|
|
|
+ case "Date-iso":
|
|
|
+ comList[len(comList)-1].DateIso = val
|
|
|
+ case "Date-rel":
|
|
|
+ comList[len(comList)-1].DateRelative = val
|
|
|
+ case "Subject":
|
|
|
+ comList[len(comList)-1].Subject = val
|
|
|
+ case "Changes":
|
|
|
+ // Setting changes flag so we know, that the next lines are probably file change notification lines.
|
|
|
+ changesFlag = true
|
|
|
+ default:
|
|
|
+ fmt.Printf("[W] commits: unexpected key %q, value %q\n", key, strings.Trim(val, "\n"))
|
|
|
+ }
|
|
|
+ } else if changesFlag && strings.Contains(l, "\t") {
|
|
|
+ comList[len(comList)-1].Changes = append(comList[len(comList)-1].Changes, l)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Breaks at the latest when EOF err is raised
|
|
|
+ if err != nil {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if err != io.EOF && err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return comList, nil
|
|
|
+}
|
|
|
+
|
|
|
+// commitsForRef executes a custom git log command for the specified ref of the
|
|
|
+// given git repository with the specified log format string and returns the resulting byte array.
|
|
|
+// Function is kept private to force handling of the []byte inside the package.
|
|
|
+func commitsForRef(repoPath, ref, usefmt string) ([]byte, error) {
|
|
|
+ gdir := fmt.Sprintf("--git-dir=%s", repoPath)
|
|
|
+
|
|
|
+ cmd := exec.Command("git", gdir, "log", ref, usefmt, "--name-status")
|
|
|
+ body, err := cmd.Output()
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("failed running git log: %s\n", err.Error())
|
|
|
+ }
|
|
|
+ return body, nil
|
|
|
+}
|
|
|
+
|
|
|
+// BranchExists runs the "git branch <branchname> --list" command.
|
|
|
+// It will return an error, if the command fails, true, if the result is not empty and false otherwise.
|
|
|
+func (repo *Repository) BranchExists(branch string) (bool, error) {
|
|
|
+ gdir := fmt.Sprintf("--git-dir=%s", repo.Path)
|
|
|
+
|
|
|
+ cmd := exec.Command("git", gdir, "branch", branch, "--list")
|
|
|
+ body, err := cmd.Output()
|
|
|
+ if err != nil {
|
|
|
+ return false, err
|
|
|
+ } else if len(body) == 0 {
|
|
|
+ return false, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return true, nil
|
|
|
+}
|