Tuesday, February 19, 2013

Rebasing in SVN

Suppose you are creating a branch from a trunk and you need to merge the commits from the trunk to the branch. Unlike Git, SVN does not really have a concept of rebasing. To do that in SVN, it requires quite a bit of work. To simplify the tasks of rebasing in SVN, I created small tools, one in Go and the other one in Python.
svnrebase.go
package main

import (
    "bufio"
    "bytes"
    "errors"
    "fmt"
    "io"
    "os"
    "os/exec"
    "strings"
)

const BaseRevFile = "base_rev.txt"

func getLatestBaseSVNRev(svnBaseURL string) (string, error) {
    out, e1 := exec.Command("svn", "info", svnBaseURL).Output()
    if e1 != nil {
        return "", errors.New("Unable to execute svn info " +
            svnBaseURL)
    }
    r := bufio.NewReader(bytes.NewReader(out))
    for {
        l, _, e2 := r.ReadLine()
        if e2 == io.EOF {
            break
        }
        s := string(l)
        if strings.Contains(s, "Revision:") {
            svnRev := strings.TrimSpace(strings.Split(s, ":")[1])
            return svnRev, nil
        }
    }
    return "", errors.New("Unable to get the SVN revision number")
}

func getOldBaseSVNRev() (string, error) {
    f, e := os.Open(BaseRevFile)
    if e != nil {
        return "", errors.New(BaseRevFile + " does not exist. " +
            "You need to create this file initially!")
    }
    defer f.Close()
    r := bufio.NewReader(f)
    l, _, _ := r.ReadLine()
    return strings.TrimSpace(string(l)), nil
}

func updateBaseSVNFile(newBaseRev string) error {
    f, e := os.OpenFile(BaseRevFile, os.O_WRONLY, 0666)
    if e != nil {
        return e
    }
    defer f.Close()
    f.Write([]byte(newBaseRev))
    return nil
}

func SVNRebase(svnBaseURL string, dryRun bool) error {
    oldBaseRev, e1 := getOldBaseSVNRev()
    if e1 != nil {
        return e1
    }
    newBaseRev, e2 := getLatestBaseSVNRev(svnBaseURL)
    if e2 != nil {
        return e2
    }
    if oldBaseRev == newBaseRev {
        fmt.Println("Your repo has already had the latest revision")
        return nil
    }
    cmd := "svn"
    args := []string{"merge"}
    if dryRun {
        args = append(args, "--dry-run")
    }
    args = append(args, svnBaseURL + "@" + oldBaseRev)
    args = append(args, svnBaseURL + "@" + newBaseRev)
    fmt.Println("Command:", cmd + " " + strings.Join(args, " "))
    c := exec.Command(cmd, args...)
    c.Stdout = os.Stdout
    c.Stdin = os.Stdout
    c.Stderr = os.Stderr
    e3 := c.Run()
    if e3 != nil {
        return e3
    }
    if !dryRun {
        updateBaseSVNFile(newBaseRev)
    }
    return nil
}

func printUsage() {
    fmt.Println("Usage:", os.Args[0], "<svn_base_url> [--dry-run]")
}

func validateArgs() {
    if len(os.Args) == 1 || len(os.Args) > 3 {
        printUsage()
        os.Exit(1)
    }
    if len(os.Args) == 3 {
        if os.Args[2] != "--dry-run" {
            fmt.Println("Error: Invalid option:", os.Args[2])
            os.Exit(1)
        }
    }
}

func main() {
    validateArgs()
    baseSVNURL := os.Args[1]
    dryRun := false
    if len(os.Args) == 3 {
        dryRun = true
    }
    if e := SVNRebase(baseSVNURL, dryRun); e != nil {
        fmt.Println("Error:", e)
    }
}
Usage: ./svnrebase  [--dry-run]
svnrebase.py
#!/usr/bin/env python
import sys, subprocess, os

BASE_REV_FILE = 'base_rev.txt'

def get_latest_base_svn_rev(svn_base_url):
    p = subprocess.Popen(['svn', 'info', svn_base_url], stdout=subprocess.PIPE)
    for line in p.communicate()[0].split(os.linesep):
        if line.startswith('Revision:'):
            return line.split(":")[1].strip()
    return None

def get_old_base_svn_rev():
    if not os.path.exists(BASE_REV_FILE):
        raise Exception(BASE_REV_FILE + ' does not exist. ' + 
                        'You need to create this file initially!')
    f = open(BASE_REV_FILE, 'r')
    return f.read().strip() 

def update_base_svn_file(new_base_rev):
     f = open(BASE_REV_FILE, 'w')
     f.write(new_base_rev)
     f.close()

def svn_rebase(svn_base_url, dry_run):
    old_base_rev = get_old_base_svn_rev()
    new_base_rev = get_latest_base_svn_rev(svn_base_url)
    if old_base_rev == new_base_rev:
        print 'Your repo has already had the latest revision'
        return
    cmd = ['svn', 'merge']
    if dry_run:
        cmd.append('--dry-run')
    cmd.append(svn_base_url + '@' + old_base_rev)
    cmd.append(svn_base_url + '@' + new_base_rev)
    print 'Command:', ' '.join(cmd)
    subprocess.call(cmd)
    if not dry_run:
       update_base_svn_file(new_base_rev)

def print_usage():
    print 'Usage:', sys.argv[0], '<svn_base_url> [--dry-run]'

def validate_args():
    if len(sys.argv) == 1 or len(sys.argv) > 3:
        print_usage()
        sys.exit(1)
    if len(sys.argv) == 3:
        if sys.argv[2] != '--dry-run':
            print 'Error: Invalid option:', sys.argv[2]
            sys.exit(1)

if __name__ == '__main__':
    validate_args()
    base_svn_url = sys.argv[1]
    dry_run = True if len(sys.argv) == 3 else False
    try:
        svn_rebase(base_svn_url, dry_run)
    except Exception, e:
        print 'Error:', e
Usage: ./svnrebase.py  [--dry-run]
Initially, you need to create base_rev.txt that specifies which revision your branch was created. Subsequently, the base_rev.txt will be automatically updated by the tool.