mirror of
https://github.com/splitsh/lite.git
synced 2024-06-11 02:02:22 +02:00
added initial set of files
This commit is contained in:
commit
7861d21ed4
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
splitter-lite-tests/
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2015-2016 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
125
README.md
Normal file
125
README.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
Monorepo to Manyrepos made easy
|
||||
===============================
|
||||
|
||||
**tl;dr**: **splitsh-lite** is a replacement for the `subtree split` Git
|
||||
build-in command that is much faster and has more features at the same time.
|
||||
|
||||
When starting a new project, do you store all the code in one monolith
|
||||
repository? Or are you creating many repositories?
|
||||
|
||||
Both strategies work well but both have drawbacks as well. **splitsh** helps use
|
||||
both strategies at the same time by providing tools that automatically
|
||||
synchronize a mono repository to many repositories.
|
||||
|
||||
**splitsh-lite** is a sub-project with the goal of providing a faster replacement
|
||||
of the `git subtree split` command.
|
||||
|
||||
If you want to learn more about monorepo vs manyrepos, watch this 4-minutes
|
||||
lightning talk I gave at dotScale... or the longer version from DrupalCon.
|
||||
|
||||
The main **splitsh-lite** feature is its ability to create a branch in a repository
|
||||
from one or many directories.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install libgit2:
|
||||
|
||||
```bash
|
||||
go get github.com/libgit2/git2go
|
||||
cd $GOPATH/src/github.com/libgit2/git2go
|
||||
git checkout next
|
||||
git submodule update --init
|
||||
make install
|
||||
```
|
||||
|
||||
Compiling
|
||||
|
||||
```bash
|
||||
go build -o splitsh-lite github.com/splitsh/lite
|
||||
```
|
||||
|
||||
If everything goes fine, a `splitsh-lite` binary should be available in the
|
||||
current directory.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Let say you want to split the `lib/` directory of a repository to its own
|
||||
branch; from the "master" Git repository (bare or clone), run:
|
||||
|
||||
```bash
|
||||
splitsh-lite --prefix=lib/
|
||||
```
|
||||
|
||||
The *sha1* of the split is displayed at the end of the execution:
|
||||
|
||||
```bash
|
||||
SHA1=`splitsh-lite --prefix=lib/`
|
||||
```
|
||||
|
||||
The sha1 can be used to create a branch or to push the commits to a new
|
||||
repository.
|
||||
|
||||
Automatically create a branch for the split by passing a branch name
|
||||
via the `--target` option:
|
||||
|
||||
```bash
|
||||
splitsh-lite --prefix=lib/ --target=branch-name
|
||||
```
|
||||
|
||||
If new commits are made on the repository, update the split by running the same
|
||||
command again. Updates are much faster as **splitsh-lite** keeps a cache of already
|
||||
split commits. Caching is possible as **splitsh-lite** guarantees that two splits of
|
||||
the same code always results in the same history and the same `sha1`s for each
|
||||
commit.
|
||||
|
||||
By default, **splitsh-lite** splits the current checkout-ed branch but you can split
|
||||
a different branch by passing it explicitly with `--origin` (mandatory when
|
||||
splitting a bare repository):
|
||||
|
||||
```bash
|
||||
splitsh-lite --prefix=lib/ --origin=origin/1.0
|
||||
```
|
||||
|
||||
You don't even need to run the command from the Git repository directory if you
|
||||
pass the `--path` option:
|
||||
|
||||
```bash
|
||||
splitsh-lite --prefix=lib/ --origin=origin/1.0 --path=/path/to/repo
|
||||
```
|
||||
|
||||
Available options:
|
||||
|
||||
* `--prefix` is the prefix of the directory to split; you can put the split
|
||||
contents in a directory by using the `--prefix=from:to` syntax; splitting
|
||||
several directories is also possible by passing multiple `--prefix` options;
|
||||
|
||||
* `--path` is the path to the repository to split (current directory by default);
|
||||
|
||||
* `--origin` is the Git reference for the origin (can be any Git reference
|
||||
like `HEAD`, `heads/xxx`, `tags/xxx`, `origin/xxx`, or any `refs/xxx`);
|
||||
|
||||
* `--target` creates a reference for the tip of the split (can be any Git reference
|
||||
like `HEAD`, `heads/xxx`, `tags/xxx`, `origin/xxx`, or any `refs/xxx`);
|
||||
|
||||
* `--progress` displays a nice progress bar during the split;
|
||||
|
||||
* `--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 corruption)
|
||||
|
||||
* `--legacy` simulates old versions of `git subtree split` where `sha1`s
|
||||
for the split commits were computed differently (useful if you are switching
|
||||
from the git command to **splitsh-lite**).
|
||||
|
||||
**splitsh** provides more features including a sanity checker, Github integration
|
||||
for real-time splitting, tagging management and synchronization, and more.
|
||||
It has been used by the Symfony project for many years but the tool is not yet
|
||||
ready for Open-Source. Stay tuned!
|
||||
|
||||
If you think that your Open-Source project might benefit from the full version
|
||||
of splitsh, send me an email and I will consider splitting your project for free
|
||||
on my servers (like I do for Symfony and Laravel).
|
104
main.go
Normal file
104
main.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/splitsh/lite/splitter"
|
||||
)
|
||||
|
||||
type prefixesFlag []*splitter.Prefix
|
||||
|
||||
func (p *prefixesFlag) String() string {
|
||||
return fmt.Sprint(*p)
|
||||
}
|
||||
|
||||
func (p *prefixesFlag) Set(value string) error {
|
||||
parts := strings.Split(value, ":")
|
||||
from := parts[0]
|
||||
to := ""
|
||||
if len(parts) > 1 {
|
||||
to = parts[1]
|
||||
}
|
||||
|
||||
// value must be unique
|
||||
for _, prefix := range []*splitter.Prefix(*p) {
|
||||
// FIXME: to should be normalized (xxx vs xxx/ for instance)
|
||||
if prefix.To == to {
|
||||
return fmt.Errorf("Cannot have two prefix split under the same directory: %s -> %s vs %s -> %s", prefix.From, prefix.To, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
*p = append(*p, &splitter.Prefix{From: from, To: to})
|
||||
return nil
|
||||
}
|
||||
|
||||
var prefixes prefixesFlag
|
||||
var origin, target, commit, path string
|
||||
var scratch, debug, quiet, legacy, progress bool
|
||||
|
||||
func init() {
|
||||
flag.Var(&prefixes, "prefix", "The directory(ies) to split")
|
||||
flag.StringVar(&origin, "origin", "HEAD", "The branch to split (optional, defaults to the current one)")
|
||||
flag.StringVar(&target, "target", "", "The branch to create when split is finished (optional)")
|
||||
flag.StringVar(&commit, "commit", "", "The commit at which to start the split (optional)")
|
||||
flag.StringVar(&path, "path", ".", "The repository path (optional, current directory by default)")
|
||||
flag.BoolVar(&scratch, "scratch", false, "Flush the cache (optional)")
|
||||
flag.BoolVar(&debug, "debug", false, "Enable the debug mode (optional)")
|
||||
flag.BoolVar(&quiet, "quiet", false, "Suppress the output (optional)")
|
||||
flag.BoolVar(&legacy, "legacy", false, "Enable the legacy mode for projects migrating from an old version of git subtree split (optional)")
|
||||
flag.BoolVar(&progress, "progress", false, "Show progress bar (optional, cannot be enabled when debug is enabled)")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if len(prefixes) == 0 {
|
||||
fmt.Println("You must provide the directory to split via the --prefix flag")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config := &splitter.Config{
|
||||
Path: path,
|
||||
Origin: origin,
|
||||
Prefixes: []*splitter.Prefix(prefixes),
|
||||
Target: target,
|
||||
Commit: commit,
|
||||
Debug: debug && !quiet,
|
||||
Scratch: scratch,
|
||||
Legacy: legacy,
|
||||
}
|
||||
|
||||
result := &splitter.Result{}
|
||||
|
||||
var ticker *time.Ticker
|
||||
if progress && !debug && !quiet {
|
||||
ticker = time.NewTicker(time.Millisecond * 50)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
fmt.Fprintf(os.Stderr, "%d commits created, %d commits traversed\r", result.Created(), result.Traversed())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err := splitter.Split(config, result)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ticker != nil {
|
||||
ticker.Stop()
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
fmt.Fprintf(os.Stderr, "%d commits created, %d commits traversed, in %s\n", result.Created(), result.Traversed(), result.Duration(time.Millisecond))
|
||||
}
|
||||
|
||||
if result.Head() != nil {
|
||||
fmt.Println(result.Head().String())
|
||||
}
|
||||
}
|
102
run-tests.sh
Executable file
102
run-tests.sh
Executable file
|
@ -0,0 +1,102 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -f
|
||||
|
||||
if [ ! -d splitter-lite-tests ]; then
|
||||
mkdir splitter-lite-tests
|
||||
fi
|
||||
cd splitter-lite-tests
|
||||
|
||||
rm -rf simple
|
||||
mkdir simple
|
||||
cd simple
|
||||
git init > /dev/null
|
||||
|
||||
export GIT_AUTHOR_NAME="Sammy Cobol"
|
||||
export GIT_AUTHOR_EMAIL="<sammy.cobol@example.com>"
|
||||
export GIT_AUTHOR_DATE="Sat, 24 Nov 1973 19:01:02 +0200"
|
||||
export GIT_COMMITTER_NAME="Fred Foobar"
|
||||
export GIT_COMMITTER_EMAIL="<fred.foobar@example.com>"
|
||||
export GIT_COMMITTER_DATE="Sat, 24 Nov 1973 19:11:22 +0200"
|
||||
echo "a" > a
|
||||
git add a
|
||||
git commit -m"added a" > /dev/null
|
||||
|
||||
export GIT_AUTHOR_NAME="Fred Foobar"
|
||||
export GIT_AUTHOR_EMAIL="<fred.foobar@example.com>"
|
||||
export GIT_AUTHOR_DATE="Sat, 24 Nov 1973 20:01:02 +0200"
|
||||
export GIT_COMMITTER_NAME="Sammy Cobol"
|
||||
export GIT_COMMITTER_EMAIL="<sammy.cobol@example.com>"
|
||||
export GIT_COMMITTER_DATE="Sat, 24 Nov 1973 20:11:22 +0200"
|
||||
mkdir b/
|
||||
echo "b" > b/b
|
||||
git add b
|
||||
git commit -m"added b" > /dev/null
|
||||
|
||||
export GIT_AUTHOR_NAME="Fred Foobar"
|
||||
export GIT_AUTHOR_EMAIL="<fred.foobar@example.com>"
|
||||
export GIT_AUTHOR_DATE="Sat, 24 Nov 1973 21:01:02 +0200"
|
||||
export GIT_COMMITTER_NAME="Sammy Cobol"
|
||||
export GIT_COMMITTER_EMAIL="<sammy.cobol@example.com>"
|
||||
export GIT_COMMITTER_DATE="Sat, 24 Nov 1973 21:11:22 +0200"
|
||||
echo "aa" > a
|
||||
git add a
|
||||
git commit -m"updated a" > /dev/null
|
||||
|
||||
export GIT_AUTHOR_NAME="Fred Foobar"
|
||||
export GIT_AUTHOR_EMAIL="<fred.foobar@example.com>"
|
||||
export GIT_AUTHOR_DATE="Sat, 24 Nov 1973 22:01:02 +0200"
|
||||
export GIT_COMMITTER_NAME="Sammy Cobol"
|
||||
export GIT_COMMITTER_EMAIL="<sammy.cobol@example.com>"
|
||||
export GIT_COMMITTER_DATE="Sat, 24 Nov 1973 22:11:22 +0200"
|
||||
git rm a > /dev/null
|
||||
git commit -m"updated a" > /dev/null
|
||||
|
||||
export GIT_AUTHOR_NAME="Fred Foobar"
|
||||
export GIT_AUTHOR_EMAIL="<fred.foobar@example.com>"
|
||||
export GIT_AUTHOR_DATE="Sat, 24 Nov 1973 23:01:02 +0200"
|
||||
export GIT_COMMITTER_NAME="Sammy Cobol"
|
||||
export GIT_COMMITTER_EMAIL="<sammy.cobol@example.com>"
|
||||
export GIT_COMMITTER_DATE="Sat, 24 Nov 1973 23:11:22 +0200"
|
||||
echo "bb" > b/b
|
||||
git add b/
|
||||
git commit -m"updated b" > /dev/null
|
||||
|
||||
GIT_SUBTREE_SPLIT_SHA1=`git subtree split --prefix=b/ -q`
|
||||
GIT_SPLITSH_SHA1=`$GOPATH/src/github.com/splitsh/lite/lite --prefix=b/ --quiet`
|
||||
|
||||
if [ "$GIT_SUBTREE_SPLIT_SHA1" == "$GIT_SUBTREE_SPLIT_SHA1" ]; then
|
||||
echo "OK ($GIT_SUBTREE_SPLIT_SHA1 == $GIT_SUBTREE_SPLIT_SHA1)"
|
||||
else
|
||||
echo "OK ($GIT_SUBTREE_SPLIT_SHA1 != $GIT_SUBTREE_SPLIT_SHA1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GIT_SUBTREE_SPLIT_SHA1=`git subtree split --prefix=b/ -q bff8cdfaaf78a8842b8d9241ccfd8fb6e026f508...`
|
||||
GIT_SPLITSH_SHA1=`$GOPATH/src/github.com/splitsh/lite/lite --prefix=b/ --quiet --commit=bff8cdfaaf78a8842b8d9241ccfd8fb6e026f508`
|
||||
|
||||
if [ "$GIT_SUBTREE_SPLIT_SHA1" == "$GIT_SUBTREE_SPLIT_SHA1" ]; then
|
||||
echo "OK ($GIT_SUBTREE_SPLIT_SHA1 == $GIT_SUBTREE_SPLIT_SHA1)"
|
||||
else
|
||||
echo "OK ($GIT_SUBTREE_SPLIT_SHA1 != $GIT_SUBTREE_SPLIT_SHA1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ../
|
||||
|
||||
# run on some Open-Source repositories
|
||||
if [ ! -d Twig ]; then
|
||||
git clone https://github.com/twigphp/Twig > /dev/null
|
||||
fi
|
||||
GIT_SUBTREE_SPLIT_SHA1="ea449b0f2acba7d489a91f88154687250d2bdf42"
|
||||
GIT_SPLITSH_SHA1=`$GOPATH/src/github.com/splitsh/lite/lite --prefix=lib/ --origin=refs/tags/v1.24.1 --path=Twig --quiet --scratch`
|
||||
|
||||
if [ "$GIT_SUBTREE_SPLIT_SHA1" == "$GIT_SUBTREE_SPLIT_SHA1" ]; then
|
||||
echo "OK ($GIT_SUBTREE_SPLIT_SHA1 == $GIT_SUBTREE_SPLIT_SHA1)"
|
||||
else
|
||||
echo "OK ($GIT_SUBTREE_SPLIT_SHA1 != $GIT_SUBTREE_SPLIT_SHA1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ../
|
174
splitter/cache.go
Normal file
174
splitter/cache.go
Normal file
|
@ -0,0 +1,174 @@
|
|||
package splitter
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/libgit2/git2go"
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
key []byte
|
||||
branch string
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
func newCache(branch string, config *Config) (*cache, error) {
|
||||
var err error
|
||||
db := config.DB
|
||||
if db == nil {
|
||||
db, err = bolt.Open(filepath.Join(GitDirectory(config.Path), "splitsh.db"), 0644, &bolt.Options{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c := &cache{
|
||||
db: db,
|
||||
branch: branch,
|
||||
key: key(config),
|
||||
}
|
||||
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
_, err1 := tx.CreateBucketIfNotExists(c.key)
|
||||
return err1
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Impossible to create bucket: %s", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *cache) close() error {
|
||||
err := c.db.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func key(config *Config) []byte {
|
||||
h := sha1.New()
|
||||
if config.Commit != "" {
|
||||
io.WriteString(h, config.Commit)
|
||||
} else {
|
||||
// value does not matter, should just be always the same
|
||||
io.WriteString(h, "oldest")
|
||||
}
|
||||
|
||||
if config.Legacy {
|
||||
io.WriteString(h, "legacy")
|
||||
}
|
||||
|
||||
for _, prefix := range config.Prefixes {
|
||||
io.WriteString(h, prefix.From)
|
||||
io.WriteString(h, prefix.To)
|
||||
}
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func (c *cache) setHead(head *git.Oid) error {
|
||||
return c.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(c.key).Put([]byte("head/"+c.branch), head[0:20])
|
||||
})
|
||||
}
|
||||
|
||||
func (c *cache) getHead() *git.Oid {
|
||||
var oid *git.Oid
|
||||
c.db.View(func(tx *bolt.Tx) error {
|
||||
result := tx.Bucket(c.key).Get([]byte("head/" + c.branch))
|
||||
if result != nil {
|
||||
oid = git.NewOidFromBytes(result)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return oid
|
||||
}
|
||||
|
||||
// which is newest or oldest
|
||||
func (c *cache) reverse(rev *git.Oid, which string) *git.Oid {
|
||||
var oid *git.Oid
|
||||
c.db.View(func(tx *bolt.Tx) error {
|
||||
result := tx.Bucket(c.key).Get(append(rev[0:20], []byte("/"+which)...))
|
||||
if result == nil && which == "newest" {
|
||||
result = tx.Bucket(c.key).Get(append(rev[0:20], []byte("/oldest")...))
|
||||
}
|
||||
if result != nil {
|
||||
oid = git.NewOidFromBytes(result)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return oid
|
||||
}
|
||||
|
||||
func (c *cache) get(rev *git.Oid) *git.Oid {
|
||||
var oid *git.Oid
|
||||
c.db.View(func(tx *bolt.Tx) error {
|
||||
result := tx.Bucket(c.key).Get(rev[0:20])
|
||||
if result != nil {
|
||||
oid = git.NewOidFromBytes(result)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return oid
|
||||
}
|
||||
|
||||
func (c *cache) set(rev, newrev *git.Oid, created bool) error {
|
||||
return c.db.Update(func(tx *bolt.Tx) error {
|
||||
err := tx.Bucket(c.key).Put(rev[0:20], newrev[0:20])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postfix := "/newest"
|
||||
if created {
|
||||
postfix = "/oldest"
|
||||
}
|
||||
|
||||
key := append(newrev[0:20], []byte(postfix)...)
|
||||
return tx.Bucket(c.key).Put(key, rev[0:20])
|
||||
})
|
||||
}
|
||||
|
||||
func (c *cache) gets(commits []*git.Oid) []*git.Oid {
|
||||
var oids []*git.Oid
|
||||
|
||||
c.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(c.key)
|
||||
for _, commit := range commits {
|
||||
result := b.Get(commit[0:20])
|
||||
if result != nil {
|
||||
oids = append(oids, git.NewOidFromBytes(result))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return oids
|
||||
}
|
||||
|
||||
func (c *cache) flush() error {
|
||||
return c.db.Update(func(tx *bolt.Tx) error {
|
||||
if tx.Bucket(c.key) != nil {
|
||||
err := tx.DeleteBucket(c.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.CreateBucketIfNotExists(c.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
58
splitter/config.go
Normal file
58
splitter/config.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package splitter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/libgit2/git2go"
|
||||
)
|
||||
|
||||
// Prefix represents which paths to split
|
||||
type Prefix struct {
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
// Config represents a split configuration
|
||||
type Config struct {
|
||||
Prefixes []*Prefix
|
||||
Path string
|
||||
Origin string
|
||||
Commit string
|
||||
Target string
|
||||
Debug bool
|
||||
Scratch bool
|
||||
Legacy bool
|
||||
|
||||
// for advanced usage only
|
||||
// naming and types subject to change anytime!
|
||||
Logger *log.Logger
|
||||
DB *bolt.DB
|
||||
RepoMu *sync.Mutex
|
||||
Repo *git.Repository
|
||||
}
|
||||
|
||||
// Split splits a configuration
|
||||
func Split(config *Config, result *Result) error {
|
||||
state, err := newState(config, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer state.close()
|
||||
return state.split()
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (config *Config) Validate() error {
|
||||
if !git.ReferenceIsValidName(config.Origin) {
|
||||
return fmt.Errorf("The origin is not a valid Git reference")
|
||||
}
|
||||
|
||||
if config.Target != "" && !git.ReferenceIsValidName(config.Target) {
|
||||
return fmt.Errorf("The target is not a valid Git reference")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
98
splitter/result.go
Normal file
98
splitter/result.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package splitter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/libgit2/git2go"
|
||||
)
|
||||
|
||||
// Result represents the outcome of a split
|
||||
type Result struct {
|
||||
mu sync.RWMutex
|
||||
traversed int
|
||||
created int
|
||||
head *git.Oid
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
// NewResult returns a pre-populated result
|
||||
func NewResult(duration time.Duration, traversed, created int) *Result {
|
||||
return &Result{
|
||||
duration: duration,
|
||||
traversed: traversed,
|
||||
created: created,
|
||||
}
|
||||
}
|
||||
|
||||
// Traversed returns the number of commits traversed during the split
|
||||
func (r *Result) Traversed() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.traversed
|
||||
}
|
||||
|
||||
// Created returns the number of created commits
|
||||
func (r *Result) Created() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.created
|
||||
}
|
||||
|
||||
// Duration returns the current duration of the split
|
||||
func (r *Result) Duration(precision time.Duration) time.Duration {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return roundDuration(r.duration, precision)
|
||||
}
|
||||
|
||||
// Head returns the latest split sha1
|
||||
func (r *Result) Head() *git.Oid {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.head
|
||||
}
|
||||
|
||||
func (r *Result) moveHead(oid *git.Oid) {
|
||||
r.mu.Lock()
|
||||
r.head = oid
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Result) incCreated() {
|
||||
r.mu.Lock()
|
||||
r.created++
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Result) incTraversed() {
|
||||
r.mu.Lock()
|
||||
r.traversed++
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Result) end(start time.Time) {
|
||||
r.mu.Lock()
|
||||
r.duration = time.Now().Sub(start)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// roundDuration rounds a duration to a given precision (use roundDuration(d, 10*time.Second) to get a 10s precision fe)
|
||||
func roundDuration(d, r time.Duration) time.Duration {
|
||||
if r <= 0 {
|
||||
return d
|
||||
}
|
||||
neg := d < 0
|
||||
if neg {
|
||||
d = -d
|
||||
}
|
||||
if m := d % r; m+m < r {
|
||||
d = d - m
|
||||
} else {
|
||||
d = d + r - m
|
||||
}
|
||||
if neg {
|
||||
return -d
|
||||
}
|
||||
return d
|
||||
}
|
521
splitter/state.go
Normal file
521
splitter/state.go
Normal file
|
@ -0,0 +1,521 @@
|
|||
package splitter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/libgit2/git2go"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
config *Config
|
||||
originBranch string
|
||||
repoMu *sync.Mutex
|
||||
repo *git.Repository
|
||||
cache *cache
|
||||
logger *log.Logger
|
||||
simplePrefix string
|
||||
result *Result
|
||||
}
|
||||
|
||||
func newState(config *Config, result *Result) (*state, error) {
|
||||
// validate config
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := config.Repo
|
||||
if config.Repo == nil {
|
||||
repo, err = git.OpenRepository(config.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
logger := config.Logger
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
originBranch, err := normalizeOriginBranch(repo, config.Origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Debug {
|
||||
logger.Printf("Splitting %s\n", originBranch)
|
||||
for _, v := range config.Prefixes {
|
||||
to := v.To
|
||||
if to == "" {
|
||||
to = "ROOT"
|
||||
}
|
||||
logger.Printf(" From \"%s\" to \"%s\"\n", v.From, to)
|
||||
}
|
||||
}
|
||||
|
||||
cache, err := newCache(originBranch, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Scratch {
|
||||
err = cache.flush()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Target != "" {
|
||||
branch, err := repo.LookupBranch(config.Target, git.BranchLocal)
|
||||
if err == nil {
|
||||
branch.Delete()
|
||||
branch.Free()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SimplePrefix contains the prefix when there is only one
|
||||
// with an empty value (target)
|
||||
simplePrefix := ""
|
||||
if len(config.Prefixes) == 1 {
|
||||
for _, prefix := range config.Prefixes {
|
||||
if prefix.To == "" {
|
||||
simplePrefix = prefix.From
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
repoMu := &sync.Mutex{}
|
||||
if config.RepoMu != nil {
|
||||
repoMu = config.RepoMu
|
||||
}
|
||||
|
||||
return &state{
|
||||
config: config,
|
||||
result: result,
|
||||
repoMu: repoMu,
|
||||
repo: repo,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
simplePrefix: simplePrefix,
|
||||
originBranch: originBranch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *state) close() error {
|
||||
err := s.cache.close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.repo.Free()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *state) split() error {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
s.result.end(startTime)
|
||||
}()
|
||||
|
||||
revWalk, err := s.walker()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Impossible to walk the repository: %s", err)
|
||||
}
|
||||
defer revWalk.Free()
|
||||
|
||||
var iterationErr error
|
||||
var lastRev *git.Oid
|
||||
err = revWalk.Iterate(func(rev *git.Commit) bool {
|
||||
defer rev.Free()
|
||||
lastRev = rev.Id()
|
||||
|
||||
if s.config.Debug {
|
||||
s.logger.Printf("Processing commit: %s\n", rev.Id().String())
|
||||
}
|
||||
|
||||
var newrev *git.Oid
|
||||
newrev, err = s.splitRev(rev)
|
||||
if err != nil {
|
||||
iterationErr = err
|
||||
return false
|
||||
}
|
||||
|
||||
if newrev != nil {
|
||||
s.result.moveHead(newrev)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if iterationErr != nil {
|
||||
return iterationErr
|
||||
}
|
||||
|
||||
if lastRev != nil {
|
||||
s.cache.setHead(lastRev)
|
||||
}
|
||||
|
||||
return s.updateTarget()
|
||||
}
|
||||
|
||||
func (s *state) walker() (*git.RevWalk, error) {
|
||||
revWalk, err := s.repo.Walk()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Impossible to walk the repository: %s", err)
|
||||
}
|
||||
|
||||
err = s.pushRevs(revWalk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Impossible to determine split range: %s", err)
|
||||
}
|
||||
|
||||
revWalk.Sorting(git.SortTopological | git.SortReverse)
|
||||
|
||||
return revWalk, nil
|
||||
}
|
||||
|
||||
func (s *state) splitRev(rev *git.Commit) (*git.Oid, error) {
|
||||
s.result.incTraversed()
|
||||
|
||||
v := s.cache.get(rev.Id())
|
||||
if v != nil {
|
||||
if s.config.Debug {
|
||||
s.logger.Printf(" prior: %s\n", v.String())
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
var parents []*git.Oid
|
||||
var n uint
|
||||
for n = 0; n < rev.ParentCount(); n++ {
|
||||
parents = append(parents, rev.ParentId(n))
|
||||
}
|
||||
|
||||
if s.config.Debug {
|
||||
debugMsg := " parents:"
|
||||
for _, parent := range parents {
|
||||
debugMsg += fmt.Sprintf(" %s", parent.String())
|
||||
}
|
||||
s.logger.Print(debugMsg)
|
||||
}
|
||||
|
||||
newParents := s.cache.gets(parents)
|
||||
|
||||
if s.config.Debug {
|
||||
debugMsg := " newparents:"
|
||||
for _, parent := range newParents {
|
||||
debugMsg += fmt.Sprintf(" %s", parent)
|
||||
}
|
||||
s.logger.Print(debugMsg)
|
||||
}
|
||||
|
||||
tree, err := s.subtreeForCommit(rev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nil == tree {
|
||||
// should never happen
|
||||
return nil, nil
|
||||
}
|
||||
defer tree.Free()
|
||||
|
||||
if s.config.Debug {
|
||||
s.logger.Printf(" tree is: %s\n", tree.Id().String())
|
||||
}
|
||||
|
||||
newrev, created, err := s.copyOrSkip(rev, tree, newParents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.config.Debug {
|
||||
s.logger.Printf(" newrev is: %s\n", newrev)
|
||||
}
|
||||
|
||||
if created {
|
||||
s.result.incCreated()
|
||||
}
|
||||
|
||||
if err := s.cache.set(rev.Id(), newrev, created); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newrev, nil
|
||||
}
|
||||
|
||||
func (s *state) subtreeForCommit(commit *git.Commit) (*git.Tree, error) {
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tree.Free()
|
||||
|
||||
if s.simplePrefix != "" {
|
||||
return s.treeByPath(tree, s.simplePrefix)
|
||||
}
|
||||
|
||||
return s.treeByPaths(tree, s.config.Prefixes)
|
||||
}
|
||||
|
||||
func (s *state) treeByPath(tree *git.Tree, prefix string) (*git.Tree, error) {
|
||||
treeEntry, err := tree.EntryByPath(prefix)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return s.repo.LookupTree(treeEntry.Id)
|
||||
}
|
||||
|
||||
func (s *state) treeByPaths(tree *git.Tree, prefixes []*Prefix) (*git.Tree, error) {
|
||||
var currentTree, prefixedTree, mergedTree *git.Tree
|
||||
for _, prefix := range s.config.Prefixes {
|
||||
// splitting
|
||||
splitTree, err := s.treeByPath(tree, prefix.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if splitTree == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// adding the prefix
|
||||
if prefix.To != "" {
|
||||
prefixedTree, err = s.addPrefixToTree(splitTree, prefix.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
prefixedTree = splitTree
|
||||
}
|
||||
|
||||
// merging with the current tree
|
||||
if currentTree != nil {
|
||||
mergedTree, err = s.mergeTrees(currentTree, prefixedTree)
|
||||
currentTree.Free()
|
||||
prefixedTree.Free()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
mergedTree = prefixedTree
|
||||
}
|
||||
|
||||
currentTree = mergedTree
|
||||
}
|
||||
|
||||
return currentTree, nil
|
||||
}
|
||||
|
||||
func (s *state) mergeTrees(t1, t2 *git.Tree) (*git.Tree, error) {
|
||||
index, err := s.repo.MergeTrees(nil, t1, t2, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer index.Free()
|
||||
|
||||
if index.HasConflicts() {
|
||||
return nil, fmt.Errorf("Cannot split as there is a merge conflict between two paths")
|
||||
}
|
||||
|
||||
oid, err := index.WriteTreeTo(s.repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.repo.LookupTree(oid)
|
||||
}
|
||||
|
||||
func (s *state) addPrefixToTree(tree *git.Tree, prefix string) (*git.Tree, error) {
|
||||
treeOid := tree.Id()
|
||||
parts := strings.Split(prefix, "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
treeBuilder, err := s.repo.TreeBuilder()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer treeBuilder.Free()
|
||||
|
||||
err = treeBuilder.Insert(parts[i], treeOid, git.FilemodeTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
treeOid, err = treeBuilder.Write()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
prefixedTree, err := s.repo.LookupTree(treeOid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prefixedTree, nil
|
||||
}
|
||||
|
||||
func (s *state) copyOrSkip(rev *git.Commit, tree *git.Tree, newParents []*git.Oid) (*git.Oid, bool, error) {
|
||||
var identical *git.Oid
|
||||
var gotParents []*git.Oid
|
||||
var p []*git.Commit
|
||||
for _, parent := range newParents {
|
||||
ptree, err := s.topTreeForCommit(parent)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if nil == ptree {
|
||||
continue
|
||||
}
|
||||
|
||||
if 0 == ptree.Cmp(tree.Id()) {
|
||||
// an identical parent could be used in place of this rev.
|
||||
identical = parent
|
||||
}
|
||||
|
||||
// sometimes both old parents map to the same newparent
|
||||
// eliminate duplicates
|
||||
isNew := true
|
||||
for _, gp := range gotParents {
|
||||
if 0 == gp.Cmp(parent) {
|
||||
isNew = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isNew {
|
||||
gotParents = append(gotParents, parent)
|
||||
commit, err := s.repo.LookupCommit(parent)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer commit.Free()
|
||||
p = append(p, commit)
|
||||
}
|
||||
}
|
||||
|
||||
if nil != identical {
|
||||
return identical, false, nil
|
||||
}
|
||||
|
||||
commit, err := s.copyCommit(rev, tree, p)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return commit, true, nil
|
||||
}
|
||||
|
||||
func (s *state) topTreeForCommit(sha *git.Oid) (*git.Oid, error) {
|
||||
commit, err := s.repo.LookupCommit(sha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer commit.Free()
|
||||
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tree.Free()
|
||||
|
||||
return tree.Id(), nil
|
||||
}
|
||||
|
||||
func (s *state) copyCommit(rev *git.Commit, tree *git.Tree, parents []*git.Commit) (*git.Oid, error) {
|
||||
if s.config.Debug {
|
||||
parentStrs := make([]string, len(parents))
|
||||
for i, parent := range parents {
|
||||
parentStrs[i] = parent.Id().String()
|
||||
}
|
||||
s.logger.Printf(" copy commit \"%s\" \"%s\" \"%s\"\n", rev.Id().String(), tree.Id().String(), strings.Join(parentStrs, " "))
|
||||
}
|
||||
|
||||
message := rev.Message()
|
||||
if s.config.Legacy {
|
||||
message = s.legacyMessage(rev)
|
||||
}
|
||||
|
||||
author := rev.Author()
|
||||
if author.Email == "" {
|
||||
author.Email = "nobody@example.com"
|
||||
}
|
||||
|
||||
oid, err := s.repo.CreateCommit("", author, rev.Committer(), message, tree, parents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
func (s *state) updateTarget() error {
|
||||
if s.config.Target == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if nil == s.result.Head() {
|
||||
return fmt.Errorf("Unable to create branch %s as it is empty (no commits were split)", s.config.Target)
|
||||
}
|
||||
|
||||
obj, ref, err := s.repo.RevparseExt(s.config.Target)
|
||||
if obj != nil {
|
||||
obj.Free()
|
||||
}
|
||||
if err != nil {
|
||||
ref, err = s.repo.References.Create(s.config.Target, s.result.Head(), false, "subtree split")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref.Free()
|
||||
} else {
|
||||
defer ref.Free()
|
||||
ref.SetTarget(s.result.Head(), "subtree split")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *state) legacyMessage(rev *git.Commit) string {
|
||||
subject, body := SplitMessage(rev.Message())
|
||||
return subject + "\n\n" + body
|
||||
}
|
||||
|
||||
// pushRevs sets the range to split
|
||||
func (s *state) pushRevs(revWalk *git.RevWalk) error {
|
||||
// this is needed as origin might be in the process of being updated by git.FetchOrigin()
|
||||
s.repoMu.Lock()
|
||||
defer s.repoMu.Unlock()
|
||||
|
||||
// find the latest split sha1 if any on origin
|
||||
var start *git.Oid
|
||||
var err error
|
||||
if s.config.Commit != "" {
|
||||
start, err = git.NewOid(s.config.Commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.result.moveHead(s.cache.get(start))
|
||||
return revWalk.PushRange(fmt.Sprintf("%s^..%s", start, s.originBranch))
|
||||
}
|
||||
|
||||
start = s.cache.getHead()
|
||||
if start != nil {
|
||||
s.result.moveHead(s.cache.get(start))
|
||||
// FIXME: CHECK that this is an ancestor of the branch?
|
||||
return revWalk.PushRange(fmt.Sprintf("%s..%s", start, s.originBranch))
|
||||
}
|
||||
|
||||
branch, err := s.repo.RevparseSingle(s.originBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return revWalk.Push(branch.Id())
|
||||
}
|
74
splitter/utils.go
Normal file
74
splitter/utils.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package splitter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/libgit2/git2go"
|
||||
)
|
||||
|
||||
var messageNormalizer = regexp.MustCompile("\\s*\\r?\\n")
|
||||
|
||||
// GitDirectory returns the .git directory for a given directory
|
||||
func GitDirectory(path string) string {
|
||||
gitPath := filepath.Join(path, ".git")
|
||||
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
||||
// this might be a bare repo
|
||||
return path
|
||||
}
|
||||
|
||||
return gitPath
|
||||
}
|
||||
|
||||
// SplitMessage splits a git message
|
||||
func SplitMessage(message string) (string, string) {
|
||||
// we split the messsage at \n\n or \r\n\r\n
|
||||
var subject, body string
|
||||
found := false
|
||||
for i := 0; i+4 <= len(message); i++ {
|
||||
if message[i] == '\n' && message[i+1] == '\n' {
|
||||
subject = message[0:i]
|
||||
body = message[i+2:]
|
||||
found = true
|
||||
break
|
||||
} else if message[i] == '\r' && message[i+1] == '\n' && message[i+2] == '\r' && message[i+3] == '\n' {
|
||||
subject = message[0:i]
|
||||
body = message[i+4:]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
subject = message
|
||||
body = ""
|
||||
}
|
||||
|
||||
// normalize \r\n and whitespaces
|
||||
subject = messageNormalizer.ReplaceAllLiteralString(subject, " ")
|
||||
|
||||
// remove spaces at the end of the subject
|
||||
subject = strings.TrimRight(subject, " ")
|
||||
body = strings.TrimLeft(body, "\r\n")
|
||||
return subject, body
|
||||
}
|
||||
|
||||
func normalizeOriginBranch(repo *git.Repository, origin string) (string, error) {
|
||||
if origin == "" {
|
||||
origin = "HEAD"
|
||||
}
|
||||
|
||||
obj, ref, err := repo.RevparseExt(origin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Bad revision for origin: %s", err)
|
||||
}
|
||||
if obj != nil {
|
||||
obj.Free()
|
||||
}
|
||||
defer ref.Free()
|
||||
|
||||
return ref.Name(), nil
|
||||
}
|
Loading…
Reference in a new issue