Skip to content

Commit

Permalink
Merge pull request #707 from pjbgf/experimental-sha256
Browse files Browse the repository at this point in the history
*: Add support for initializing SHA256 repositories
  • Loading branch information
pjbgf committed Apr 11, 2023
2 parents ce62f3e + 8e82810 commit d5b1afd
Show file tree
Hide file tree
Showing 23 changed files with 298 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/git.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- name: Test
run: make test-coverage

- name: Test SHA256
run: make test-sha256

- name: Build go-git with CGO disabled
run: go build ./...
env:
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ test:
@echo "running against `git version`"; \
$(GOTEST) -race ./...

TEMP_REPO := $(shell mktemp)
test-sha256:
$(GOCMD) run -tags sha256 _examples/sha256/main.go $(TEMP_REPO)
cd $(TEMP_REPO) && git fsck
rm -rf $(TEMP_REPO)

test-coverage:
@echo "running against `git version`"; \
echo "" > $(COVERAGE_REPORT); \
Expand Down
66 changes: 66 additions & 0 deletions _examples/sha256/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"

"github.com/go-git/go-git/v5"
. "github.com/go-git/go-git/v5/_examples"
"github.com/go-git/go-git/v5/plumbing/format/config"
"github.com/go-git/go-git/v5/plumbing/object"
)

// This example requires building with the sha256 tag for it to work:
// go run -tags sha256 main.go /tmp/repository

// Basic example of how to initialise a repository using sha256 as the hashing algorithmn.
func main() {
CheckArgs("<directory>")
directory := os.Args[1]

os.RemoveAll(directory)

// Init a new repository using the ObjectFormat SHA256.
r, err := git.PlainInitWithOptions(directory, &git.PlainInitOptions{ObjectFormat: config.SHA256})
CheckIfError(err)

w, err := r.Worktree()
CheckIfError(err)

// ... we need a file to commit so let's create a new file inside of the
// worktree of the project using the go standard library.
Info("echo \"hello world!\" > example-git-file")
filename := filepath.Join(directory, "example-git-file")
err = ioutil.WriteFile(filename, []byte("hello world!"), 0644)
CheckIfError(err)

// Adds the new file to the staging area.
Info("git add example-git-file")
_, err = w.Add("example-git-file")
CheckIfError(err)

// Commits the current staging area to the repository, with the new file
// just created. We should provide the object.Signature of Author of the
// commit Since version 5.0.1, we can omit the Author signature, being read
// from the git config files.
Info("git commit -m \"example go-git commit\"")
commit, err := w.Commit("example go-git commit", &git.CommitOptions{
Author: &object.Signature{
Name: "John Doe",
Email: "john@doe.org",
When: time.Now(),
},
})

CheckIfError(err)

// Prints the current HEAD to verify that all worked well.
Info("git show -s")
obj, err := r.CommitObject(commit)
CheckIfError(err)

fmt.Println(obj)
}
73 changes: 51 additions & 22 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type Config struct {
// CommentChar is the character indicating the start of a
// comment for commands like commit and tag
CommentChar string
// RepositoryFormatVersion identifies the repository format and layout version.
RepositoryFormatVersion format.RepositoryFormatVersion
}

User struct {
Expand Down Expand Up @@ -96,6 +98,17 @@ type Config struct {
DefaultBranch string
}

Extensions struct {
// ObjectFormat specifies the hash algorithm to use. The
// acceptable values are sha1 and sha256. If not specified,
// sha1 is assumed. It is an error to specify this key unless
// core.repositoryFormatVersion is 1.
//
// This setting must not be changed after repository initialization
// (e.g. clone or init).
ObjectFormat format.ObjectFormat
}

// Remotes list of repository remotes, the key of the map is the name
// of the remote, should equal to RemoteConfig.Name.
Remotes map[string]*RemoteConfig
Expand Down Expand Up @@ -226,28 +239,31 @@ func (c *Config) Validate() error {
}

const (
remoteSection = "remote"
submoduleSection = "submodule"
branchSection = "branch"
coreSection = "core"
packSection = "pack"
userSection = "user"
authorSection = "author"
committerSection = "committer"
initSection = "init"
urlSection = "url"
fetchKey = "fetch"
urlKey = "url"
bareKey = "bare"
worktreeKey = "worktree"
commentCharKey = "commentChar"
windowKey = "window"
mergeKey = "merge"
rebaseKey = "rebase"
nameKey = "name"
emailKey = "email"
descriptionKey = "description"
defaultBranchKey = "defaultBranch"
remoteSection = "remote"
submoduleSection = "submodule"
branchSection = "branch"
coreSection = "core"
packSection = "pack"
userSection = "user"
authorSection = "author"
committerSection = "committer"
initSection = "init"
urlSection = "url"
extensionsSection = "extensions"
fetchKey = "fetch"
urlKey = "url"
bareKey = "bare"
worktreeKey = "worktree"
commentCharKey = "commentChar"
windowKey = "window"
mergeKey = "merge"
rebaseKey = "rebase"
nameKey = "name"
emailKey = "email"
descriptionKey = "description"
defaultBranchKey = "defaultBranch"
repositoryFormatVersionKey = "repositoryformatversion"
objectFormat = "objectformat"

// DefaultPackWindow holds the number of previous objects used to
// generate deltas. The value 10 is the same used by git command.
Expand Down Expand Up @@ -391,6 +407,7 @@ func (c *Config) unmarshalInit() {
// Marshal returns Config encoded as a git-config file.
func (c *Config) Marshal() ([]byte, error) {
c.marshalCore()
c.marshalExtensions()
c.marshalUser()
c.marshalPack()
c.marshalRemotes()
Expand All @@ -410,12 +427,24 @@ func (c *Config) Marshal() ([]byte, error) {
func (c *Config) marshalCore() {
s := c.Raw.Section(coreSection)
s.SetOption(bareKey, fmt.Sprintf("%t", c.Core.IsBare))
if string(c.Core.RepositoryFormatVersion) != "" {
s.SetOption(repositoryFormatVersionKey, string(c.Core.RepositoryFormatVersion))
}

if c.Core.Worktree != "" {
s.SetOption(worktreeKey, c.Core.Worktree)
}
}

func (c *Config) marshalExtensions() {
// Extensions are only supported on Version 1, therefore
// ignore them otherwise.
if c.Core.RepositoryFormatVersion == format.Version_1 {
s := c.Raw.Section(extensionsSection)
s.SetOption(objectFormat, string(c.Extensions.ObjectFormat))
}
}

func (c *Config) marshalUser() {
s := c.Raw.Section(userSection)
if c.User.Name != "" {
Expand Down
8 changes: 8 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
formatcfg "github.com/go-git/go-git/v5/plumbing/format/config"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/sideband"
"github.com/go-git/go-git/v5/plumbing/transport"
Expand Down Expand Up @@ -672,3 +673,10 @@ type PlainOpenOptions struct {

// Validate validates the fields and sets the default values.
func (o *PlainOpenOptions) Validate() error { return nil }

type PlainInitOptions struct {
ObjectFormat formatcfg.ObjectFormat
}

// Validate validates the fields and sets the default values.
func (o *PlainInitOptions) Validate() error { return nil }
7 changes: 3 additions & 4 deletions plumbing/format/commitgraph/encoder.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package commitgraph

import (
"crypto"
"io"

"github.com/go-git/go-git/v5/plumbing"
Expand All @@ -17,7 +16,7 @@ type Encoder struct {

// NewEncoder returns a new stream encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
h := hash.New(crypto.SHA1)
h := hash.New(hash.CryptoType)
mw := io.MultiWriter(w, h)
return &Encoder{mw, h}
}
Expand All @@ -31,7 +30,7 @@ func (e *Encoder) Encode(idx Index) error {
hashToIndex, fanout, extraEdgesCount := e.prepare(idx, hashes)

chunkSignatures := [][]byte{oidFanoutSignature, oidLookupSignature, commitDataSignature}
chunkSizes := []uint64{4 * 256, uint64(len(hashes)) * 20, uint64(len(hashes)) * 36}
chunkSizes := []uint64{4 * 256, uint64(len(hashes)) * hash.Size, uint64(len(hashes)) * 36}
if extraEdgesCount > 0 {
chunkSignatures = append(chunkSignatures, extraEdgeListSignature)
chunkSizes = append(chunkSizes, uint64(extraEdgesCount)*4)
Expand Down Expand Up @@ -183,6 +182,6 @@ func (e *Encoder) encodeExtraEdges(extraEdges []uint32) (err error) {
}

func (e *Encoder) encodeChecksum() error {
_, err := e.Write(e.hash.Sum(nil)[:20])
_, err := e.Write(e.hash.Sum(nil)[:hash.Size])
return err
}
53 changes: 53 additions & 0 deletions plumbing/format/config/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package config

// RepositoryFormatVersion represents the repository format version,
// as per defined at:
//
// https://git-scm.com/docs/repository-version
type RepositoryFormatVersion string

const (
// Version_0 is the format defined by the initial version of git,
// including but not limited to the format of the repository
// directory, the repository configuration file, and the object
// and ref storage.
//
// Specifying the complete behavior of git is beyond the scope
// of this document.
Version_0 = "0"

// Version_1 is identical to version 0, with the following exceptions:
//
// 1. When reading the core.repositoryformatversion variable, a git
// implementation which supports version 1 MUST also read any
// configuration keys found in the extensions section of the
// configuration file.
//
// 2. If a version-1 repository specifies any extensions.* keys that
// the running git has not implemented, the operation MUST NOT proceed.
// Similarly, if the value of any known key is not understood by the
// implementation, the operation MUST NOT proceed.
//
// Note that if no extensions are specified in the config file, then
// core.repositoryformatversion SHOULD be set to 0 (setting it to 1 provides
// no benefit, and makes the repository incompatible with older
// implementations of git).
Version_1 = "1"

// DefaultRepositoryFormatVersion holds the default repository format version.
DefaultRepositoryFormatVersion = Version_0
)

// ObjectFormat defines the object format.
type ObjectFormat string

const (
// SHA1 represents the object format used for SHA1.
SHA1 ObjectFormat = "sha1"

// SHA256 represents the object format used for SHA256.
SHA256 ObjectFormat = "sha256"

// DefaultObjectFormat holds the default object format.
DefaultObjectFormat = SHA1
)
3 changes: 2 additions & 1 deletion plumbing/format/idxfile/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"io"

"github.com/go-git/go-git/v5/plumbing/hash"
"github.com/go-git/go-git/v5/utils/binary"
)

Expand All @@ -19,7 +20,7 @@ var (

const (
fanout = 256
objectIDLength = 20
objectIDLength = hash.Size
)

// Decoder reads and decodes idx files from an input stream.
Expand Down
7 changes: 3 additions & 4 deletions plumbing/format/idxfile/encoder.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package idxfile

import (
"crypto"
"io"

"github.com/go-git/go-git/v5/plumbing/hash"
Expand All @@ -16,7 +15,7 @@ type Encoder struct {

// NewEncoder returns a new stream encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
h := hash.New(crypto.SHA1)
h := hash.New(hash.CryptoType)
mw := io.MultiWriter(w, h)
return &Encoder{mw, h}
}
Expand Down Expand Up @@ -133,10 +132,10 @@ func (e *Encoder) encodeChecksums(idx *MemoryIndex) (int, error) {
return 0, err
}

copy(idx.IdxChecksum[:], e.hash.Sum(nil)[:20])
copy(idx.IdxChecksum[:], e.hash.Sum(nil)[:hash.Size])
if _, err := e.Write(idx.IdxChecksum[:]); err != nil {
return 0, err
}

return 40, nil
return hash.HexSize, nil
}
5 changes: 3 additions & 2 deletions plumbing/format/idxfile/idxfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
encbin "encoding/binary"

"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/hash"
)

const (
Expand Down Expand Up @@ -53,8 +54,8 @@ type MemoryIndex struct {
Offset32 [][]byte
CRC32 [][]byte
Offset64 []byte
PackfileChecksum [20]byte
IdxChecksum [20]byte
PackfileChecksum [hash.Size]byte
IdxChecksum [hash.Size]byte

offsetHash map[int64]plumbing.Hash
offsetHashIsFull bool
Expand Down
3 changes: 1 addition & 2 deletions plumbing/format/index/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package index
import (
"bufio"
"bytes"
"crypto"
"errors"
"io"
"io/ioutil"
Expand Down Expand Up @@ -49,7 +48,7 @@ type Decoder struct {

// NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder {
h := hash.New(crypto.SHA1)
h := hash.New(hash.CryptoType)
return &Decoder{
r: io.TeeReader(r, h),
hash: h,
Expand Down

0 comments on commit d5b1afd

Please sign in to comment.