short hash support

---
type: change
message: short hash support
change_hash: AAgqKEF108rXJcNRh8iYiusJfKurEPZaPABTuJyHXnY9
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6OhBMACgkQlcRvpqQRSKyShA/9HbpnpIjZdxe3igjMSqtvEghthIW0SOCQoMiMQSVQ1Kn/8AaZnRqW1bR+BL5Pug8EJX0iPFpQjzi/UifSCyJEV7ZvViBFeb8rjhvBdymnXGs+LVWT5czduRbWUOYlyyjUvdJdXg86mWik6vllLcezABVhq2TE2jOrgkrohpP5BFAh3oC9VVG8Ed/52b00imCBOWuOid9SBqcY7HorxGHxfjDVXxnpR3G7rQ3kBL4DZ1zxwvj+c8t/7rhu/U9D83KgxbGzsiAL0iE6+IY4AdUBDmNn5a0w2N9K4o9rxXgk6OERZAH+A0PxzVacW8eU9DU5Swq6BkpA90xuLeNfv48VIYSnUyErsr7BuIToL3lSIj1z7LGvGfVtkU05gRsf2j3E/d09CXOcFS5Dn6/gY2BdKb0tk277BaPtCKU+VFY3AOlSzcF26ioEZX49qw56C6Lg4ywQBLKcw7RQQAGRbnZyN6GKdT8LjHjhGtmsZYvUdyoXU/7tscZzZZXagknVzjVRMMcbjX6kUlT350EbgmJ8wqZgkzbWPFnbDAfflijmkjPlGx4NRVeiwZYZHecJvYlT6WK3sfrAw+PDHMUBf2QfpeUIiiLp9Mx+i0GN0/4tDT78EzATHe6GNewd+guZgnXDZ/+0TvZ9sOyRzAKx8q0KsKYho7PqV9B14Ni0Dp0Fe2w=
  account: mediocregopher
This commit is contained in:
mediocregopher 2020-04-08 20:10:33 -06:00
parent ca4887bf07
commit e78efcce74
3 changed files with 110 additions and 16 deletions

View File

@ -12,7 +12,6 @@ to accept help from people asking to help.
* Fast-forward perms on branches (so they can be deleted) * Fast-forward perms on branches (so they can be deleted)
* Ammending commits. * Ammending commits.
* Figure out commit range syntax, use that everywhere. * Figure out commit range syntax, use that everywhere.
* Support short hash names
* Ability to specify a pgp key manually, even if it's not in the project. * Ability to specify a pgp key manually, even if it's not in the project.
* Ability to require _any_ signature on a commit, even if it's not in the * Ability to require _any_ signature on a commit, even if it's not in the
config. config.

106
repo.go
View File

@ -3,11 +3,13 @@ package dehub
import ( import (
"bytes" "bytes"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"dehub.dev/src/dehub.git/fs" "dehub.dev/src/dehub.git/fs"
@ -354,21 +356,6 @@ func (r *Repo) GetGitCommit(h plumbing.Hash) (gc GitCommit, err error) {
return return
} }
// GetGitRevision resolves the revision and returns the GitCommit it references.
func (r *Repo) GetGitRevision(rev plumbing.Revision) (GitCommit, error) {
// This returns a pointer for some reason, not sure why.
h, err := r.GitRepo.ResolveRevision(rev)
if err != nil {
return GitCommit{}, fmt.Errorf("resolving revision: %w", err)
}
gc, err := r.GetGitCommit(*h)
if err != nil {
return GitCommit{}, fmt.Errorf("getting commit %q: %w", *h, err)
}
return gc, nil
}
// ErrHeadIsZero is used to indicate that HEAD resolves to the zero hash. An // ErrHeadIsZero is used to indicate that HEAD resolves to the zero hash. An
// example of when this can happen is if the repo was just initialized and has // example of when this can happen is if the repo was just initialized and has
// no commits, or if an orphan branch is checked out. // no commits, or if an orphan branch is checked out.
@ -436,10 +423,85 @@ func (r *Repo) GetGitCommitRange(start, end plumbing.Hash) ([]GitCommit, error)
return commits, nil return commits, nil
} }
var (
hashLen = len(plumbing.ZeroHash)
hashStrLen = len(plumbing.ZeroHash.String())
errNotHex = errors.New("not a valid hex string")
)
func (r *Repo) findCommitByShortHash(hashStr string) (plumbing.Hash, error) {
paddedHashStr := hashStr
if len(hashStr)%2 > 0 {
paddedHashStr += "0"
}
if hashB, err := hex.DecodeString(paddedHashStr); err != nil {
return plumbing.ZeroHash, errNotHex
} else if len(hashStr) == hashStrLen {
var hash plumbing.Hash
copy(hash[:], hashB)
return hash, nil
} else if len(hashStr) < 2 {
return plumbing.ZeroHash, errors.New("hash string must be 2 characters long or more")
}
for i := 2; i < hashStrLen; i++ {
hashPrefix, hashTail := hashStr[:i], hashStr[i:]
path := filepath.Join("objects", hashPrefix)
fileInfos, err := r.GitDirFS.ReadDir(path)
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("listing files in %q: %w", path, err)
}
var matchedHash plumbing.Hash
for _, fileInfo := range fileInfos {
objFileName := fileInfo.Name()
if !strings.HasPrefix(objFileName, hashTail) {
continue
}
objHash := plumbing.NewHash(hashPrefix + objFileName)
obj, err := r.GitRepo.Storer.EncodedObject(plumbing.AnyObject, objHash)
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("reading object %q off disk: %w", objHash, err)
} else if obj.Type() != plumbing.CommitObject {
continue
} else if matchedHash == plumbing.ZeroHash {
matchedHash = objHash
continue
}
return plumbing.ZeroHash, fmt.Errorf("both %q and %q match", matchedHash, objHash)
}
if matchedHash != plumbing.ZeroHash {
return matchedHash, nil
}
}
return plumbing.ZeroHash, errors.New("failed to find a commit object with a matching prefix")
}
func (r *Repo) resolveRev(rev plumbing.Revision) (plumbing.Hash, error) { func (r *Repo) resolveRev(rev plumbing.Revision) (plumbing.Hash, error) {
if rev == plumbing.Revision(plumbing.ZeroHash.String()) { if rev == plumbing.Revision(plumbing.ZeroHash.String()) {
return plumbing.ZeroHash, nil return plumbing.ZeroHash, nil
} }
{
// pretend the revision is a short hash until proven otherwise
shortHash := string(rev)
hash, err := r.findCommitByShortHash(shortHash)
if errors.Is(err, errNotHex) {
// ok, continue
} else if err != nil {
return plumbing.ZeroHash, fmt.Errorf("resolving as short hash: %w", err)
} else {
// guess it _is_ a short hash, knew it!
return hash, nil
}
}
h, err := r.GitRepo.ResolveRevision(rev) h, err := r.GitRepo.ResolveRevision(rev)
if err != nil { if err != nil {
return plumbing.ZeroHash, fmt.Errorf("resolving revision %q: %w", rev, err) return plumbing.ZeroHash, fmt.Errorf("resolving revision %q: %w", rev, err)
@ -447,6 +509,20 @@ func (r *Repo) resolveRev(rev plumbing.Revision) (plumbing.Hash, error) {
return *h, nil return *h, nil
} }
// GetGitRevision resolves the revision and returns the GitCommit it references.
func (r *Repo) GetGitRevision(rev plumbing.Revision) (GitCommit, error) {
hash, err := r.resolveRev(rev)
if err != nil {
return GitCommit{}, err
}
gc, err := r.GetGitCommit(hash)
if err != nil {
return GitCommit{}, fmt.Errorf("getting commit %q: %w", hash, err)
}
return gc, nil
}
// GetGitRevisionRange is like GetGitCommitRange, first resolving the given // GetGitRevisionRange is like GetGitCommitRange, first resolving the given
// revisions into hashes before continuing with GetGitCommitRange's behavior. // revisions into hashes before continuing with GetGitCommitRange's behavior.
func (r *Repo) GetGitRevisionRange(startRev, endRev plumbing.Revision) ([]GitCommit, error) { func (r *Repo) GetGitRevisionRange(startRev, endRev plumbing.Revision) ([]GitCommit, error) {

View File

@ -309,3 +309,22 @@ func TestThisRepoStillVerifies(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestShortHashResolving(t *testing.T) {
// TODO ideally this test would test the conflicting hashes are noticed, but
// that's hard...
h := newHarness(t)
hash := h.changeCommit("first commit", h.cfg.Accounts[0].ID, h.sig).GitCommit.Hash
hashStr := hash.String()
t.Log(hashStr)
for i := 2; i < len(hashStr); i++ {
gotCommit, err := h.repo.GetGitRevision(plumbing.Revision(hashStr[:i]))
if err != nil {
t.Fatalf("resolving %q: %v", hashStr[:i], err)
} else if gotCommit.GitCommit.Hash != hash {
t.Fatalf("expected hash %q but got %q",
gotCommit.GitCommit.Hash, hash)
}
}
}