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 --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 }