Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .entire/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ tmp/
settings.local.json
metadata/
logs/
redactors/local/
2 changes: 2 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ linters:
- github.com/go-git/go-git/v6/plumbing/storer.EncodedObjectIter
- github.com/go-git/go-billy/v6.Filesystem
- entire.io/entire/git-sync/internal/auth.Method
- entire.io/entire/git-sync/internal/gitproto.Conn
- entire.io/entire/git-sync/internal/gitproto.AuthMethod
nolintlint:
require-explanation: true
require-specific: true
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The main commands are:

`sync` automatically bootstraps an empty target, so the same command covers initial seeding and ongoing sync. To preview what would happen without pushing, run `git-sync plan` — it takes the same flags as `sync`, and `--mode replicate` previews a `replicate` run.

For one-off SHA1 → SHA256 repo conversion, `git-sync convert-sha256` fetches from an HTTP source and writes a new SHA256 bare repo on disk, with optional commit-message hash rewrites, an origin-notes ref, and a sidecar mapping file. See [docs/convert-sha256.md](docs/convert-sha256.md).

For command examples, JSON output, auth, protocol flags, and advanced command notes, see [docs/usage.md](docs/usage.md).

## Library API
Expand Down Expand Up @@ -93,6 +95,7 @@ Extended and environment-specific test instructions are in [docs/testing.md](doc
- [docs/usage.md](docs/usage.md) — CLI commands, examples, sync behavior, JSON output, auth, protocol notes
- [docs/architecture.md](docs/architecture.md) — product rationale, package layout, operation modes vs transfer modes, memory model
- [docs/protocol.md](docs/protocol.md) — smart HTTP, pkt-line, capability negotiation, sideband, relay framing
- [docs/convert-sha256.md](docs/convert-sha256.md) — one-off SHA1 → SHA256 repo conversion, mapping outputs, sharp edges
- [docs/testing.md](docs/testing.md) — test suites and integration coverage

## FAQ
Expand Down
127 changes: 127 additions & 0 deletions cmd/git-sync/convert_sha256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"errors"
"fmt"

gitsync "entire.io/entire/git-sync"
"entire.io/entire/git-sync/cmd/git-sync/internal/sha256convert"
"github.com/spf13/cobra"
)

func newConvertSHA256Cmd() *cobra.Command {
var (
req = sha256convert.Request{}
jsonOutput bool
protocolVal = newProtocolFlag()
)

cmd := &cobra.Command{
Use: "convert-sha256 [flags] <source-url> <target-dir>",
Short: "One-off SHA1 → SHA256 conversion of a remote repo into a local bare repo",
Long: `convert-sha256 fetches a pack from a SHA1 HTTP source and writes a new
SHA256 bare repository on disk at <target-dir>. Every reachable object is
re-hashed under SHA256 and tree/commit/tag references are rewritten.

All branches and tags on the source are always converted — partial scope
risks stranding cross-branch references in commit messages. Pass
--all-refs to also include refs/notes/*, refs/pull/*, and other custom
namespaces; pass --exclude-ref-prefix to subtract specific namespaces
from --all-refs. Exclude prefixes that would drop any branch or tag
(e.g. refs/heads/feature/, refs/tags/, refs/) are rejected at run time
to preserve the always-convert invariant.

The conversion is destructive in two ways the caller should be aware of:
GPG signatures on commits and tags are dropped (they sign over the
original SHA1 content and would be invalid post-rewrite), and any
submodule gitlink fails the run — its .gitmodules upstream still
advertises SHA1 hashes, so a rewritten SHA256 gitlink would point at a
hash the upstream cannot resolve and break ` + "`git submodule update`" + ` in
every clone. Exclude refs that reference submodules, or convert the
submodule repository first and re-point .gitmodules.`,
Args: cobra.MaximumNArgs(2),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
req.ProtocolMode = gitsync.ProtocolMode(protocolVal)
if err := resolveConvertSHA256Args(&req, args); err != nil {
return err
}

result, err := sha256convert.Run(cmd.Context(), req)
// Print whatever state Run produced even on error: signed
// tags landed before signBranchTips failed, --check
// findings, and the --keep-source-objects temp dir are
// all things the user needs to see to clean up or debug.
// Run zero-values fields it never touched, so this is
// safe to call on a half-populated result.
if result.SourceURL != "" || result.TargetDir != "" {
printOutput(jsonOutput, result)
}
if err != nil {
return fmt.Errorf("convert-sha256: %w", err)
}
return nil
},
}

cmd.Flags().StringVar(&req.SourceURL, "source-url", "", "source repository URL")
cmd.Flags().BoolVar(&req.SourceFollowInfoRefsRedirect, "source-follow-info-refs-redirect",
envBool("GITSYNC_SOURCE_FOLLOW_INFO_REFS_REDIRECT"),
"send follow-up source RPCs to the final /info/refs redirect host")
cmd.Flags().StringVar(&req.SourceAuth.Token, "source-token",
envOr("GITSYNC_SOURCE_TOKEN", ""), "source token/password")
cmd.Flags().StringVar(&req.SourceAuth.Username, "source-username",
envOr("GITSYNC_SOURCE_USERNAME", "git"), "source basic auth username")
cmd.Flags().StringVar(&req.SourceAuth.BearerToken, "source-bearer-token",
envOr("GITSYNC_SOURCE_BEARER_TOKEN", ""), "source bearer token")
cmd.Flags().BoolVar(&req.SourceAuth.SkipTLSVerify, "source-insecure-skip-tls-verify",
envBool("GITSYNC_SOURCE_INSECURE_SKIP_TLS_VERIFY"),
"skip TLS certificate verification for the source")
cmd.Flags().StringVar(&req.TargetDir, "target-dir", "", "directory to initialize as a SHA256 bare repository")

allRefsFlag(cmd, allRefsUsageScopeOnly, &req.AllRefs)
excludeRefPrefixFlag(cmd, &req.ExcludeRefPrefixes)
addProtocolFlag(cmd, &protocolVal)
cmd.Flags().BoolVarP(&req.Verbose, "verbose", "v", false, "verbose logging")
cmd.Flags().BoolVar(&req.Progress, "progress", false,
"show live per-phase object counts on stderr (TTY only)")
cmd.Flags().BoolVar(&req.Check, "check", false,
"verify the output after conversion (config, HEAD, refs, git fsck --full)")
cmd.Flags().BoolVar(&req.Sign, "sign", false,
"after conversion, sign each branch tip as refs/tags/converted/<branch> via `git tag -s`")
cmd.Flags().StringVar(&req.SignKey, "sign-key", "",
"signing key id to pass to `git tag -s -u`; default uses the repo's user.signingkey")
cmd.Flags().BoolVar(&req.KeepSourceObjects, "keep-source-objects", false,
"keep the temporary SHA1 store on disk after conversion (for debugging)")
cmd.Flags().StringVar(&req.MappingFile, "write-mapping", "",
"write the full SHA1 → SHA256 mapping as a TSV to this path; useful for rewriting external references")
cmd.Flags().BoolVar(&req.SkipMessageRewrite, "no-rewrite-messages", false,
"do not rewrite SHA1 hash references found in commit and tag messages")
cmd.Flags().BoolVar(&req.SkipOriginNotes, "no-origin-notes", false,
"do not write a refs/notes/sha1-origin ref recording each commit's original SHA1")
cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output")

return cmd
}

// resolveConvertSHA256Args consumes positional args left-to-right,
// skipping fields the user already supplied via flags. Without that
// rule, `--source-url <url> <dir>` would look like one positional and
// land in SourceURL — leaving TargetDir empty even though the user
// gave both. The two-flags-no-positionals and zero-flags-two-positionals
// shapes also work, as do the symmetric --target-dir + positional URL.
func resolveConvertSHA256Args(req *sha256convert.Request, args []string) error {
positional := args
if req.SourceURL == "" && len(positional) > 0 {
req.SourceURL = positional[0]
positional = positional[1:]
}
if req.TargetDir == "" && len(positional) > 0 {
req.TargetDir = positional[0]
}
if req.SourceURL == "" || req.TargetDir == "" {
return errors.New("convert-sha256 requires a source URL and a target directory")
}
return nil
}
84 changes: 84 additions & 0 deletions cmd/git-sync/convert_sha256_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"strings"
"testing"

"entire.io/entire/git-sync/cmd/git-sync/internal/sha256convert"
)

func TestResolveConvertSHA256Args(t *testing.T) {
const url = "http://example.invalid/repo.git"
const dir = "/tmp/out"

tests := []struct {
name string
req sha256convert.Request
args []string
wantURL string
wantDir string
wantErr string
}{
{
name: "both positionals",
args: []string{url, dir},
wantURL: url,
wantDir: dir,
},
{
name: "url flag plus positional dir — the reported bug",
req: sha256convert.Request{SourceURL: url},
args: []string{dir},
wantURL: url,
wantDir: dir,
},
{
name: "dir flag plus positional url",
req: sha256convert.Request{TargetDir: dir},
args: []string{url},
wantURL: url,
wantDir: dir,
},
{
name: "both flags, no positionals",
req: sha256convert.Request{SourceURL: url, TargetDir: dir},
args: nil,
wantURL: url,
wantDir: dir,
},
{
name: "missing dir",
req: sha256convert.Request{SourceURL: url},
args: nil,
wantErr: "requires a source URL and a target directory",
},
{
name: "missing both",
args: nil,
wantErr: "requires a source URL and a target directory",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := tt.req
err := resolveConvertSHA256Args(&req, tt.args)
switch {
case tt.wantErr == "" && err != nil:
t.Fatalf("unexpected error: %v", err)
case tt.wantErr != "" && err == nil:
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
case tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr):
t.Fatalf("error %q does not contain %q", err.Error(), tt.wantErr)
}
if tt.wantErr != "" {
return
}
if req.SourceURL != tt.wantURL {
t.Errorf("SourceURL: got %q, want %q", req.SourceURL, tt.wantURL)
}
if req.TargetDir != tt.wantDir {
t.Errorf("TargetDir: got %q, want %q", req.TargetDir, tt.wantDir)
}
})
}
}
Loading
Loading