The idea is to import a cvs repository using git cvsimport; build a perfect history in git by cherry picking commits that are only in cvs but not in git; and export only summaries back to cvs. Cvs imports are organized on a separate git branch. git is used for merging. The differences can be sent back to cvs as a squashed commit together with a shortlog. Sent git commits are noted in the cvs commit message and will be ignored in subsequent cvs imports. To get the idea you can run t/t9250-git-cvstrack.sh and explore the git repository created in t/trash/gitwork. Signed-off-by: Steffen Prohaska <prohaska@xxxxxx> --- .gitignore | 4 + Documentation/git-cvstrack.txt | 128 +++++++++++++++++++ Makefile | 1 + git-cvstrack-fetch.sh | 52 ++++++++ git-cvstrack-init.sh | 20 +++ git-cvstrack-pull.sh | 10 ++ git-cvstrack-pushcvs.sh | 60 +++++++++ t/t9250-git-cvstrack.sh | 270 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 545 insertions(+), 0 deletions(-) create mode 100644 Documentation/git-cvstrack.txt create mode 100755 git-cvstrack-fetch.sh create mode 100755 git-cvstrack-init.sh create mode 100755 git-cvstrack-pull.sh create mode 100755 git-cvstrack-pushcvs.sh create mode 100755 t/t9250-git-cvstrack.sh I am using these scripts successfully for some time. What do you think? - Is the general concept useful? - Comments on the coding style? - Is using branches and tags to maintain the state a bad idea? diff --git a/.gitignore b/.gitignore index e8b060c..9924d07 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,10 @@ git-count-objects git-cvsexportcommit git-cvsimport git-cvsserver +git-cvstrack-fetch +git-cvstrack-init +git-cvstrack-pull +git-cvstrack-pushcvs git-daemon git-diff git-diff-files diff --git a/Documentation/git-cvstrack.txt b/Documentation/git-cvstrack.txt new file mode 100644 index 0000000..10f2de1 --- /dev/null +++ b/Documentation/git-cvstrack.txt @@ -0,0 +1,128 @@ +git-cvstrack(1) +============= + +NAME +---- +git-cvstrack - work on imported cvs and other git branches + +SYNOPSIS +-------- +'git-cvstrack-init' <cvsbranch> +'git-cvstrack-fetch' [<cvsbranch>] +'git-cvstrack-pull' <cvsbranch> +'git-cvstrack-pushcvs' <cvsbranch> <cvsworkingcopy> + +DESCRIPTION +----------- +git-cvstrack tracks cvs branches imported by git-cvsimport and provides a +mechanism to sent changes from git back to cvs squashed into a single commit. + +cvstrack can be used to start building a sane git history that integrates an +exiting, active cvs and work done in git. Eventually the cvs repository can +be closed and git will serve as the only SCM. At that time you already have a +sane history from the point you started to use cvstrack. + +The basic workflow is as follows. + +1) Initial import from cvs and clone a tracking repository + + git cvsimport -d /path/to/cvsroot -i -k -u -C cvsimported.git -o cvshead + git clone cvsimported.git cvstracker + +2) Initialize tracking + + cd cvstracker + git checkout master + git cvstrack-init cvshead + cd .. + +3) Incrementally import work from cvs + + git cvsimport -d /path/to/cvsroot -i -k -u -C cvsimported.git -o cvshead module + +4) Incrementally fetch to cvstracker + + cd cvstracker + git checkout master + git cvstrack-pull cvshead # or git-cvstrack-fetch to avoid merge + cd .. + +5) Work on a git branch (you may want to clone cvstracker first to separate + working branches from cvstrack) + + cd cvstracker + git checkout -b topic master + # edit files + git commit + cd .. + +6) Send changes to cvs + + cvs -d /path/to/cvsroot checkout -d cvsworkingcopy module # or cvs update + cd cvstracker + git checkout master + git merge topic + git cvstrack-pushcvs cvshead ../cvsworkingcopy + # follow instructions from cvstrack-pushcvs here: + # edit message and cvs commit + +You can do (3)-(6) in any order. Note however, it is recommended to merge topic +branches only if you plan to send them to cvs right away. It is a bad idea to +let master and cvs diverge too much. + +Technical Details +----------------- +git-cvstrack maintains its state using two git branches and one tag per cvs +branch. Commits imported by git-cvsimport are on origin/cvsbranch. From there +commits are cherry-picked to cvspending/cvsbranch. Only commits that did not +originate from git are cherry-picked. Commits originating from git are detected +by examining if the cvs commit message contains 'git-cvstrack-revs:'. The tag +cvspicked/cvsbranch marks the latest commit that was cherry picked. + +Because commits originating from git are filtered from the imported cvs branch +the git history only contains 'real' changes. The git history is in a sense a +perfect git history. Changes from cvs are handled as just another topic branch. +Merges sent to cvs are filtered and will not enter the git side of the history +twice. + +All the merging machinery of git can be used to merge git and cvs topic +branches. cvs is treated as a second level citizen. Changes done in git are +only sent back as a squashed commit. This is a good compromise because cvs +can't handle nonlinear history anyway. + +Advanced Topics +--------------- +You can track several cvsbranches by running git cvstrack-init multiple times. + +If a new cvs branch X is created after you started tracking with cvstrack +you'll need to rebase cvstrack/X created by git cvstrack-init X to the parent +of an existing cvstrack/... branch that corresponds to the cherry picked commit +of the root point of the cvs topic branch. + +You may need to fix the parent commit of a cvs topic branch. git-cvsimport +chooses the wrong parent for cvs topic branches if changes were commited to the +head between the creation of the cvs branch and the first commit to it. +See the following threads on the mailing list: + +http://marc.info/?l=git&m=118260312708709 +http://marc.info/?l=git&m=118262688431613 + +OPTIONS +------- +<cvsbranch>:: + Branch imported from cvs by git-cvsimport. + +<cvsworkingcopy>:: + CVS working copy that matches <cvsbranch>. + +Author +------ +Written by Steffen Prohaska <prohaska@xxxxxx> + +Documentation +------------- +Documentation by Steffen Prohaska <prohaska@xxxxxx> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Makefile b/Makefile index 5d60dc8..8026e38 100644 --- a/Makefile +++ b/Makefile @@ -212,6 +212,7 @@ SCRIPT_SH = \ git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \ git-merge-resolve.sh git-merge-ours.sh \ git-lost-found.sh git-quiltimport.sh git-submodule.sh \ + git-cvstrack-fetch.sh git-cvstrack-init.sh git-cvstrack-pull.sh git-cvstrack-pushcvs.sh \ git-filter-branch.sh SCRIPT_PERL = \ diff --git a/git-cvstrack-fetch.sh b/git-cvstrack-fetch.sh new file mode 100755 index 0000000..d4aa13b --- /dev/null +++ b/git-cvstrack-fetch.sh @@ -0,0 +1,52 @@ +#!/bin/sh + + +# need exactly one argument +USAGE="[<branch>]" +. git-sh-setup +[[ $# -le 1 ]] || usage + +if [[ $# -eq 1 ]] +then + branch=$1 +# which must be a remote branch + git branch -r | grep -q "/$branch\$" || die "Error: $branch must be a remote branch." + +# safety nets + git tag -l "cvspicked/$branch" >/dev/null || die "Error: missing tag cvspicked/$branch." + git branch | grep -q "cvspending/$branch" || die "Error: missing branch cvspending/$branch." + + branches=$branch +else + branches=$(git branch -l | cut -b 3- | grep ^cvspending/ | cut -b 12-) +fi + +# fetch and cherry pick +git fetch || exit 1 + +origbranch=$(git branch | grep '^\*' | cut -b3-) + +for branch in $branches ; do + echo "Fetching new cvs commits to cvspending/$branch" + git branch -r | grep -q "/$branch\$" || { "Error: missing remote branch $branch, skipping ..." ; continue ; } + git tag -l "cvspicked/$branch" >/dev/null || { "Error: missing tag cvspicked/$branch, skipping ..." ; continue ; } + + git checkout "cvspending/$branch" || exit 1 +#for r in $(git rev-list --pretty=oneline --reverse cvspicked/$branch..origin/$branch | grep -v '\[from git\]' | cut -d ' ' -f 1) ; do + for r in $(git rev-list --reverse "cvspicked/$branch..origin/$branch") ; do + git cat-file -p "$r" | egrep -q '^\[from git\]' && { echo "skipping $r" ; continue ; } + git cat-file -p "$r" | egrep -q '^git-cvstrack-revs: [0-9a-f]{40}\.\.[0-9a-f]{40}' && { echo "skipping $r" ; continue ; } + echo "picking $r" + output="$(git cherry-pick "$r")" || + { + ret=$? + echo "$output" + echo "$output" | grep -q "CONFLICT" && die "Error: git cherry-pick detected CONFLICT and exited with $ret." + echo "Warning: git cherry-pick exited with $ret but didn't report a severe problem, continuing." + } + done + rm -f .msg + git tag -f -m "cherry-picked cvs $branch up to here" "cvspicked/$branch" "origin/$branch" +done + +git checkout "$origbranch" diff --git a/git-cvstrack-init.sh b/git-cvstrack-init.sh new file mode 100755 index 0000000..cb60fe9 --- /dev/null +++ b/git-cvstrack-init.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# need exactly one argument +USAGE="<branch>" +. git-sh-setup +[[ $# == 1 ]] || usage + +branch=$1 + +# which must be a remote branch +git branch -r | grep -q "/$branch\$" || die "Error: $branch must be a remote branch" + +# safety nets +git tag -l "cvspicked/$branch" | grep -q "cvspicked/$branch" && die "Error: tag cvspicked/$branch already exists" +git branch | grep -q "cvspending/$branch" && die "Error: branch cvspending/$branch already exists" + + +# setup tracking +git tag -f -m "cherry-picked cvs $branch up to here" cvspicked/$branch origin/$branch || exit 1 +git branch --no-track "cvspending/$branch" "origin/$branch" || exit 1 diff --git a/git-cvstrack-pull.sh b/git-cvstrack-pull.sh new file mode 100755 index 0000000..3fd61c3 --- /dev/null +++ b/git-cvstrack-pull.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +USAGE="<branch>" +. git-sh-setup +[[ $# == 1 ]] || usage + +. git-cvstrack-fetch + +echo "Pulling new cvs commits" +git pull . "cvspending/$branch" diff --git a/git-cvstrack-pushcvs.sh b/git-cvstrack-pushcvs.sh new file mode 100755 index 0000000..85e85fa --- /dev/null +++ b/git-cvstrack-pushcvs.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# need exactly two arguments +USAGE="<branch> <cvsworkingcopy>" +. git-sh-setup +[[ $# == 2 ]] || usage + +branch=$1 +cvswc=$2 + +# branch must be a remote branch +git branch -r | grep -q "/$branch\$" || die "Error: $branch must be a remote branch." + +[[ -d "$cvswc/CVS" ]] || die "Error: $cvswc must be a CVS working copy" + +# safety nets +git tag -l "cvspicked/$branch" >/dev/null || die "Error: missing tag cvspicked/$branch." +git branch | grep -q "cvspending/$branch" || die "Error: missing branch cvspending/$branch." + +# User should have fetched and pulled. +# However, we don't require a fetch but enforce at least a local pull. +origbranch="$(git branch | grep '^\*' | cut -b3-)" +[[ $(git rev-parse "$origbranch") != $(git rev-parse "cvspending/$branch") ]] || { echo "Nothing to do. $origbranch and cvspending/$branch are identical." ; git checkout "$origbranch"; exit 0 ; } + +origbranchhead="$(git rev-parse HEAD)" + +git pull --no-commit . "cvspending/$branch" || die "local pull failed" +git commit -m "Merge cvspending/$branch into $origbranch (preparing cvstrack-pushcvs)" +git checkout "cvspending/$branch" || die "checkout cvspending/$branch failed" +from=$(git rev-parse HEAD) +git pull . "$origbranch" || die "local pull $origbranch failed" +to=$(git rev-parse HEAD) + +echo "applying changes to cvs working copy $cvswc" +cvsexportmsg="$( export GIT_DIR=$(cd "$GIT_DIR" ; pwd) ; cd "$cvswc" ; git cvsexportcommit -p -v -P "$from" "cvspending/$branch" )" \ + || \ + { + echo "git cvsexportcommit failed: rolling back changes to git repository." + git reset --hard "$from" + git checkout "$origbranch" + git reset --hard "$origbranchhead" + die "Error: git cvsexporcommit failed. Please carefully check message above." + } +commitcmd=$(echo "$cvsexportmsg" | egrep '^ *cvs commit -F .msg') + +echo "YOUR SUMMARY HERE" >$cvswc/.msg +echo "" >>$cvswc/.msg +git log $from..$to | git shortlog >>$cvswc/.msg +echo "" >>$cvswc/.msg +echo "git-cvstrack-revs: $from..$to" >>$cvswc/.msg + +git checkout "$origbranch" + +echo "" +echo "*Did not commit changes to CVS.*" +echo "You need to edit the message in" +echo " $cvswc/.msg" +echo "and commit with" +echo " (cd $cvswc ; $commitcmd)" +echo "" diff --git a/t/t9250-git-cvstrack.sh b/t/t9250-git-cvstrack.sh new file mode 100755 index 0000000..274ed1d --- /dev/null +++ b/t/t9250-git-cvstrack.sh @@ -0,0 +1,270 @@ +#!/bin/sh + +test_description='CVS export comit. ' + +. ./test-lib.sh + +cvs >/dev/null 2>&1 +if test $? -ne 1 +then + test_expect_success 'skipping git-cvstrack tests, cvs not found' : + test_done + exit +fi + +CVSROOT=$(pwd)/cvsroot +CVSWORK=$(pwd)/cvswork +export CVSROOT CVSWORK + +cvspscache="$HOME/.cvsps/$(echo $CVSROOT | sed -e 's%/%#%g')#src" +rm -f $cvspscache + +rm -rf "$CVSROOT" "$CVSWORK" +mkdir "$CVSROOT" && +cvs init && +mkdir "$CVSROOT/src" +cvs -Q co -d "$CVSWORK" src && +rm -rf .git || +exit 1 + +test_expect_success \ + 'initial cvs commit' \ + '( cd "$CVSWORK" && + echo "a: line 1" >>a.txt && + echo "b: line 1" >>b.txt && + cvs add a.txt b.txt && + cvs commit -m "cvs commit 1" + )' + +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'cloning git and initializing cvstrack' \ + 'git-clone cvstracker gitwork && + ( cd gitwork && + git-cvstrack-init cvshead + )' + +test_expect_success \ + 'second initialization should fail' \ + '( cd gitwork && + ! git-cvstrack-init cvshead + )' + +test_expect_success \ + 'initialization of non-existing branch should fail' \ + '( cd gitwork && + ! git-cvstrack-init wrong-name + )' + +test_expect_success \ + 'cvs commit 2' \ + '( cd "$CVSWORK" && + echo "a: line 2" >>a.txt && + echo "b: line 2" >>b.txt && + cvs commit -m "cvs commit 2" + )' + +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'cvs commit 3' \ + '( cd "$CVSWORK" && + echo "a: line 3" >>a.txt && + echo "b: line 3" >>b.txt && + cvs commit -m "cvs commit 3" + )' + +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'commiting to git' \ + '( cd gitwork && + git-checkout master && + echo "a: line 3 from git" >>a.txt && + git-add a.txt && + git-commit -m "git commit 1" && + echo "c: line 1" >>c.txt && + git-add c.txt && + git-commit -a -m "git commit 2" && + rm b.txt && + git-rm b.txt && + git-commit -a -m "git commit 3" + )' + +test_expect_success \ + 'tracking cvs commits 2-3' \ + '( cd gitwork && + git-cvstrack-fetch cvshead + )' + +test_expect_success \ + 'cvs commit 4' \ + '( cd "$CVSWORK" && + echo "a: line 4" >>a.txt && + echo "b: line 4" >>b.txt && + cvs commit -m "cvs commit 4" + )' + +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'tracking cvs commits 4' \ + '( cd gitwork && + git-cvstrack-fetch cvshead + )' + +test_expect_success \ + 'merging' \ + '( cd gitwork && + git-checkout master && + ! git-cvstrack-pull cvshead && + git-rm b.txt && + echo "a: line 1" >a.txt && + echo "a: line 2" >>a.txt && + echo "a: line 3" from git >>a.txt && + echo "a: line 4" >>a.txt && + git-add a.txt && + git-commit -m "Merge cvspending/cvshead into master (resolved conflicts)" + )' + +test_expect_success \ + 'cvs commit 5' \ + '( cd "$CVSWORK" && + echo "a: line 5" >>a.txt && + echo "b: line 5" >>b.txt && + cvs commit -m "cvs commit 5" + )' + +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'tracking cvs commits 5' \ + '( cd gitwork && + git-cvstrack-fetch cvshead + )' + +test_expect_success \ + 'merging' \ + '( cd gitwork && + git-checkout master && + ! git-cvstrack-pull cvshead && + git-rm b.txt && + git-commit -m "Merge cvspending/cvshead into master (resolved conflicts)" + )' + +test_expect_success \ + 'cvs commit 6' \ + '( cd "$CVSWORK" && + echo "a: line 6" >>a.txt && + cvs commit -m "cvs commit 6" + )' +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'tracking cvs commits 6' \ + '( cd gitwork && + git-cvstrack-fetch cvshead + )' + +test_expect_success \ + 'pushcvs' \ + '( cd gitwork && + git-cvstrack-pushcvs cvshead "$CVSWORK" + ) && + ( cd "$CVSWORK" && + cvs commit -F .msg && + rm .msg + )' +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'cvs commit 7' \ + '( cd "$CVSWORK" && + echo "a: line 7" >>a.txt && + cvs commit -m "cvs commit 7" + )' +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'tracking cvs commits 7' \ + '( cd gitwork && + git-cvstrack-fetch cvshead + )' + +test_expect_success \ + 'more work in git' \ + '( cd gitwork && + git-cvstrack-pull cvshead && + echo "c: line 2 from git" >>c.txt && + git-commit -a -m "another git commit" + )' + +test_expect_success \ + 'if pushcvs fails git repository must be unchanged' \ + '( rm "$CVSWORK/c.txt" && + cd gitwork && + before=$(find .git/refs -type f -print -exec cat \{\} \;) && + ! git-cvstrack-pushcvs cvshead "$CVSWORK" && + after=$(find .git/refs -type f -print -exec cat \{\} \;) && + test "$before" = "$after" + )' + +test_expect_success \ + 'pushcvs' \ + '( cd "$CVSWORK" && + cvs up + ) && + ( cd gitwork && + git-cvstrack-pushcvs cvshead "$CVSWORK" + ) && + ( cd "$CVSWORK" && + cvs commit -F .msg && + rm .msg + )' +# fighting cvsps' fuzz +sleep 2 + +test_expect_success \ + 'importing to git' \ + 'git-cvsimport -v -a -i -k -u -z 1 -a -C cvstracker -o cvshead src' + +test_expect_success \ + 'tracking cvs commits' \ + '( cd gitwork && + git-cvstrack-fetch cvshead + )' + +test_done -- 1.5.2.2.660.gc5eb3 - To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html