Skip to content
Merged
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 .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @amenocal
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b #v4.1.4
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 #v5.0.1
with:
go-version: 1.21
go-version-file: go.mod
- run: go get -v -t -d ./...
- run: go build -v .
- run: go test ./... --coverprofile=cover.out
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,56 @@ old new
...
```

You can provide a directory where your migration archive has been expanded, or a `.tar.gz` file, via the `--migration-archive` argument. In either case, this tool will produce a new migration archive in the current directory, appending `-REMAPPED` to the base filename of the original migration archive.
You can provide a directory where your migration archive has been expanded, or a `.tar.gz` file, via the `--migration-archive` argument. In either case, this tool will produce a new migration archive in the current directory, appending `-REMAPPED` to the base filename of the original migration archive.

## Using as a library

The remap and archive packaging logic is also available as a Go library under `pkg/`:

```go
package main

import (
"log"

"github.com/mona-actions/gh-commit-remap/pkg/archive"
"github.com/mona-actions/gh-commit-remap/pkg/commitremap"
)

func main() {
// 1. Extract a .tar.gz migration archive (auto-creates destDir).
extracted, err := archive.UnTar("migration-archive.tar.gz", "")
if err != nil {
log.Fatal(err)
}

// 2. Parse the commit-map produced by `git filter-repo`.
commitMap, err := commitremap.ParseCommitMap("commit-map")
if err != nil {
log.Fatal(err)
}

// 3. Rewrite SHAs in the archive's metadata JSON files.
if err := commitremap.ProcessFiles(extracted, commitremap.DefaultPrefixes(), commitMap); err != nil {
log.Fatal(err)
}

// 4. Re-package to a path of your choosing.
if err := archive.ReTarDir(extracted, "migration-archive-REMAPPED.tar.gz"); err != nil {
log.Fatal(err)
}
}
```

### Known limitations

- **Whole-string SHA match only.** `ProcessFiles` only rewrites JSON string
values that exactly equal a commit-map key. SHAs embedded in URLs
(`/commits/<sha>`), markdown bodies, or `_links.*.href` are not rewritten.
Tracked as a follow-up issue.
- **Limited prefix coverage.** `DefaultPrefixes` covers `pull_requests`,
`issues`, `issue_events`. Other archive files containing SHAs
(`commit_comments_*.json`, `pull_request_review*_*.json`,
`releases_*.json`'s `target_commitish`, etc.) are not processed by default.
Pass a custom `prefixes` slice to `ProcessFiles` to widen coverage, or
track the follow-up issue for a default-list expansion.
101 changes: 73 additions & 28 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ Copyright © 2024 NAME HERE <EMAIL ADDRESS>
package cmd

import (
"log"
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/mona-actions/gh-commit-remap/internal/archive"
"github.com/mona-actions/gh-commit-remap/internal/commitremap"
"github.com/mona-actions/gh-commit-remap/pkg/archive"
"github.com/mona-actions/gh-commit-remap/pkg/commitremap"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

Expand All @@ -19,6 +22,38 @@ func init() {

rootCmd.Flags().StringP("migration-archive", "m", "", "Path to the migration archive Example: /path/to/migration-archive.tar.gz")
rootCmd.MarkFlagRequired("migration-archive")

rootCmd.SilenceErrors = true
rootCmd.SilenceUsage = true
}

func renderSummaryTable(stats commitremap.Stats, extractedDir string) {
if stats.FilesChanged() == 0 {
pterm.Info.Println("No files were modified — none of the SHAs in the archive matched the commit-map.")
return
}

type row struct {
rel string
count int
}
rows := make([]row, 0, len(stats.PerFile))
for absPath, count := range stats.PerFile {
rel, err := filepath.Rel(extractedDir, absPath)
if err != nil {
rel = filepath.Base(absPath)
}
rows = append(rows, row{rel: rel, count: count})
}
sort.Slice(rows, func(i, j int) bool { return rows[i].rel < rows[j].rel })

data := pterm.TableData{{"File", "SHAs rewritten"}}
for _, r := range rows {
data = append(data, []string{r.rel, fmt.Sprintf("%d", r.count)})
}
if err := pterm.DefaultTable.WithHasHeader().WithData(data).Render(); err != nil {
pterm.Warning.Printfln("failed to render summary table: %v", err)
}
}

// rootCmd represents the base command when called without any subcommands
Expand All @@ -27,56 +62,66 @@ var rootCmd = &cobra.Command{
Short: "remaps commit hashes in a GitHub archive",
Long: `Is a CLI tool that can remap commits hashed
after performing a history re-write when performing a migration For exam`,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
mapPath, _ := cmd.Flags().GetString("mapping-file")
commitMap, err := commitremap.ParseCommitMap(mapPath)
if err != nil {
log.Fatalf("Error parsing commit map: %v", err)
return fmt.Errorf("parsing commit map: %w", err)
}

// config to define the types of files to process
types := []string{"pull_requests", "issues", "issue_events"}

archivePath, _ := cmd.Flags().GetString("migration-archive")

var extractedDir string
untarAndRetar := strings.HasSuffix(archivePath, ".tar.gz")

if untarAndRetar {
// Extract the provided migration archive so we can modify its JSON contents
pterm.DefaultSection.Println("Extract")
spinner, _ := pterm.DefaultSpinner.Start("Extracting archive...")
extractedDir, err = archive.UnTar(archivePath, "")
if err != nil {
log.Fatalf("Error extracting migration archive: %v", err)
spinner.Fail("Extraction failed")
return fmt.Errorf("extracting migration archive: %w", err)
}
defer func() {
if err := os.RemoveAll(extractedDir); err != nil {
pterm.Warning.Printfln("failed to remove extracted directory %s: %v", extractedDir, err)
}
}()
spinner.Success(fmt.Sprintf("Extracted to %s", extractedDir))
} else {
// Treat provided path as an already-extracted directory
extractedDir = archivePath
}

if err := commitremap.ProcessFiles(extractedDir, types, commitMap); err != nil {
log.Fatal(err)
} else {
// Re-package the modified directory into a new archive
tarPath, err := archive.ReTar(extractedDir)
if err != nil {
log.Fatal(err)
}
log.Printf("New archive created: %s", tarPath)
if untarAndRetar {
// Cleanup extracted directory after successful re-tar
if err := os.RemoveAll(extractedDir); err != nil {
log.Printf("Warning: failed to remove extracted directory %s: %v", extractedDir, err)
}
}
pterm.DefaultSection.Println("Remap")
remapSpinner, _ := pterm.DefaultSpinner.Start("Remapping SHAs...")
stats, err := commitremap.ProcessFiles(extractedDir, commitremap.DefaultPrefixes(), commitMap)
if err != nil {
remapSpinner.Fail("Remap failed")
renderSummaryTable(stats, extractedDir)
return fmt.Errorf("remapping SHAs: %w", err)
}
remapSpinner.Success(fmt.Sprintf("Remapped %d SHAs across %d files (scanned %d)", stats.TotalReplacements(), stats.FilesChanged(), stats.FilesScanned))
renderSummaryTable(stats, extractedDir)

pterm.DefaultSection.Println("Repack")
tarPath := filepath.Base(extractedDir) + "-REMAPPED.tar.gz"
repackSpinner, _ := pterm.DefaultSpinner.Start("Creating new archive...")
if err := archive.ReTarDir(extractedDir, tarPath); err != nil {
repackSpinner.Fail("Repack failed")
return fmt.Errorf("creating new archive: %w", err)
}
repackSpinner.Success(fmt.Sprintf("Created %s", tarPath))
pterm.Success.Printfln("New archive created: %s", tarPath)

return nil
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
if err := rootCmd.Execute(); err != nil {
pterm.Error.Println(err)
os.Exit(1)
}
}
19 changes: 17 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
module github.com/mona-actions/gh-commit-remap

go 1.21
go 1.25

require github.com/spf13/cobra v1.8.1
require (
github.com/pterm/pterm v0.12.83
github.com/spf13/cobra v1.8.1
)

require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/gookit/color v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
)
Loading
Loading