This commit is contained in:
Fabien Potencier 2017-03-02 09:54:54 -08:00 committed by Fabien Potencier
parent e25ec42c93
commit f567acdfbb
6 changed files with 164 additions and 242 deletions

View file

@ -17,7 +17,8 @@ monolithic repository to standalone repositories** in real-time.
**splitsh-lite** is a sub-project that provides a faster implementation of the
`git subtree split` command, which helps create standalone repositories for one
or more sub-directories of a main repository.
or more sub-directories of a main repository. It also provides a `publish`
command that helps manage those standalone repositories.
If you want to learn more about monorepo vs manyrepos, watch this [4-minute
lightning talk](http://www.thedotpost.com/2016/05/fabien-potencier-monolithic-repositories-vs-many-repositories)
@ -53,9 +54,24 @@ cp splitsh-lite "$(git --exec-path)"/git-splitsh
Usage
-----
The easiest way to use splitsh is to integrate it with Git:
git splitsh publish "src/Silex/Api:git@github.com:splitsh/Silex-Api-Test.git" "src/Silex/Provider:git@github.com:splitsh/Silex-Providers-Test.git"
git splitsh split --prefix=src/Silex/Api --prefix=src/Silex/Provider:Provider --push=git@github.com:splitsh/Silex-Api-Test.git
--heads : sync all heads
--tags : sync all tags
--ref : heads/master (renamed from --origin) -> can we have several --ref? then several target?
--ref : --ref=HEAD:master --ref=master:foobar --ref=v1.0.0 --ref=v1.0.0:1.0.0
master -> try heads first, then tags?
--commit: depreated use --ref=heads/master@sha1:master
--target : deprecated use --ref=ORIGIN:TARGET instead
--scratch : deprecated? Just remove the DB? -> rename to --force (how to apply on one only? -> that's not a poblem use --force with the right ref/tag/..., done)
git splitsh --prefix=src/Silex/Api --heads --push=git@github.com:splitsh/Silex-Api-Test.git
git splitsh --prefix=src/Silex/Api --ref=heads/HEAD:master --ref=heads/1.0 --push=git@github.com:splitsh/Silex-Api-Test.git
cp bin/git-splitsh.sh "$(git --exec-path)"/git-splitsh
Let's say you want to split the `lib/` directory of a repository to its own
branch; from the "master" Git repository (bare or clone), run:
@ -118,9 +134,6 @@ Available options:
* `--progress` displays a progress bar;
* `--quiet` suppresses all output on stderr (useful when run from an automated
script);
* `--scratch` flushes the cache (useful when a branch is force pushed or in
case of a cache corruption).

View file

@ -1,6 +1,8 @@
package git
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"os/exec"
@ -15,7 +17,7 @@ type Repo struct {
}
// CreateRemote registers a remote if it is not already registered
func (r *Repo) CreateRemote(name, URL string) error {
func (r *Repo) CreateRemote(URL string) error {
cmd := exec.Command("git", "remote")
cmd.Dir = r.Path
output, err := cmd.Output()
@ -23,6 +25,9 @@ func (r *Repo) CreateRemote(name, URL string) error {
return err
}
sum := sha256.Sum256([]byte(URL))
name := hex.EncodeToString(sum[:])
for _, n := range strings.Split(string(output), "\n") {
if n == name {
return nil
@ -35,8 +40,9 @@ func (r *Repo) CreateRemote(name, URL string) error {
}
// RemoteRefs returns the current remotes
func (r *Repo) RemoteRefs(remote string) []string {
if refs, ok := r.remoteRefs[remote]; ok {
func (r *Repo) RemoteRefs(remote, prefix string) []string {
key := remote + ":" + prefix
if refs, ok := r.remoteRefs[key]; ok {
return refs
}
@ -51,37 +57,14 @@ func (r *Repo) RemoteRefs(remote string) []string {
if r.remoteRefs == nil {
r.remoteRefs = make(map[string][]string)
}
// r.remoteRefs[remote] = []string{}
for _, line := range strings.Split(string(output), "\n") {
parts := strings.Split(line, "\t")
if len(parts) > 1 {
r.remoteRefs[remote] = append(r.remoteRefs[remote], parts[1])
if len(parts) > 1 && !strings.Contains(parts[1], "^{}") && strings.HasPrefix(parts[1], prefix) {
r.remoteRefs[key] = append(r.remoteRefs[key], parts[1])
}
}
return r.remoteRefs[remote]
}
// RemoteTags returns tags defined on origin
func (r *Repo) RemoteTags(remote string) []string {
tags := []string{}
for _, ref := range r.RemoteRefs(remote) {
if !strings.Contains(ref, "^{}") && strings.HasPrefix(ref, "refs/tags/") {
tags = append(tags, strings.TrimPrefix(ref, "refs/tags/"))
}
}
return tags
}
// RemoteHeads returns heads defined on origin
func (r *Repo) RemoteHeads(remote string) []string {
heads := []string{}
for _, ref := range r.RemoteRefs(remote) {
if strings.HasPrefix(ref, "refs/heads/") {
heads = append(heads, strings.TrimPrefix(ref, "refs/heads/"))
}
}
return heads
return r.remoteRefs[key]
}
// CheckRef returns true if the head exists

88
main.go
View file

@ -6,7 +6,6 @@ import (
"os"
"strings"
"github.com/splitsh/lite/git"
"github.com/splitsh/lite/splitter"
)
@ -36,25 +35,35 @@ func (p *prefixesFlag) Set(value string) error {
return nil
}
type refsFlag []*splitter.Ref
func (r *refsFlag) String() string {
return fmt.Sprint(*r)
}
func (r *refsFlag) Set(value string) error {
parts := strings.Split(value, ":")
from := parts[0]
to := ""
if len(parts) > 1 {
to = parts[1]
}
parts = strings.Split(from, "@")
from = parts[0]
commit := ""
if len(parts) > 1 {
commit = parts[1]
}
*r = append(*r, &splitter.Ref{From: from, To: to, Commit: commit})
return nil
}
var version = "dev"
var prefixes prefixesFlag
var origin, target, commit, path, gitVersion string
var scratch, debug, legacy, progress, v, update bool
type publishFlags struct {
path string
update bool
noHeads bool
heads string
noTags bool
tags string
config string
debug bool
dry bool
project *splitter.Project
repo *git.Repo
}
var refs refsFlag
var progress, v bool
func main() {
if len(os.Args) < 2 {
@ -65,11 +74,29 @@ func main() {
if os.Args[1] == "publish" {
run := &splitter.Run{}
publishCmdFlagSet(run).Parse(os.Args[2:])
printVersion(v)
if run.Config == "" {
fmt.Fprintln(os.Stderr, "You must provide the configuration via the --config flag")
if len(prefixes) == 0 {
fmt.Fprintln(os.Stderr, "You must provide the directory to split via the --prefix flag")
os.Exit(1)
}
run.Prefixes = []*splitter.Prefix(prefixes)
run.Refs = []*splitter.Ref(refs)
if run.Heads {
if len(run.Refs) > 0 {
fmt.Fprintln(os.Stderr, "You cannot use the --heads flag with the --ref one")
os.Exit(1)
}
}
if run.Tags {
if len(run.Refs) > 0 {
fmt.Fprintln(os.Stderr, "You cannot use the --tags flag with the --ref one")
os.Exit(1)
}
}
if err := run.Sync(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
@ -91,11 +118,6 @@ func runSplitCmd(config *splitter.Config) {
os.Exit(1)
}
if legacy {
fmt.Fprintln(os.Stderr, `The --legacy option is deprecated (use --git="<1.8.2" instead)`)
gitVersion = "<1.8.2"
}
config.Prefixes = []*splitter.Prefix(prefixes)
sha1 := config.SplitWithFeedback(progress && !config.Debug)
fmt.Fprintln(os.Stderr, "")
@ -114,14 +136,15 @@ func printVersion(v bool) {
func publishCmdFlagSet(run *splitter.Run) *flag.FlagSet {
publishCmd := flag.NewFlagSet("publish", flag.ExitOnError)
publishCmd.BoolVar(&run.NoUpdate, "no-update", false, "Do not fetch origin changes")
publishCmd.BoolVar(&run.NoHeads, "no-heads", false, "Do not publish any heads")
publishCmd.StringVar(&run.Heads, "heads", "", "Only publish for listed heads instead of all heads")
publishCmd.StringVar(&run.Config, "config", "", "JSON file path for the configuration")
publishCmd.BoolVar(&run.NoTags, "no-tags", false, "Do not publish any tags")
publishCmd.StringVar(&run.Tags, "tags", "", "Only publish for listed tags instead of all tags")
publishCmd.BoolVar(&run.Debug, "debug", false, "Display debug information")
publishCmd.BoolVar(&run.DryRun, "dry-run", false, "Do everything except actually send the updates")
publishCmd.BoolVar(&run.Heads, "heads", false, "Split all heads")
publishCmd.BoolVar(&run.Tags, "tags", false, "Split all tags")
publishCmd.Var(&refs, "ref", "Split this reference only (can be used multiple times)")
publishCmd.StringVar(&run.RemoteURL, "push", "", "Git URL to push splits")
publishCmd.Var(&prefixes, "prefix", "The directory(ies) to split")
//publishCmd.BoolVar(&run.Scratch, "scratch", false, "Flush the cache (optional)")
publishCmd.StringVar(&run.GitVersion, "git", "latest", "Simulate a given version of Git (optional)")
publishCmd.BoolVar(&run.Progress, "progress", false, "Display splitting progress information")
publishCmd.BoolVar(&run.Debug, "debug", false, "Display debug information")
publishCmd.BoolVar(&v, "version", false, "Show version")
publishCmd.StringVar(&run.Path, "path", ".", "The repository path (optional, current directory by default)")
return publishCmd
@ -135,7 +158,6 @@ func splitCmdFlagSet(config *splitter.Config) *flag.FlagSet {
splitCmd.StringVar(&config.Commit, "commit", "", "The commit at which to start the split (optional)")
splitCmd.BoolVar(&config.Scratch, "scratch", false, "Flush the cache (optional)")
splitCmd.BoolVar(&config.Debug, "debug", false, "Enable the debug mode (optional)")
splitCmd.BoolVar(&legacy, "legacy", false, "[DEPRECATED] Enable the legacy mode for projects migrating from an old version of git subtree split (optional)")
splitCmd.StringVar(&config.GitVersion, "git", "latest", "Simulate a given version of Git (optional)")
splitCmd.BoolVar(&progress, "progress", false, "Show progress bar (optional, cannot be enabled when debug is enabled)")
splitCmd.BoolVar(&v, "version", false, "Show version")

View file

@ -17,6 +17,14 @@ type Prefix struct {
To string
}
func (p *Prefix) String() string {
s := p.From
if p.To != "" {
s = fmt.Sprintf("%s > %s", s, p.To)
}
return s
}
// Config represents a split configuration
type Config struct {
Prefixes []*Prefix

View file

@ -1,92 +0,0 @@
package splitter
import (
"encoding/json"
"fmt"
"strings"
"github.com/xeipuuv/gojsonschema"
)
const (
schema = `
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Project configuration",
"type" : "object",
"additionalProperties": false,
"required": ["git-version", "subtrees"],
"properties": {
"git-version": { "type": "string" },
"subtrees": {
"type": "object",
"patternProperties": {
"^\\w[\\w\\d\\-_\\.]+$": {
"title": "Split",
"type": "object",
"required": ["target", "prefixes"],
"additionalProperties": false,
"properties": {
"prefixes": {
"type": "array",
"items": { "type": "string" }
},
"target": { "type": "string" }
}
}
}
}
}
}
`
)
// Project represents a project
type Project struct {
Subtrees map[string]*Subtree `json:"subtrees"`
GitVersion string `json:"git-version"`
}
// Subtree represents a split configuration
type Subtree struct {
Prefixes []string `json:"prefixes"`
Target string
}
// NewProject creates a project from a JSON string
func NewProject(config []byte) (*Project, error) {
if err := validateProject(config); err != nil {
return nil, err
}
var project *Project
if err := json.Unmarshal(config, &project); err != nil {
return nil, err
}
return project, nil
}
// validateProject validates a project JSON string
func validateProject(config []byte) error {
schema, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(schema))
if err != nil {
return fmt.Errorf("Could not create the JSON validator: %s", err)
}
result, err := schema.Validate(gojsonschema.NewStringLoader(string(config)))
if err != nil {
return fmt.Errorf("Project configuration is not valid JSON: %s", err)
}
if !result.Valid() {
errors := []string{}
for _, desc := range result.Errors() {
errors = append(errors, desc.Description())
}
return fmt.Errorf("Project configuration is not valid JSON: %s", strings.Join(errors, ", "))
}
return nil
}

View file

@ -2,37 +2,66 @@ package splitter
import (
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/splitsh/lite/git"
)
// Ref represents a refence to split
type Ref struct {
From string
To string
Commit string
}
func (r *Ref) String() string {
s := r.From
if r.Commit != "" {
s = fmt.Sprintf("%s@%s", s, r.Commit)
}
if r.To != "" {
s = fmt.Sprintf("%s:%s", s, r.To)
}
return s
}
// Run represents a run from the CLI
type Run struct {
Path string
NoUpdate bool
NoHeads bool
Heads string
NoTags bool
Tags string
Config string
Debug bool
Progress bool
DryRun bool
Path string
NoUpdate bool
Refs []*Ref
Prefixes []*Prefix
Heads bool
Tags bool
Debug bool
Progress bool
DryRun bool
RemoteURL string
GitVersion string
repo *git.Repo
}
// Sync synchronizes branches and tags
func (r *Run) Sync() error {
project, err := r.createProject()
if err != nil {
return err
}
r.repo = &git.Repo{Path: r.Path}
if r.Heads {
for _, ref := range r.repo.RemoteRefs("origin", "refs/heads/") {
r.Refs = append(r.Refs, &Ref{From: ref})
}
}
if r.Tags {
for _, ref := range r.repo.RemoteRefs("origin", "refs/tags/") {
r.Refs = append(r.Refs, &Ref{From: ref})
}
}
for _, ref := range r.Refs {
fmt.Println(ref)
}
os.Exit(0)
if !r.NoUpdate {
fmt.Fprintln(os.Stderr, "Fetching changes from origin")
if err := r.repo.Update(); err != nil {
@ -40,22 +69,26 @@ func (r *Run) Sync() error {
}
}
for name, subtree := range project.Subtrees {
if err := r.repo.CreateRemote(name, subtree.Target); err != nil {
if r.RemoteURL != "" {
if err := r.repo.CreateRemote(r.RemoteURL); err != nil {
return fmt.Errorf("Could create remote: %s\n", err)
}
for _, prefix := range subtree.Prefixes {
fmt.Fprintf(os.Stderr, "Syncing %s -> %s\n", prefix, subtree.Target)
}
r.syncHeads(project, subtree)
r.syncTags(project, subtree)
}
for _, prefix := range r.Prefixes {
if r.RemoteURL != "" {
fmt.Fprintf(os.Stderr, " %s -> %s\n", prefix, r.RemoteURL)
} else {
fmt.Fprintf(os.Stderr, " %s\n", prefix)
}
}
r.syncHeads()
r.syncTags()
return nil
}
func (r *Run) syncHeads(project *Project, subtree *Subtree) {
func (r *Run) syncHeads() {
for _, head := range r.getHeads() {
fmt.Fprintf(os.Stderr, " Head %s", head)
if !r.repo.CheckRef("refs/heads/" + head) {
@ -64,10 +97,10 @@ func (r *Run) syncHeads(project *Project, subtree *Subtree) {
}
fmt.Fprint(os.Stderr, " > ")
config := r.createConfig(project, subtree, "refs/heads/"+head)
config := r.createConfig("refs/heads/" + head)
if sha1 := config.SplitWithFeedback(r.Progress); sha1 != "" {
fmt.Fprint(os.Stderr, " > pushing")
r.repo.Push(subtree.Target, sha1, "refs/heads/"+head, r.DryRun)
r.repo.Push(r.RemoteURL, sha1, "refs/heads/"+head, r.DryRun)
fmt.Fprintln(os.Stderr, " > pushed")
} else {
fmt.Fprintln(os.Stderr, " > empty, not pushed")
@ -75,8 +108,8 @@ func (r *Run) syncHeads(project *Project, subtree *Subtree) {
}
}
func (r *Run) syncTags(project *Project, subtree *Subtree) {
targetTags := r.repo.RemoteTags(subtree.Target)
func (r *Run) syncTags() {
targetTags := r.repo.RemoteRefs(r.RemoteURL, "refs/tags/")
NextTag:
for _, tag := range r.getTags() {
fmt.Fprintf(os.Stderr, " Tag %s", tag)
@ -93,10 +126,10 @@ NextTag:
}
fmt.Fprint(os.Stderr, " > ")
config := r.createConfig(project, subtree, "refs/tags/"+tag)
config := r.createConfig("refs/tags/" + tag)
if sha1 := config.SplitWithFeedback(r.Progress); sha1 != "" {
fmt.Fprint(os.Stderr, " > pushing")
r.repo.Push(subtree.Target, sha1, "refs/tags/"+tag, r.DryRun)
r.repo.Push(r.RemoteURL, sha1, "refs/tags/"+tag, r.DryRun)
fmt.Fprintln(os.Stderr, " > pushed")
} else {
fmt.Fprintln(os.Stderr, " > empty, not pushed")
@ -104,65 +137,20 @@ NextTag:
}
}
func (r *Run) createConfig(project *Project, subtree *Subtree, ref string) *Config {
prefixes := []*Prefix{}
for _, prefix := range subtree.Prefixes {
parts := strings.Split(prefix, ":")
from := parts[0]
to := ""
if len(parts) > 1 {
to = parts[1]
}
prefixes = append(prefixes, &Prefix{From: from, To: to})
}
func (r *Run) createConfig(ref string) *Config {
return &Config{
Path: r.Path,
Origin: ref,
Prefixes: prefixes,
GitVersion: project.GitVersion,
Prefixes: r.Prefixes,
GitVersion: r.GitVersion,
Debug: r.Debug,
}
}
func (r *Run) getHeads() []string {
var heads []string
if r.NoHeads {
return heads
}
if r.Heads != "" {
return strings.Split(r.Heads, " ")
}
return r.repo.RemoteHeads("origin")
return []string{}
}
func (r *Run) getTags() []string {
var tags []string
if r.NoTags {
return tags
}
if r.Tags != "" {
return strings.Split(r.Tags, " ")
}
return r.repo.RemoteTags("origin")
}
func (r *Run) createProject() (*Project, error) {
config, err := ioutil.ReadFile(r.Config)
if err != nil {
return nil, fmt.Errorf("Could not read config file: %s\n", err)
}
project, err := NewProject(config)
if err != nil {
return nil, fmt.Errorf("Could read project: %s\n", err)
}
return project, nil
return []string{}
}