diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6d0a261 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "remap", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "args": [ + "-m", + "/absolute/path/to/migration-archive.tar.gz", + "-c", + "/absolute/path/to/commit-map" + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 0fcfa7d..a74def6 100644 --- a/README.md +++ b/README.md @@ -27,5 +27,15 @@ Usage: Flags: -h, --help help for gh-commit-remap -c, --mapping-file string Path to the commit map file Example: /path/to/commit-map - -m, --migration-archive string Path to the migration archive Example: /path/to/migration-archive + -m, --migration-archive string Path to the migration archive Example: /path/to/migration-archive or /path/to/migration-archive.tar.gz ``` + +The mapping file should be in the format produced by [git-filter-repo](https://github.com/newren/git-filter-repo). For example: +``` +old new +892d146b368d74a64ad8a83af8ecc949ff20a408 0000000000000000000000000000000000000000 +12f9bdd865d5f8b19eaaff8c94af10ec1d7f6e27 26b42504e90f3cb96e684be18ffbc3ff42d12383 +... +``` + +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. \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index f94c337..f84b1c7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ package cmd import ( "log" "os" + "strings" "github.com/mona-actions/gh-commit-remap/internal/archive" "github.com/mona-actions/gh-commit-remap/internal/commitremap" @@ -38,18 +39,36 @@ var rootCmd = &cobra.Command{ archivePath, _ := cmd.Flags().GetString("migration-archive") - err = commitremap.ProcessFiles(archivePath, types, commitMap) - if err != nil { - log.Fatal(err) + var extractedDir string + untarAndRetar := strings.HasSuffix(archivePath, ".tar.gz") + + if untarAndRetar { + // Extract the provided migration archive so we can modify its JSON contents + extractedDir, err = archive.UnTar(archivePath, "") + if err != nil { + log.Fatalf("Error extracting migration archive: %v", err) + } + } else { + // Treat provided path as an already-extracted directory + extractedDir = archivePath } - tarPath, err := archive.ReTar(archivePath) - if err != nil { + 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) + } + } } - - log.Printf("New archive created: %s", tarPath) - }, } diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 45104eb..9f0e6f1 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -2,8 +2,10 @@ package archive import ( "fmt" + "os" "os/exec" "path/filepath" + "strings" ) // reTarFiles creates a new tar archive from the files in the given directory. @@ -12,8 +14,8 @@ func ReTar(archivePath string) (string, error) { // Extract the directory name from the archivePath dirName := filepath.Base(archivePath) - // Create the name of the new archive - archiveName := dirName + ".tar.gz" + // Create the name of the new archive with -REMAPPED suffix + archiveName := fmt.Sprintf("%s-REMAPPED.tar.gz", dirName) err := checkTarAvailability() if err != nil { @@ -30,6 +32,52 @@ func ReTar(archivePath string) (string, error) { return archiveName, nil } +// UnTar extracts the given .tar.gz archive into the specified destination directory. +// If destDir is empty, a directory will be created in the current working directory +// using the archive base name (without the .tar.gz suffix). It returns the directory +// where the archive was extracted. +func UnTar(archiveFile, destDir string) (string, error) { + if archiveFile == "" { + return "", fmt.Errorf("archive file path is empty") + } + + if err := checkTarAvailability(); err != nil { + return "", err + } + + // Ensure the archive exists before attempting extraction + if _, err := os.Stat(archiveFile); err != nil { + return "", fmt.Errorf("cannot stat archive file: %w", err) + } + + // Determine destination directory if not provided + if destDir == "" { + base := filepath.Base(archiveFile) + // strip .tar.gz if present + base = strings.TrimSuffix(base, ".tar.gz") + if base == filepath.Base(archiveFile) { // no .tar.gz suffix + base = strings.TrimSuffix(base, ".tgz") + } + if base == "" { + base = "extracted" + } + destDir = base + } + + // Create destination directory if it does not exist + if err := os.MkdirAll(destDir, 0o755); err != nil { + return "", fmt.Errorf("failed to create destination directory: %w", err) + } + + // Run tar extraction + tarCmd := exec.Command("tar", "-xzf", archiveFile, "-C", destDir) + if err := tarCmd.Run(); err != nil { + return "", fmt.Errorf("error extracting archive: %w", err) + } + + return destDir, nil +} + // checkTarAvailability checks if the 'tar' command is available in the system's PATH. func checkTarAvailability() error { _, err := exec.LookPath("tar") diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 5d081fd..8634723 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -62,3 +62,50 @@ func TestReTar(t *testing.T) { t.Fatalf("Original file content and extracted file content do not match") } } + +func TestUnTar(t *testing.T) { + // Setup: create temp dir with a file, tar it using ReTar, then UnTar into new dir + srcDir, err := os.MkdirTemp("", "untar-src") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(srcDir) + + content := []byte("hello world") + if err := os.WriteFile(filepath.Join(srcDir, "file.txt"), content, 0o644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + + archiveName, err := ReTar(srcDir) + if err != nil { + t.Fatalf("ReTar failed: %v", err) + } + defer os.Remove(archiveName) + + // Extract using UnTar with empty destination (auto-generated) + destDir, err := UnTar(archiveName, "") + if err != nil { + t.Fatalf("UnTar failed: %v", err) + } + defer os.RemoveAll(destDir) + + extractedContent, err := os.ReadFile(filepath.Join(destDir, "file.txt")) + if err != nil { + t.Fatalf("failed to read extracted file: %v", err) + } + if !bytes.Equal(content, extractedContent) { + t.Fatalf("extracted content mismatch") + } +} + +func TestUnTarErrors(t *testing.T) { + // Non-existent archive + if _, err := UnTar("does-not-exist.tar.gz", ""); err == nil { + t.Fatalf("expected error for non-existent archive") + } + + // Empty archive path + if _, err := UnTar("", ""); err == nil { + t.Fatalf("expected error for empty archive path") + } +}