Skip to content

Commit

Permalink
Implement MVP merge logic (#2)
Browse files Browse the repository at this point in the history
What?
- Add default cobra CLI project structure
- Implement bare minimum logic to merge repos into the current working
directory using:
  - https://jeffkreeftmeijer.com/git-combine/
  - https://alexharv074.github.io/puppet/2017/10/04/merge-a-git-repository-and-its-history-into-a-subdirectory-of-a-second-git-repository.html

Notes
- Does not currently support merging local repos, or remotes pulled from
a non SSH remote.
- Documentation has not been added yet
  • Loading branch information
oliver-hohn authored Aug 1, 2023
1 parent 2ae157c commit 150c88f
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 2 deletions.
19 changes: 19 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "many2mono",
Short: "TBD",
}

func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
72 changes: 72 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cmd

import (
"log"
"strings"

"github.com/oliverhohn/many2mono/helper"
"github.com/oliverhohn/many2mono/model"
"github.com/spf13/cobra"
)

// CLI flags
var defaultBranch string
var dryRun bool

var runCmd = &cobra.Command{
Use: "run",
Short: "TBD",
Args: cobra.MatchAll(cobra.MinimumNArgs(1)),
Run: func(cmd *cobra.Command, args []string) {
repos := []*model.Repo{}
for _, path := range args {
r, err := model.NewRepo(path)
if err != nil {
log.Fatal(err)
}

repos = append(repos, r)
}

if duplicateRepos := findDuplicateRepoNames(repos); len(duplicateRepos) > 0 {
log.Fatalf("Cannot merge as repos with duplicate names found: %s", strings.Join(duplicateRepos, ", "))
}

for _, r := range repos {
helper.FetchRemote(r, dryRun)
helper.MergeHistories(r, defaultBranch, dryRun)
helper.PrefixFiles(r, defaultBranch, dryRun)
helper.CommitChange(r, dryRun)
helper.RemoveRemote(r, dryRun)
}
},
}

func findDuplicateRepoNames(repos []*model.Repo) []string {
reposByName := map[string][]*model.Repo{}

for _, r := range repos {
if _, ok := reposByName[r.NameWithoutOrg()]; !ok {
reposByName[r.NameWithoutOrg()] = []*model.Repo{}
}

reposByName[r.NameWithoutOrg()] = append(reposByName[r.NameWithoutOrg()], r)
}

ret := []string{}
for name, repos := range reposByName {
if len(repos) > 1 {
ret = append(ret, name)
}
}

return ret
}

func init() {
rootCmd.AddCommand(runCmd)

runCmd.Flags().StringVarP(&defaultBranch, "branch", "b", "main", "TBD")

runCmd.Flags().BoolVar(&dryRun, "dry-run", false, "TBD")
}
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
module github.com/oliverhohn/many2mono

go 1.20

require (
github.com/spf13/cobra v1.7.0
github.com/whilp/git-urls v1.0.0
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU=
github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
69 changes: 69 additions & 0 deletions helper/operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package helper

import (
"fmt"
"log"
"os/exec"

"github.com/oliverhohn/many2mono/model"
)

func FetchRemote(r *model.Repo, dryRun bool) {
cmd := exec.Command("git", "remote", "add", "-f", r.Name(), r.URL.String())

err := runCommand(cmd, dryRun)
if err != nil {
log.Fatalf("unable to fetch remote %s, due to: %v", r.URL.String(), err)
}
}

func RemoveRemote(r *model.Repo, dryRun bool) {
cmd := exec.Command("git", "remote", "remove", r.Name())

err := runCommand(cmd, dryRun)
if err != nil {
log.Fatalf("unable to remove remote %s, due to: %v", r.Name(), err)
}
}

func MergeHistories(r *model.Repo, branch string, dryRun bool) {
remote := fmt.Sprintf("%s/%s", r.Name(), branch)
cmd := exec.Command("git", "merge", "--allow-unrelated-histories", "--strategy=ours", "--no-commit", remote)

err := runCommand(cmd, dryRun)
if err != nil {
log.Fatalf("unable to merge histories from remote %s, due to: %v", remote, err)
}
}

func PrefixFiles(r *model.Repo, branch string, dryRun bool) {
remote := fmt.Sprintf("%s/%s", r.Name(), branch)
cmd := exec.Command("git", "read-tree", fmt.Sprintf("--prefix=%s", r.NameWithoutOrg()), "-u", remote)

err := runCommand(cmd, dryRun)
if err != nil {
log.Fatalf("unable to prefix files from remote %s, due to: %v", remote, err)
}
}

func CommitChange(r *model.Repo, dryRun bool) {
cmd := exec.Command("git", "commit", fmt.Sprintf("--message=\"Merged %s\"", r.Name()))

err := runCommand(cmd, dryRun)
if err != nil {
log.Fatalf("unable to commit merge from remote %s, due to: %v", r.Name(), err)
}
}

func runCommand(cmd *exec.Cmd, dryRun bool) error {
fmt.Printf("Running %s\n", cmd.String())

if !dryRun {
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v\ntrace:%s", err, out)
}
}

return nil
}
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import "fmt"
import "github.com/oliverhohn/many2mono/cmd"

func main() {
fmt.Println("Hello, world!")
cmd.Execute()
}
41 changes: 41 additions & 0 deletions model/repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package model

import (
"fmt"
"net/url"
"path/filepath"
"strings"

gitURLs "github.com/whilp/git-urls"
)

type Repo struct {
URL *url.URL
}

func NewRepo(path string) (*Repo, error) {
u, err := gitURLs.Parse(path)
if err != nil {
return nil, fmt.Errorf("unable to initialize repo from %s, due to: %w", path, err)
}

if u.Scheme != "ssh" {
return nil, fmt.Errorf("%s is an unsupported scheme for repo URLs, only ssh is supported", u.Scheme)
}

return &Repo{URL: u}, nil
}

func (r *Repo) Name() string {
ext := filepath.Ext(r.URL.Path)

name := strings.TrimSuffix(r.URL.Path, ext)

return strings.TrimSpace(name)
}

func (r *Repo) NameWithoutOrg() string {
nameComponents := strings.Split(r.Name(), "/")

return nameComponents[len(nameComponents)-1]
}

0 comments on commit 150c88f

Please sign in to comment.