From 0ca71b3737cbb26fbf037aa15b3f58735785e6e3 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 14:13:34 -0400 Subject: basic options parsing and whatnot. --- .gitignore | 1 + git-subtree | 1 + git-subtree.sh | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ shellopts.sh | 1 + 4 files changed, 126 insertions(+) create mode 100644 .gitignore create mode 120000 git-subtree create mode 100755 git-subtree.sh create mode 100644 shellopts.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..b25c15b81f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/git-subtree b/git-subtree new file mode 120000 index 0000000000..7d7539894e --- /dev/null +++ b/git-subtree @@ -0,0 +1 @@ +git-subtree.sh \ No newline at end of file diff --git a/git-subtree.sh b/git-subtree.sh new file mode 100755 index 0000000000..5e5b27f8ad --- /dev/null +++ b/git-subtree.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# +# git-subtree.sh: split/join git repositories in subdirectories of this one +# +# Copyright (c) 2009 Avery Pennarun +# +OPTS_SPEC="\ +git subtree split -- +git subtree merge + +git subtree does foo and bar! +-- +h,help show the help +q quiet +v verbose +" +eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) +. git-sh-setup +require_work_tree + +quiet= +command= + +debug() +{ + if [ -z "$quiet" ]; then + echo "$@" >&2 + fi +} + +#echo "Options: $*" + +while [ $# -gt 0 ]; do + opt="$1" + shift + case "$opt" in + -q) quiet=1 ;; + --) break ;; + esac +done + +command="$1" +shift +case "$command" in + split|merge) ;; + *) die "Unknown command '$command'" ;; +esac + +revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $? +dirs="$(git rev-parse --sq --no-revs --no-flags "$@")" || exit $? + +#echo "dirs is {$dirs}" +eval $(echo set -- $dirs) +if [ "$#" -ne 1 ]; then + die "Must provide exactly one subtree dir (got $#)" +fi +dir="$1" + +debug "command: {$command}" +debug "quiet: {$quiet}" +debug "revs: {$revs}" +debug "dir: {$dir}" + +cache_setup() +{ + cachedir="$GIT_DIR/subtree-cache/$dir" + rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" + mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" + debug "Using cachedir: $cachedir" >&2 + echo "$cachedir" +} + +cache_get() +{ + for oldrev in $*; do + if [ -r "$cachedir/$oldrev" ]; then + read newrev <"$cachedir/$oldrev" + echo $newrev + fi + done +} + +cache_set() +{ + oldrev="$1" + newrev="$2" + if [ -e "$cachedir/$oldrev" ]; then + die "cache for $oldrev already exists!" + fi + echo "$newrev" >"$cachedir/$oldrev" +} + +cmd_split() +{ + debug "Splitting $dir..." + cache_setup || exit $? + + git rev-list --reverse --parents $revs -- "$dir" | + while read rev parents; do + newparents=$(cache_get $parents) + echo "rev: $rev / $newparents" + + git ls-tree $rev -- "$dir" | + while read mode type tree name; do + p="" + for parent in $newparents; do + p="$p -p $parent" + done + newrev=$(echo synthetic | git commit-tree $tree $p) \ + || die "Can't create new commit for $rev / $tree" + cache_set $rev $newrev + done + done + + exit 0 +} + +cmd_merge() +{ + die "merge command not implemented yet" +} + +"cmd_$command" diff --git a/shellopts.sh b/shellopts.sh new file mode 100644 index 0000000000..42644cd019 --- /dev/null +++ b/shellopts.sh @@ -0,0 +1 @@ +export PATH=$PWD:$PATH -- cgit v1.2.1 From 2573354e9b1619e92b8847d098555a0b6997c231 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 14:24:38 -0400 Subject: 'git subtree split' now basically works. --- git-subtree.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 5e5b27f8ad..c59759baa6 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -28,6 +28,16 @@ debug() fi } +assert() +{ + if "$@"; then + : + else + die "assertion failed: " "$@" + fi +} + + #echo "Options: $*" while [ $# -gt 0 ]; do @@ -63,7 +73,7 @@ debug "dir: {$dir}" cache_setup() { - cachedir="$GIT_DIR/subtree-cache/$dir" + cachedir="$GIT_DIR/subtree-cache/$$" rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" debug "Using cachedir: $cachedir" >&2 @@ -98,19 +108,23 @@ cmd_split() git rev-list --reverse --parents $revs -- "$dir" | while read rev parents; do newparents=$(cache_get $parents) - echo "rev: $rev / $newparents" + debug + debug "Processing commit: $rev / $newparents" git ls-tree $rev -- "$dir" | while read mode type tree name; do + assert [ "$name" = "$dir" ] + debug " tree is: $tree" p="" for parent in $newparents; do p="$p -p $parent" done newrev=$(echo synthetic | git commit-tree $tree $p) \ || die "Can't create new commit for $rev / $tree" + echo " newrev is: $newrev" cache_set $rev $newrev - done - done + done || exit $? + done || exit $? exit 0 } -- cgit v1.2.1 From fd9500eef2eba7e8f66f984c924a11282daaa870 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 14:45:02 -0400 Subject: We now copy the other stuff about a commit (changelog, author, etc). --- git-subtree.sh | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index c59759baa6..256946b0de 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -77,7 +77,6 @@ cache_setup() rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" debug "Using cachedir: $cachedir" >&2 - echo "$cachedir" } cache_get() @@ -100,6 +99,24 @@ cache_set() echo "$newrev" >"$cachedir/$oldrev" } +copy_commit() +{ + # We're doing to set some environment vars here, so + # do it in a subshell to get rid of them safely later + git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | + ( + read GIT_AUTHOR_NAME + read GIT_AUTHOR_EMAIL + read GIT_AUTHOR_DATE + read GIT_COMMITTER_NAME + read GIT_COMMITTER_EMAIL + read GIT_COMMITTER_DATE + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE + export GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_COMMITTER_DATE + git commit-tree "$2" $3 # reads the rest of stdin + ) || die "Can't copy commit $1" +} + cmd_split() { debug "Splitting $dir..." @@ -119,9 +136,9 @@ cmd_split() for parent in $newparents; do p="$p -p $parent" done - newrev=$(echo synthetic | git commit-tree $tree $p) \ - || die "Can't create new commit for $rev / $tree" - echo " newrev is: $newrev" + + newrev=$(copy_commit $rev $tree "$p") || exit $? + debug " newrev is: $newrev" cache_set $rev $newrev done || exit $? done || exit $? -- cgit v1.2.1 From e25a6bf837ba378b0a8264e580c61069951dce66 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 14:52:27 -0400 Subject: Print out the newly created commitid at the end, for use in other scripts. --- git-subtree.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 256946b0de..5f8b0f6c59 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -93,7 +93,7 @@ cache_set() { oldrev="$1" newrev="$2" - if [ -e "$cachedir/$oldrev" ]; then + if [ "$oldrev" != "latest" -a -e "$cachedir/$oldrev" ]; then die "cache for $oldrev already exists!" fi echo "$newrev" >"$cachedir/$oldrev" @@ -140,9 +140,14 @@ cmd_split() newrev=$(copy_commit $rev $tree "$p") || exit $? debug " newrev is: $newrev" cache_set $rev $newrev + cache_set latest $newrev done || exit $? done || exit $? - + latest=$(cache_get latest) + if [ -z "$latest" ]; then + die "No new revisions were found" + fi + echo $latest exit 0 } -- cgit v1.2.1 From b77172f8140d830a9160306c8ee10ceed413ba23 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 15:48:41 -0400 Subject: Add a new --rejoin option. The idea is to join the new split branch back into this one, so future splits can append themselves to the old split branch. We mark the split branch's history in our merge commit, so we can pull it back out later. --- git-subtree.sh | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 5f8b0f6c59..a31b0b2f6a 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -2,10 +2,10 @@ # # git-subtree.sh: split/join git repositories in subdirectories of this one # -# Copyright (c) 2009 Avery Pennarun +# Copyright (C) 2009 Avery Pennarun # OPTS_SPEC="\ -git subtree split -- +git subtree split [--rejoin] [--onto rev] -- git subtree merge git subtree does foo and bar! @@ -13,6 +13,8 @@ git subtree does foo and bar! h,help show the help q quiet v verbose +onto= existing subtree revision to connect, if any +rejoin merge the new branch back into HEAD " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) . git-sh-setup @@ -20,6 +22,8 @@ require_work_tree quiet= command= +onto= +rejoin= debug() { @@ -42,9 +46,12 @@ assert() while [ $# -gt 0 ]; do opt="$1" + debug "option: $1" shift case "$opt" in -q) quiet=1 ;; + --onto) onto="$1"; shift ;; + --rejoin) rejoin=1 ;; --) break ;; esac done @@ -93,7 +100,9 @@ cache_set() { oldrev="$1" newrev="$2" - if [ "$oldrev" != "latest" -a -e "$cachedir/$oldrev" ]; then + if [ "$oldrev" != "latest_old" \ + -a "$oldrev" != "latest_new" \ + -a -e "$cachedir/$oldrev" ]; then die "cache for $oldrev already exists!" fi echo "$newrev" >"$cachedir/$oldrev" @@ -111,12 +120,29 @@ copy_commit() read GIT_COMMITTER_NAME read GIT_COMMITTER_EMAIL read GIT_COMMITTER_DATE - export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE - export GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_COMMITTER_DATE + export GIT_AUTHOR_NAME \ + GIT_AUTHOR_EMAIL \ + GIT_AUTHOR_DATE \ + GIT_COMMITTER_NAME \ + GIT_COMMITTER_EMAIL \ + GIT_COMMITTER_DATE git commit-tree "$2" $3 # reads the rest of stdin ) || die "Can't copy commit $1" } +merge_msg() +{ + dir="$1" + latest_old="$2" + latest_new="$3" + cat <<-EOF + Split changes from '$dir/' into commit '$latest_new' + + git-subtree-dir: $dir + git-subtree-includes: $latest_old + EOF +} + cmd_split() { debug "Splitting $dir..." @@ -140,14 +166,23 @@ cmd_split() newrev=$(copy_commit $rev $tree "$p") || exit $? debug " newrev is: $newrev" cache_set $rev $newrev - cache_set latest $newrev + cache_set latest_new $newrev + cache_set latest_old $rev done || exit $? done || exit $? - latest=$(cache_get latest) - if [ -z "$latest" ]; then + latest_new=$(cache_get latest_new) + if [ -z "$latest_new" ]; then die "No new revisions were found" fi - echo $latest + + if [ -n "$rejoin" ]; then + debug "Merging split branch into HEAD..." + latest_old=$(cache_get latest_old) + git merge -s ours \ + -m "$(merge_msg $dir $latest_old $latest_new)" \ + $latest_new + fi + echo $latest_new exit 0 } -- cgit v1.2.1 From 8b4a77f2a16d730f7261d66991107ab404b3b4ac Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 16:48:08 -0400 Subject: Use information about prior splits to make sure merges work right. --- git-subtree.sh | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index a31b0b2f6a..af1d332e24 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -46,7 +46,6 @@ assert() while [ $# -gt 0 ]; do opt="$1" - debug "option: $1" shift case "$opt" in -q) quiet=1 ;; @@ -108,6 +107,30 @@ cache_set() echo "$newrev" >"$cachedir/$oldrev" } +find_existing_splits() +{ + debug "Looking for prior splits..." + dir="$1" + revs="$2" + git log --grep="^git-subtree-dir: $dir\$" \ + --pretty=format:'%s%n%n%b%nEND' "$revs" | + while read a b junk; do + case "$a" in + git-subtree-mainline:) main="$b" ;; + git-subtree-split:) sub="$b" ;; + *) + if [ -n "$main" -a -n "$sub" ]; then + debug " Prior: $main -> $sub" + cache_set $main $sub + echo "^$main^ ^$sub^" + main= + sub= + fi + ;; + esac + done +} + copy_commit() { # We're doing to set some environment vars here, so @@ -136,10 +159,11 @@ merge_msg() latest_old="$2" latest_new="$3" cat <<-EOF - Split changes from '$dir/' into commit '$latest_new' + Split '$dir/' into commit '$latest_new' git-subtree-dir: $dir - git-subtree-includes: $latest_old + git-subtree-mainline: $latest_old + git-subtree-split: $latest_new EOF } @@ -148,12 +172,20 @@ cmd_split() debug "Splitting $dir..." cache_setup || exit $? - git rev-list --reverse --parents $revs -- "$dir" | + unrevs="$(find_existing_splits "$dir" "$revs")" + + git rev-list --reverse --parents $revs $unrevs -- "$dir" | while read rev parents; do + exists=$(cache_get $rev) newparents=$(cache_get $parents) debug debug "Processing commit: $rev / $newparents" + if [ -n "$exists" ]; then + debug " prior: $exists" + continue + fi + git ls-tree $rev -- "$dir" | while read mode type tree name; do assert [ "$name" = "$dir" ] -- cgit v1.2.1 From 33ff583ad7a52edb5ed9d869109e04c846d6209d Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 17:05:14 -0400 Subject: Added a --onto option, but it's so complicated I can't tell if it works. --- git-subtree.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index af1d332e24..7e1707ae2a 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -13,7 +13,7 @@ git subtree does foo and bar! h,help show the help q quiet v verbose -onto= existing subtree revision to connect, if any +onto= existing subtree revision to search for parent rejoin merge the new branch back into HEAD " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) @@ -172,6 +172,16 @@ cmd_split() debug "Splitting $dir..." cache_setup || exit $? + if [ -n "$onto" ]; then + echo "Reading history for $onto..." + git rev-list $onto | + while read rev; do + # the 'onto' history is already just the subdir, so + # any parent we find there can be used verbatim + cache_set $rev $rev + done + fi + unrevs="$(find_existing_splits "$dir" "$revs")" git rev-list --reverse --parents $revs $unrevs -- "$dir" | -- cgit v1.2.1 From 2c71b7c46d1ff887fb276a9d42346678a2948a00 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 17:42:33 -0400 Subject: Hmm... can't actually filter rev-list on the subdir name. Otherwise we can't keep track of parent relationships. Argh. This change makes it "work", but we get a bunch of empty commits. --- git-subtree.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 7e1707ae2a..1e1237f520 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -173,28 +173,31 @@ cmd_split() cache_setup || exit $? if [ -n "$onto" ]; then - echo "Reading history for $onto..." + echo "Reading history for --onto=$onto..." git rev-list $onto | while read rev; do # the 'onto' history is already just the subdir, so # any parent we find there can be used verbatim + debug " cache: $rev" cache_set $rev $rev done fi unrevs="$(find_existing_splits "$dir" "$revs")" - git rev-list --reverse --parents $revs $unrevs -- "$dir" | + debug "git rev-list --reverse $revs $unrevs" + git rev-list --reverse --parents $revs $unrevsx | while read rev parents; do - exists=$(cache_get $rev) - newparents=$(cache_get $parents) debug - debug "Processing commit: $rev / $newparents" - + debug "Processing commit: $rev" + exists=$(cache_get $rev) if [ -n "$exists" ]; then debug " prior: $exists" continue fi + debug " parents: $parents" + newparents=$(cache_get $parents) + debug " newparents: $newparents" git ls-tree $rev -- "$dir" | while read mode type tree name; do -- cgit v1.2.1 From 768d6d10051cbf2d0261b9cf671b1f969fb77a61 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 17:53:10 -0400 Subject: Skip over empty commits. But we still need to get rid of unnecessary merge commits somehow... --- git-subtree.sh | 55 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 1e1237f520..7ae71886f4 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -167,6 +167,32 @@ merge_msg() EOF } +tree_for_commit() +{ + git ls-tree "$1" -- "$dir" | + while read mode type tree name; do + assert [ "$name" = "$dir" ] + echo $tree + break + done +} + +tree_changed() +{ + tree=$1 + shift + if [ $# -ne 1 ]; then + return 0 # weird parents, consider it changed + else + ptree=$(tree_for_commit $1) + if [ "$ptree" != "$tree" ]; then + return 0 # changed + else + return 1 # not changed + fi + fi +} + cmd_split() { debug "Splitting $dir..." @@ -199,21 +225,24 @@ cmd_split() newparents=$(cache_get $parents) debug " newparents: $newparents" - git ls-tree $rev -- "$dir" | - while read mode type tree name; do - assert [ "$name" = "$dir" ] - debug " tree is: $tree" - p="" - for parent in $newparents; do - p="$p -p $parent" - done + tree=$(tree_for_commit $rev) + debug " tree is: $tree" + [ -z $tree ] && continue + + p="" + for parent in $newparents; do + p="$p -p $parent" + done + if tree_changed $tree $parents; then newrev=$(copy_commit $rev $tree "$p") || exit $? - debug " newrev is: $newrev" - cache_set $rev $newrev - cache_set latest_new $newrev - cache_set latest_old $rev - done || exit $? + else + newrev="$newparents" + fi + debug " newrev is: $newrev" + cache_set $rev $newrev + cache_set latest_new $newrev + cache_set latest_old $rev done || exit $? latest_new=$(cache_get latest_new) if [ -z "$latest_new" ]; then -- cgit v1.2.1 From 847e868167eb0e8a270004cbccec4ae36db06626 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 21:35:50 -0400 Subject: Quick test script for generating reasonably complex merge scenarios. --- git-subtree.sh | 5 +-- test.sh | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100755 test.sh diff --git a/git-subtree.sh b/git-subtree.sh index 7ae71886f4..ffffb5ed88 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -149,6 +149,7 @@ copy_commit() GIT_COMMITTER_NAME \ GIT_COMMITTER_EMAIL \ GIT_COMMITTER_DATE + (echo -n '*'; cat ) | # FIXME git commit-tree "$2" $3 # reads the rest of stdin ) || die "Can't copy commit $1" } @@ -199,7 +200,7 @@ cmd_split() cache_setup || exit $? if [ -n "$onto" ]; then - echo "Reading history for --onto=$onto..." + debug "Reading history for --onto=$onto..." git rev-list $onto | while read rev; do # the 'onto' history is already just the subdir, so @@ -254,7 +255,7 @@ cmd_split() latest_old=$(cache_get latest_old) git merge -s ours \ -m "$(merge_msg $dir $latest_old $latest_new)" \ - $latest_new + $latest_new >&2 fi echo $latest_new exit 0 diff --git a/test.sh b/test.sh new file mode 100755 index 0000000000..8a9d92f703 --- /dev/null +++ b/test.sh @@ -0,0 +1,96 @@ +#!/bin/bash -x +. shellopts.sh +set -e + +rm -rf mainline subproj +mkdir mainline subproj + +cd subproj +git init + +touch sub1 +git add sub1 +git commit -m 'sub-1' +git branch sub1 +git branch -m master subproj + +touch sub2 +git add sub2 +git commit -m 'sub-2' +git branch sub2 + +touch sub3 +git add sub3 +git commit -m 'sub-3' +git branch sub3 + +cd ../mainline +git init +touch main1 +git add main1 +git commit -m 'main-1' +git branch -m master mainline + +git fetch ../subproj sub1 +git branch sub1 FETCH_HEAD +git read-tree --prefix=subdir FETCH_HEAD +git checkout subdir +git commit -m 'initial-subdir-merge' + +git merge -m 'merge -s -ours' -s ours FETCH_HEAD + +touch subdir/main-sub3 +git add subdir/main-sub3 +git commit -m 'main-sub3' + +touch main-2 +git add main-2 +git commit -m 'main-2 boring' + +touch subdir/main-sub4 +git add subdir/main-sub4 +git commit -m 'main-sub4' + +git fetch ../subproj sub2 +git branch sub2 FETCH_HEAD +git merge -s subtree FETCH_HEAD +git branch pre-split + +split1=$(git subtree split --onto FETCH_HEAD subdir --rejoin) +echo "split1={$split1}" +git branch split1 "$split1" + +touch subdir/main-sub5 +git add subdir/main-sub5 +git commit -m 'main-sub5' + +cd ../subproj +git fetch ../mainline split1 +git branch split1 FETCH_HEAD +git merge FETCH_HEAD + +touch sub6 +git add sub6 +git commit -m 'sub6' + +cd ../mainline +split2=$(git subtree split subdir --rejoin) +git branch split2 "$split2" + +touch subdir/main-sub7 +git add subdir/main-sub7 +git commit -m 'main-sub7' + +split3=$(git subtree split subdir --rejoin) +git branch split3 "$split3" + +cd ../subproj +git fetch ../mainline split3 +git branch split3 FETCH_HEAD +git merge FETCH_HEAD +git branch subproj-merge-split3 + +cd ../mainline +git fetch ../subproj subproj-merge-split3 +git branch subproj-merge-split3 FETCH_HEAD +git merge subproj-merge-split3 -- cgit v1.2.1 From 210d083904914dd4668e032870f92ff0d5d441cc Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 21:49:19 -0400 Subject: Prune out some extra merge commits by comparing their parents correctly. --- git-subtree.sh | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index ffffb5ed88..e6d8ce8817 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -168,9 +168,17 @@ merge_msg() EOF } -tree_for_commit() +toptree_for_commit() { - git ls-tree "$1" -- "$dir" | + commit="$1" + git log -1 --pretty=format:'%T' "$commit" -- || exit $? +} + +subtree_for_commit() +{ + commit="$1" + dir="$2" + git ls-tree "$commit" -- "$dir" | while read mode type tree name; do assert [ "$name" = "$dir" ] echo $tree @@ -185,7 +193,7 @@ tree_changed() if [ $# -ne 1 ]; then return 0 # weird parents, consider it changed else - ptree=$(tree_for_commit $1) + ptree=$(toptree_for_commit $1) if [ "$ptree" != "$tree" ]; then return 0 # changed else @@ -226,7 +234,7 @@ cmd_split() newparents=$(cache_get $parents) debug " newparents: $newparents" - tree=$(tree_for_commit $rev) + tree=$(subtree_for_commit $rev "$dir") debug " tree is: $tree" [ -z $tree ] && continue @@ -235,7 +243,7 @@ cmd_split() p="$p -p $parent" done - if tree_changed $tree $parents; then + if tree_changed $tree $newparents; then newrev=$(copy_commit $rev $tree "$p") || exit $? else newrev="$newparents" -- cgit v1.2.1 From d691265880abea4428783beb858683be56f3b340 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 22:05:30 -0400 Subject: Even more aggressive commit trimming. Now we cut out a commit if any of its parents had the same tree; just use that parent in its place. This makes the history look nice, but I don't think it's quite right... --- git-subtree.sh | 34 ++++++++++++++++++++++++---------- test.sh | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index e6d8ce8817..03107e2251 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -202,6 +202,29 @@ tree_changed() fi } +copy_or_skip() +{ + rev="$1" + tree="$2" + newparents="$3" + assert [ -n "$tree" ] + + p="" + for parent in $newparents; do + ptree=$(toptree_for_commit $parent) || exit $? + if [ "$ptree" = "$tree" ]; then + # any identical parent means this commit is unnecessary + echo $parent + return 0 + elif [ -n "$ptree" ]; then + # an existing, non-identical parent is important + p="$p -p $parent" + fi + done + + copy_commit $rev $tree "$p" || exit $? +} + cmd_split() { debug "Splitting $dir..." @@ -238,16 +261,7 @@ cmd_split() debug " tree is: $tree" [ -z $tree ] && continue - p="" - for parent in $newparents; do - p="$p -p $parent" - done - - if tree_changed $tree $newparents; then - newrev=$(copy_commit $rev $tree "$p") || exit $? - else - newrev="$newparents" - fi + newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? debug " newrev is: $newrev" cache_set $rev $newrev cache_set latest_new $newrev diff --git a/test.sh b/test.sh index 8a9d92f703..a8ed0dbc7d 100755 --- a/test.sh +++ b/test.sh @@ -93,4 +93,4 @@ git branch subproj-merge-split3 cd ../mainline git fetch ../subproj subproj-merge-split3 git branch subproj-merge-split3 FETCH_HEAD -git merge subproj-merge-split3 +git merge -s subtree subproj-merge-split3 -- cgit v1.2.1 From 96db2c0448c2f6040c098d73570a96413338c662 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 22:36:06 -0400 Subject: Okay, that was a little too aggressive. Now we only prune out a commit if it has exactly one remaining parent and that parent's tree is identical to ours. But I also changed the test to create the initial "-s ours" merge in one step instead of two, and that merge can be eliminated since one of its parents doesn't affect the subdir at all, and is thus deleted. --- git-subtree.sh | 46 ++++++++++++++++++++++++++++++++-------------- test.sh | 5 ++++- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 03107e2251..d42cc1a164 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -4,17 +4,21 @@ # # Copyright (C) 2009 Avery Pennarun # +if [ $# -eq 0 ]; then + set -- -h +fi OPTS_SPEC="\ -git subtree split [--rejoin] [--onto rev] -- +git subtree split [options...] -- git subtree merge git subtree does foo and bar! -- -h,help show the help -q quiet -v verbose -onto= existing subtree revision to search for parent -rejoin merge the new branch back into HEAD +h,help show the help +q quiet +v verbose +onto= try connecting new tree to an existing one +rejoin merge the new branch back into HEAD +ignore-joins ignore prior --rejoin commits " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) . git-sh-setup @@ -24,6 +28,7 @@ quiet= command= onto= rejoin= +ignore_joins= debug() { @@ -50,7 +55,11 @@ while [ $# -gt 0 ]; do case "$opt" in -q) quiet=1 ;; --onto) onto="$1"; shift ;; + --no-onto) onto= ;; --rejoin) rejoin=1 ;; + --no-rejoin) rejoin= ;; + --ignore-joins) ignore_joins=1 ;; + --no-ignore-joins) ignore_joins= ;; --) break ;; esac done @@ -209,20 +218,25 @@ copy_or_skip() newparents="$3" assert [ -n "$tree" ] - p="" + identical= + p= for parent in $newparents; do ptree=$(toptree_for_commit $parent) || exit $? if [ "$ptree" = "$tree" ]; then - # any identical parent means this commit is unnecessary - echo $parent - return 0 - elif [ -n "$ptree" ]; then - # an existing, non-identical parent is important + # an identical parent could be used in place of this rev. + identical="$parent" + fi + if [ -n "$ptree" ]; then + parentmatch="$parentmatch$parent" p="$p -p $parent" fi done - copy_commit $rev $tree "$p" || exit $? + if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then + echo $identical + else + copy_commit $rev $tree "$p" || exit $? + fi } cmd_split() @@ -241,7 +255,11 @@ cmd_split() done fi - unrevs="$(find_existing_splits "$dir" "$revs")" + if [ -n "$ignore_joins" ]; then + unrevs= + else + unrevs="$(find_existing_splits "$dir" "$revs")" + fi debug "git rev-list --reverse $revs $unrevs" git rev-list --reverse --parents $revs $unrevsx | diff --git a/test.sh b/test.sh index a8ed0dbc7d..39c4382f0d 100755 --- a/test.sh +++ b/test.sh @@ -35,7 +35,10 @@ git fetch ../subproj sub1 git branch sub1 FETCH_HEAD git read-tree --prefix=subdir FETCH_HEAD git checkout subdir -git commit -m 'initial-subdir-merge' +tree=$(git write-tree) +com=$(echo initial-subdir-merge | git commit-tree $tree -p HEAD -p FETCH_HEAD) +git reset $com +#git commit -m 'initial-subdir-merge' git merge -m 'merge -s -ours' -s ours FETCH_HEAD -- cgit v1.2.1 From 9a8821ff33d5abc9d2603dd91c04872016d40982 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 22:57:14 -0400 Subject: Pass the path using the --prefix option instead of on the command line. I like this better. It's more like git-read-tree and some other commands. --- git-subtree.sh | 28 ++++++++++++++++------------ test.sh | 6 +++--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index d42cc1a164..0c7b755cf1 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -8,14 +8,12 @@ if [ $# -eq 0 ]; then set -- -h fi OPTS_SPEC="\ -git subtree split [options...] -- +git subtree split [options...] <--prefix=prefix> -- git subtree merge - -git subtree does foo and bar! -- h,help show the help q quiet -v verbose +prefix= the name of the subdir to split out onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD ignore-joins ignore prior --rejoin commits @@ -54,6 +52,8 @@ while [ $# -gt 0 ]; do shift case "$opt" in -q) quiet=1 ;; + --prefix) prefix="$1"; shift ;; + --no-prefix) prefix= ;; --onto) onto="$1"; shift ;; --no-onto) onto= ;; --rejoin) rejoin=1 ;; @@ -72,14 +72,16 @@ case "$command" in esac revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $? -dirs="$(git rev-parse --sq --no-revs --no-flags "$@")" || exit $? -#echo "dirs is {$dirs}" -eval $(echo set -- $dirs) -if [ "$#" -ne 1 ]; then - die "Must provide exactly one subtree dir (got $#)" +if [ -z "$prefix" ]; then + die "You must provide the --prefix option." +fi +dir="$prefix" + +dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? +if [ -n "$dirs" ]; then + die "Error: Use --prefix instead of bare filenames." fi -dir="$1" debug "command: {$command}" debug "quiet: {$quiet}" @@ -261,8 +263,10 @@ cmd_split() unrevs="$(find_existing_splits "$dir" "$revs")" fi - debug "git rev-list --reverse $revs $unrevs" - git rev-list --reverse --parents $revs $unrevsx | + # We can't restrict rev-list to only "$dir" here, because that leaves out + # critical information about commit parents. + debug "git rev-list --reverse --parents $revs $unrevs" + git rev-list --reverse --parents $revs $unrevs | while read rev parents; do debug debug "Processing commit: $rev" diff --git a/test.sh b/test.sh index 39c4382f0d..dac9b3559a 100755 --- a/test.sh +++ b/test.sh @@ -59,7 +59,7 @@ git branch sub2 FETCH_HEAD git merge -s subtree FETCH_HEAD git branch pre-split -split1=$(git subtree split --onto FETCH_HEAD subdir --rejoin) +split1=$(git subtree split --prefix subdir --onto FETCH_HEAD --rejoin) echo "split1={$split1}" git branch split1 "$split1" @@ -77,14 +77,14 @@ git add sub6 git commit -m 'sub6' cd ../mainline -split2=$(git subtree split subdir --rejoin) +split2=$(git subtree split --prefix subdir --rejoin) git branch split2 "$split2" touch subdir/main-sub7 git add subdir/main-sub7 git commit -m 'main-sub7' -split3=$(git subtree split subdir --rejoin) +split3=$(git subtree split --prefix subdir --rejoin) git branch split3 "$split3" cd ../subproj -- cgit v1.2.1 From eb7b590c8c63994051545bf1fe1f650f5dc4aedb Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 23:28:30 -0400 Subject: Add a new 'git subtree add' command for adding subtrees from a given rev. --- git-subtree.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 0c7b755cf1..2dc99e82cd 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -8,7 +8,8 @@ if [ $# -eq 0 ]; then set -- -h fi OPTS_SPEC="\ -git subtree split [options...] <--prefix=prefix> -- +git subtree add <--prefix=prefix +git subtree split [options...] <--prefix=prefix> git subtree merge -- h,help show the help @@ -67,11 +68,12 @@ done command="$1" shift case "$command" in - split|merge) ;; + add|merge) default= ;; + split) default="--default HEAD" ;; *) die "Unknown command '$command'" ;; esac -revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $? +revs=$(git rev-parse $default --revs-only "$@") || exit $? if [ -z "$prefix" ]; then die "You must provide the --prefix option." @@ -87,6 +89,7 @@ debug "command: {$command}" debug "quiet: {$quiet}" debug "revs: {$revs}" debug "dir: {$dir}" +debug cache_setup() { @@ -165,6 +168,20 @@ copy_commit() ) || die "Can't copy commit $1" } +add_msg() +{ + dir="$1" + latest_old="$2" + latest_new="$3" + cat <<-EOF + Add '$dir/' from commit '$latest_new' + + git-subtree-dir: $dir + git-subtree-mainline: $latest_old + git-subtree-split: $latest_new + EOF +} + merge_msg() { dir="$1" @@ -241,6 +258,39 @@ copy_or_skip() fi } +cmd_add() +{ + if [ -e "$dir" ]; then + die "'$dir' already exists. Cannot add." + fi + if ! git diff-index HEAD --exit-code --quiet; then + die "Working tree has modifications. Cannot add." + fi + if ! git diff-index --cached HEAD --exit-code --quiet; then + die "Index has modifications. Cannot add." + fi + set -- $revs + if [ $# -ne 1 ]; then + die "You must provide exactly one revision. Got: '$revs'" + fi + rev="$1" + + debug "Adding $dir as '$rev'..." + git read-tree --prefix="$dir" $rev || exit $? + git checkout "$dir" || exit $? + tree=$(git write-tree) || exit $? + + headrev=$(git rev-parse HEAD) || exit $? + if [ -n "$headrev" -a "$headrev" != "$rev" ]; then + headp="-p $headrev" + else + headp= + fi + commit=$(add_msg "$dir" "$headrev" "$rev" | + git commit-tree $tree $headp -p "$rev") || exit $? + git reset "$commit" || exit $? +} + cmd_split() { debug "Splitting $dir..." -- cgit v1.2.1 From 13648af5eecd0880a64b6ea4b14f485f53daa2f1 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 24 Apr 2009 23:41:19 -0400 Subject: Add 'git subtree merge' and 'git subtree pull'. These are simple shortcuts for 'git merge -s subtree' and 'git pull -s subtree', but it makes sense to have it all in one command. --- git-subtree.sh | 55 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 2dc99e82cd..f2a1c6aae4 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -8,13 +8,15 @@ if [ $# -eq 0 ]; then set -- -h fi OPTS_SPEC="\ -git subtree add <--prefix=prefix -git subtree split [options...] <--prefix=prefix> -git subtree merge +git subtree add --prefix= +git subtree split [options...] --prefix= +git subtree merge --prefix= +git subtree pull --prefix= -- h,help show the help q quiet prefix= the name of the subdir to split out + options for 'split' onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD ignore-joins ignore prior --rejoin commits @@ -68,27 +70,29 @@ done command="$1" shift case "$command" in - add|merge) default= ;; + add|merge|pull) default= ;; split) default="--default HEAD" ;; *) die "Unknown command '$command'" ;; esac -revs=$(git rev-parse $default --revs-only "$@") || exit $? - if [ -z "$prefix" ]; then die "You must provide the --prefix option." fi dir="$prefix" -dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? -if [ -n "$dirs" ]; then - die "Error: Use --prefix instead of bare filenames." +if [ "$command" != "pull" ]; then + revs=$(git rev-parse $default --revs-only "$@") || exit $? + dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? + if [ -n "$dirs" ]; then + die "Error: Use --prefix instead of bare filenames." + fi fi debug "command: {$command}" debug "quiet: {$quiet}" debug "revs: {$revs}" debug "dir: {$dir}" +debug "opts: {$*}" debug cache_setup() @@ -258,17 +262,23 @@ copy_or_skip() fi } -cmd_add() +ensure_clean() { - if [ -e "$dir" ]; then - die "'$dir' already exists. Cannot add." - fi if ! git diff-index HEAD --exit-code --quiet; then die "Working tree has modifications. Cannot add." fi if ! git diff-index --cached HEAD --exit-code --quiet; then die "Index has modifications. Cannot add." fi +} + +cmd_add() +{ + if [ -e "$dir" ]; then + die "'$dir' already exists. Cannot add." + fi + ensure_clean + set -- $revs if [ $# -ne 1 ]; then die "You must provide exactly one revision. Got: '$revs'" @@ -357,7 +367,22 @@ cmd_split() cmd_merge() { - die "merge command not implemented yet" + ensure_clean + + set -- $revs + if [ $# -ne 1 ]; then + die "You must provide exactly one revision. Got: '$revs'" + fi + rev="$1" + + git merge -s subtree $rev +} + +cmd_pull() +{ + ensure_clean + set -x + git pull -s subtree "$@" } -"cmd_$command" +"cmd_$command" "$@" -- cgit v1.2.1 From b9de53532c3e7aa6b01aa188e0f0f17a266c099d Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 25 Apr 2009 00:06:45 -0400 Subject: Handle it successfully if a given parent commit has no parents. --- git-subtree.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index f2a1c6aae4..aeafadac95 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -125,6 +125,16 @@ cache_set() echo "$newrev" >"$cachedir/$oldrev" } +# if a commit doesn't have a parent, this might not work. But we only want +# to remove the parent from the rev-list, and since it doesn't exist, it won't +# be there anyway, so do nothing in that case. +try_remove_previous() +{ + if git rev-parse "$1^" >/dev/null 2>&1; then + echo "^$1^" + fi +} + find_existing_splits() { debug "Looking for prior splits..." @@ -140,7 +150,8 @@ find_existing_splits() if [ -n "$main" -a -n "$sub" ]; then debug " Prior: $main -> $sub" cache_set $main $sub - echo "^$main^ ^$sub^" + try_remove_previous "$main" + try_remove_previous "$sub" main= sub= fi -- cgit v1.2.1 From a13a299996725a979f5a7d6bd878b0237de0f26d Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 25 Apr 2009 00:07:04 -0400 Subject: Change test.sh to test the new add, merge, and pull commands. --- test.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test.sh b/test.sh index dac9b3559a..16fb8f217e 100755 --- a/test.sh +++ b/test.sh @@ -33,13 +33,9 @@ git branch -m master mainline git fetch ../subproj sub1 git branch sub1 FETCH_HEAD -git read-tree --prefix=subdir FETCH_HEAD -git checkout subdir -tree=$(git write-tree) -com=$(echo initial-subdir-merge | git commit-tree $tree -p HEAD -p FETCH_HEAD) -git reset $com -#git commit -m 'initial-subdir-merge' +git subtree add --prefix=subdir FETCH_HEAD +# this shouldn't actually do anything, since FETCH_HEAD is already a parent git merge -m 'merge -s -ours' -s ours FETCH_HEAD touch subdir/main-sub3 @@ -56,7 +52,7 @@ git commit -m 'main-sub4' git fetch ../subproj sub2 git branch sub2 FETCH_HEAD -git merge -s subtree FETCH_HEAD +git subtree merge --prefix=subdir FETCH_HEAD git branch pre-split split1=$(git subtree split --prefix subdir --onto FETCH_HEAD --rejoin) @@ -96,4 +92,4 @@ git branch subproj-merge-split3 cd ../mainline git fetch ../subproj subproj-merge-split3 git branch subproj-merge-split3 FETCH_HEAD -git merge -s subtree subproj-merge-split3 +git subtree pull --prefix=subdir ../subproj subproj-merge-split3 -- cgit v1.2.1 From 8c384754d8db80f74d3916100772171753bee4f9 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 08:53:14 -0400 Subject: todo list --- todo | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 todo diff --git a/todo b/todo new file mode 100644 index 0000000000..b5b8e257aa --- /dev/null +++ b/todo @@ -0,0 +1,8 @@ + + delete tempdir + --annotate-sometimes: only annotate if the patch also changes files + outside the subdir? + 'git subtree rejoin' option to do the same as --rejoin, eg. after a + rebase + "-s subtree" should be given an explicit subtree option? + \ No newline at end of file -- cgit v1.2.1 From d0eb1b1417b855b262b5ad31d025840ea6e52094 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 08:59:12 -0400 Subject: Add --annotate option, and create recognizable file content during tests. --- git-subtree.sh | 6 +++++- test.sh | 67 ++++++++++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index aeafadac95..e54651c336 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -17,6 +17,7 @@ h,help show the help q quiet prefix= the name of the subdir to split out options for 'split' +annotate= add a prefix to commit message of new commits onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD ignore-joins ignore prior --rejoin commits @@ -30,6 +31,7 @@ command= onto= rejoin= ignore_joins= +annotate= debug() { @@ -55,6 +57,8 @@ while [ $# -gt 0 ]; do shift case "$opt" in -q) quiet=1 ;; + --annotate) annotate="$1"; shift ;; + --no-annotate) annotate= ;; --prefix) prefix="$1"; shift ;; --no-prefix) prefix= ;; --onto) onto="$1"; shift ;; @@ -178,7 +182,7 @@ copy_commit() GIT_COMMITTER_NAME \ GIT_COMMITTER_EMAIL \ GIT_COMMITTER_DATE - (echo -n '*'; cat ) | # FIXME + (echo -n "$annotate"; cat ) | git commit-tree "$2" $3 # reads the rest of stdin ) || die "Can't copy commit $1" } diff --git a/test.sh b/test.sh index 16fb8f217e..85ed7ce549 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,11 @@ #!/bin/bash -x +create() +{ + for d in 1 2 3 4 5 6 7 8 9 10; do + echo "$1" + done >"$1" +} + . shellopts.sh set -e @@ -8,27 +15,27 @@ mkdir mainline subproj cd subproj git init -touch sub1 +create sub1 git add sub1 -git commit -m 'sub-1' +git commit -m 'sub1' git branch sub1 git branch -m master subproj -touch sub2 +create sub2 git add sub2 -git commit -m 'sub-2' +git commit -m 'sub2' git branch sub2 -touch sub3 +create sub3 git add sub3 -git commit -m 'sub-3' +git commit -m 'sub3' git branch sub3 cd ../mainline git init -touch main1 -git add main1 -git commit -m 'main-1' +create main4 +git add main4 +git commit -m 'main4' git branch -m master mainline git fetch ../subproj sub1 @@ -38,49 +45,49 @@ git subtree add --prefix=subdir FETCH_HEAD # this shouldn't actually do anything, since FETCH_HEAD is already a parent git merge -m 'merge -s -ours' -s ours FETCH_HEAD -touch subdir/main-sub3 -git add subdir/main-sub3 -git commit -m 'main-sub3' +create subdir/main-sub5 +git add subdir/main-sub5 +git commit -m 'main-sub5' -touch main-2 -git add main-2 -git commit -m 'main-2 boring' +create main6 +git add main6 +git commit -m 'main6 boring' -touch subdir/main-sub4 -git add subdir/main-sub4 -git commit -m 'main-sub4' +create subdir/main-sub7 +git add subdir/main-sub7 +git commit -m 'main-sub7' git fetch ../subproj sub2 git branch sub2 FETCH_HEAD git subtree merge --prefix=subdir FETCH_HEAD git branch pre-split -split1=$(git subtree split --prefix subdir --onto FETCH_HEAD --rejoin) +split1=$(git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --rejoin) echo "split1={$split1}" git branch split1 "$split1" -touch subdir/main-sub5 -git add subdir/main-sub5 -git commit -m 'main-sub5' +create subdir/main-sub8 +git add subdir/main-sub8 +git commit -m 'main-sub8' cd ../subproj git fetch ../mainline split1 git branch split1 FETCH_HEAD git merge FETCH_HEAD -touch sub6 -git add sub6 -git commit -m 'sub6' +create sub9 +git add sub9 +git commit -m 'sub9' cd ../mainline -split2=$(git subtree split --prefix subdir --rejoin) +split2=$(git subtree split --annotate='*' --prefix subdir --rejoin) git branch split2 "$split2" -touch subdir/main-sub7 -git add subdir/main-sub7 -git commit -m 'main-sub7' +create subdir/main-sub10 +git add subdir/main-sub10 +git commit -m 'main-sub10' -split3=$(git subtree split --prefix subdir --rejoin) +split3=$(git subtree split --annotate='*' --prefix subdir --rejoin) git branch split3 "$split3" cd ../subproj -- cgit v1.2.1 From 86de04c6eb9ffcb4ed11f1f18bcc62f868cfb743 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 09:55:59 -0400 Subject: Typo when searching for existing splits. --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index e54651c336..8b797dfc23 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -145,7 +145,7 @@ find_existing_splits() dir="$1" revs="$2" git log --grep="^git-subtree-dir: $dir\$" \ - --pretty=format:'%s%n%n%b%nEND' "$revs" | + --pretty=format:'%s%n%n%b%nEND' $revs | while read a b junk; do case "$a" in git-subtree-mainline:) main="$b" ;; -- cgit v1.2.1 From 1f73862f3b63bbc9f0a8a8a12dd58e1a39a3355f Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 15:54:42 -0400 Subject: Clarify why we can't do 'git rev-list' with a path. --- git-subtree.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 8b797dfc23..19ac2ef1c1 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -338,9 +338,9 @@ cmd_split() unrevs="$(find_existing_splits "$dir" "$revs")" fi - # We can't restrict rev-list to only "$dir" here, because that leaves out - # critical information about commit parents. - debug "git rev-list --reverse --parents $revs $unrevs" + # We can't restrict rev-list to only $dir here, because some of our + # parents have the $dir contents the root, and those won't match. + # (and rev-list --follow doesn't seem to solve this) git rev-list --reverse --parents $revs $unrevs | while read rev parents; do debug -- cgit v1.2.1 From 0ad3dd8534215648e381663979ea99db855578b6 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 15:55:56 -0400 Subject: Add a 'create' helper function in test.sh. --- test.sh | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/test.sh b/test.sh index 85ed7ce549..276f40d1da 100755 --- a/test.sh +++ b/test.sh @@ -1,13 +1,13 @@ #!/bin/bash -x +. shellopts.sh +set -e + create() { - for d in 1 2 3 4 5 6 7 8 9 10; do - echo "$1" - done >"$1" + echo "$1" >"$1" + git add "$1" } -. shellopts.sh -set -e rm -rf mainline subproj mkdir mainline subproj @@ -16,25 +16,21 @@ cd subproj git init create sub1 -git add sub1 git commit -m 'sub1' git branch sub1 git branch -m master subproj create sub2 -git add sub2 git commit -m 'sub2' git branch sub2 create sub3 -git add sub3 git commit -m 'sub3' git branch sub3 cd ../mainline git init create main4 -git add main4 git commit -m 'main4' git branch -m master mainline @@ -46,15 +42,12 @@ git subtree add --prefix=subdir FETCH_HEAD git merge -m 'merge -s -ours' -s ours FETCH_HEAD create subdir/main-sub5 -git add subdir/main-sub5 git commit -m 'main-sub5' create main6 -git add main6 git commit -m 'main6 boring' create subdir/main-sub7 -git add subdir/main-sub7 git commit -m 'main-sub7' git fetch ../subproj sub2 @@ -67,7 +60,6 @@ echo "split1={$split1}" git branch split1 "$split1" create subdir/main-sub8 -git add subdir/main-sub8 git commit -m 'main-sub8' cd ../subproj @@ -76,7 +68,6 @@ git branch split1 FETCH_HEAD git merge FETCH_HEAD create sub9 -git add sub9 git commit -m 'sub9' cd ../mainline @@ -84,7 +75,6 @@ split2=$(git subtree split --annotate='*' --prefix subdir --rejoin) git branch split2 "$split2" create subdir/main-sub10 -git add subdir/main-sub10 git commit -m 'main-sub10' split3=$(git subtree split --annotate='*' --prefix subdir --rejoin) -- cgit v1.2.1 From 1490e1546a380814fdd68dc3776023e58da60d48 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 16:28:56 -0400 Subject: Add some basic assertions to test.sh. --- test.sh | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 276f40d1da..4f2b674e5d 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ -#!/bin/bash -x +#!/bin/bash . shellopts.sh set -e @@ -8,6 +8,50 @@ create() git add "$1" } +check() +{ + echo + echo "check:" "$@" + if "$@"; then + echo ok + return 0 + else + echo FAILED + exit 1 + fi +} + +check_equal() +{ + echo + echo "check a:" "$1" + echo " b:" "$2" + if [ "$1" = "$2" ]; then + return 0 + else + echo FAILED + exit 1 + fi +} + +fixnl() +{ + t="" + while read x; do + t="$t$x " + done + echo $t +} + +multiline() +{ + while read x; do + set -- $x + for d in "$@"; do + echo "$d" + done + done +} rm -rf mainline subproj mkdir mainline subproj @@ -19,6 +63,7 @@ create sub1 git commit -m 'sub1' git branch sub1 git branch -m master subproj +check true create sub2 git commit -m 'sub2' @@ -86,7 +131,33 @@ git branch split3 FETCH_HEAD git merge FETCH_HEAD git branch subproj-merge-split3 +chkm="main4 main6" +chkms="main-sub10 main-sub5 main-sub7 main-sub8" +chkms_sub=$(echo $chkms | multiline | sed 's,^,subdir/,' | fixnl) +chks="sub1 sub2 sub3 sub9" +chks_sub=$(echo $chks | multiline | sed 's,^,subdir/,' | fixnl) + +# make sure exactly the right set of files ends up in the subproj +subfiles=$(git ls-files | fixnl) +check_equal "$subfiles" "$chkms $chks" + +# make sure the subproj history *only* contains commits that affect the subdir. +allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) +check_equal "$allchanges" "$chkms $chks" + cd ../mainline git fetch ../subproj subproj-merge-split3 git branch subproj-merge-split3 FETCH_HEAD git subtree pull --prefix=subdir ../subproj subproj-merge-split3 + +# make sure exactly the right set of files ends up in the mainline +mainfiles=$(git ls-files | fixnl) +check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" + +# make sure each filename changed exactly once in the entire history. +# 'main-sub??' and '/subdir/main-sub??' both change, because those are the +# changes that were split into their own history. And 'subdir/sub??' never +# change, since they were *only* changed in the subtree branch. +allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) +check_equal "$allchanges" "$chkm $chkms $chks $chkms_sub" + -- cgit v1.2.1 From a046c7b124de17c4f8aa8f1c3ee7fa5745dde30e Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 16:33:38 -0400 Subject: test.sh tweak --- test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 4f2b674e5d..ef1c70e1e6 100755 --- a/test.sh +++ b/test.sh @@ -100,7 +100,8 @@ git branch sub2 FETCH_HEAD git subtree merge --prefix=subdir FETCH_HEAD git branch pre-split -split1=$(git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --rejoin) +split1=$(git subtree split --annotate='*' \ + --prefix subdir --onto FETCH_HEAD --rejoin) echo "split1={$split1}" git branch split1 "$split1" @@ -161,3 +162,5 @@ check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) check_equal "$allchanges" "$chkm $chkms $chks $chkms_sub" +echo +echo 'ok' -- cgit v1.2.1 From a64f3a7286c4e554b38cd3f7dc8c722aab97cf98 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 16:53:57 -0400 Subject: Trim some extra merge commits that don't need to go into the split tree. ...and update test.sh to test for this. --- git-subtree.sh | 19 ++++++++++++++++--- test.sh | 37 ++++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 19ac2ef1c1..ffd3e0b865 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -168,6 +168,7 @@ copy_commit() { # We're doing to set some environment vars here, so # do it in a subshell to get rid of them safely later + debug copy_commit "{$1}" "{$2}" "{$3}" git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | ( read GIT_AUTHOR_NAME @@ -258,19 +259,31 @@ copy_or_skip() identical= p= + gotparents= for parent in $newparents; do ptree=$(toptree_for_commit $parent) || exit $? + [ -z "$ptree" ] && continue if [ "$ptree" = "$tree" ]; then # an identical parent could be used in place of this rev. identical="$parent" fi - if [ -n "$ptree" ]; then - parentmatch="$parentmatch$parent" + + # sometimes both old parents map to the same newparent; + # eliminate duplicates + is_new=1 + for gp in $gotparents; do + if [ "$gp" = "$parent" ]; then + is_new= + break + fi + done + if [ -n "$is_new" ]; then + gotparents="$gotparents $parent" p="$p -p $parent" fi done - if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then + if [ -n "$identical" -a "$gotparents" = " $identical" ]; then echo $identical else copy_commit $rev $tree "$p" || exit $? diff --git a/test.sh b/test.sh index ef1c70e1e6..44d5da3f20 100755 --- a/test.sh +++ b/test.sh @@ -24,8 +24,8 @@ check() check_equal() { echo - echo "check a:" "$1" - echo " b:" "$2" + echo "check a:" "{$1}" + echo " b:" "{$2}" if [ "$1" = "$2" ]; then return 0 else @@ -100,17 +100,17 @@ git branch sub2 FETCH_HEAD git subtree merge --prefix=subdir FETCH_HEAD git branch pre-split -split1=$(git subtree split --annotate='*' \ +spl1=$(git subtree split --annotate='*' \ --prefix subdir --onto FETCH_HEAD --rejoin) -echo "split1={$split1}" -git branch split1 "$split1" +echo "spl1={$spl1}" +git branch spl1 "$spl1" create subdir/main-sub8 git commit -m 'main-sub8' cd ../subproj -git fetch ../mainline split1 -git branch split1 FETCH_HEAD +git fetch ../mainline spl1 +git branch spl1 FETCH_HEAD git merge FETCH_HEAD create sub9 @@ -123,14 +123,14 @@ git branch split2 "$split2" create subdir/main-sub10 git commit -m 'main-sub10' -split3=$(git subtree split --annotate='*' --prefix subdir --rejoin) -git branch split3 "$split3" +spl3=$(git subtree split --annotate='*' --prefix subdir --rejoin) +git branch spl3 "$spl3" cd ../subproj -git fetch ../mainline split3 -git branch split3 FETCH_HEAD +git fetch ../mainline spl3 +git branch spl3 FETCH_HEAD git merge FETCH_HEAD -git branch subproj-merge-split3 +git branch subproj-merge-spl3 chkm="main4 main6" chkms="main-sub10 main-sub5 main-sub7 main-sub8" @@ -147,9 +147,9 @@ allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) check_equal "$allchanges" "$chkms $chks" cd ../mainline -git fetch ../subproj subproj-merge-split3 -git branch subproj-merge-split3 FETCH_HEAD -git subtree pull --prefix=subdir ../subproj subproj-merge-split3 +git fetch ../subproj subproj-merge-spl3 +git branch subproj-merge-spl3 FETCH_HEAD +git subtree pull --prefix=subdir ../subproj subproj-merge-spl3 # make sure exactly the right set of files ends up in the mainline mainfiles=$(git ls-files | fixnl) @@ -162,5 +162,12 @@ check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) check_equal "$allchanges" "$chkm $chkms $chks $chkms_sub" +# make sure the --rejoin commits never make it into subproj +check_equal "$(git log --pretty=format:'%s' HEAD^2 | grep -i split)" "" + +# make sure no 'git subtree' tagged commits make it into subproj. (They're +# meaningless to subproj since one side of the merge refers to the mainline) +check_equal "$(git log --pretty=format:'%s%n%b' HEAD^2 | grep 'git-subtree.*:')" "" + echo echo 'ok' -- cgit v1.2.1 From 49cf82288aac5f0dcb152e2d75cd340e48d9e760 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 17:07:16 -0400 Subject: Only copy a commit if it has at least one nonidentical parent. This is a simplification of the previous logic. I don't *think* it'll break anything. Results in far fewer useless merge commmits when playing with gitweb in the git project: git subtree split --prefix=gitweb --annotate='(split) ' 0a8f4f0^^..f2e7330 --onto=1130ef3 ...and it doesn't *seem* to eliminate anything important. --- git-subtree.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index ffd3e0b865..90e22ad8be 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -258,6 +258,7 @@ copy_or_skip() assert [ -n "$tree" ] identical= + nonidentical= p= gotparents= for parent in $newparents; do @@ -266,6 +267,8 @@ copy_or_skip() if [ "$ptree" = "$tree" ]; then # an identical parent could be used in place of this rev. identical="$parent" + else + nonidentical="$parent" fi # sometimes both old parents map to the same newparent; @@ -283,7 +286,7 @@ copy_or_skip() fi done - if [ -n "$identical" -a "$gotparents" = " $identical" ]; then + if [ -n "$identical" -a -z "$nonidentical" ]; then echo $identical else copy_commit $rev $tree "$p" || exit $? -- cgit v1.2.1 From fa16ab36ad014bcc03acc4313bb0918fb241b54d Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 17:43:53 -0400 Subject: test.sh: make sure no commit changes more than one file at a time. --- test.sh | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test.sh b/test.sh index 44d5da3f20..4e00b536d7 100755 --- a/test.sh +++ b/test.sh @@ -169,5 +169,39 @@ check_equal "$(git log --pretty=format:'%s' HEAD^2 | grep -i split)" "" # meaningless to subproj since one side of the merge refers to the mainline) check_equal "$(git log --pretty=format:'%s%n%b' HEAD^2 | grep 'git-subtree.*:')" "" +# make sure no patch changes more than one file. The original set of commits +# changed only one file each. A multi-file change would imply that we pruned +# commits too aggressively. +joincommits() +{ + echo "hello world" + commit= + all= + while read x y; do + echo "{$x}" >&2 + if [ -z "$x" ]; then + continue + elif [ "$x" = "commit:" ]; then + if [ -n "$commit" ]; then + echo "$commit $all" + all= + fi + commit="$y" + else + all="$all $y" + fi + done + echo "$commit $all" +} +x=0 +git log --pretty=format:'commit: %H' | joincommits | +( while read commit a b; do + echo "Verifying commit $commit" + check_equal "$b" "" + x=$(($x + 1)) + done + check_equal $x 23 +) || exit 1 + echo echo 'ok' -- cgit v1.2.1 From 795e730e71e8a068ce0cb0790512dab0f1922369 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 17:44:18 -0400 Subject: Simplify merges even more aggressively. If any one of the parents is the same as the current one, then clearly the other parent branch isn't important, so throw it away entirely. Can't remember why I didn't do this before, but if I rediscover it, it definitely needs a unit test. --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 90e22ad8be..e2e47f82ac 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -286,7 +286,7 @@ copy_or_skip() fi done - if [ -n "$identical" -a -z "$nonidentical" ]; then + if [ -n "$identical" ]; then echo $identical else copy_commit $rev $tree "$p" || exit $? -- cgit v1.2.1 From 34a82bda7766f000ef646130ed3f6af58ca23aa2 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 18:05:49 -0400 Subject: test.sh: oops, never intended to count the raw number of commits. Just needed to make sure the count was non-zero. --- test.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test.sh b/test.sh index 4e00b536d7..38dff7a41a 100755 --- a/test.sh +++ b/test.sh @@ -174,7 +174,6 @@ check_equal "$(git log --pretty=format:'%s%n%b' HEAD^2 | grep 'git-subtree.*:')" # commits too aggressively. joincommits() { - echo "hello world" commit= all= while read x y; do @@ -193,14 +192,14 @@ joincommits() done echo "$commit $all" } -x=0 +x= git log --pretty=format:'commit: %H' | joincommits | ( while read commit a b; do echo "Verifying commit $commit" check_equal "$b" "" - x=$(($x + 1)) + x=1 done - check_equal $x 23 + check_equal "$x" 1 ) || exit 1 echo -- cgit v1.2.1 From 942dce5578c8eb03fdc7f9109c8418d499e931ff Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 26 Apr 2009 18:06:08 -0400 Subject: debug messages are off by default; use -d to enable. Instead of debug messages, we print a progress counter to stderr. --- git-subtree.sh | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index e2e47f82ac..39c377c173 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -15,6 +15,7 @@ git subtree pull --prefix= -- h,help show the help q quiet +d show debug messages prefix= the name of the subdir to split out options for 'split' annotate= add a prefix to commit message of new commits @@ -27,6 +28,7 @@ eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) require_work_tree quiet= +debug= command= onto= rejoin= @@ -34,6 +36,13 @@ ignore_joins= annotate= debug() +{ + if [ -n "$debug" ]; then + echo "$@" >&2 + fi +} + +say() { if [ -z "$quiet" ]; then echo "$@" >&2 @@ -57,6 +66,7 @@ while [ $# -gt 0 ]; do shift case "$opt" in -q) quiet=1 ;; + -d) debug=1 ;; --annotate) annotate="$1"; shift ;; --no-annotate) annotate= ;; --prefix) prefix="$1"; shift ;; @@ -357,15 +367,21 @@ cmd_split() # We can't restrict rev-list to only $dir here, because some of our # parents have the $dir contents the root, and those won't match. # (and rev-list --follow doesn't seem to solve this) - git rev-list --reverse --parents $revs $unrevs | + grl='git rev-list --reverse --parents $revs $unrevs' + revmax=$(eval "$grl" | wc -l) + revcount=0 + createcount=0 + eval "$grl" | while read rev parents; do - debug + revcount=$(($revcount + 1)) + say -n "$revcount/$revmax ($createcount) " debug "Processing commit: $rev" exists=$(cache_get $rev) if [ -n "$exists" ]; then debug " prior: $exists" continue fi + createcount=$(($createcount + 1)) debug " parents: $parents" newparents=$(cache_get $parents) debug " newparents: $newparents" -- cgit v1.2.1 From ea28d674423aa580bfdbe24991ffcf796f2dd3dc Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 30 Apr 2009 21:57:32 -0400 Subject: Abort if --rejoin fails. Thanks to Eduardo Kienetz for noticing this. --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 39c377c173..692792b864 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -406,7 +406,7 @@ cmd_split() latest_old=$(cache_get latest_old) git merge -s ours \ -m "$(merge_msg $dir $latest_old $latest_new)" \ - $latest_new >&2 + $latest_new >&2 || exit $? fi echo $latest_new exit 0 -- cgit v1.2.1 From 7b7ba4bb3792572bd6c22c95082e064754de47be Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 24 May 2009 15:28:54 -0400 Subject: More to-do items based on feedback --- todo | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/todo b/todo index b5b8e257aa..97142fa4e2 100644 --- a/todo +++ b/todo @@ -1,8 +1,20 @@ + + write proper docs (asciidoc format for git compatibility) delete tempdir + --annotate-sometimes: only annotate if the patch also changes files outside the subdir? + 'git subtree rejoin' option to do the same as --rejoin, eg. after a rebase + "-s subtree" should be given an explicit subtree option? - \ No newline at end of file + + --prefix doesn't force the subtree correctly in merge/pull + + add a 'push' subcommand to parallel 'pull' + + add a --squash option so we don't merge histories but can still split + + add to-submodule and from-submodule commands -- cgit v1.2.1 From f96bc79019083cdc41cf7f699979f9b5e250d160 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 00:47:59 -0400 Subject: typo in comment --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 692792b864..825dd1dcad 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -176,7 +176,7 @@ find_existing_splits() copy_commit() { - # We're doing to set some environment vars here, so + # We're going to set some environment vars here, so # do it in a subshell to get rid of them safely later debug copy_commit "{$1}" "{$2}" "{$3}" git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | -- cgit v1.2.1 From 43a3951243ebcfb8ef4c47259ecb2d0dfaadbce4 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 01:05:43 -0400 Subject: New --branch option to split command. This is just a handy way to create a new branch from the newly-split subtree. --- git-subtree.sh | 26 ++++++++++++++++++++++++-- todo | 10 +++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 825dd1dcad..f6bdef3001 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -19,15 +19,17 @@ d show debug messages prefix= the name of the subdir to split out options for 'split' annotate= add a prefix to commit message of new commits +b,branch= create a new branch from the split subtree +ignore-joins ignore prior --rejoin commits onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD -ignore-joins ignore prior --rejoin commits " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) . git-sh-setup require_work_tree quiet= +branch= debug= command= onto= @@ -69,6 +71,7 @@ while [ $# -gt 0 ]; do -d) debug=1 ;; --annotate) annotate="$1"; shift ;; --no-annotate) annotate= ;; + -b) branch="$1"; shift ;; --prefix) prefix="$1"; shift ;; --no-prefix) prefix= ;; --onto) onto="$1"; shift ;; @@ -78,6 +81,7 @@ while [ $# -gt 0 ]; do --ignore-joins) ignore_joins=1 ;; --no-ignore-joins) ignore_joins= ;; --) break ;; + *) die "Unexpected option: $opt" ;; esac done @@ -139,12 +143,21 @@ cache_set() echo "$newrev" >"$cachedir/$oldrev" } +rev_exists() +{ + if git rev-parse "$1" >/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + # if a commit doesn't have a parent, this might not work. But we only want # to remove the parent from the rev-list, and since it doesn't exist, it won't # be there anyway, so do nothing in that case. try_remove_previous() { - if git rev-parse "$1^" >/dev/null 2>&1; then + if rev_exists "$1^"; then echo "^$1^" fi } @@ -344,6 +357,10 @@ cmd_add() cmd_split() { + if [ -n "$branch" ] && rev_exists "refs/heads/$branch"; then + die "Branch '$branch' already exists." + fi + debug "Splitting $dir..." cache_setup || exit $? @@ -408,6 +425,11 @@ cmd_split() -m "$(merge_msg $dir $latest_old $latest_new)" \ $latest_new >&2 || exit $? fi + if [ -n "$branch" ]; then + git update-ref -m 'subtree split' "refs/heads/$branch" \ + $latest_new "" || exit $? + say "Created branch '$branch'" + fi echo $latest_new exit 0 } diff --git a/todo b/todo index 97142fa4e2..f23a6d4ff2 100644 --- a/todo +++ b/todo @@ -3,17 +3,17 @@ delete tempdir - --annotate-sometimes: only annotate if the patch also changes files - outside the subdir? - 'git subtree rejoin' option to do the same as --rejoin, eg. after a rebase + --prefix doesn't force the subtree correctly in merge/pull: "-s subtree" should be given an explicit subtree option? - - --prefix doesn't force the subtree correctly in merge/pull + There doesn't seem to be a way to do this. We'd have to + patch git-merge-subtree. Ugh. add a 'push' subcommand to parallel 'pull' + + add a 'log' subcommand to see what's new in a subtree? add a --squash option so we don't merge histories but can still split -- cgit v1.2.1 From f4f29557e7e02c318931395c1cd4fd1ba090fdfd Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 01:10:14 -0400 Subject: slightly rearrange help message for split. --- git-subtree.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index f6bdef3001..ea0294fb79 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -8,10 +8,10 @@ if [ $# -eq 0 ]; then set -- -h fi OPTS_SPEC="\ -git subtree add --prefix= -git subtree split [options...] --prefix= +git subtree add --prefix= git subtree merge --prefix= git subtree pull --prefix= +git subtree split --prefix= -- h,help show the help q quiet -- cgit v1.2.1 From 8e79043c47d69032a86e5587a69289304dd5ca23 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 00:48:07 -0400 Subject: FIXME help for --squash option --- git-subtree.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/git-subtree.sh b/git-subtree.sh index ea0294fb79..65b6348fe4 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -23,6 +23,8 @@ b,branch= create a new branch from the split subtree ignore-joins ignore prior --rejoin commits onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD + options for 'merge' and 'pull' +squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) . git-sh-setup @@ -36,6 +38,7 @@ onto= rejoin= ignore_joins= annotate= +squash= debug() { @@ -80,6 +83,8 @@ while [ $# -gt 0 ]; do --no-rejoin) rejoin= ;; --ignore-joins) ignore_joins=1 ;; --no-ignore-joins) ignore_joins= ;; + --squash) squash=1 ;; + --no-squash) squash= ;; --) break ;; *) die "Unexpected option: $opt" ;; esac -- cgit v1.2.1 From 7ee9eef340170da6d13b9a0ab5a8d23950523b0d Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 01:28:20 -0400 Subject: merge_msg() is really more like rejoin_msg(). --- git-subtree.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 65b6348fe4..d82e03e6fd 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -178,15 +178,15 @@ find_existing_splits() case "$a" in git-subtree-mainline:) main="$b" ;; git-subtree-split:) sub="$b" ;; - *) + END) if [ -n "$main" -a -n "$sub" ]; then debug " Prior: $main -> $sub" cache_set $main $sub try_remove_previous "$main" try_remove_previous "$sub" - main= - sub= fi + main= + sub= ;; esac done @@ -230,7 +230,7 @@ add_msg() EOF } -merge_msg() +rejoin_msg() { dir="$1" latest_old="$2" @@ -410,6 +410,9 @@ cmd_split() tree=$(subtree_for_commit $rev "$dir") debug " tree is: $tree" + + # ugly. is there no better way to tell if this is a subtree + # vs. a mainline commit? Does it matter? [ -z $tree ] && continue newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? @@ -427,7 +430,7 @@ cmd_split() debug "Merging split branch into HEAD..." latest_old=$(cache_get latest_old) git merge -s ours \ - -m "$(merge_msg $dir $latest_old $latest_new)" \ + -m "$(rejoin_msg $dir $latest_old $latest_new)" \ $latest_new >&2 || exit $? fi if [ -n "$branch" ]; then -- cgit v1.2.1 From 1cc2cfff91c61bb56236914da7be7b15584951df Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 03:18:27 -0400 Subject: Basic "subtree merge --squash" support. Instead of merging in the history of the entire subproject, just squash it all into one commit, but try to at least track which commits we used so that we can do future merges correctly. Bonus feature: we can actually switch branches of the subproject this way, just by "squash merging" back and forth from one tag to another. --- git-subtree.sh | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index d82e03e6fd..863e28bb74 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -167,15 +167,46 @@ try_remove_previous() fi } +find_latest_squash() +{ + debug "Looking for latest squash..." + dir="$1" + git log --grep="^git-subtree-dir: $dir\$" \ + --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD | + while read a b junk; do + case "$a" in + START) sq="$b" ;; + git-subtree-mainline:) main="$b" ;; + git-subtree-split:) sub="$b" ;; + END) + if [ -n "$sub" ]; then + if [ -n "$main" ]; then + # a rejoin commit? + # Pretend its sub was a squash. + sq="$sub" + fi + debug "Squash found: $sq $sub" + echo "$sq" "$sub" + break + fi + sq= + main= + sub= + ;; + esac + done +} + find_existing_splits() { debug "Looking for prior splits..." dir="$1" revs="$2" git log --grep="^git-subtree-dir: $dir\$" \ - --pretty=format:'%s%n%n%b%nEND' $revs | + --pretty=format:'%s%n%n%b%nEND%n' $revs | while read a b junk; do case "$a" in + START) main="$b" ;; git-subtree-mainline:) main="$b" ;; git-subtree-split:) sub="$b" ;; END) @@ -244,6 +275,28 @@ rejoin_msg() EOF } +squash_msg() +{ + dir="$1" + oldsub="$2" + newsub="$3" + oldsub_short=$(git rev-parse --short "$oldsub") + newsub_short=$(git rev-parse --short "$newsub") + cat <<-EOF + Squashed '$dir/' changes from $oldsub_short..$newsub_short + + EOF + + git log --pretty=tformat:'%h %s' "$oldsub..$newsub" + git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub" + + cat <<-EOF + + git-subtree-dir: $dir + git-subtree-split: $newsub + EOF +} + toptree_for_commit() { commit="$1" @@ -278,6 +331,16 @@ tree_changed() fi } +new_squash_commit() +{ + old="$1" + oldsub="$2" + newsub="$3" + tree=$(toptree_for_commit $newsub) || exit $? + squash_msg "$dir" "$oldsub" "$newsub" | + git commit-tree "$tree" -p "$old" || exit $? +} + copy_or_skip() { rev="$1" @@ -452,6 +515,19 @@ cmd_merge() fi rev="$1" + if [ -n "$squash" ]; then + first_split="$(find_latest_squash "$dir")" + if [ -z "$first_split" ]; then + die "Can't squash-merge: '$dir' was never added." + fi + set $first_split + old=$1 + sub=$2 + new=$(new_squash_commit "$old" "$sub" "$rev") || exit $? + debug "New squash commit: $new" + rev="$new" + fi + git merge -s subtree $rev } -- cgit v1.2.1 From eb4fb91094cf5e20bf7570da3fadb50e444284bc Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 03:33:17 -0400 Subject: Don't squash-merge if the old and new commits are the same. --- git-subtree.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git-subtree.sh b/git-subtree.sh index 863e28bb74..f7fe111178 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -523,6 +523,10 @@ cmd_merge() set $first_split old=$1 sub=$2 + if [ "$sub" = "$rev" ]; then + say "Subtree is already at commit $rev." + exit 0 + fi new=$(new_squash_commit "$old" "$sub" "$rev") || exit $? debug "New squash commit: $new" rev="$new" -- cgit v1.2.1 From 1a8c36dc5fdd8c439c65f18a91ad211050201fc8 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 03:33:39 -0400 Subject: Fix splitting after using a squash merge. --- git-subtree.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index f7fe111178..1fff10e854 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -203,13 +203,17 @@ find_existing_splits() dir="$1" revs="$2" git log --grep="^git-subtree-dir: $dir\$" \ - --pretty=format:'%s%n%n%b%nEND%n' $revs | + --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | while read a b junk; do case "$a" in - START) main="$b" ;; + START) main="$b"; sq="$b" ;; git-subtree-mainline:) main="$b" ;; git-subtree-split:) sub="$b" ;; END) + if [ -z "$main" -a -n "$sub" ]; then + # squash commits refer to a subtree + cache_set "$sq" "$sub" + fi if [ -n "$main" -a -n "$sub" ]; then debug " Prior: $main -> $sub" cache_set $main $sub -- cgit v1.2.1 From d713e2d87a5003da02d95a4ac5be28a1e9fdc3ce Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 04:11:43 -0400 Subject: Make --squash work with the 'add' command too. --- git-subtree.sh | 57 +++++++++++++++++++++++++++++++++++++++------------------ todo | 7 +++++-- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 1fff10e854..962d5ff509 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -23,7 +23,7 @@ b,branch= create a new branch from the split subtree ignore-joins ignore prior --rejoin commits onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD - options for 'merge' and 'pull' + options for 'add', 'merge', and 'pull' squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) @@ -169,11 +169,16 @@ try_remove_previous() find_latest_squash() { - debug "Looking for latest squash..." + debug "Looking for latest squash ($dir)..." dir="$1" + sq= + main= + sub= git log --grep="^git-subtree-dir: $dir\$" \ --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD | while read a b junk; do + debug "$a $b $junk" + debug "{{$sq/$main/$sub}}" case "$a" in START) sq="$b" ;; git-subtree-mainline:) main="$b" ;; @@ -202,6 +207,8 @@ find_existing_splits() debug "Looking for prior splits..." dir="$1" revs="$2" + main= + sub= git log --grep="^git-subtree-dir: $dir\$" \ --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | while read a b junk; do @@ -284,21 +291,21 @@ squash_msg() dir="$1" oldsub="$2" newsub="$3" - oldsub_short=$(git rev-parse --short "$oldsub") newsub_short=$(git rev-parse --short "$newsub") - cat <<-EOF - Squashed '$dir/' changes from $oldsub_short..$newsub_short - - EOF - git log --pretty=tformat:'%h %s' "$oldsub..$newsub" - git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub" + if [ -n "$oldsub" ]; then + oldsub_short=$(git rev-parse --short "$oldsub") + echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short" + echo + git log --pretty=tformat:'%h %s' "$oldsub..$newsub" + git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub" + else + echo "Squashed '$dir/' content from commit $newsub_short" + fi - cat <<-EOF - - git-subtree-dir: $dir - git-subtree-split: $newsub - EOF + echo + echo "git-subtree-dir: $dir" + echo "git-subtree-split: $newsub" } toptree_for_commit() @@ -341,8 +348,13 @@ new_squash_commit() oldsub="$2" newsub="$3" tree=$(toptree_for_commit $newsub) || exit $? - squash_msg "$dir" "$oldsub" "$newsub" | - git commit-tree "$tree" -p "$old" || exit $? + if [ -n "$old" ]; then + squash_msg "$dir" "$oldsub" "$newsub" | + git commit-tree "$tree" -p "$old" || exit $? + else + squash_msg "$dir" "" "$newsub" | + git commit-tree "$tree" || exit $? + fi } copy_or_skip() @@ -422,9 +434,18 @@ cmd_add() else headp= fi - commit=$(add_msg "$dir" "$headrev" "$rev" | - git commit-tree $tree $headp -p "$rev") || exit $? + + if [ -n "$squash" ]; then + rev=$(new_squash_commit "" "" "$rev") || exit $? + commit=$(echo "Merge commit '$rev' as '$dir'" | + git commit-tree $tree $headp -p "$rev") || exit $? + else + commit=$(add_msg "$dir" "$headrev" "$rev" | + git commit-tree $tree $headp -p "$rev") || exit $? + fi git reset "$commit" || exit $? + + say "Added dir '$dir'" } cmd_split() diff --git a/todo b/todo index f23a6d4ff2..a15a378da5 100644 --- a/todo +++ b/todo @@ -10,11 +10,14 @@ "-s subtree" should be given an explicit subtree option? There doesn't seem to be a way to do this. We'd have to patch git-merge-subtree. Ugh. + (but we could avoid this problem by generating squashes with + exactly the right subtree structure, rather than using + subtree merge...) add a 'push' subcommand to parallel 'pull' add a 'log' subcommand to see what's new in a subtree? - add a --squash option so we don't merge histories but can still split - add to-submodule and from-submodule commands + + automated tests for --squash stuff -- cgit v1.2.1 From e75d1da38a7091c15ebd3c80539e4aab20faf5b7 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 14:05:33 -0400 Subject: Add basic git-subtree manpage in asciidoc format. --- .gitignore | 2 + Makefile | 18 +++++ asciidoc.conf | 91 +++++++++++++++++++++ git-subtree.txt | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++++ manpage-base.xsl | 35 ++++++++ manpage-normal.xsl | 13 +++ 6 files changed, 392 insertions(+) create mode 100644 Makefile create mode 100644 asciidoc.conf create mode 100644 git-subtree.txt create mode 100644 manpage-base.xsl create mode 100644 manpage-normal.xsl diff --git a/.gitignore b/.gitignore index b25c15b81f..e358b18b78 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *~ +git-subtree.xml +git-subtree.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..bc163dd390 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +default: + @echo "git-subtree doesn't need to be built." + @echo + @echo "Try: make doc" + @false + +doc: git-subtree.1 + +%.1: %.xml + xmlto -m manpage-normal.xsl man $^ + +%.xml: %.txt + asciidoc -b docbook -d manpage -f asciidoc.conf \ + -agit_version=1.6.3 $^ + +clean: + rm -f *~ *.xml *.html *.1 + rm -rf subproj mainline diff --git a/asciidoc.conf b/asciidoc.conf new file mode 100644 index 0000000000..dc76e7f073 --- /dev/null +++ b/asciidoc.conf @@ -0,0 +1,91 @@ +## linkgit: macro +# +# Usage: linkgit:command[manpage-section] +# +# Note, {0} is the manpage section, while {target} is the command. +# +# Show GIT link as: (
); if section is defined, else just show +# the command. + +[macros] +(?su)[\\]?(?Plinkgit):(?P\S*?)\[(?P.*?)\]= + +[attributes] +asterisk=* +plus=+ +caret=^ +startsb=[ +endsb=] +tilde=~ + +ifdef::backend-docbook[] +[linkgit-inlinemacro] +{0%{target}} +{0#} +{0#{target}{0}} +{0#} +endif::backend-docbook[] + +ifdef::backend-docbook[] +ifndef::git-asciidoc-no-roff[] +# "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this. +# v1.72 breaks with this because it replaces dots not in roff requests. +[listingblock] +{title} + +ifdef::doctype-manpage[] + .ft C +endif::doctype-manpage[] +| +ifdef::doctype-manpage[] + .ft +endif::doctype-manpage[] + +{title#} +endif::git-asciidoc-no-roff[] + +ifdef::git-asciidoc-no-roff[] +ifdef::doctype-manpage[] +# The following two small workarounds insert a simple paragraph after screen +[listingblock] +{title} + +| + +{title#} + +[verseblock] +{title} +{title%} +{title#} +| + +{title#} +{title%} +endif::doctype-manpage[] +endif::git-asciidoc-no-roff[] +endif::backend-docbook[] + +ifdef::doctype-manpage[] +ifdef::backend-docbook[] +[header] +template::[header-declarations] + + +{mantitle} +{manvolnum} +Git +{git_version} +Git Manual + + + {manname} + {manpurpose} + +endif::backend-docbook[] +endif::doctype-manpage[] + +ifdef::backend-xhtml11[] +[linkgit-inlinemacro] +{target}{0?({0})} +endif::backend-xhtml11[] diff --git a/git-subtree.txt b/git-subtree.txt new file mode 100644 index 0000000000..d10630180d --- /dev/null +++ b/git-subtree.txt @@ -0,0 +1,233 @@ +git-subtree(1) +============== + +NAME +---- +git-subtree - add, merge, and split subprojects stored in subtrees + + +SYNOPSIS +-------- +[verse] +'git subtree' add --prefix= +'git subtree' merge --prefix= +'git subtree' pull --prefix= +'git subtree' split --prefix= + + +DESCRIPTION +----------- +git subtree allows you to include an subproject in your +own repository as a subdirectory, optionally including the +subproject's entire history. For example, you could +include the source code for a library as a subdirectory of your +application. + +You can also extract the entire history of a subdirectory from +your project and make it into a standalone project. For +example, if a library you made for one application ends up being +useful elsewhere, you can extract its entire history and publish +that as its own git repository, without accidentally +intermingling the history of your application project. + +Most importantly, you can alternate back and forth between these +two operations. If the standalone library gets updated, you can +automatically merge the changes into your project; if you +update the library inside your project, you can "split" the +changes back out again and merge them back into the library +project. + +Unlike the 'git submodule' command, git subtree doesn't produce +any special constructions (like .gitmodule files or gitlinks) in +your repository, and doesn't require end-users of your +repository to do anything special or to understand how subtrees +work. A subtree is just another subdirectory and can be +committed to, branched, and merged along with your project in +any way you want. + +In order to keep your commit messages clean, we recommend that +people split their commits between the subtrees and the main +project as much as possible. That is, if you make a change that +affects both the library and the main application, commit it in +two pieces. That way, when you split the library commits out +later, their descriptions will still make sense. But if this +isn't important to you, it's not *necessary*. git subtree will +simply leave out the non-library-related parts of the commit +when it splits it out into the subproject later. + + +COMMANDS +-------- +add:: + Create the subtree by importing its contents + from the given commit. A new commit is created + automatically, joining the imported project's history + with your own. With '--squash', imports only a single + commit from the subproject, rather than its entire + history. + +merge:: + Merge recent changes up to into the + subtree. As with normal 'git merge', this doesn't + remove your own local changes; it just merges those + changes into the latest . With '--squash', + creates only one commit that contains all the changes, + rather than merging in the entire history. + + If you use '--squash', the merge direction doesn't + always have to be forward; you can use this command to + go back in time from v2.5 to v2.4, for example. If your + merge introduces a conflict, you can resolve it in the + usual ways. + +pull:: + Exactly like 'merge', but parallels 'git pull' in that + it fetches the given commit from the specified remote + repository. + +split:: + Extract a new, synthetic project history from the + history of the subtree. The new history + includes only the commits (including merges) that + affected , and each of those commits now has the + contents of at the root of the project instead + of in a subdirectory. Thus, the newly created history + is suitable for export as a separate git repository. + + After splitting successfully, a single commit id is + printed to stdout. This corresponds to the HEAD of the + newly created tree, which you can manipulate however you + want. + + Repeated splits of exactly the same history are + guaranteed to be identical (ie. to produce the same + commit ids). Because of this, if you add new commits + and then re-split, the new commits will be attached as + commits on top of the history you generated last time, + so 'git merge' and friends will work as expected. + + Note that if you use '--squash' when you merge, you + should usually not just '--rejoin' when you split. + + +OPTIONS +------- +-q:: +--quiet:: + Suppress unnecessary output messages on stderr. + +-d:: +--debug:: + Produce even more unnecessary output messages on stderr. + +--prefix=:: + Specify the path in the repository to the subtree you + want to manipulate. This option is currently mandatory + for all commands. + + +OPTIONS FOR add, merge, AND pull +-------------------------------- +--squash:: + Instead of merging the entire history from the subtree + project, produce only a single commit that contains all + the differences you want to merge, and then merge that + new commit into your project. + + Using this option helps to reduce log clutter. People + rarely want to see every change that happened between + v1.0 and v1.1 of the library they're using, since none of the + interim versions were ever included in their application. + + Using '--squash' also helps avoid problems when the same + subproject is included multiple times in the same + project, or is removed and then re-added. In such a + case, it doesn't make sense to combine the histories + anyway, since it's unclear which part of the history + belongs to which subtree. + + Furthermore, with '--squash', you can switch back and + forth between different versions of a subtree, rather + than strictly forward. 'git subtree merge --squash' + always adjusts the subtree to match the exactly + specified commit, even if getting to that commit would + require undoing some changes that were added earlier. + + Whether or not you use '--squash', changes made in your + local repository remain intact and can be later split + and send upstream to the subproject. + + +OPTIONS FOR split +----------------- +--annotate=:: + When generating synthetic history, add as a + prefix to each commit message. Since we're creating new + commits with the same commit message, but possibly + different content, from the original commits, this can help + to differentiate them and avoid confusion. + + Whenever you split, you need to use the same + , or else you don't have a guarantee that + the new re-created history will be identical to the old + one. That will prevent merging from working correctly. + git subtree tries to make it work anyway, particularly + if you use --rejoin, but it may not always be effective. + +-b :: +--branch=:: + After generating the synthetic history, create a new + branch called that contains the new history. + This is suitable for immediate pushing upstream. + must not already exist. + +--ignore-joins:: + If you use '--rejoin', git subtree attempts to optimize + its history reconstruction to generate only the new + commits since the last '--rejoin'. '--ignore-join' + disables this behaviour, forcing it to regenerate the + entire history. In a large project, this can take a + long time. + +--onto=:: + If your subtree was originally imported using something + other than git subtree, its history may not match what + git subtree is expecting. In that case, you can specify + the commit id that corresponds to the first + revision of the subproject's history that was imported + into your project, and git subtree will attempt to build + its history from there. + + If you used 'git subtree add', you should never need + this option. + +--rejoin:: + After splitting, merge the newly created synthetic + history back into your main project. That way, future + splits can search only the part of history that has + been added since the most recent --rejoin. + + If your split commits end up merged into the upstream + subproject, and then you want to get the latest upstream + version, this will allow git's merge algorithm to more + intelligently avoid conflicts (since it knows these + synthetic commits are already part of the upstream + repository). + + Unfortunately, using this option results in 'git log' + showing an extra copy of every new commit that was + created (the original, and the synthetic one). + + If you do all your merges with '--squash', don't use + '--rejoin' when you split, because you don't want the + subproject's history to be part of your project anyway. + + +AUTHOR +------ +Written by Avery Pennarun + + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/manpage-base.xsl b/manpage-base.xsl new file mode 100644 index 0000000000..a264fa6160 --- /dev/null +++ b/manpage-base.xsl @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + sp + + + + + + + + br + + + diff --git a/manpage-normal.xsl b/manpage-normal.xsl new file mode 100644 index 0000000000..a48f5b11f3 --- /dev/null +++ b/manpage-normal.xsl @@ -0,0 +1,13 @@ + + + + + + +\ +. + + -- cgit v1.2.1 From dd07906252a3983fdec396ea6c58de12548713e1 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 30 May 2009 14:24:31 -0400 Subject: man page: add an EXAMPLES section. --- git-subtree.txt | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/git-subtree.txt b/git-subtree.txt index d10630180d..649cc30989 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -223,6 +223,68 @@ OPTIONS FOR split subproject's history to be part of your project anyway. +EXAMPLES +-------- +Let's use the repository for the git source code as an example. +First, get your own copy of the git.git repository: + + $ git clone git://git.kernel.org/pub/scm/git/git.git test-git + $ cd test-git + +gitweb (commit 1130ef3) was merged into git as of commit +0a8f4f0, after which it was no longer maintained separately. +But imagine it had been maintained separately, and we wanted to +extract git's changes to gitweb since that time, to share with +the upstream. You could do this: + + $ git subtree split --prefix=gitweb --annotate='(split) ' \ + 0a8f4f0^.. --onto=1130ef3 --rejoin \ + --branch gitweb-latest + $ gitk gitweb-latest + $ git push git@github.com:whatever/gitweb gitweb-latest:master + +(We use '0a8f4f0^..' because that means "all the changes from +0a8f4f0 to the current version, including 0a8f4f0 itself.") + +If gitweb had originally been merged using 'git subtree add' (or +a previous split had already been done with --rejoin specified) +then you can do all your splits without having to remember any +weird commit ids: + + $ git subtree split --prefix=gitweb --annotate='(split) ' --rejoin \ + --branch gitweb-latest2 + +And you can merge changes back in from the upstream project just +as easily: + + $ git subtree pull --prefix=gitweb \ + git@github.com:whatever/gitweb gitweb-latest:master + +Or, using '--squash', you can actually rewind to an earlier +version of gitweb: + + $ git subtree merge --prefix=gitweb --squash gitweb-latest~10 + +Then make some changes: + + $ date >gitweb/myfile + $ git add gitweb/myfile + $ git commit -m 'created myfile' + +And fast forward again: + + $ git subtree merge --prefix=gitweb --squash gitweb-latest + +And notice that your change is still intact: + + $ ls -l gitweb/myfile + +And you can split it out and look at your changes versus +the standard gitweb: + + git log gitweb-latest..$(git subtree split --prefix=gitweb) + + AUTHOR ------ Written by Avery Pennarun -- cgit v1.2.1 From c8a98d4f8730d85a6f693d5dbcd4c1309d97f7c8 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 15 Jun 2009 14:12:40 -0400 Subject: update todo --- todo | 3 +++ 1 file changed, 3 insertions(+) diff --git a/todo b/todo index a15a378da5..88a4359916 100644 --- a/todo +++ b/todo @@ -21,3 +21,6 @@ add to-submodule and from-submodule commands automated tests for --squash stuff + + test.sh fails in msysgit? + sort error - see Thell's email -- cgit v1.2.1 From 6aa76263eed68ffed7405687f407e66deec30b02 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 2 Jul 2009 12:39:48 -0400 Subject: Some todo items reported by pmccurdy --- todo | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/todo b/todo index 88a4359916..1a9d64408d 100644 --- a/todo +++ b/todo @@ -1,5 +1,3 @@ - - write proper docs (asciidoc format for git compatibility) delete tempdir @@ -24,3 +22,24 @@ test.sh fails in msysgit? sort error - see Thell's email + + "add" command non-obviously requires a commitid; would be easier if + it had a "pull" sort of mode instead + + "pull" and "merge" commands should fail if you've never merged + that --prefix before + + docs should provide an example of "add" + + note that the initial split doesn't *have* to have a commitid + specified... that's just an optimization + + if you try to add (or maybe merge?) with an invalid commitid, you + get a misleading "prefix must end with /" message from + one of the other git tools that git-subtree calls. Should + detect this situation and print the *real* problem. + + In fact, the prefix should *not* end with slash, and we + should detect (and fix) it if it does. Otherwise the + log message looks weird. + -- cgit v1.2.1 From 76a7356f56655dbcbbc9d472e0cdc85c6ed3759f Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Tue, 7 Jul 2009 17:41:38 -0400 Subject: todo --- todo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/todo b/todo index 1a9d64408d..01d552979a 100644 --- a/todo +++ b/todo @@ -43,3 +43,5 @@ should detect (and fix) it if it does. Otherwise the log message looks weird. + totally weird behavior in 'git subtree add' if --prefix matches + a branch name -- cgit v1.2.1 From b64a7aa26c72fcae7b6e1decd88ed706c185cda7 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Wed, 8 Jul 2009 20:17:31 -0400 Subject: Docs: when pushing to github, the repo path needs to end in .git Reported by Thell Fowler. --- git-subtree.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index 649cc30989..e7ce2d3654 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -241,7 +241,7 @@ the upstream. You could do this: 0a8f4f0^.. --onto=1130ef3 --rejoin \ --branch gitweb-latest $ gitk gitweb-latest - $ git push git@github.com:whatever/gitweb gitweb-latest:master + $ git push git@github.com:whatever/gitweb.git gitweb-latest:master (We use '0a8f4f0^..' because that means "all the changes from 0a8f4f0 to the current version, including 0a8f4f0 itself.") @@ -258,7 +258,7 @@ And you can merge changes back in from the upstream project just as easily: $ git subtree pull --prefix=gitweb \ - git@github.com:whatever/gitweb gitweb-latest:master + git@github.com:whatever/gitweb.git gitweb-latest:master Or, using '--squash', you can actually rewind to an earlier version of gitweb: -- cgit v1.2.1 From 5d1a5da411a8ec90cd7a7819bfc74440e3f2545c Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 10 Jul 2009 15:14:33 -0400 Subject: todo --- todo | 3 +++ 1 file changed, 3 insertions(+) diff --git a/todo b/todo index 01d552979a..e67f713ad4 100644 --- a/todo +++ b/todo @@ -45,3 +45,6 @@ totally weird behavior in 'git subtree add' if --prefix matches a branch name + + "pull --squash" should do fetch-synthesize-merge, but instead just + does "pull" directly, which doesn't work at all. -- cgit v1.2.1 From 344f58abe599cba53a056c0a707c9a99e2cd13a8 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 16 Jul 2009 14:31:50 -0400 Subject: todo^ --- todo | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/todo b/todo index e67f713ad4..155c4be155 100644 --- a/todo +++ b/todo @@ -48,3 +48,8 @@ "pull --squash" should do fetch-synthesize-merge, but instead just does "pull" directly, which doesn't work at all. + + make a 'force-update' that does what 'add' does even if the subtree + already exists. That way we can help people who imported + subtrees "incorrectly" (eg. by just copying in the files) in + the past. -- cgit v1.2.1 From 0af6aa46751aa6c1ec393155b938601ce0b5f7ae Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 1 Aug 2009 01:19:48 -0400 Subject: todo --- todo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/todo b/todo index 155c4be155..b59713064b 100644 --- a/todo +++ b/todo @@ -53,3 +53,5 @@ already exists. That way we can help people who imported subtrees "incorrectly" (eg. by just copying in the files) in the past. + + guess --prefix automatically if possible based on pwd -- cgit v1.2.1 From ef7596677c181c649436fe97356dd31c1cb4a13e Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 2 Aug 2009 17:48:20 -0400 Subject: todo: idea for a 'git subtree grafts' command --- todo | 3 +++ 1 file changed, 3 insertions(+) diff --git a/todo b/todo index b59713064b..3040b9f171 100644 --- a/todo +++ b/todo @@ -55,3 +55,6 @@ the past. guess --prefix automatically if possible based on pwd + + make a 'git subtree grafts' that automatically expands --squash'd + commits so you can see the full history if you want it. -- cgit v1.2.1 From e1a5b9d3e708d6be1a4c5220dc492da9f2694411 Mon Sep 17 00:00:00 2001 From: Amiel Martin Date: Wed, 12 Aug 2009 15:24:50 -0700 Subject: fixed order of assertion in tests --- test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 38dff7a41a..4229f840ac 100755 --- a/test.sh +++ b/test.sh @@ -160,7 +160,7 @@ check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" # changes that were split into their own history. And 'subdir/sub??' never # change, since they were *only* changed in the subtree branch. allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) -check_equal "$allchanges" "$chkm $chkms $chks $chkms_sub" +check_equal "$allchanges" "$chkms $chkm $chks $chkms_sub" # make sure the --rejoin commits never make it into subproj check_equal "$(git log --pretty=format:'%s' HEAD^2 | grep -i split)" "" -- cgit v1.2.1 From 558e7a57e20fe68aee05f74f8005b7a39795ac15 Mon Sep 17 00:00:00 2001 From: Amiel Martin Date: Wed, 12 Aug 2009 15:38:00 -0700 Subject: sort assertion to make it more generic --- test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 4229f840ac..8283fadaad 100755 --- a/test.sh +++ b/test.sh @@ -160,7 +160,7 @@ check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" # changes that were split into their own history. And 'subdir/sub??' never # change, since they were *only* changed in the subtree branch. allchanges=$(git log --name-only --pretty=format:'' | sort | fixnl) -check_equal "$allchanges" "$chkms $chkm $chks $chkms_sub" +check_equal "$allchanges" "$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)" # make sure the --rejoin commits never make it into subproj check_equal "$(git log --pretty=format:'%s' HEAD^2 | grep -i split)" "" -- cgit v1.2.1 From 2987e6add32f3367be8cc196ecac9195a213e415 Mon Sep 17 00:00:00 2001 From: kTln2 Date: Thu, 20 Aug 2009 13:30:45 +0200 Subject: Add explicit path of git installation by 'git --exec-path'. As pointed out by documentation, the correct use of 'git-sh-setup' is using $(git --exec-path) to avoid problems with not standard installations. Signed-off-by: gianluca.pacchiella --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 962d5ff509..c5c0201448 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -27,7 +27,7 @@ rejoin merge the new branch back into HEAD squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) -. git-sh-setup +. $(git --exec-path)/git-sh-setup require_work_tree quiet= -- cgit v1.2.1 From 33aaa697a2386a02002f9fb8439d11243f12e1c7 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Wed, 26 Aug 2009 10:41:03 -0400 Subject: Improve patch to use git --exec-path: add to PATH instead. If you (like me) are using a modified git straight out of its source directory (ie. without installing), then --exec-path isn't actually correct. Add it to the PATH instead, so if it is correct, it'll work, but if it's not, we fall back to the previous behaviour. --- git-subtree.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index c5c0201448..f7d2fe408d 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -27,7 +27,8 @@ rejoin merge the new branch back into HEAD squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) -. $(git --exec-path)/git-sh-setup +PATH=$(git --exec-path):$PATH +. git-sh-setup require_work_tree quiet= -- cgit v1.2.1 From 227f78114752eee2e8ae3368089716d73d32dd8b Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Wed, 26 Aug 2009 10:43:43 -0400 Subject: Fix behaviour if you have a branch named the same as your --prefix We were trying to 'git checkout $prefix', which is ambiguous if $prefix names a directory *and* a branch. Do 'git checkout -- $prefix' instead. The main place this appeared was in 'git subtree add'. Reported by several people. --- git-subtree.sh | 2 +- test.sh | 1 + todo | 6 ------ 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index f7d2fe408d..b7c741cfd4 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -426,7 +426,7 @@ cmd_add() debug "Adding $dir as '$rev'..." git read-tree --prefix="$dir" $rev || exit $? - git checkout "$dir" || exit $? + git checkout -- "$dir" || exit $? tree=$(git write-tree) || exit $? headrev=$(git rev-parse HEAD) || exit $? diff --git a/test.sh b/test.sh index 8283fadaad..bed7f27906 100755 --- a/test.sh +++ b/test.sh @@ -78,6 +78,7 @@ git init create main4 git commit -m 'main4' git branch -m master mainline +git branch subdir git fetch ../subproj sub1 git branch sub1 FETCH_HEAD diff --git a/todo b/todo index 3040b9f171..5e72b2e510 100644 --- a/todo +++ b/todo @@ -20,9 +20,6 @@ automated tests for --squash stuff - test.sh fails in msysgit? - sort error - see Thell's email - "add" command non-obviously requires a commitid; would be easier if it had a "pull" sort of mode instead @@ -43,9 +40,6 @@ should detect (and fix) it if it does. Otherwise the log message looks weird. - totally weird behavior in 'git subtree add' if --prefix matches - a branch name - "pull --squash" should do fetch-synthesize-merge, but instead just does "pull" directly, which doesn't work at all. -- cgit v1.2.1 From 8ac5eca1eaa88bd5b998e91531937404bc6425c4 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Wed, 30 Sep 2009 14:29:42 +0200 Subject: Check that the type of the tree really is a tree and not a commit as it seems to sometimes become when eg. a submodule has existed in the same position previously. --- git-subtree.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/git-subtree.sh b/git-subtree.sh index b7c741cfd4..454ce7ef22 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -322,6 +322,7 @@ subtree_for_commit() git ls-tree "$commit" -- "$dir" | while read mode type tree name; do assert [ "$name" = "$dir" ] + assert [ "$type" = "tree" ] echo $tree break done -- cgit v1.2.1 From add00a3229ba4ada0cb47a447fdc483658df78e9 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 2 Oct 2009 11:51:25 -0400 Subject: Add a README that says to email me instead of using github mail. What's with this new generation who hates email so much? --- README | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000000..c686b4a69b --- /dev/null +++ b/README @@ -0,0 +1,8 @@ + +Please read git-subtree.txt for documentation. + +Please don't contact me using github mail; it's slow, ugly, and worst of +all, redundant. Email me instead at apenwarr@gmail.com and I'll be happy to +help. + +Avery -- cgit v1.2.1 From 6f2012cdc021f6b47ed19bc7fe64159ce9eeda8a Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 2 Oct 2009 15:22:15 -0400 Subject: If someone provides a --prefix that ends with slash, strip the slash. Prefixes that differ only in the trailing slash should be considered identical. Also update the test to check that this works. --- git-subtree.sh | 6 +++--- test.sh | 4 ++-- todo | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 454ce7ef22..0949fefe20 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -102,7 +102,7 @@ esac if [ -z "$prefix" ]; then die "You must provide the --prefix option." fi -dir="$prefix" +dir="$(dirname "$prefix/.")" if [ "$command" != "pull" ]; then revs=$(git rev-parse $default --revs-only "$@") || exit $? @@ -175,7 +175,7 @@ find_latest_squash() sq= main= sub= - git log --grep="^git-subtree-dir: $dir\$" \ + git log --grep="^git-subtree-dir: $dir/*\$" \ --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD | while read a b junk; do debug "$a $b $junk" @@ -210,7 +210,7 @@ find_existing_splits() revs="$2" main= sub= - git log --grep="^git-subtree-dir: $dir\$" \ + git log --grep="^git-subtree-dir: $dir/*\$" \ --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | while read a b junk; do case "$a" in diff --git a/test.sh b/test.sh index bed7f27906..12b0456574 100755 --- a/test.sh +++ b/test.sh @@ -82,7 +82,7 @@ git branch subdir git fetch ../subproj sub1 git branch sub1 FETCH_HEAD -git subtree add --prefix=subdir FETCH_HEAD +git subtree add --prefix=subdir/ FETCH_HEAD # this shouldn't actually do anything, since FETCH_HEAD is already a parent git merge -m 'merge -s -ours' -s ours FETCH_HEAD @@ -118,7 +118,7 @@ create sub9 git commit -m 'sub9' cd ../mainline -split2=$(git subtree split --annotate='*' --prefix subdir --rejoin) +split2=$(git subtree split --annotate='*' --prefix subdir/ --rejoin) git branch split2 "$split2" create subdir/main-sub10 diff --git a/todo b/todo index 5e72b2e510..7e44b0024f 100644 --- a/todo +++ b/todo @@ -36,10 +36,6 @@ one of the other git tools that git-subtree calls. Should detect this situation and print the *real* problem. - In fact, the prefix should *not* end with slash, and we - should detect (and fix) it if it does. Otherwise the - log message looks weird. - "pull --squash" should do fetch-synthesize-merge, but instead just does "pull" directly, which doesn't work at all. -- cgit v1.2.1 From 2275f7077d5ea2bb9201599dec0dd8f2a5de2e40 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 2 Oct 2009 16:09:09 -0400 Subject: Fix a minor problem in identifying squashes vs. normal splits. This didn't seem to have any noticeable side effects other than suspicious-looking log messages when you used -d. --- git-subtree.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 0949fefe20..cccc3400fd 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -214,12 +214,14 @@ find_existing_splits() --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | while read a b junk; do case "$a" in - START) main="$b"; sq="$b" ;; + START) sq="$b" ;; git-subtree-mainline:) main="$b" ;; git-subtree-split:) sub="$b" ;; END) + debug " Main is: '$main'" if [ -z "$main" -a -n "$sub" ]; then # squash commits refer to a subtree + debug " Squash: $sq from $sub" cache_set "$sq" "$sub" fi if [ -n "$main" -a -n "$sub" ]; then -- cgit v1.2.1 From e31d1e2f30c943473de7a23bbbcd2dcea698e312 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 2 Oct 2009 18:23:54 -0400 Subject: cmd_pull didn't support --squash correctly. We should implement it as git fetch ... git subtree merge ... But we were instead just calling git pull -s subtree ... because 'git subtree merge' used to be just an alias for 'git merge -s subtree', but it no longer is. --- git-subtree.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index cccc3400fd..8baa376fe5 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -567,8 +567,9 @@ cmd_merge() cmd_pull() { ensure_clean - set -x - git pull -s subtree "$@" + git fetch "$@" || exit $? + revs=FETCH_HEAD + cmd_merge } "cmd_$command" "$@" -- cgit v1.2.1 From c567d9e59fceb93d7334a1414d0a2a9d6f4913de Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Wed, 4 Nov 2009 14:50:33 -0500 Subject: Add some tips for how to install. --- INSTALL | 13 +++++++++++++ Makefile | 1 + 2 files changed, 14 insertions(+) create mode 100644 INSTALL diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000000..5966dde46c --- /dev/null +++ b/INSTALL @@ -0,0 +1,13 @@ + +HOW TO INSTALL git-subtree +========================== + +Copy the file 'git-subtree.sh' to /usr/local/bin/git-subtree. + +That will make a 'git subtree' (note: space instead of dash) command +available. See the file git-subtree.txt for more. + +You can also install the man page by doing: + + make doc + cp git-subtree.1 /usr/share/man/man1/ diff --git a/Makefile b/Makefile index bc163dd390..3e97c6246f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ default: @echo "git-subtree doesn't need to be built." + @echo "Just copy it somewhere on your PATH, like /usr/local/bin." @echo @echo "Try: make doc" @false -- cgit v1.2.1 From d8b2c0da177ddfc2bc8d1e47278aa1c240d63034 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sun, 15 Nov 2009 12:13:08 -0500 Subject: Oops, forgot a COPYING file. It's GPLv2. Thanks to Ben Walton for pointing this out. --- COPYING | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 COPYING diff --git a/COPYING b/COPYING new file mode 100644 index 0000000000..d511905c16 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. -- cgit v1.2.1 From 0d31de303f9e8e28cc1649dbf41c1cc635bae2d8 Mon Sep 17 00:00:00 2001 From: Ben Walton Date: Fri, 13 Nov 2009 20:29:39 -0500 Subject: add installation support to Makefile Signed-off-by: Ben Walton --- Makefile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Makefile b/Makefile index 3e97c6246f..faefffded5 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,13 @@ +prefix ?= /usr/local +mandir ?= $(prefix)/share/man +gitdir ?= $(shell git --exec-path) + +# this should be set to a 'standard' bsd-type install program +INSTALL ?= install +INSTALL_DATA = $(INSTALL) -c -m 0644 +INSTALL_EXE = $(INSTALL) -c -m 0755 +INSTALL_DIR = $(INSTALL) -c -d -m 0755 + default: @echo "git-subtree doesn't need to be built." @echo "Just copy it somewhere on your PATH, like /usr/local/bin." @@ -5,6 +15,16 @@ default: @echo "Try: make doc" @false +install: install-exe install-doc + +install-exe: git-subtree.sh + $(INSTALL_DIR) $(DESTDIR)/$(gitdir) + $(INSTALL_EXE) $< $(DESTDIR)/$(gitdir)/git-subtree + +install-doc: git-subtree.1 + $(INSTALL_DIR) $(DESTDIR)/$(mandir)/man1/ + $(INSTALL_DATA) $< $(DESTDIR)/$(mandir)/man1/ + doc: git-subtree.1 %.1: %.xml -- cgit v1.2.1 From d20ac24c2fe860dba99b40d76fe371a323e9918d Mon Sep 17 00:00:00 2001 From: Ben Walton Date: Fri, 13 Nov 2009 20:29:40 -0500 Subject: make git version dynamic when building documentation Signed-off-by: Ben Walton --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index faefffded5..9b204bdcae 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ prefix ?= /usr/local mandir ?= $(prefix)/share/man gitdir ?= $(shell git --exec-path) +gitver ?= $(word 3,$(shell git --version)) + # this should be set to a 'standard' bsd-type install program INSTALL ?= install INSTALL_DATA = $(INSTALL) -c -m 0644 @@ -32,7 +34,7 @@ doc: git-subtree.1 %.xml: %.txt asciidoc -b docbook -d manpage -f asciidoc.conf \ - -agit_version=1.6.3 $^ + -agit_version=$(gitver) $^ clean: rm -f *~ *.xml *.html *.1 -- cgit v1.2.1 From d344532afd3d99c9cdd6d7cfc46bd92ff7def4e8 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 20 Nov 2009 19:43:47 -0500 Subject: Weird, I forgot to have 'make test' call test.sh. --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 9b204bdcae..91e0cc08ea 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ default: @echo "Just copy it somewhere on your PATH, like /usr/local/bin." @echo @echo "Try: make doc" + @echo " or: make test" @false install: install-exe install-doc @@ -35,6 +36,9 @@ doc: git-subtree.1 %.xml: %.txt asciidoc -b docbook -d manpage -f asciidoc.conf \ -agit_version=$(gitver) $^ + +test: + ./test.sh clean: rm -f *~ *.xml *.html *.1 -- cgit v1.2.1 From 6da401386ea37f1bda3f6717d7c77fb0e5c351b2 Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Wed, 6 Jan 2010 23:11:43 +0100 Subject: added -p alias for --prefix --- git-subtree.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 8baa376fe5..28fb8e81fb 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -16,7 +16,7 @@ git subtree split --prefix= h,help show the help q quiet d show debug messages -prefix= the name of the subdir to split out +p,prefix= the name of the subdir to split out options for 'split' annotate= add a prefix to commit message of new commits b,branch= create a new branch from the split subtree @@ -76,7 +76,7 @@ while [ $# -gt 0 ]; do --annotate) annotate="$1"; shift ;; --no-annotate) annotate= ;; -b) branch="$1"; shift ;; - --prefix) prefix="$1"; shift ;; + -p) prefix="$1"; shift ;; --no-prefix) prefix= ;; --onto) onto="$1"; shift ;; --no-onto) onto= ;; -- cgit v1.2.1 From 2da0969a794eac50401fd25ea072224d4378a5fe Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Sat, 9 Jan 2010 19:55:35 +0100 Subject: added -m/--message option for setting merge commit message --- git-subtree.sh | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 28fb8e81fb..96118735b2 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -17,6 +17,7 @@ h,help show the help q quiet d show debug messages p,prefix= the name of the subdir to split out +m,message= use the given message as the commit message for the merge commit options for 'split' annotate= add a prefix to commit message of new commits b,branch= create a new branch from the split subtree @@ -40,6 +41,7 @@ rejoin= ignore_joins= annotate= squash= +message= debug() { @@ -77,6 +79,7 @@ while [ $# -gt 0 ]; do --no-annotate) annotate= ;; -b) branch="$1"; shift ;; -p) prefix="$1"; shift ;; + -m) message="$1"; shift ;; --no-prefix) prefix= ;; --onto) onto="$1"; shift ;; --no-onto) onto= ;; @@ -266,8 +269,13 @@ add_msg() dir="$1" latest_old="$2" latest_new="$3" + if [ -n "$message" ]; then + commit_message="$message" + else + commit_message="Add '$dir/' from commit '$latest_new'" + fi cat <<-EOF - Add '$dir/' from commit '$latest_new' + $commit_message git-subtree-dir: $dir git-subtree-mainline: $latest_old @@ -275,13 +283,27 @@ add_msg() EOF } +add_squashed_msg() +{ + if [ -n "$message" ]; then + echo "$message" + else + echo "Merge commit '$1' as '$2'" + fi +} + rejoin_msg() { dir="$1" latest_old="$2" latest_new="$3" + if [ -n "$message" ]; then + commit_message="$message" + else + commit_message="Split '$dir/' into commit '$latest_new'" + fi cat <<-EOF - Split '$dir/' into commit '$latest_new' + $message git-subtree-dir: $dir git-subtree-mainline: $latest_old @@ -441,7 +463,7 @@ cmd_add() if [ -n "$squash" ]; then rev=$(new_squash_commit "" "" "$rev") || exit $? - commit=$(echo "Merge commit '$rev' as '$dir'" | + commit=$(add_squashed_msg "$rev" "$dir" | git commit-tree $tree $headp -p "$rev") || exit $? else commit=$(add_msg "$dir" "$headrev" "$rev" | @@ -561,7 +583,7 @@ cmd_merge() rev="$new" fi - git merge -s subtree $rev + git merge -s subtree --message="$message" $rev } cmd_pull() -- cgit v1.2.1 From 0a562948ae1fe7130f4e9a29ce4107311ab93b91 Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Sat, 9 Jan 2010 19:56:05 +0100 Subject: allow using --branch with existing branches if it makes sense --- git-subtree.sh | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 96118735b2..09992e39d5 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -161,6 +161,20 @@ rev_exists() fi } +rev_is_descendant_of_branch() +{ + newrev="$1" + branch="$2" + branch_hash=$(git rev-parse $branch) + match=$(git rev-list $newrev | grep $branch_hash) + + if [ -n "$match" ]; then + return 0 + else + return 1 + fi +} + # if a commit doesn't have a parent, this might not work. But we only want # to remove the parent from the rev-list, and since it doesn't exist, it won't # be there anyway, so do nothing in that case. @@ -476,10 +490,6 @@ cmd_add() cmd_split() { - if [ -n "$branch" ] && rev_exists "refs/heads/$branch"; then - die "Branch '$branch' already exists." - fi - debug "Splitting $dir..." cache_setup || exit $? @@ -510,7 +520,8 @@ cmd_split() eval "$grl" | while read rev parents; do revcount=$(($revcount + 1)) - say -n "$revcount/$revmax ($createcount) " + say -n "$revcount/$revmax ($createcount) +" debug "Processing commit: $rev" exists=$(cache_get $rev) if [ -n "$exists" ]; then @@ -548,9 +559,16 @@ cmd_split() $latest_new >&2 || exit $? fi if [ -n "$branch" ]; then - git update-ref -m 'subtree split' "refs/heads/$branch" \ - $latest_new "" || exit $? - say "Created branch '$branch'" + if rev_exists "refs/heads/$branch"; then + if ! rev_is_descendant_of_branch $latest_new $branch; then + die "Branch '$branch' is not an ancestor of commit '$latest_new'." + fi + action='Updated' + else + action='Created' + fi + git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $? + say "$action branch '$branch'" fi echo $latest_new exit 0 -- cgit v1.2.1 From da949cc554304bf9dc2b20ffcd470fb6b8a35576 Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Sat, 9 Jan 2010 23:01:39 +0100 Subject: fix for subtree split not finding proper base for new commits --- git-subtree.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 09992e39d5..cdf7b0992b 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -538,7 +538,10 @@ cmd_split() # ugly. is there no better way to tell if this is a subtree # vs. a mainline commit? Does it matter? - [ -z $tree ] && continue + if [ -z $tree ]; then + cache_set $rev $rev + continue + fi newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? debug " newrev is: $newrev" -- cgit v1.2.1 From 6e25f79f353a1c1f454ae0c44aa45ff3ab44551c Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Tue, 12 Jan 2010 22:38:21 +0100 Subject: changed alias for --prefix from -p to -P --- git-subtree.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index cdf7b0992b..0a5cafa77f 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -16,7 +16,7 @@ git subtree split --prefix= h,help show the help q quiet d show debug messages -p,prefix= the name of the subdir to split out +P,prefix= the name of the subdir to split out m,message= use the given message as the commit message for the merge commit options for 'split' annotate= add a prefix to commit message of new commits @@ -78,7 +78,7 @@ while [ $# -gt 0 ]; do --annotate) annotate="$1"; shift ;; --no-annotate) annotate= ;; -b) branch="$1"; shift ;; - -p) prefix="$1"; shift ;; + -P) prefix="$1"; shift ;; -m) message="$1"; shift ;; --no-prefix) prefix= ;; --onto) onto="$1"; shift ;; -- cgit v1.2.1 From 12629161a89511847ec5703ca457ee373deb33cb Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Tue, 12 Jan 2010 22:38:34 +0100 Subject: fixed bug in commit message for split --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 0a5cafa77f..48bc570390 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -317,7 +317,7 @@ rejoin_msg() commit_message="Split '$dir/' into commit '$latest_new'" fi cat <<-EOF - $message + $commit_message git-subtree-dir: $dir git-subtree-mainline: $latest_old -- cgit v1.2.1 From 13ea2b5e0786e70780f91637fb82ca18dd03a730 Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Tue, 12 Jan 2010 23:04:25 +0100 Subject: added tests for recent changes --- test.sh | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index 12b0456574..cfe3a3c258 100755 --- a/test.sh +++ b/test.sh @@ -53,6 +53,16 @@ multiline() done } +undo() +{ + git reset --hard HEAD~ +} + +last_commit_message() +{ + git log --format=%s -1 +} + rm -rf mainline subproj mkdir mainline subproj @@ -82,7 +92,24 @@ git branch subdir git fetch ../subproj sub1 git branch sub1 FETCH_HEAD + +# check if --message works for add +git subtree add --prefix=subdir --message="Added subproject" sub1 +check_equal "$(last_commit_message)" "Added subproject" +undo + +# check if --message works as -m and --prefix as -P +git subtree add -P subdir -m "Added subproject using git subtree" sub1 +check_equal "$(last_commit_message)" "Added subproject using git subtree" +undo + +# check if --message works with squash too +git subtree add -P subdir -m "Added subproject with squash" --squash sub1 +check_equal "$(last_commit_message)" "Added subproject with squash" +undo + git subtree add --prefix=subdir/ FETCH_HEAD +check_equal "$(last_commit_message)" "Add 'subdir/' from commit '$(git rev-parse sub1)'" # this shouldn't actually do anything, since FETCH_HEAD is already a parent git merge -m 'merge -s -ours' -s ours FETCH_HEAD @@ -98,13 +125,44 @@ git commit -m 'main-sub7' git fetch ../subproj sub2 git branch sub2 FETCH_HEAD + +# check if --message works for merge +git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 +check_equal "$(last_commit_message)" "Merged changes from subproject" +undo + +# check if --message for merge works with squash too +git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 +check_equal "$(last_commit_message)" "Merged changes from subproject using squash" +undo + git subtree merge --prefix=subdir FETCH_HEAD git branch pre-split +check_equal "$(last_commit_message)" "Merge commit '$(git rev-parse sub2)' into mainline" -spl1=$(git subtree split --annotate='*' \ - --prefix subdir --onto FETCH_HEAD --rejoin) +# check if --message works for split+rejoin +spl1=$(git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) echo "spl1={$spl1}" git branch spl1 "$spl1" +check_equal "$(last_commit_message)" "Split & rejoin" +undo + +# check split with --branch +git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --branch splitbr1 +check_equal "$(git rev-parse splitbr1)" "$spl1" + +# check split with --branch for an existing branch +git branch splitbr2 sub1 +git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --branch splitbr2 +check_equal "$(git rev-parse splitbr2)" "$spl1" + +# check split with --branch for an incompatible branch +result=$(git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir || echo "caught error") +check_equal "$result" "caught error" + + +git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --rejoin +check_equal "$(last_commit_message)" "Split 'subdir/' into commit '$spl1'" create subdir/main-sub8 git commit -m 'main-sub8' @@ -170,6 +228,50 @@ check_equal "$(git log --pretty=format:'%s' HEAD^2 | grep -i split)" "" # meaningless to subproj since one side of the merge refers to the mainline) check_equal "$(git log --pretty=format:'%s%n%b' HEAD^2 | grep 'git-subtree.*:')" "" + +# check if split can find proper base without --onto +# prepare second pair of repositories +mkdir test2 +cd test2 + +mkdir main +cd main +git init +create main1 +git commit -m "main1" + +cd .. +mkdir sub +cd sub +git init +create sub2 +git commit -m "sub2" + +cd ../main +git fetch ../sub master +git branch sub2 FETCH_HEAD +git subtree add --prefix subdir sub2 + +cd ../sub +create sub3 +git commit -m "sub3" + +cd ../main +git fetch ../sub master +git branch sub3 FETCH_HEAD +git subtree merge --prefix subdir sub3 + +create subdir/main-sub4 +git commit -m "main-sub4" +git subtree split --prefix subdir --branch mainsub4 + +# at this point, the new commit's parent should be sub3 +# if it's not, something went wrong (the "newparent" of "master~" commit should have been sub3, +# but it wasn't, because it's cache was not set to itself) +check_equal "$(git log --format=%P -1 mainsub4)" "$(git rev-parse sub3)" + + + # make sure no patch changes more than one file. The original set of commits # changed only one file each. A multi-file change would imply that we pruned # commits too aggressively. -- cgit v1.2.1 From 4a6ea5ce301c9844722bfdb9291bd9cb7797d9ed Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Tue, 12 Jan 2010 23:04:56 +0100 Subject: added temporary test dirs to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e358b18b78..7e77c9d022 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *~ git-subtree.xml git-subtree.1 +mainline +subproj -- cgit v1.2.1 From 6bd910a82155ae3def5cf38acb27d36a192c449e Mon Sep 17 00:00:00 2001 From: Jakub Suder Date: Tue, 12 Jan 2010 23:34:52 +0100 Subject: improved rev_is_descendant_of_branch() function --- git-subtree.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 48bc570390..66ce251eaa 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -166,9 +166,9 @@ rev_is_descendant_of_branch() newrev="$1" branch="$2" branch_hash=$(git rev-parse $branch) - match=$(git rev-list $newrev | grep $branch_hash) + match=$(git rev-list -1 $branch_hash ^$newrev) - if [ -n "$match" ]; then + if [ -z "$match" ]; then return 0 else return 1 -- cgit v1.2.1 From e1ce417d0cb4bfe719efa07417c690b1ce0326e9 Mon Sep 17 00:00:00 2001 From: Arlen Cuss Date: Tue, 19 Jan 2010 12:51:19 -0700 Subject: Fix refspecs in given example for git subtree pull. (Updated slightly by apenwarr) --- git-subtree.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.txt b/git-subtree.txt index e7ce2d3654..9b2d48e334 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -258,7 +258,7 @@ And you can merge changes back in from the upstream project just as easily: $ git subtree pull --prefix=gitweb \ - git@github.com:whatever/gitweb.git gitweb-latest:master + git@github.com:whatever/gitweb.git master Or, using '--squash', you can actually rewind to an earlier version of gitweb: -- cgit v1.2.1 From e2d0a4502f115ee63b8ff96661cfcc8aad075822 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Tue, 2 Feb 2010 10:30:11 -0500 Subject: Jakub's changes broke the progress message slightly. We really need that ^M (\r), not a ^J (\n) if we want the status message to overwrite itself nicely. --- git-subtree.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 66ce251eaa..11cda9ea82 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -520,8 +520,7 @@ cmd_split() eval "$grl" | while read rev parents; do revcount=$(($revcount + 1)) - say -n "$revcount/$revmax ($createcount) -" + say -n "$revcount/$revmax ($createcount) " debug "Processing commit: $rev" exists=$(cache_get $rev) if [ -n "$exists" ]; then -- cgit v1.2.1 From 37668a13edbc8bd8f8ac5ecbd5bf839a4171c09b Mon Sep 17 00:00:00 2001 From: Win Treese Date: Fri, 5 Feb 2010 19:48:11 -0500 Subject: git-subtree.txt: add another example. --- git-subtree.txt | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index 9b2d48e334..2200aaeaf2 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -223,8 +223,8 @@ OPTIONS FOR split subproject's history to be part of your project anyway. -EXAMPLES --------- +EXAMPLE 1 +--------- Let's use the repository for the git source code as an example. First, get your own copy of the git.git repository: @@ -284,6 +284,23 @@ the standard gitweb: git log gitweb-latest..$(git subtree split --prefix=gitweb) +EXAMPLE 2 +--------- +Suppose you have a source directory with many files and +subdirectories, and you want to extract the lib directory to its own +git project. Here's a short way to do it: + +First, make the new repository wherever you want: + + git init --bare + +Back in your original directory: + git subtree split --prefix=lib --annotate="(split)" -b split + +Then push the new branch onto the new empty repository: + git push split:master + + AUTHOR ------ -- cgit v1.2.1 From 349a70d5cf127222c8a089f116070614ebd18732 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 6 Feb 2010 15:05:17 -0500 Subject: Make tests pass with recent git (1.7.0 and up). It seems that in older versions, --message="" was interpreted as "use the default commit message" instead of "use an empty commit message", and git-subtree was depending on this behaviour. Now we don't, so tests pass again. --- git-subtree.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 11cda9ea82..009c0db9bc 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -603,7 +603,11 @@ cmd_merge() rev="$new" fi - git merge -s subtree --message="$message" $rev + if [ -n "$message" ]; then + git merge -s subtree --message="$message" $rev + else + git merge -s subtree $rev + fi } cmd_pull() -- cgit v1.2.1 From ec54f0d9adb50baa14eae44a1b8c410f794d32de Mon Sep 17 00:00:00 2001 From: Win Treese Date: Fri, 5 Feb 2010 22:02:43 -0500 Subject: Make sure that exists when splitting. And test cases for that check, as well as for an error if no prefix is specified at all. --- git-subtree.sh | 5 +++++ test.sh | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/git-subtree.sh b/git-subtree.sh index 009c0db9bc..52d4c0aeb1 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -105,6 +105,11 @@ esac if [ -z "$prefix" ]; then die "You must provide the --prefix option." fi + +if [ "$command" = "split" -a """"! -e "$prefix" ]; then + die "$prefix does not exist." +fi + dir="$(dirname "$prefix/.")" if [ "$command" != "pull" ]; then diff --git a/test.sh b/test.sh index cfe3a3c258..d0a2c86c24 100755 --- a/test.sh +++ b/test.sh @@ -140,6 +140,14 @@ git subtree merge --prefix=subdir FETCH_HEAD git branch pre-split check_equal "$(last_commit_message)" "Merge commit '$(git rev-parse sub2)' into mainline" +# Check that prefix argument is required for split (exits with warning and exit status = 1) +! result=$(git subtree split 2>&1) +check_equal "You must provide the --prefix option." "$result" + +# Check that the exists for a split. +! result=$(git subtree split --prefix=non-existent-directory 2>&1) +check_equal "non-existent-directory does not exist." "$result" + # check if --message works for split+rejoin spl1=$(git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) echo "spl1={$spl1}" -- cgit v1.2.1 From 77ba30585213ee8e2be21841ba38786fd5bb26a5 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 8 Feb 2010 15:00:42 -0500 Subject: Improve checking for existence of the --prefix directory. For add, the prefix must *not* already exist. For all the other commands, it *must* already exist. --- git-subtree.sh | 9 ++++++--- test.sh | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 52d4c0aeb1..e76b45c2dd 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -106,9 +106,12 @@ if [ -z "$prefix" ]; then die "You must provide the --prefix option." fi -if [ "$command" = "split" -a """"! -e "$prefix" ]; then - die "$prefix does not exist." -fi +case "$command" in + add) [ -e "$prefix" ] && + die "prefix '$prefix' already exists." ;; + *) [ -e "$prefix" ] || + die "'$prefix' does not exist; use 'git subtree add'" ;; +esac dir="$(dirname "$prefix/.")" diff --git a/test.sh b/test.sh index d0a2c86c24..1446cfeed8 100755 --- a/test.sh +++ b/test.sh @@ -21,6 +21,19 @@ check() fi } +check_not() +{ + echo + echo "check: NOT " "$@" + if "$@"; then + echo FAILED + exit 1 + else + echo ok + return 0 + fi +} + check_equal() { echo @@ -94,6 +107,8 @@ git fetch ../subproj sub1 git branch sub1 FETCH_HEAD # check if --message works for add +check_not git subtree merge --prefix=subdir sub1 +check_not git subtree pull --prefix=subdir ../subproj sub1 git subtree add --prefix=subdir --message="Added subproject" sub1 check_equal "$(last_commit_message)" "Added subproject" undo -- cgit v1.2.1 From 00889c8ca7017d7b9d07d47843e3a3e245a1de26 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 8 Feb 2010 19:42:15 -0500 Subject: Oops. Apparently I didn't run 'make test' after most recent change. Thanks to Dan Sabath for pointing that out. --- test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 1446cfeed8..c6ecde20bd 100755 --- a/test.sh +++ b/test.sh @@ -161,7 +161,8 @@ check_equal "You must provide the --prefix option." "$result" # Check that the exists for a split. ! result=$(git subtree split --prefix=non-existent-directory 2>&1) -check_equal "non-existent-directory does not exist." "$result" +check_equal "'non-existent-directory' does not exist; use 'git subtree add'" \ + "$result" # check if --message works for split+rejoin spl1=$(git subtree split --annotate='*' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) -- cgit v1.2.1 From 6fe986307d983704d3c6b3540f91fa301ff48549 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 8 Feb 2010 19:44:41 -0500 Subject: Some recent tests accidentally depended on very new versions of git. The "--format" option is too new. Use "--pretty=format:" (which means the same thing) instead. Now it works again on git 1.6.0 (at least). --- test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index c6ecde20bd..8c1f1ea6bd 100755 --- a/test.sh +++ b/test.sh @@ -73,7 +73,7 @@ undo() last_commit_message() { - git log --format=%s -1 + git log --pretty=format:%s -1 } rm -rf mainline subproj @@ -292,7 +292,7 @@ git subtree split --prefix subdir --branch mainsub4 # at this point, the new commit's parent should be sub3 # if it's not, something went wrong (the "newparent" of "master~" commit should have been sub3, # but it wasn't, because it's cache was not set to itself) -check_equal "$(git log --format=%P -1 mainsub4)" "$(git rev-parse sub3)" +check_equal "$(git log --pretty=format:%P -1 mainsub4)" "$(git rev-parse sub3)" -- cgit v1.2.1 From c6ca48d4dc5b93a7576d2afa66b057f8c8a20718 Mon Sep 17 00:00:00 2001 From: Dan Sabath Date: Mon, 8 Feb 2010 20:31:51 -0500 Subject: docs: add simple 'add' case to clarify setup. This patch adds a simple use case for adding a library to an existing repository. Signed-off-by: Dan Sabath --- git-subtree.txt | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index 2200aaeaf2..cde5a7e73e 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -225,7 +225,31 @@ OPTIONS FOR split EXAMPLE 1 --------- -Let's use the repository for the git source code as an example. +Let's assume that you have a local repository that you would like +to add an external vendor library to. In this case we will add the +git-subtree repository as a subdirectory of your already existing +git-extensions repository in ~/git-extensions/. + +First we need to fetch the remote objects + $ cd ~/git-extensions + $ git fetch git://github.com/apenwarr/git-subtree.git master + +'master' needs to be a valid remote ref and can be a different branch +name + +Now we add the vendor library with + $ git subtree add --prefix=git-subtree --squash FETCH_HEAD + +You can omit the --squash flag, but doing so will increase the number +of commits that are incldued in your local repository. + +We now have ~/git-extensions/git-subtree directory with the git-subtree +subdirectory containing code from the master branch of +git://github.com/apenwarr/git-subtree.git + +EXAMPLE 2 +--------- +Let's use the repository for the git source code as an example. First, get your own copy of the git.git repository: $ git clone git://git.kernel.org/pub/scm/git/git.git test-git @@ -284,7 +308,7 @@ the standard gitweb: git log gitweb-latest..$(git subtree split --prefix=gitweb) -EXAMPLE 2 +EXAMPLE 3 --------- Suppose you have a source directory with many files and subdirectories, and you want to extract the lib directory to its own -- cgit v1.2.1 From ae3301876cfea3b7260fbe0840635d508968a047 Mon Sep 17 00:00:00 2001 From: Dan Sabath Date: Mon, 8 Feb 2010 17:43:09 -0800 Subject: Docs: cleaning up example textual redundancy Signed-off-by: Dan Sabath --- git-subtree.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index cde5a7e73e..c455f6912b 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -243,9 +243,9 @@ Now we add the vendor library with You can omit the --squash flag, but doing so will increase the number of commits that are incldued in your local repository. -We now have ~/git-extensions/git-subtree directory with the git-subtree -subdirectory containing code from the master branch of -git://github.com/apenwarr/git-subtree.git +We now have a ~/git-extensions/git-subtree directory containing code +from the master branch of git://github.com/apenwarr/git-subtree.git +in our git-extensions repository. EXAMPLE 2 --------- -- cgit v1.2.1 From c00d1d11688dc02f066196ed18783effdb7767ab Mon Sep 17 00:00:00 2001 From: Wayne Walter Date: Sat, 13 Feb 2010 14:32:21 -0500 Subject: Added new 'push' command and 2-parameter form of 'add'. Now you can do: git subtree add --prefix=whatever git://wherever branchname to add a new branch, instead of rather weirdly having to do 'git fetch' first. You can also split and push in one step: git subtree push --prefix=whatever git://wherever newbranch (Somewhat cleaned up by apenwarr.) --- INSTALL | 11 ++++++++++- git-subtree.sh | 58 ++++++++++++++++++++++++++++++++++++++++++++++++--------- git-subtree.txt | 24 ++++++++++++++++-------- install.sh | 2 ++ 4 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 install.sh diff --git a/INSTALL b/INSTALL index 5966dde46c..81ac702ad2 100644 --- a/INSTALL +++ b/INSTALL @@ -2,7 +2,16 @@ HOW TO INSTALL git-subtree ========================== -Copy the file 'git-subtree.sh' to /usr/local/bin/git-subtree. +You simply need to copy the file 'git-subtree.sh' to where +the rest of the git scripts are stored. + +From the Git bash window just run: + +install.sh + +Or if you have the full Cygwin installed, you can use make: + +make install That will make a 'git subtree' (note: space instead of dash) command available. See the file git-subtree.txt for more. diff --git a/git-subtree.sh b/git-subtree.sh index e76b45c2dd..501c6dc2f1 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -11,6 +11,7 @@ OPTS_SPEC="\ git subtree add --prefix= git subtree merge --prefix= git subtree pull --prefix= +git subtree push --prefix= git subtree split --prefix= -- h,help show the help @@ -24,7 +25,7 @@ b,branch= create a new branch from the split subtree ignore-joins ignore prior --rejoin commits onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD - options for 'add', 'merge', and 'pull' + options for 'add', 'merge', 'pull' and 'push' squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) @@ -98,7 +99,7 @@ command="$1" shift case "$command" in add|merge|pull) default= ;; - split) default="--default HEAD" ;; + split|push) default="--default HEAD" ;; *) die "Unknown command '$command'" ;; esac @@ -115,7 +116,7 @@ esac dir="$(dirname "$prefix/.")" -if [ "$command" != "pull" ]; then +if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then revs=$(git rev-parse $default --revs-only "$@") || exit $? dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? if [ -n "$dirs" ]; then @@ -450,10 +451,10 @@ copy_or_skip() ensure_clean() { - if ! git diff-index HEAD --exit-code --quiet; then + if ! git diff-index HEAD --exit-code --quiet 2>&1; then die "Working tree has modifications. Cannot add." fi - if ! git diff-index --cached HEAD --exit-code --quiet; then + if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then die "Index has modifications. Cannot add." fi } @@ -463,12 +464,34 @@ cmd_add() if [ -e "$dir" ]; then die "'$dir' already exists. Cannot add." fi + ensure_clean - set -- $revs - if [ $# -ne 1 ]; then - die "You must provide exactly one revision. Got: '$revs'" + if [ $# -eq 1 ]; then + "cmd_add_commit" "$@" + elif [ $# -eq 2 ]; then + "cmd_add_repository" "$@" + else + say "error: parameters were '$@'" + die "Provide either a refspec or a repository and refspec." fi +} + +cmd_add_repository() +{ + echo "git fetch" "$@" + repository=$1 + refspec=$2 + git fetch "$@" || exit $? + revs=FETCH_HEAD + set -- $revs + cmd_add_commit "$@" +} + +cmd_add_commit() +{ + revs=$(git rev-parse $default --revs-only "$@") || exit $? + set -- $revs rev="$1" debug "Adding $dir as '$rev'..." @@ -586,6 +609,7 @@ cmd_split() cmd_merge() { + revs=$(git rev-parse $default --revs-only "$@") || exit $? ensure_clean set -- $revs @@ -623,7 +647,23 @@ cmd_pull() ensure_clean git fetch "$@" || exit $? revs=FETCH_HEAD - cmd_merge + set -- $revs + cmd_merge "$@" +} + +cmd_push() +{ + if [ $# -ne 2 ]; then + die "You must provide " + fi + if [ -e "$dir" ]; then + repository=$1 + refspec=$2 + echo "git push using: " $repository $refspec + git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec + else + die "'$dir' must already exist. Try 'git subtree add'." + fi } "cmd_$command" "$@" diff --git a/git-subtree.txt b/git-subtree.txt index c455f6912b..4f715c640b 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -9,10 +9,12 @@ git-subtree - add, merge, and split subprojects stored in subtrees SYNOPSIS -------- [verse] -'git subtree' add --prefix= -'git subtree' merge --prefix= +'git subtree' add --prefix= 'git subtree' pull --prefix= -'git subtree' split --prefix= +'git subtree' push --prefix= +'git subtree' add --prefix= +'git subtree' merge --prefix= +'git subtree' split --prefix= DESCRIPTION @@ -60,11 +62,11 @@ COMMANDS -------- add:: Create the subtree by importing its contents - from the given commit. A new commit is created - automatically, joining the imported project's history - with your own. With '--squash', imports only a single - commit from the subproject, rather than its entire - history. + from the given or and remote . + A new commit is created automatically, joining the imported + project's history with your own. With '--squash', imports + only a single commit from the subproject, rather than its + entire history. merge:: Merge recent changes up to into the @@ -84,6 +86,12 @@ pull:: Exactly like 'merge', but parallels 'git pull' in that it fetches the given commit from the specified remote repository. + +push:: + Does a 'split' (see above) using the supplied + and then does a 'git push' to push the result to the + repository and refspec. This can be used to push your + subtree to different branches of the remote repository. split:: Extract a new, synthetic project history from the diff --git a/install.sh b/install.sh new file mode 100644 index 0000000000..1f87a62434 --- /dev/null +++ b/install.sh @@ -0,0 +1,2 @@ +# copy Git to where the rest of the Git scripts are found. +cp git-subtree.sh "$(git --exec-path)"/git-subtree \ No newline at end of file -- cgit v1.2.1 From 448e71e2637e0d7546fb0a2b386e74bc7aa93be8 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 7 May 2010 21:21:25 +0200 Subject: Use 'git merge -Xsubtree' when git version >= 1.7.0. It's possible to specify the subdir of a subtree since Git 1.7.0 - adding support for that functionality to make the merge more stable. Also checking for git version - now only uses the new subtree subdir option when on at least 1.7. --- git-subtree.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index 501c6dc2f1..b7c350107e 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -634,11 +634,20 @@ cmd_merge() debug "New squash commit: $new" rev="$new" fi - - if [ -n "$message" ]; then - git merge -s subtree --message="$message" $rev + + version=$(git version) + if [ "$version" \< "git version 1.7" ]; then + if [ -n "$message" ]; then + git merge -s subtree --message="$message" $rev + else + git merge -s subtree $rev + fi else - git merge -s subtree $rev + if [ -n "$message" ]; then + git merge -Xsubtree="$prefix" --message="$message" $rev + else + git merge -Xsubtree="$prefix" $rev + fi fi } -- cgit v1.2.1 From 39f5fff0d53bcc68f5c698150d2d3b35eececc27 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Thu, 20 May 2010 22:40:09 +0200 Subject: Fixed regression with splitting out new subtree A folder in a repository that wasn't initially imported as a subtree could no longer be splitted into an entirely new subtree with no parent. A fix and a new test to fix that regression is added here. --- git-subtree.sh | 5 ++++- test.sh | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index b7c350107e..a86cfd8b9f 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -253,6 +253,7 @@ find_existing_splits() if [ -n "$main" -a -n "$sub" ]; then debug " Prior: $main -> $sub" cache_set $main $sub + cache_set $sub $sub try_remove_previous "$main" try_remove_previous "$sub" fi @@ -569,7 +570,9 @@ cmd_split() # ugly. is there no better way to tell if this is a subtree # vs. a mainline commit? Does it matter? if [ -z $tree ]; then - cache_set $rev $rev + if [ -n "$newparents" ]; then + cache_set $rev $rev + fi continue fi diff --git a/test.sh b/test.sh index 8c1f1ea6bd..45237c3374 100755 --- a/test.sh +++ b/test.sh @@ -294,6 +294,15 @@ git subtree split --prefix subdir --branch mainsub4 # but it wasn't, because it's cache was not set to itself) check_equal "$(git log --pretty=format:%P -1 mainsub4)" "$(git rev-parse sub3)" +mkdir subdir2 +create subdir2/main-sub5 +git commit -m "main-sub5" +git subtree split --prefix subdir2 --branch mainsub5 + +# also test that we still can split out an entirely new subtree +# if the parent of the first commit in the tree isn't empty, +# then the new subtree has accidently been attached to something +check_equal "$(git log --pretty=format:%P -1 mainsub5)" "" # make sure no patch changes more than one file. The original set of commits -- cgit v1.2.1 From 9c632ea29ccd58a9967690c2670edec31dc468cd Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 24 Jun 2010 01:53:05 -0400 Subject: (Hopefully) fix PATH setting for msysgit. Reported by Evan Shaw. The problem is that $(git --exec-path) includes a 'git' binary which is incompatible with the one in /usr/bin; if you run it, it gives you an error about libiconv2.dll. You might think we could just add $(git --exec-path) at the *end* of PATH, but then if there are multiple versions of git installed, we could end up with the wrong one; earlier versions used to put git-sh-setup in /usr/bin, so we'd pick up that one before the new one. So now we just set PATH back to its original value right after running git-sh-setup, and we should be okay. --- git-subtree.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git-subtree.sh b/git-subtree.sh index 501c6dc2f1..935dfca7f3 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -29,8 +29,12 @@ rejoin merge the new branch back into HEAD squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) + +OPATH=$PATH PATH=$(git --exec-path):$PATH . git-sh-setup +PATH=$OPATH # apparently needed for some versions of msysgit + require_work_tree quiet= -- cgit v1.2.1 From df2302d77449832eb4167999d747aef57e4bd1fc Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 24 Jun 2010 16:57:58 -0400 Subject: Another fix for PATH and msysgit. Evan Shaw tells me the previous fix didn't work. Let's use this one instead, which he says does work. This fix is kind of wrong because it will run the "correct" git-sh-setup *after* the one in /usr/bin, if there is one, which could be weird if you have multiple versions of git installed. But it works on my Linux and his msysgit, so it's obviously better than what we had before. --- git-subtree.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/git-subtree.sh b/git-subtree.sh index a15d91ffb1..781eef3783 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -30,10 +30,8 @@ squash merge subtree changes as a single commit " eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) -OPATH=$PATH -PATH=$(git --exec-path):$PATH +PATH=$PATH:$(git --exec-path) . git-sh-setup -PATH=$OPATH # apparently needed for some versions of msysgit require_work_tree -- cgit v1.2.1 From 242b20dc0ae51719c32776fb5996c5d6cb463928 Mon Sep 17 00:00:00 2001 From: Bryan Larsen Date: Wed, 21 Jul 2010 12:58:05 -0400 Subject: docs: simplify example 1 The documentation was written prior to Wayne Walter's 2-parameter add. Using 2-parameter add in example 1 makes the example much simpler. --- git-subtree.txt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index 4f715c640b..dbcba31346 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -236,18 +236,14 @@ EXAMPLE 1 Let's assume that you have a local repository that you would like to add an external vendor library to. In this case we will add the git-subtree repository as a subdirectory of your already existing -git-extensions repository in ~/git-extensions/. +git-extensions repository in ~/git-extensions/: -First we need to fetch the remote objects - $ cd ~/git-extensions - $ git fetch git://github.com/apenwarr/git-subtree.git master + $ git subtree add --prefix=git-subtree --squash \ + git://github.com/apenwarr/git-subtree.git master 'master' needs to be a valid remote ref and can be a different branch name -Now we add the vendor library with - $ git subtree add --prefix=git-subtree --squash FETCH_HEAD - You can omit the --squash flag, but doing so will increase the number of commits that are incldued in your local repository. -- cgit v1.2.1 From 7f74d65b12ecb5142442c276da805b42e3023a9d Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 12 Aug 2010 11:15:57 -0400 Subject: Fix typo: an -> a Thanks to Vanuan on github. --- git-subtree.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.txt b/git-subtree.txt index dbcba31346..a30293d607 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -19,7 +19,7 @@ SYNOPSIS DESCRIPTION ----------- -git subtree allows you to include an subproject in your +git subtree allows you to include a subproject in your own repository as a subdirectory, optionally including the subproject's entire history. For example, you could include the source code for a library as a subdirectory of your -- cgit v1.2.1 From 11f1511e7650a78709d0d7198bb150cd5392d9d1 Mon Sep 17 00:00:00 2001 From: Cole Stanfield Date: Mon, 13 Sep 2010 13:16:57 -0600 Subject: Fixing eval syntax error. --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index 781eef3783..ce94d363dc 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -28,7 +28,7 @@ rejoin merge the new branch back into HEAD options for 'add', 'merge', 'pull' and 'push' squash merge subtree changes as a single commit " -eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) +eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" PATH=$PATH:$(git --exec-path) . git-sh-setup -- cgit v1.2.1 From 7f86ff0fe292a5061757f3ceaffdd992c5feaa9f Mon Sep 17 00:00:00 2001 From: John Yani Date: Thu, 12 Aug 2010 19:54:55 +0300 Subject: docs: Description, synopsys, options and examples changes. Description: Made the difference from submodules and the subtree merge strategy clearer. Synopsys and options: Synchronize with 'git subtree -h' output. I hope, properly. Examples: Added example descriptions in captions. Small fixes. Signed-off-by: John Yani --- git-subtree.txt | 112 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 43 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index a30293d607..18a9af501f 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -3,50 +3,55 @@ git-subtree(1) NAME ---- -git-subtree - add, merge, and split subprojects stored in subtrees +git-subtree - Merge subtrees together and split repository into subtrees SYNOPSIS -------- [verse] -'git subtree' add --prefix= -'git subtree' pull --prefix= -'git subtree' push --prefix= -'git subtree' add --prefix= -'git subtree' merge --prefix= -'git subtree' split --prefix= - +'git subtree' add -P |--prefix= +'git subtree' pull -P |--prefix= +'git subtree' push -P |--prefix= +'git subtree' merge -P |--prefix= +'git subtree' split -P |--prefix= [OPTIONS] [] + DESCRIPTION ----------- -git subtree allows you to include a subproject in your -own repository as a subdirectory, optionally including the -subproject's entire history. For example, you could -include the source code for a library as a subdirectory of your -application. - -You can also extract the entire history of a subdirectory from -your project and make it into a standalone project. For -example, if a library you made for one application ends up being -useful elsewhere, you can extract its entire history and publish -that as its own git repository, without accidentally -intermingling the history of your application project. +Subtrees allow subprojects to be included within a subdirectory +of the main project, optionally including the subproject's +entire history. -Most importantly, you can alternate back and forth between these -two operations. If the standalone library gets updated, you can +For example, you could include the source code for a library +as a subdirectory of your application. + +Subtrees are not to be confused with submodules, which are meant for +the same task. Unlike submodules, subtrees do not need any special +constructions (like .gitmodule files or gitlinks) be present in +your repository, and do not force end-users of your +repository to do anything special or to understand how subtrees +work. A subtree is just a subdirectory that can be +committed to, branched, and merged along with your project in +any way you want. + +They are neither not to be confused with using the subtree merge +strategy. The main difference is that, besides merging +of the other project as a subdirectory, you can also extract the +entire history of a subdirectory from your project and make it +into a standalone project. Unlike the subtree merge strategy +you can alternate back and forth between these +two operations. If the standalone library gets updated, you can automatically merge the changes into your project; if you update the library inside your project, you can "split" the changes back out again and merge them back into the library project. -Unlike the 'git submodule' command, git subtree doesn't produce -any special constructions (like .gitmodule files or gitlinks) in -your repository, and doesn't require end-users of your -repository to do anything special or to understand how subtrees -work. A subtree is just another subdirectory and can be -committed to, branched, and merged along with your project in -any way you want. +For example, if a library you made for one application ends up being +useful elsewhere, you can extract its entire history and publish +that as its own git repository, without accidentally +intermingling the history of your application project. +[TIP] In order to keep your commit messages clean, we recommend that people split their commits between the subtrees and the main project as much as possible. That is, if you make a change that @@ -128,20 +133,29 @@ OPTIONS --debug:: Produce even more unnecessary output messages on stderr. +-P :: --prefix=:: Specify the path in the repository to the subtree you - want to manipulate. This option is currently mandatory + want to manipulate. This option is mandatory for all commands. +-m :: +--message=:: + This option is only valid for add, merge and pull (unsure). + Specify as the commit message for the merge commit. -OPTIONS FOR add, merge, AND pull --------------------------------- + +OPTIONS FOR add, merge, push, pull +---------------------------------- --squash:: + This option is only valid for add, merge, push and pull + commands. + Instead of merging the entire history from the subtree project, produce only a single commit that contains all the differences you want to merge, and then merge that new commit into your project. - + Using this option helps to reduce log clutter. People rarely want to see every change that happened between v1.0 and v1.1 of the library they're using, since none of the @@ -169,6 +183,8 @@ OPTIONS FOR add, merge, AND pull OPTIONS FOR split ----------------- --annotate=:: + This option is only valid for the split command. + When generating synthetic history, add as a prefix to each commit message. Since we're creating new commits with the same commit message, but possibly @@ -184,12 +200,16 @@ OPTIONS FOR split -b :: --branch=:: + This option is only valid for the split command. + After generating the synthetic history, create a new branch called that contains the new history. This is suitable for immediate pushing upstream. must not already exist. --ignore-joins:: + This option is only valid for the split command. + If you use '--rejoin', git subtree attempts to optimize its history reconstruction to generate only the new commits since the last '--rejoin'. '--ignore-join' @@ -198,6 +218,8 @@ OPTIONS FOR split long time. --onto=:: + This option is only valid for the split command. + If your subtree was originally imported using something other than git subtree, its history may not match what git subtree is expecting. In that case, you can specify @@ -210,6 +232,8 @@ OPTIONS FOR split this option. --rejoin:: + This option is only valid for the split command. + After splitting, merge the newly created synthetic history back into your main project. That way, future splits can search only the part of history that has @@ -231,8 +255,8 @@ OPTIONS FOR split subproject's history to be part of your project anyway. -EXAMPLE 1 ---------- +EXAMPLE 1. Add command +---------------------- Let's assume that you have a local repository that you would like to add an external vendor library to. In this case we will add the git-subtree repository as a subdirectory of your already existing @@ -251,8 +275,8 @@ We now have a ~/git-extensions/git-subtree directory containing code from the master branch of git://github.com/apenwarr/git-subtree.git in our git-extensions repository. -EXAMPLE 2 ---------- +EXAMPLE 2. Extract a subtree using commit, merge and pull +--------------------------------------------------------- Let's use the repository for the git source code as an example. First, get your own copy of the git.git repository: @@ -312,22 +336,24 @@ the standard gitweb: git log gitweb-latest..$(git subtree split --prefix=gitweb) -EXAMPLE 3 ---------- +EXAMPLE 3. Extract a subtree using branch +----------------------------------------- Suppose you have a source directory with many files and subdirectories, and you want to extract the lib directory to its own git project. Here's a short way to do it: First, make the new repository wherever you want: - - git init --bare + + $ + $ git init --bare Back in your original directory: - git subtree split --prefix=lib --annotate="(split)" -b split + + $ git subtree split --prefix=lib --annotate="(split)" -b split Then push the new branch onto the new empty repository: - git push split:master + $ git push split:master AUTHOR -- cgit v1.2.1 From 9a40fcc2014cc4a85eca71f81ff4c86d49b7a9e2 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Thu, 21 Oct 2010 12:28:18 -0700 Subject: Fix a few typos/grammar-o's in the preceding commit. --- git-subtree.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/git-subtree.txt b/git-subtree.txt index 18a9af501f..0c44fda011 100644 --- a/git-subtree.txt +++ b/git-subtree.txt @@ -9,11 +9,11 @@ git-subtree - Merge subtrees together and split repository into subtrees SYNOPSIS -------- [verse] -'git subtree' add -P |--prefix= -'git subtree' pull -P |--prefix= -'git subtree' push -P |--prefix= -'git subtree' merge -P |--prefix= -'git subtree' split -P |--prefix= [OPTIONS] [] +'git subtree' add -P +'git subtree' pull -P +'git subtree' push -P +'git subtree' merge -P +'git subtree' split -P [OPTIONS] [] DESCRIPTION @@ -34,9 +34,9 @@ work. A subtree is just a subdirectory that can be committed to, branched, and merged along with your project in any way you want. -They are neither not to be confused with using the subtree merge +They are also not to be confused with using the subtree merge strategy. The main difference is that, besides merging -of the other project as a subdirectory, you can also extract the +the other project as a subdirectory, you can also extract the entire history of a subdirectory from your project and make it into a standalone project. Unlike the subtree merge strategy you can alternate back and forth between these -- cgit v1.2.1 From 6f4f84fa2a03f441826f323c291ca6566acd0d3a Mon Sep 17 00:00:00 2001 From: Jesse Greenwald Date: Tue, 9 Nov 2010 08:34:49 -0600 Subject: Split cmd now processes commits in topo order. Added the "--topo-order" option to git rev-list. Without this, it seems that the revision list is coming back in reverse order but it is sorted chronologically. This does not gurantee that parent commits are handled before child commits. --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index ce94d363dc..390c0fc574 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -547,7 +547,7 @@ cmd_split() # We can't restrict rev-list to only $dir here, because some of our # parents have the $dir contents the root, and those won't match. # (and rev-list --follow doesn't seem to solve this) - grl='git rev-list --reverse --parents $revs $unrevs' + grl='git rev-list --topo-order --reverse --parents $revs $unrevs' revmax=$(eval "$grl" | wc -l) revcount=0 createcount=0 -- cgit v1.2.1 From 915b9894abe94169e60a14e4dc671f6bd15131f3 Mon Sep 17 00:00:00 2001 From: Jesse Greenwald Date: Tue, 9 Nov 2010 22:18:36 -0600 Subject: Added check to order of processed commits. With debug messages enabled, "incorrect order" will be output whenever a commit is processed before its parents have been processed. This can be determined by checking to see if a parent isn't mapped to a new commit, but it has been processed. --- git-subtree.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/git-subtree.sh b/git-subtree.sh index 390c0fc574..cf50de150c 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -138,6 +138,7 @@ cache_setup() cachedir="$GIT_DIR/subtree-cache/$$" rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" + mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree" debug "Using cachedir: $cachedir" >&2 } @@ -151,6 +152,30 @@ cache_get() done } +cache_miss() +{ + for oldrev in $*; do + if [ ! -r "$cachedir/$oldrev" ]; then + echo $oldrev + fi + done +} + +check_parents() +{ + missed=$(cache_miss $*) + for miss in $missed; do + if [ ! -r "$cachedir/notree/$miss" ]; then + debug " incorrect order: $miss" + fi + done +} + +set_notree() +{ + echo "1" > "$cachedir/notree/$1" +} + cache_set() { oldrev="$1" @@ -568,10 +593,13 @@ cmd_split() tree=$(subtree_for_commit $rev "$dir") debug " tree is: $tree" + + check_parents $parents # ugly. is there no better way to tell if this is a subtree # vs. a mainline commit? Does it matter? if [ -z $tree ]; then + set_notree $rev if [ -n "$newparents" ]; then cache_set $rev $rev fi -- cgit v1.2.1 From 856bea1f71db30339539b0c917d6d7dc32729d0f Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 28 Feb 2011 16:49:42 -0800 Subject: It's also okay if an expected tree object is actually a commit. ...that happens with submodules sometimes, so don't panic. Reported by Sum-Wai Low. --- git-subtree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-subtree.sh b/git-subtree.sh index cf50de150c..fa4e3e3661 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -397,7 +397,7 @@ subtree_for_commit() git ls-tree "$commit" -- "$dir" | while read mode type tree name; do assert [ "$name" = "$dir" ] - assert [ "$type" = "tree" ] + assert [ "$type" = "tree" -o "$type" = "commit" ] echo $tree break done -- cgit v1.2.1 From 2793ee6ba6da57d97e9c313741041f7eb2e88974 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Mon, 28 Feb 2011 20:48:50 -0800 Subject: Skip commit objects that should be trees, rather than copying them. An improvement on the previous patch, based on more reports from Sum-Wai Low. --- git-subtree.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/git-subtree.sh b/git-subtree.sh index fa4e3e3661..920c664bb7 100755 --- a/git-subtree.sh +++ b/git-subtree.sh @@ -398,6 +398,7 @@ subtree_for_commit() while read mode type tree name; do assert [ "$name" = "$dir" ] assert [ "$type" = "tree" -o "$type" = "commit" ] + [ "$type" = "commit" ] && continue # ignore submodules echo $tree break done -- cgit v1.2.1 From 41529bbce407fbf1a925cfbc7c1aa30064f66ae7 Mon Sep 17 00:00:00 2001 From: David Barr Date: Sat, 5 Mar 2011 13:30:23 +1100 Subject: vcs-svn: set up channel to read fast-import cat-blob response Set up some plumbing: teach the svndump lib to pass a file descriptor number to the fast_export lib, representing where cat-blob/ls responses can be read from, and add a get_response_line helper function to the fast_export lib to read a line from that file. Unfortunately this means that svn-fe needs file descriptor 3 to be redirected from somewhere (preferrably the cat-blob stream of a fast-import backend); otherwise it will fail: $ svndump | svn-fe fatal: cannot read from file descriptor 3: Bad file descriptor For the moment, "svn-fe 3 Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- contrib/svn-fe/svn-fe.txt | 6 ++- t/t9010-svn-fe.sh | 118 ++++++++++++++++++++++++++-------------------- vcs-svn/fast_export.c | 28 +++++++++++ vcs-svn/fast_export.h | 4 ++ vcs-svn/svndump.c | 5 ++ 5 files changed, 109 insertions(+), 52 deletions(-) diff --git a/contrib/svn-fe/svn-fe.txt b/contrib/svn-fe/svn-fe.txt index cd075b96c5..85f7b83028 100644 --- a/contrib/svn-fe/svn-fe.txt +++ b/contrib/svn-fe/svn-fe.txt @@ -7,7 +7,11 @@ svn-fe - convert an SVN "dumpfile" to a fast-import stream SYNOPSIS -------- -svnadmin dump --incremental REPO | svn-fe [url] | git fast-import +[verse] +mkfifo backchannel && +svnadmin dump --incremental REPO | + svn-fe [url] 3backchannel DESCRIPTION ----------- diff --git a/t/t9010-svn-fe.sh b/t/t9010-svn-fe.sh index 5a6a4b9b7a..2ae5374de3 100755 --- a/t/t9010-svn-fe.sh +++ b/t/t9010-svn-fe.sh @@ -5,8 +5,26 @@ test_description='check svn dumpfile importer' . ./test-lib.sh reinit_git () { + if ! test_declared_prereq PIPE + then + echo >&4 "reinit_git: need to declare PIPE prerequisite" + return 127 + fi rm -fr .git && - git init + rm -f stream backflow && + git init && + mkfifo stream backflow +} + +try_dump () { + input=$1 && + maybe_fail=${2:+test_$2} && + + { + $maybe_fail test-svn-fe "$input" >stream 3backflow && + wait $! } properties () { @@ -35,21 +53,27 @@ text_no_props () { >empty -test_expect_success 'empty dump' ' +test_expect_success 'setup: have pipes?' ' + rm -f frob && + if mkfifo frob + then + test_set_prereq PIPE + fi +' + +test_expect_success PIPE 'empty dump' ' reinit_git && echo "SVN-fs-dump-format-version: 2" >input && - test-svn-fe input >stream && - git fast-import v4.dump && - test_must_fail test-svn-fe v4.dump >stream && - test_cmp empty stream + try_dump v4.dump must_fail ' -test_expect_failure 'empty revision' ' +test_expect_failure PIPE 'empty revision' ' reinit_git && printf "rev : %s\n" "" "" >expect && cat >emptyrev.dump <<-\EOF && @@ -64,13 +88,12 @@ test_expect_failure 'empty revision' ' Content-length: 0 EOF - test-svn-fe emptyrev.dump >stream && - git fast-import actual && test_cmp expect actual ' -test_expect_success 'empty properties' ' +test_expect_success PIPE 'empty properties' ' reinit_git && printf "rev : %s\n" "" "" >expect && cat >emptyprop.dump <<-\EOF && @@ -88,13 +111,12 @@ test_expect_success 'empty properties' ' PROPS-END EOF - test-svn-fe emptyprop.dump >stream && - git fast-import actual && test_cmp expect actual ' -test_expect_success 'author name and commit message' ' +test_expect_success PIPE 'author name and commit message' ' reinit_git && echo "" >expect.author && cat >message <<-\EOF && @@ -121,15 +143,14 @@ test_expect_success 'author name and commit message' ' echo && cat props } >log.dump && - test-svn-fe log.dump >stream && - git fast-import actual.log && git log --format="<%an, %ae>" >actual.author && test_cmp message actual.log && test_cmp expect.author actual.author ' -test_expect_success 'unsupported properties are ignored' ' +test_expect_success PIPE 'unsupported properties are ignored' ' reinit_git && echo author >expect && cat >extraprop.dump <<-\EOF && @@ -149,13 +170,12 @@ test_expect_success 'unsupported properties are ignored' ' author PROPS-END EOF - test-svn-fe extraprop.dump >stream && - git fast-import actual && test_cmp expect actual ' -test_expect_failure 'timestamp and empty file' ' +test_expect_failure PIPE 'timestamp and empty file' ' echo author@example.com >expect.author && echo 1999-01-01 >expect.date && echo file >expect.files && @@ -186,8 +206,7 @@ test_expect_failure 'timestamp and empty file' ' EOF } >emptyfile.dump && - test-svn-fe emptyfile.dump >stream && - git fast-import actual.author && git log --date=short --format=%ad HEAD >actual.date && git ls-tree -r --name-only HEAD >actual.files && @@ -198,7 +217,7 @@ test_expect_failure 'timestamp and empty file' ' test_cmp empty file ' -test_expect_success 'directory with files' ' +test_expect_success PIPE 'directory with files' ' reinit_git && printf "%s\n" directory/file1 directory/file2 >expect.files && echo hi >hi && @@ -242,8 +261,7 @@ test_expect_success 'directory with files' ' EOF text_no_props hi } >directory.dump && - test-svn-fe directory.dump >stream && - git fast-import actual.files && git checkout HEAD directory && @@ -252,7 +270,8 @@ test_expect_success 'directory with files' ' test_cmp hi directory/file2 ' -test_expect_success 'node without action' ' +test_expect_success PIPE 'node without action' ' + reinit_git && cat >inaction.dump <<-\EOF && SVN-fs-dump-format-version: 3 @@ -269,10 +288,11 @@ test_expect_success 'node without action' ' PROPS-END EOF - test_must_fail test-svn-fe inaction.dump + try_dump inaction.dump must_fail ' -test_expect_success 'action: add node without text' ' +test_expect_success PIPE 'action: add node without text' ' + reinit_git && cat >textless.dump <<-\EOF && SVN-fs-dump-format-version: 3 @@ -290,10 +310,10 @@ test_expect_success 'action: add node without text' ' PROPS-END EOF - test_must_fail test-svn-fe textless.dump + try_dump textless.dump must_fail ' -test_expect_failure 'change file mode but keep old content' ' +test_expect_failure PIPE 'change file mode but keep old content' ' reinit_git && cat >expect <<-\EOF && OBJID @@ -356,8 +376,7 @@ test_expect_failure 'change file mode but keep old content' ' PROPS-END EOF - test-svn-fe filemode.dump >stream && - git fast-import expect <<-\EOF && OBJID @@ -382,7 +401,7 @@ test_expect_success 'change file mode and reiterate content' ' EOF echo "link hello" >expect.blob && echo hello >hello && - cat >filemode.dump <<-\EOF && + cat >filemode2.dump <<-\EOF && SVN-fs-dump-format-version: 3 Revision-number: 1 @@ -437,8 +456,7 @@ test_expect_success 'change file mode and reiterate content' ' PROPS-END link hello EOF - test-svn-fe filemode.dump >stream && - git fast-import delta.dump && - test_must_fail test-svn-fe delta.dump + test_must_fail try_dump delta.dump ' -test_expect_success 'property deltas supported' ' +test_expect_success PIPE 'property deltas supported' ' reinit_git && cat >expect <<-\EOF && OBJID @@ -570,8 +589,7 @@ test_expect_success 'property deltas supported' ' PROPS-END EOF } >propdelta.dump && - test-svn-fe propdelta.dump >stream && - git fast-import expect && OBJID @@ -625,8 +643,7 @@ test_expect_success 'properties on /' ' PROPS-END EOF - test-svn-fe changeroot.dump >stream && - git fast-import expect <<-\EOF && OBJID @@ -711,8 +728,7 @@ test_expect_success 'deltas for typechange' ' PROPS-END link testing 321 EOF - test-svn-fe deleteprop.dump >stream && - git fast-import simple.fe && +test_expect_success SVNREPO,PIPE 't9135/svn.dump' ' + mkdir -p simple-git && ( cd simple-git && - git fast-import <../simple.fe + reinit_git && + try_dump "$TEST_DIRECTORY/t9135/svn.dump" ) && ( cd simple-svnco && diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 260cf50e77..70bd9597e7 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -12,6 +12,24 @@ #define MAX_GITSVN_LINE_LEN 4096 static uint32_t first_commit_done; +static struct line_buffer report_buffer = LINE_BUFFER_INIT; + +void fast_export_init(int fd) +{ + if (buffer_fdinit(&report_buffer, fd)) + die_errno("cannot read from file descriptor %d", fd); +} + +void fast_export_deinit(void) +{ + if (buffer_deinit(&report_buffer)) + die_errno("error closing fast-import feedback stream"); +} + +void fast_export_reset(void) +{ + buffer_reset(&report_buffer); +} void fast_export_delete(uint32_t depth, uint32_t *path) { @@ -63,6 +81,16 @@ void fast_export_commit(uint32_t revision, uint32_t author, char *log, printf("progress Imported commit %"PRIu32".\n\n", revision); } +static const char *get_response_line(void) +{ + const char *line = buffer_read_line(&report_buffer); + if (line) + return line; + if (buffer_ferror(&report_buffer)) + die_errno("error reading from fast-import"); + die("unexpected end of fast-import feedback"); +} + void fast_export_blob(uint32_t mode, uint32_t mark, uint32_t len, struct line_buffer *input) { if (mode == REPO_MODE_LNK) { diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index 054e7d5eb1..fed30c14e6 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -3,6 +3,10 @@ #include "line_buffer.h" +void fast_export_init(int fd); +void fast_export_deinit(void); +void fast_export_reset(void); + void fast_export_delete(uint32_t depth, uint32_t *path); void fast_export_modify(uint32_t depth, uint32_t *path, uint32_t mode, uint32_t mark); diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index e6d84bada5..e05a99d51f 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -14,6 +14,8 @@ #include "obj_pool.h" #include "string_pool.h" +#define REPORT_FILENO 3 + #define NODEACT_REPLACE 4 #define NODEACT_DELETE 3 #define NODEACT_ADD 2 @@ -367,6 +369,7 @@ int svndump_init(const char *filename) if (buffer_init(&input, filename)) return error("cannot open %s: %s", filename, strerror(errno)); repo_init(); + fast_export_init(REPORT_FILENO); reset_dump_ctx(~0); reset_rev_ctx(0); reset_node_ctx(NULL); @@ -377,6 +380,7 @@ int svndump_init(const char *filename) void svndump_deinit(void) { log_reset(); + fast_export_deinit(); repo_reset(); reset_dump_ctx(~0); reset_rev_ctx(0); @@ -390,6 +394,7 @@ void svndump_deinit(void) void svndump_reset(void) { log_reset(); + fast_export_reset(); buffer_reset(&input); repo_reset(); reset_dump_ctx(~0); -- cgit v1.2.1 From d38f84484f21e7e509ff009d3a17167c9c09f893 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Fri, 10 Dec 2010 04:21:35 -0600 Subject: vcs-svn: use higher mark numbers for blobs Prepare to use mark :5 for the commit corresponding to r5 (and so on). 1 billion seems sufficiently high for blob marks to avoid conflicting with rev marks, while still leaving room for 3 billion blobs. Such high mark numbers cause trouble with ancient fast-import versions, but this topic cannot support git fast-import versions before 1.7.4 (which introduces the cat-blob command) anyway. Signed-off-by: Jonathan Nieder --- vcs-svn/repo_tree.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcs-svn/repo_tree.c b/vcs-svn/repo_tree.c index 14bcc192b6..036a6866b9 100644 --- a/vcs-svn/repo_tree.c +++ b/vcs-svn/repo_tree.c @@ -292,7 +292,7 @@ void repo_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, static void mark_init(void) { uint32_t i; - mark = 0; + mark = 1024 * 1024 * 1024; for (i = 0; i < dent_pool.size; i++) if (!repo_dirent_is_dir(dent_pointer(i)) && dent_pointer(i)->content_offset > mark) -- cgit v1.2.1 From 78e1a3ff236af3afaf1ba9db92985df42141cb0e Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Thu, 9 Dec 2010 18:57:13 -0600 Subject: vcs-svn: save marks for imported commits This way, a person can use svnadmin dump $path | svn-fe | git fast-import --relative-marks --export-marks=svn-revs to get a list of what commit corresponds to each svn revision (plus some irrelevant blob names) in .git/info/fast-import/svn-revs. Signed-off-by: Jonathan Nieder Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 1 + 1 file changed, 1 insertion(+) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 70bd9597e7..0ad5382bfb 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -63,6 +63,7 @@ void fast_export_commit(uint32_t revision, uint32_t author, char *log, *gitsvnline = '\0'; } printf("commit refs/heads/master\n"); + printf("mark :%"PRIu32"\n", revision); printf("committer %s <%s@%s> %ld +0000\n", ~author ? pool_fetch(author) : "nobody", ~author ? pool_fetch(author) : "nobody", -- cgit v1.2.1 From 7e11902c995715836dec140eb55cfef1d24334bb Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Tue, 4 Jan 2011 21:53:33 -0600 Subject: vcs-svn: add a comment before each commit Current svn-fe produces output like this: blob mark :7382321 data 5 hello blob mark :7382322 data 5 Hello commit mark :3 [...] M 100644 :7382321 hello.c M 100644 :7382322 hello2.c This means svn-fe has to keep track of the paths modified in each commit and the corresponding marks, instead of dealing with each file as it arrives in input and then forgetting about it. A better strategy would be to use inline blobs: commit mark :3 [...] M 100644 inline hello.c data 5 hello [...] As a first step towards that, teach svn-fe to notice when the collection of blobs for each commit starts and write a comment ("# commit 3.") there. Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 5 +++++ vcs-svn/fast_export.h | 1 + vcs-svn/svndump.c | 29 ++++++++++++++++++++++------- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 0ad5382bfb..8786ed234a 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -48,6 +48,11 @@ void fast_export_modify(uint32_t depth, uint32_t *path, uint32_t mode, putchar('\n'); } +void fast_export_begin_commit(uint32_t revision) +{ + printf("# commit %"PRIu32".\n", revision); +} + static char gitsvnline[MAX_GITSVN_LINE_LEN]; void fast_export_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, uint32_t url, diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index fed30c14e6..09b2033772 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -10,6 +10,7 @@ void fast_export_reset(void); void fast_export_delete(uint32_t depth, uint32_t *path); void fast_export_modify(uint32_t depth, uint32_t *path, uint32_t mode, uint32_t mark); +void fast_export_begin_commit(uint32_t revision); void fast_export_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, uint32_t url, unsigned long timestamp); void fast_export_blob(uint32_t mode, uint32_t mark, uint32_t len, diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index e05a99d51f..3cc4135892 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -22,9 +22,11 @@ #define NODEACT_CHANGE 1 #define NODEACT_UNKNOWN 0 -#define DUMP_CTX 0 -#define REV_CTX 1 -#define NODE_CTX 2 +/* States: */ +#define DUMP_CTX 0 /* dump metadata */ +#define REV_CTX 1 /* revision metadata */ +#define NODE_CTX 2 /* node metadata */ +#define INTERNODE_CTX 3 /* between nodes */ #define LENGTH_UNKNOWN (~0) #define DATE_RFC2822_LEN 31 @@ -269,7 +271,14 @@ static void handle_node(void) node_ctx.textLength, &input); } -static void handle_revision(void) +static void begin_revision(void) +{ + if (!rev_ctx.revision) /* revision 0 gets no git commit. */ + return; + fast_export_begin_commit(rev_ctx.revision); +} + +static void end_revision(void) { if (rev_ctx.revision) repo_commit(rev_ctx.revision, rev_ctx.author, rev_ctx.log, @@ -303,13 +312,17 @@ void svndump_read(const char *url) } else if (key == keys.revision_number) { if (active_ctx == NODE_CTX) handle_node(); + if (active_ctx == REV_CTX) + begin_revision(); if (active_ctx != DUMP_CTX) - handle_revision(); + end_revision(); active_ctx = REV_CTX; reset_rev_ctx(atoi(val)); } else if (key == keys.node_path) { if (active_ctx == NODE_CTX) handle_node(); + if (active_ctx == REV_CTX) + begin_revision(); active_ctx = NODE_CTX; reset_node_ctx(val); } else if (key == keys.node_kind) { @@ -351,7 +364,7 @@ void svndump_read(const char *url) read_props(); } else if (active_ctx == NODE_CTX) { handle_node(); - active_ctx = REV_CTX; + active_ctx = INTERNODE_CTX; } else { fprintf(stderr, "Unexpected content length header: %"PRIu32"\n", len); buffer_skip_bytes(&input, len); @@ -360,8 +373,10 @@ void svndump_read(const char *url) } if (active_ctx == NODE_CTX) handle_node(); + if (active_ctx == REV_CTX) + begin_revision(); if (active_ctx != DUMP_CTX) - handle_revision(); + end_revision(); } int svndump_init(const char *filename) -- cgit v1.2.1 From 723b7a2789d66c1365390cc9b9213e34ab8513d7 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Fri, 10 Dec 2010 04:00:55 -0600 Subject: vcs-svn: eliminate repo_tree structure Rely on fast-import for information about previous revs. This requires always setting up backward flow of information, even for v2 dumps. On the plus side, it simplifies the code by quite a bit and opens the door to further simplifications. [db: adjusted to support final version of the cat-blob patch] [jn: avoiding hard-coding git's name for the empty tree for portability to other backends] Signed-off-by: Jonathan Nieder Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 108 +++++++++++++--- vcs-svn/fast_export.h | 22 ++-- vcs-svn/repo_tree.c | 335 ++++++-------------------------------------------- vcs-svn/repo_tree.h | 2 +- vcs-svn/string_pool.c | 2 +- vcs-svn/string_pool.h | 2 +- vcs-svn/svndump.c | 53 +++++--- 7 files changed, 184 insertions(+), 340 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 8786ed234a..a8ce5c64b2 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -8,6 +8,7 @@ #include "line_buffer.h" #include "repo_tree.h" #include "string_pool.h" +#include "strbuf.h" #define MAX_GITSVN_LINE_LEN 4096 @@ -31,7 +32,7 @@ void fast_export_reset(void) buffer_reset(&report_buffer); } -void fast_export_delete(uint32_t depth, uint32_t *path) +void fast_export_delete(uint32_t depth, const uint32_t *path) { putchar('D'); putchar(' '); @@ -39,22 +40,27 @@ void fast_export_delete(uint32_t depth, uint32_t *path) putchar('\n'); } -void fast_export_modify(uint32_t depth, uint32_t *path, uint32_t mode, - uint32_t mark) +static void fast_export_truncate(uint32_t depth, const uint32_t *path, uint32_t mode) { - /* Mode must be 100644, 100755, 120000, or 160000. */ - printf("M %06"PRIo32" :%"PRIu32" ", mode, mark); - pool_print_seq(depth, path, '/', stdout); - putchar('\n'); + fast_export_modify(depth, path, mode, "inline"); + printf("data 0\n\n"); } -void fast_export_begin_commit(uint32_t revision) +void fast_export_modify(uint32_t depth, const uint32_t *path, uint32_t mode, + const char *dataref) { - printf("# commit %"PRIu32".\n", revision); + /* Mode must be 100644, 100755, 120000, or 160000. */ + if (!dataref) { + fast_export_truncate(depth, path, mode); + return; + } + printf("M %06"PRIo32" %s ", mode, dataref); + pool_print_seq(depth, path, '/', stdout); + putchar('\n'); } static char gitsvnline[MAX_GITSVN_LINE_LEN]; -void fast_export_commit(uint32_t revision, uint32_t author, char *log, +void fast_export_begin_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, uint32_t url, unsigned long timestamp) { @@ -81,12 +87,31 @@ void fast_export_commit(uint32_t revision, uint32_t author, char *log, printf("from refs/heads/master^0\n"); first_commit_done = 1; } - repo_diff(revision - 1, revision); - fputc('\n', stdout); +} +void fast_export_end_commit(uint32_t revision) +{ printf("progress Imported commit %"PRIu32".\n\n", revision); } +static void ls_from_rev(uint32_t rev, uint32_t depth, const uint32_t *path) +{ + /* ls :5 path/to/old/file */ + printf("ls :%"PRIu32" ", rev); + pool_print_seq(depth, path, '/', stdout); + putchar('\n'); + fflush(stdout); +} + +static void ls_from_active_commit(uint32_t depth, const uint32_t *path) +{ + /* ls "path/to/file" */ + printf("ls \""); + pool_print_seq(depth, path, '/', stdout); + printf("\"\n"); + fflush(stdout); +} + static const char *get_response_line(void) { const char *line = buffer_read_line(&report_buffer); @@ -97,14 +122,69 @@ static const char *get_response_line(void) die("unexpected end of fast-import feedback"); } -void fast_export_blob(uint32_t mode, uint32_t mark, uint32_t len, struct line_buffer *input) +void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input) { if (mode == REPO_MODE_LNK) { /* svn symlink blobs start with "link " */ buffer_skip_bytes(input, 5); len -= 5; } - printf("blob\nmark :%"PRIu32"\ndata %"PRIu32"\n", mark, len); + printf("data %"PRIu32"\n", len); buffer_copy_bytes(input, len); fputc('\n', stdout); } + +static int parse_ls_response(const char *response, uint32_t *mode, + struct strbuf *dataref) +{ + const char *tab; + const char *response_end; + + assert(response); + response_end = response + strlen(response); + + if (*response == 'm') { /* Missing. */ + errno = ENOENT; + return -1; + } + + /* Mode. */ + if (response_end - response < strlen("100644") || + response[strlen("100644")] != ' ') + die("invalid ls response: missing mode: %s", response); + *mode = 0; + for (; *response != ' '; response++) { + char ch = *response; + if (ch < '0' || ch > '7') + die("invalid ls response: mode is not octal: %s", response); + *mode *= 8; + *mode += ch - '0'; + } + + /* ' blob ' or ' tree ' */ + if (response_end - response < strlen(" blob ") || + (response[1] != 'b' && response[1] != 't')) + die("unexpected ls response: not a tree or blob: %s", response); + response += strlen(" blob "); + + /* Dataref. */ + tab = memchr(response, '\t', response_end - response); + if (!tab) + die("invalid ls response: missing tab: %s", response); + strbuf_add(dataref, response, tab - response); + return 0; +} + +int fast_export_ls_rev(uint32_t rev, uint32_t depth, const uint32_t *path, + uint32_t *mode, struct strbuf *dataref) +{ + ls_from_rev(rev, depth, path); + return parse_ls_response(get_response_line(), mode, dataref); +} + +int fast_export_ls(uint32_t depth, const uint32_t *path, + uint32_t *mode, struct strbuf *dataref) +{ + ls_from_active_commit(depth, path); + return parse_ls_response(get_response_line(), mode, dataref); +} diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index 09b2033772..633d21944e 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -1,19 +1,25 @@ #ifndef FAST_EXPORT_H_ #define FAST_EXPORT_H_ -#include "line_buffer.h" +struct strbuf; +struct line_buffer; void fast_export_init(int fd); void fast_export_deinit(void); void fast_export_reset(void); -void fast_export_delete(uint32_t depth, uint32_t *path); -void fast_export_modify(uint32_t depth, uint32_t *path, uint32_t mode, - uint32_t mark); -void fast_export_begin_commit(uint32_t revision); -void fast_export_commit(uint32_t revision, uint32_t author, char *log, +void fast_export_delete(uint32_t depth, const uint32_t *path); +void fast_export_modify(uint32_t depth, const uint32_t *path, + uint32_t mode, const char *dataref); +void fast_export_begin_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, uint32_t url, unsigned long timestamp); -void fast_export_blob(uint32_t mode, uint32_t mark, uint32_t len, - struct line_buffer *input); +void fast_export_end_commit(uint32_t revision); +void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input); + +/* If there is no such file at that rev, returns -1, errno == ENOENT. */ +int fast_export_ls_rev(uint32_t rev, uint32_t depth, const uint32_t *path, + uint32_t *mode_out, struct strbuf *dataref_out); +int fast_export_ls(uint32_t depth, const uint32_t *path, + uint32_t *mode_out, struct strbuf *dataref_out); #endif diff --git a/vcs-svn/repo_tree.c b/vcs-svn/repo_tree.c index 036a6866b9..e75f58087c 100644 --- a/vcs-svn/repo_tree.c +++ b/vcs-svn/repo_tree.c @@ -4,322 +4,61 @@ */ #include "git-compat-util.h" - -#include "string_pool.h" +#include "strbuf.h" #include "repo_tree.h" -#include "obj_pool.h" #include "fast_export.h" -#include "trp.h" - -struct repo_dirent { - uint32_t name_offset; - struct trp_node children; - uint32_t mode; - uint32_t content_offset; -}; - -struct repo_dir { - struct trp_root entries; -}; - -struct repo_commit { - uint32_t root_dir_offset; -}; - -/* Memory pools for commit, dir and dirent */ -obj_pool_gen(commit, struct repo_commit, 4096) -obj_pool_gen(dir, struct repo_dir, 4096) -obj_pool_gen(dent, struct repo_dirent, 4096) - -static uint32_t active_commit; -static uint32_t mark; - -static int repo_dirent_name_cmp(const void *a, const void *b); - -/* Treap for directory entries */ -trp_gen(static, dent_, struct repo_dirent, children, dent, repo_dirent_name_cmp); - -uint32_t next_blob_mark(void) +const char *repo_read_path(const uint32_t *path) { - return mark++; -} + int err; + uint32_t dummy; + static struct strbuf buf = STRBUF_INIT; -static struct repo_dir *repo_commit_root_dir(struct repo_commit *commit) -{ - return dir_pointer(commit->root_dir_offset); -} - -static struct repo_dirent *repo_first_dirent(struct repo_dir *dir) -{ - return dent_first(&dir->entries); -} - -static int repo_dirent_name_cmp(const void *a, const void *b) -{ - const struct repo_dirent *dent1 = a, *dent2 = b; - uint32_t a_offset = dent1->name_offset; - uint32_t b_offset = dent2->name_offset; - return (a_offset > b_offset) - (a_offset < b_offset); -} - -static int repo_dirent_is_dir(struct repo_dirent *dent) -{ - return dent != NULL && dent->mode == REPO_MODE_DIR; -} - -static struct repo_dir *repo_dir_from_dirent(struct repo_dirent *dent) -{ - if (!repo_dirent_is_dir(dent)) + strbuf_reset(&buf); + err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, &dummy, &buf); + if (err) { + if (errno != ENOENT) + die_errno("BUG: unexpected fast_export_ls error"); return NULL; - return dir_pointer(dent->content_offset); -} - -static struct repo_dir *repo_clone_dir(struct repo_dir *orig_dir) -{ - uint32_t orig_o, new_o; - orig_o = dir_offset(orig_dir); - if (orig_o >= dir_pool.committed) - return orig_dir; - new_o = dir_alloc(1); - orig_dir = dir_pointer(orig_o); - *dir_pointer(new_o) = *orig_dir; - return dir_pointer(new_o); -} - -static struct repo_dirent *repo_read_dirent(uint32_t revision, - const uint32_t *path) -{ - uint32_t name = 0; - struct repo_dirent *key = dent_pointer(dent_alloc(1)); - struct repo_dir *dir = NULL; - struct repo_dirent *dent = NULL; - dir = repo_commit_root_dir(commit_pointer(revision)); - while (~(name = *path++)) { - key->name_offset = name; - dent = dent_search(&dir->entries, key); - if (dent == NULL || !repo_dirent_is_dir(dent)) - break; - dir = repo_dir_from_dirent(dent); - } - dent_free(1); - return dent; -} - -static void repo_write_dirent(const uint32_t *path, uint32_t mode, - uint32_t content_offset, uint32_t del) -{ - uint32_t name, revision, dir_o = ~0, parent_dir_o = ~0; - struct repo_dir *dir; - struct repo_dirent *key; - struct repo_dirent *dent = NULL; - revision = active_commit; - dir = repo_commit_root_dir(commit_pointer(revision)); - dir = repo_clone_dir(dir); - commit_pointer(revision)->root_dir_offset = dir_offset(dir); - while (~(name = *path++)) { - parent_dir_o = dir_offset(dir); - - key = dent_pointer(dent_alloc(1)); - key->name_offset = name; - - dent = dent_search(&dir->entries, key); - if (dent == NULL) - dent = key; - else - dent_free(1); - - if (dent == key) { - dent->mode = REPO_MODE_DIR; - dent->content_offset = 0; - dent = dent_insert(&dir->entries, dent); - } - - if (dent_offset(dent) < dent_pool.committed) { - dir_o = repo_dirent_is_dir(dent) ? - dent->content_offset : ~0; - dent_remove(&dir->entries, dent); - dent = dent_pointer(dent_alloc(1)); - dent->name_offset = name; - dent->mode = REPO_MODE_DIR; - dent->content_offset = dir_o; - dent = dent_insert(&dir->entries, dent); - } - - dir = repo_dir_from_dirent(dent); - dir = repo_clone_dir(dir); - dent->content_offset = dir_offset(dir); } - if (dent == NULL) - return; - dent->mode = mode; - dent->content_offset = content_offset; - if (del && ~parent_dir_o) - dent_remove(&dir_pointer(parent_dir_o)->entries, dent); -} - -uint32_t repo_read_path(const uint32_t *path) -{ - uint32_t content_offset = 0; - struct repo_dirent *dent = repo_read_dirent(active_commit, path); - if (dent != NULL) - content_offset = dent->content_offset; - return content_offset; + return buf.buf; } uint32_t repo_read_mode(const uint32_t *path) { - struct repo_dirent *dent = repo_read_dirent(active_commit, path); - if (dent == NULL) - die("invalid dump: path to be modified is missing"); - return dent->mode; + int err; + uint32_t result; + static struct strbuf dummy = STRBUF_INIT; + + strbuf_reset(&dummy); + err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, &result, &dummy); + if (err) { + if (errno != ENOENT) + die_errno("BUG: unexpected fast_export_ls error"); + /* Treat missing paths as directories. */ + return REPO_MODE_DIR; + } + return result; } void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst) { - uint32_t mode = 0, content_offset = 0; - struct repo_dirent *src_dent; - src_dent = repo_read_dirent(revision, src); - if (src_dent != NULL) { - mode = src_dent->mode; - content_offset = src_dent->content_offset; - repo_write_dirent(dst, mode, content_offset, 0); + int err; + uint32_t mode; + static struct strbuf data = STRBUF_INIT; + + strbuf_reset(&data); + err = fast_export_ls_rev(revision, REPO_MAX_PATH_DEPTH, src, &mode, &data); + if (err) { + if (errno != ENOENT) + die_errno("BUG: unexpected fast_export_ls_rev error"); + fast_export_delete(REPO_MAX_PATH_DEPTH, dst); + return; } -} - -void repo_add(uint32_t *path, uint32_t mode, uint32_t blob_mark) -{ - repo_write_dirent(path, mode, blob_mark, 0); + fast_export_modify(REPO_MAX_PATH_DEPTH, dst, mode, data.buf); } void repo_delete(uint32_t *path) { - repo_write_dirent(path, 0, 0, 1); -} - -static void repo_git_add_r(uint32_t depth, uint32_t *path, struct repo_dir *dir); - -static void repo_git_add(uint32_t depth, uint32_t *path, struct repo_dirent *dent) -{ - if (repo_dirent_is_dir(dent)) - repo_git_add_r(depth, path, repo_dir_from_dirent(dent)); - else - fast_export_modify(depth, path, - dent->mode, dent->content_offset); -} - -static void repo_git_add_r(uint32_t depth, uint32_t *path, struct repo_dir *dir) -{ - struct repo_dirent *de = repo_first_dirent(dir); - while (de) { - path[depth] = de->name_offset; - repo_git_add(depth + 1, path, de); - de = dent_next(&dir->entries, de); - } -} - -static void repo_diff_r(uint32_t depth, uint32_t *path, struct repo_dir *dir1, - struct repo_dir *dir2) -{ - struct repo_dirent *de1, *de2; - de1 = repo_first_dirent(dir1); - de2 = repo_first_dirent(dir2); - - while (de1 && de2) { - if (de1->name_offset < de2->name_offset) { - path[depth] = de1->name_offset; - fast_export_delete(depth + 1, path); - de1 = dent_next(&dir1->entries, de1); - continue; - } - if (de1->name_offset > de2->name_offset) { - path[depth] = de2->name_offset; - repo_git_add(depth + 1, path, de2); - de2 = dent_next(&dir2->entries, de2); - continue; - } - path[depth] = de1->name_offset; - - if (de1->mode == de2->mode && - de1->content_offset == de2->content_offset) { - ; /* No change. */ - } else if (repo_dirent_is_dir(de1) && repo_dirent_is_dir(de2)) { - repo_diff_r(depth + 1, path, - repo_dir_from_dirent(de1), - repo_dir_from_dirent(de2)); - } else if (!repo_dirent_is_dir(de1) && !repo_dirent_is_dir(de2)) { - repo_git_add(depth + 1, path, de2); - } else { - fast_export_delete(depth + 1, path); - repo_git_add(depth + 1, path, de2); - } - de1 = dent_next(&dir1->entries, de1); - de2 = dent_next(&dir2->entries, de2); - } - while (de1) { - path[depth] = de1->name_offset; - fast_export_delete(depth + 1, path); - de1 = dent_next(&dir1->entries, de1); - } - while (de2) { - path[depth] = de2->name_offset; - repo_git_add(depth + 1, path, de2); - de2 = dent_next(&dir2->entries, de2); - } -} - -static uint32_t path_stack[REPO_MAX_PATH_DEPTH]; - -void repo_diff(uint32_t r1, uint32_t r2) -{ - repo_diff_r(0, - path_stack, - repo_commit_root_dir(commit_pointer(r1)), - repo_commit_root_dir(commit_pointer(r2))); -} - -void repo_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, - uint32_t url, unsigned long timestamp) -{ - fast_export_commit(revision, author, log, uuid, url, timestamp); - dent_commit(); - dir_commit(); - active_commit = commit_alloc(1); - commit_pointer(active_commit)->root_dir_offset = - commit_pointer(active_commit - 1)->root_dir_offset; -} - -static void mark_init(void) -{ - uint32_t i; - mark = 1024 * 1024 * 1024; - for (i = 0; i < dent_pool.size; i++) - if (!repo_dirent_is_dir(dent_pointer(i)) && - dent_pointer(i)->content_offset > mark) - mark = dent_pointer(i)->content_offset; - mark++; -} - -void repo_init(void) -{ - mark_init(); - if (commit_pool.size == 0) { - /* Create empty tree for commit 0. */ - commit_alloc(1); - commit_pointer(0)->root_dir_offset = dir_alloc(1); - dir_pointer(0)->entries.trp_root = ~0; - dir_commit(); - } - /* Preallocate next commit, ready for changes. */ - active_commit = commit_alloc(1); - commit_pointer(active_commit)->root_dir_offset = - commit_pointer(active_commit - 1)->root_dir_offset; -} - -void repo_reset(void) -{ - pool_reset(); - commit_reset(); - dir_reset(); - dent_reset(); + fast_export_delete(REPO_MAX_PATH_DEPTH, path); } diff --git a/vcs-svn/repo_tree.h b/vcs-svn/repo_tree.h index 11d48c2444..d690784fbb 100644 --- a/vcs-svn/repo_tree.h +++ b/vcs-svn/repo_tree.h @@ -14,7 +14,7 @@ uint32_t next_blob_mark(void); void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst); void repo_add(uint32_t *path, uint32_t mode, uint32_t blob_mark); -uint32_t repo_read_path(const uint32_t *path); +const char *repo_read_path(const uint32_t *path); uint32_t repo_read_mode(const uint32_t *path); void repo_delete(uint32_t *path); void repo_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, diff --git a/vcs-svn/string_pool.c b/vcs-svn/string_pool.c index f5b1da836e..c08abac71d 100644 --- a/vcs-svn/string_pool.c +++ b/vcs-svn/string_pool.c @@ -65,7 +65,7 @@ uint32_t pool_tok_r(char *str, const char *delim, char **saveptr) return token ? pool_intern(token) : ~0; } -void pool_print_seq(uint32_t len, uint32_t *seq, char delim, FILE *stream) +void pool_print_seq(uint32_t len, const uint32_t *seq, char delim, FILE *stream) { uint32_t i; for (i = 0; i < len && ~seq[i]; i++) { diff --git a/vcs-svn/string_pool.h b/vcs-svn/string_pool.h index 222fb66e68..3720cf8164 100644 --- a/vcs-svn/string_pool.h +++ b/vcs-svn/string_pool.h @@ -4,7 +4,7 @@ uint32_t pool_intern(const char *key); const char *pool_fetch(uint32_t entry); uint32_t pool_tok_r(char *str, const char *delim, char **saveptr); -void pool_print_seq(uint32_t len, uint32_t *seq, char delim, FILE *stream); +void pool_print_seq(uint32_t len, const uint32_t *seq, char delim, FILE *stream); uint32_t pool_tok_seq(uint32_t sz, uint32_t *seq, const char *delim, char *str); void pool_reset(void); diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index 3cc4135892..7ecb227a6d 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -36,6 +36,8 @@ obj_pool_gen(log, char, 4096) static struct line_buffer input = LINE_BUFFER_INIT; +#define REPORT_FILENO 3 + static char *log_copy(uint32_t length, const char *log) { char *buffer; @@ -202,15 +204,21 @@ static void read_props(void) static void handle_node(void) { - uint32_t mark = 0; const uint32_t type = node_ctx.type; const int have_props = node_ctx.propLength != LENGTH_UNKNOWN; const int have_text = node_ctx.textLength != LENGTH_UNKNOWN; + /* + * Old text for this node: + * NULL - directory or bug + * empty_blob - empty + * "" - data retrievable from fast-import + */ + static const char *const empty_blob = "::empty::"; + const char *old_data = NULL; if (node_ctx.text_delta) die("text deltas not supported"); - if (have_text) - mark = next_blob_mark(); + if (node_ctx.action == NODEACT_DELETE) { if (have_text || have_props || node_ctx.srcRev) die("invalid dump: deletion node has " @@ -230,15 +238,15 @@ static void handle_node(void) die("invalid dump: directories cannot have text attached"); /* - * Decide on the new content (mark) and mode (node_ctx.type). + * Find old content (old_data) and decide on the new mode. */ if (node_ctx.action == NODEACT_CHANGE && !~*node_ctx.dst) { if (type != REPO_MODE_DIR) die("invalid dump: root of tree is not a regular file"); + old_data = NULL; } else if (node_ctx.action == NODEACT_CHANGE) { uint32_t mode; - if (!have_text) - mark = repo_read_path(node_ctx.dst); + old_data = repo_read_path(node_ctx.dst); mode = repo_read_mode(node_ctx.dst); if (mode == REPO_MODE_DIR && type != REPO_MODE_DIR) die("invalid dump: cannot modify a directory into a file"); @@ -246,7 +254,11 @@ static void handle_node(void) die("invalid dump: cannot modify a file into a directory"); node_ctx.type = mode; } else if (node_ctx.action == NODEACT_ADD) { - if (!have_text && type != REPO_MODE_DIR) + if (type == REPO_MODE_DIR) + old_data = NULL; + else if (have_text) + old_data = empty_blob; + else die("invalid dump: adds node without text"); } else { die("invalid dump: Node-path block lacks Node-action"); @@ -265,24 +277,34 @@ static void handle_node(void) /* * Save the result. */ - repo_add(node_ctx.dst, node_ctx.type, mark); - if (have_text) - fast_export_blob(node_ctx.type, mark, - node_ctx.textLength, &input); + if (type == REPO_MODE_DIR) /* directories are not tracked. */ + return; + assert(old_data); + if (old_data == empty_blob) + /* For the fast_export_* functions, NULL means empty. */ + old_data = NULL; + if (!have_text) { + fast_export_modify(REPO_MAX_PATH_DEPTH, node_ctx.dst, + node_ctx.type, old_data); + return; + } + fast_export_modify(REPO_MAX_PATH_DEPTH, node_ctx.dst, + node_ctx.type, "inline"); + fast_export_data(node_ctx.type, node_ctx.textLength, &input); } static void begin_revision(void) { if (!rev_ctx.revision) /* revision 0 gets no git commit. */ return; - fast_export_begin_commit(rev_ctx.revision); + fast_export_begin_commit(rev_ctx.revision, rev_ctx.author, rev_ctx.log, + dump_ctx.uuid, dump_ctx.url, rev_ctx.timestamp); } static void end_revision(void) { if (rev_ctx.revision) - repo_commit(rev_ctx.revision, rev_ctx.author, rev_ctx.log, - dump_ctx.uuid, dump_ctx.url, rev_ctx.timestamp); + fast_export_end_commit(rev_ctx.revision); } void svndump_read(const char *url) @@ -383,7 +405,6 @@ int svndump_init(const char *filename) { if (buffer_init(&input, filename)) return error("cannot open %s: %s", filename, strerror(errno)); - repo_init(); fast_export_init(REPORT_FILENO); reset_dump_ctx(~0); reset_rev_ctx(0); @@ -396,7 +417,6 @@ void svndump_deinit(void) { log_reset(); fast_export_deinit(); - repo_reset(); reset_dump_ctx(~0); reset_rev_ctx(0); reset_node_ctx(NULL); @@ -411,7 +431,6 @@ void svndump_reset(void) log_reset(); fast_export_reset(); buffer_reset(&input); - repo_reset(); reset_dump_ctx(~0); reset_rev_ctx(0); reset_node_ctx(NULL); -- cgit v1.2.1 From e43581120843f6f55f411af470faf806e052ad9d Mon Sep 17 00:00:00 2001 From: David Barr Date: Sun, 12 Dec 2010 03:59:31 +1100 Subject: vcs-svn: quote paths correctly for ls command This bug was found while importing rev 601865 of ASF. [jn: with test] Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- t/t9010-svn-fe.sh | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++ vcs-svn/fast_export.c | 2 +- vcs-svn/string_pool.c | 11 ++++++ vcs-svn/string_pool.h | 1 + 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/t/t9010-svn-fe.sh b/t/t9010-svn-fe.sh index 2ae5374de3..720fd6b5a3 100755 --- a/t/t9010-svn-fe.sh +++ b/t/t9010-svn-fe.sh @@ -270,6 +270,105 @@ test_expect_success PIPE 'directory with files' ' test_cmp hi directory/file2 ' +test_expect_success PIPE 'branch name with backslash' ' + reinit_git && + sort <<-\EOF >expect.branch-files && + trunk/file1 + trunk/file2 + "branches/UpdateFOPto094\\/file1" + "branches/UpdateFOPto094\\/file2" + EOF + + echo hi >hi && + echo hello >hello && + { + properties \ + svn:author author@example.com \ + svn:date "1999-02-02T00:01:02.000000Z" \ + svn:log "add directory with some files in it" && + echo PROPS-END + } >props.setup && + { + properties \ + svn:author brancher@example.com \ + svn:date "2007-12-06T21:38:34.000000Z" \ + svn:log "Updating fop to .94 and adjust fo-stylesheets" && + echo PROPS-END + } >props.branch && + { + cat <<-EOF && + SVN-fs-dump-format-version: 3 + + Revision-number: 1 + EOF + echo Prop-content-length: $(wc -c branch.dump && + try_dump branch.dump && + + git ls-tree -r --name-only HEAD | + sort >actual.branch-files && + test_cmp expect.branch-files actual.branch-files +' + test_expect_success PIPE 'node without action' ' reinit_git && cat >inaction.dump <<-\EOF && diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index a8ce5c64b2..4d57efabd5 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -107,7 +107,7 @@ static void ls_from_active_commit(uint32_t depth, const uint32_t *path) { /* ls "path/to/file" */ printf("ls \""); - pool_print_seq(depth, path, '/', stdout); + pool_print_seq_q(depth, path, '/', stdout); printf("\"\n"); fflush(stdout); } diff --git a/vcs-svn/string_pool.c b/vcs-svn/string_pool.c index c08abac71d..be43598d5b 100644 --- a/vcs-svn/string_pool.c +++ b/vcs-svn/string_pool.c @@ -4,6 +4,7 @@ */ #include "git-compat-util.h" +#include "quote.h" #include "trp.h" #include "obj_pool.h" #include "string_pool.h" @@ -75,6 +76,16 @@ void pool_print_seq(uint32_t len, const uint32_t *seq, char delim, FILE *stream) } } +void pool_print_seq_q(uint32_t len, const uint32_t *seq, char delim, FILE *stream) +{ + uint32_t i; + for (i = 0; i < len && ~seq[i]; i++) { + quote_c_style(pool_fetch(seq[i]), NULL, stream, 1); + if (i < len - 1 && ~seq[i + 1]) + fputc(delim, stream); + } +} + uint32_t pool_tok_seq(uint32_t sz, uint32_t *seq, const char *delim, char *str) { char *context = NULL; diff --git a/vcs-svn/string_pool.h b/vcs-svn/string_pool.h index 3720cf8164..96e501dc53 100644 --- a/vcs-svn/string_pool.h +++ b/vcs-svn/string_pool.h @@ -5,6 +5,7 @@ uint32_t pool_intern(const char *key); const char *pool_fetch(uint32_t entry); uint32_t pool_tok_r(char *str, const char *delim, char **saveptr); void pool_print_seq(uint32_t len, const uint32_t *seq, char delim, FILE *stream); +void pool_print_seq_q(uint32_t len, const uint32_t *seq, char delim, FILE *stream); uint32_t pool_tok_seq(uint32_t sz, uint32_t *seq, const char *delim, char *str); void pool_reset(void); -- cgit v1.2.1 From 1ae469b06c50aade4781931ca1587453082f57eb Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Sat, 11 Dec 2010 17:08:51 -0600 Subject: vcs-svn: handle filenames with dq correctly Quote paths passed to fast-import so filenames with double quotes are not misinterpreted. One might imagine this could help with filenames with newlines, too, but svn does not allow those. Helped-by: David Barr Signed-off-by: Jonathan Nieder Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 4d57efabd5..9c03f3e16d 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -34,10 +34,9 @@ void fast_export_reset(void) void fast_export_delete(uint32_t depth, const uint32_t *path) { - putchar('D'); - putchar(' '); - pool_print_seq(depth, path, '/', stdout); - putchar('\n'); + printf("D \""); + pool_print_seq_q(depth, path, '/', stdout); + printf("\"\n"); } static void fast_export_truncate(uint32_t depth, const uint32_t *path, uint32_t mode) @@ -54,9 +53,9 @@ void fast_export_modify(uint32_t depth, const uint32_t *path, uint32_t mode, fast_export_truncate(depth, path, mode); return; } - printf("M %06"PRIo32" %s ", mode, dataref); - pool_print_seq(depth, path, '/', stdout); - putchar('\n'); + printf("M %06"PRIo32" %s \"", mode, dataref); + pool_print_seq_q(depth, path, '/', stdout); + printf("\"\n"); } static char gitsvnline[MAX_GITSVN_LINE_LEN]; @@ -97,9 +96,9 @@ void fast_export_end_commit(uint32_t revision) static void ls_from_rev(uint32_t rev, uint32_t depth, const uint32_t *path) { /* ls :5 path/to/old/file */ - printf("ls :%"PRIu32" ", rev); - pool_print_seq(depth, path, '/', stdout); - putchar('\n'); + printf("ls :%"PRIu32" \"", rev); + pool_print_seq_q(depth, path, '/', stdout); + printf("\"\n"); fflush(stdout); } -- cgit v1.2.1 From dd3f42ad793b5334d506a451addcefd0054c27bb Mon Sep 17 00:00:00 2001 From: David Barr Date: Sun, 12 Dec 2010 13:41:38 +1100 Subject: vcs-svn: use mark from previous import for parent commit With this patch, overlapping incremental imports work. Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 9c03f3e16d..f19db9ae82 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -83,7 +83,7 @@ void fast_export_begin_commit(uint32_t revision, uint32_t author, char *log, log, gitsvnline); if (!first_commit_done) { if (revision > 1) - printf("from refs/heads/master^0\n"); + printf("from :%"PRIu32"\n", revision - 1); first_commit_done = 1; } } -- cgit v1.2.1 From 030879718f696b67fe1c958ab0a238971773ac96 Mon Sep 17 00:00:00 2001 From: David Barr Date: Mon, 13 Dec 2010 16:41:12 +1100 Subject: vcs-svn: pass paths through to fast-import Now that there is no internal representation of the repo, it is not necessary to tokenise paths. Use strbuf instead and bypass string_pool. This means svn-fe can handle arbitrarily long paths (as long as a strbuf can fit them), with arbitrarily many path components. While at it, since we now treat paths in their entirety, only quote when necessary. Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 48 ++++++++++++++++++++++++------------------------ vcs-svn/fast_export.h | 9 ++++----- vcs-svn/repo_tree.c | 20 ++++++++++---------- vcs-svn/repo_tree.h | 13 +++++-------- vcs-svn/svndump.c | 34 +++++++++++++++++++--------------- 5 files changed, 62 insertions(+), 62 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index a64a3c5633..ec323e9b39 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -4,10 +4,11 @@ */ #include "git-compat-util.h" +#include "strbuf.h" +#include "quote.h" #include "fast_export.h" #include "line_buffer.h" #include "repo_tree.h" -#include "string_pool.h" #include "strbuf.h" #define MAX_GITSVN_LINE_LEN 4096 @@ -32,30 +33,30 @@ void fast_export_reset(void) buffer_reset(&report_buffer); } -void fast_export_delete(uint32_t depth, const uint32_t *path) +void fast_export_delete(const char *path) { - printf("D \""); - pool_print_seq_q(depth, path, '/', stdout); - printf("\"\n"); + putchar('D'); + putchar(' '); + quote_c_style(path, NULL, stdout, 0); + putchar('\n'); } -static void fast_export_truncate(uint32_t depth, const uint32_t *path, uint32_t mode) +static void fast_export_truncate(const char *path, uint32_t mode) { - fast_export_modify(depth, path, mode, "inline"); + fast_export_modify(path, mode, "inline"); printf("data 0\n\n"); } -void fast_export_modify(uint32_t depth, const uint32_t *path, uint32_t mode, - const char *dataref) +void fast_export_modify(const char *path, uint32_t mode, const char *dataref) { /* Mode must be 100644, 100755, 120000, or 160000. */ if (!dataref) { - fast_export_truncate(depth, path, mode); + fast_export_truncate(path, mode); return; } - printf("M %06"PRIo32" %s \"", mode, dataref); - pool_print_seq_q(depth, path, '/', stdout); - printf("\"\n"); + printf("M %06"PRIo32" %s ", mode, dataref); + quote_c_style(path, NULL, stdout, 0); + putchar('\n'); } static char gitsvnline[MAX_GITSVN_LINE_LEN]; @@ -93,20 +94,20 @@ void fast_export_end_commit(uint32_t revision) printf("progress Imported commit %"PRIu32".\n\n", revision); } -static void ls_from_rev(uint32_t rev, uint32_t depth, const uint32_t *path) +static void ls_from_rev(uint32_t rev, const char *path) { /* ls :5 path/to/old/file */ - printf("ls :%"PRIu32" \"", rev); - pool_print_seq_q(depth, path, '/', stdout); - printf("\"\n"); + printf("ls :%"PRIu32" ", rev); + quote_c_style(path, NULL, stdout, 0); + putchar('\n'); fflush(stdout); } -static void ls_from_active_commit(uint32_t depth, const uint32_t *path) +static void ls_from_active_commit(const char *path) { /* ls "path/to/file" */ printf("ls \""); - pool_print_seq_q(depth, path, '/', stdout); + quote_c_style(path, NULL, stdout, 1); printf("\"\n"); fflush(stdout); } @@ -183,16 +184,15 @@ static int parse_ls_response(const char *response, uint32_t *mode, return 0; } -int fast_export_ls_rev(uint32_t rev, uint32_t depth, const uint32_t *path, +int fast_export_ls_rev(uint32_t rev, const char *path, uint32_t *mode, struct strbuf *dataref) { - ls_from_rev(rev, depth, path); + ls_from_rev(rev, path); return parse_ls_response(get_response_line(), mode, dataref); } -int fast_export_ls(uint32_t depth, const uint32_t *path, - uint32_t *mode, struct strbuf *dataref) +int fast_export_ls(const char *path, uint32_t *mode, struct strbuf *dataref) { - ls_from_active_commit(depth, path); + ls_from_active_commit(path); return parse_ls_response(get_response_line(), mode, dataref); } diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index fc14242420..12b0bbb419 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -8,9 +8,8 @@ void fast_export_init(int fd); void fast_export_deinit(void); void fast_export_reset(void); -void fast_export_delete(uint32_t depth, const uint32_t *path); -void fast_export_modify(uint32_t depth, const uint32_t *path, - uint32_t mode, const char *dataref); +void fast_export_delete(const char *path); +void fast_export_modify(const char *path, uint32_t mode, const char *dataref); void fast_export_begin_commit(uint32_t revision, const char *author, char *log, const char *uuid, const char *url, unsigned long timestamp); @@ -18,9 +17,9 @@ void fast_export_end_commit(uint32_t revision); void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input); /* If there is no such file at that rev, returns -1, errno == ENOENT. */ -int fast_export_ls_rev(uint32_t rev, uint32_t depth, const uint32_t *path, +int fast_export_ls_rev(uint32_t rev, const char *path, uint32_t *mode_out, struct strbuf *dataref_out); -int fast_export_ls(uint32_t depth, const uint32_t *path, +int fast_export_ls(const char *path, uint32_t *mode_out, struct strbuf *dataref_out); #endif diff --git a/vcs-svn/repo_tree.c b/vcs-svn/repo_tree.c index e75f58087c..f2466bc634 100644 --- a/vcs-svn/repo_tree.c +++ b/vcs-svn/repo_tree.c @@ -8,14 +8,14 @@ #include "repo_tree.h" #include "fast_export.h" -const char *repo_read_path(const uint32_t *path) +const char *repo_read_path(const char *path) { int err; uint32_t dummy; static struct strbuf buf = STRBUF_INIT; strbuf_reset(&buf); - err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, &dummy, &buf); + err = fast_export_ls(path, &dummy, &buf); if (err) { if (errno != ENOENT) die_errno("BUG: unexpected fast_export_ls error"); @@ -24,14 +24,14 @@ const char *repo_read_path(const uint32_t *path) return buf.buf; } -uint32_t repo_read_mode(const uint32_t *path) +uint32_t repo_read_mode(const char *path) { int err; uint32_t result; static struct strbuf dummy = STRBUF_INIT; strbuf_reset(&dummy); - err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, &result, &dummy); + err = fast_export_ls(path, &result, &dummy); if (err) { if (errno != ENOENT) die_errno("BUG: unexpected fast_export_ls error"); @@ -41,24 +41,24 @@ uint32_t repo_read_mode(const uint32_t *path) return result; } -void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst) +void repo_copy(uint32_t revision, const char *src, const char *dst) { int err; uint32_t mode; static struct strbuf data = STRBUF_INIT; strbuf_reset(&data); - err = fast_export_ls_rev(revision, REPO_MAX_PATH_DEPTH, src, &mode, &data); + err = fast_export_ls_rev(revision, src, &mode, &data); if (err) { if (errno != ENOENT) die_errno("BUG: unexpected fast_export_ls_rev error"); - fast_export_delete(REPO_MAX_PATH_DEPTH, dst); + fast_export_delete(dst); return; } - fast_export_modify(REPO_MAX_PATH_DEPTH, dst, mode, data.buf); + fast_export_modify(dst, mode, data.buf); } -void repo_delete(uint32_t *path) +void repo_delete(const char *path) { - fast_export_delete(REPO_MAX_PATH_DEPTH, path); + fast_export_delete(path); } diff --git a/vcs-svn/repo_tree.h b/vcs-svn/repo_tree.h index 29887f9765..44e6e8fabc 100644 --- a/vcs-svn/repo_tree.h +++ b/vcs-svn/repo_tree.h @@ -8,15 +8,12 @@ #define REPO_MODE_EXE 0100755 #define REPO_MODE_LNK 0120000 -#define REPO_MAX_PATH_LEN 4096 -#define REPO_MAX_PATH_DEPTH 1000 - uint32_t next_blob_mark(void); -void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst); -void repo_add(uint32_t *path, uint32_t mode, uint32_t blob_mark); -const char *repo_read_path(const uint32_t *path); -uint32_t repo_read_mode(const uint32_t *path); -void repo_delete(uint32_t *path); +void repo_copy(uint32_t revision, const char *src, const char *dst); +void repo_add(const char *path, uint32_t mode, uint32_t blob_mark); +const char *repo_read_path(const char *path); +uint32_t repo_read_mode(const char *path); +void repo_delete(const char *path); void repo_commit(uint32_t revision, const char *author, char *log, const char *uuid, const char *url, long unsigned timestamp); diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index f5de49cbeb..363503d4ea 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -11,7 +11,6 @@ #include "repo_tree.h" #include "fast_export.h" #include "line_buffer.h" -#include "string_pool.h" #include "strbuf.h" #define REPORT_FILENO 3 @@ -41,7 +40,7 @@ static struct line_buffer input = LINE_BUFFER_INIT; static struct { uint32_t action, propLength, textLength, srcRev, type; - uint32_t src[REPO_MAX_PATH_DEPTH], dst[REPO_MAX_PATH_DEPTH]; + struct strbuf src, dst; uint32_t text_delta, prop_delta; } node_ctx; @@ -62,9 +61,11 @@ static void reset_node_ctx(char *fname) node_ctx.action = NODEACT_UNKNOWN; node_ctx.propLength = LENGTH_UNKNOWN; node_ctx.textLength = LENGTH_UNKNOWN; - node_ctx.src[0] = ~0; + strbuf_reset(&node_ctx.src); node_ctx.srcRev = 0; - pool_tok_seq(REPO_MAX_PATH_DEPTH, node_ctx.dst, "/", fname); + strbuf_reset(&node_ctx.dst); + if (fname) + strbuf_addstr(&node_ctx.dst, fname); node_ctx.text_delta = 0; node_ctx.prop_delta = 0; } @@ -228,14 +229,14 @@ static void handle_node(void) if (have_text || have_props || node_ctx.srcRev) die("invalid dump: deletion node has " "copyfrom info, text, or properties"); - return repo_delete(node_ctx.dst); + return repo_delete(node_ctx.dst.buf); } if (node_ctx.action == NODEACT_REPLACE) { - repo_delete(node_ctx.dst); + repo_delete(node_ctx.dst.buf); node_ctx.action = NODEACT_ADD; } if (node_ctx.srcRev) { - repo_copy(node_ctx.srcRev, node_ctx.src, node_ctx.dst); + repo_copy(node_ctx.srcRev, node_ctx.src.buf, node_ctx.dst.buf); if (node_ctx.action == NODEACT_ADD) node_ctx.action = NODEACT_CHANGE; } @@ -245,14 +246,14 @@ static void handle_node(void) /* * Find old content (old_data) and decide on the new mode. */ - if (node_ctx.action == NODEACT_CHANGE && !~*node_ctx.dst) { + if (node_ctx.action == NODEACT_CHANGE && !*node_ctx.dst.buf) { if (type != REPO_MODE_DIR) die("invalid dump: root of tree is not a regular file"); old_data = NULL; } else if (node_ctx.action == NODEACT_CHANGE) { uint32_t mode; - old_data = repo_read_path(node_ctx.dst); - mode = repo_read_mode(node_ctx.dst); + old_data = repo_read_path(node_ctx.dst.buf); + mode = repo_read_mode(node_ctx.dst.buf); if (mode == REPO_MODE_DIR && type != REPO_MODE_DIR) die("invalid dump: cannot modify a directory into a file"); if (mode != REPO_MODE_DIR && type == REPO_MODE_DIR) @@ -289,12 +290,10 @@ static void handle_node(void) /* For the fast_export_* functions, NULL means empty. */ old_data = NULL; if (!have_text) { - fast_export_modify(REPO_MAX_PATH_DEPTH, node_ctx.dst, - node_ctx.type, old_data); + fast_export_modify(node_ctx.dst.buf, node_ctx.type, old_data); return; } - fast_export_modify(REPO_MAX_PATH_DEPTH, node_ctx.dst, - node_ctx.type, "inline"); + fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); fast_export_data(node_ctx.type, node_ctx.textLength, &input); } @@ -395,7 +394,8 @@ void svndump_read(const char *url) case sizeof("Node-copyfrom-path"): if (constcmp(t, "Node-copyfrom-path")) continue; - pool_tok_seq(REPO_MAX_PATH_DEPTH, node_ctx.src, "/", val); + strbuf_reset(&node_ctx.src); + strbuf_addstr(&node_ctx.src, val); break; case sizeof("Node-copyfrom-rev"): if (constcmp(t, "Node-copyfrom-rev")) @@ -460,6 +460,8 @@ int svndump_init(const char *filename) strbuf_init(&dump_ctx.url, 4096); strbuf_init(&rev_ctx.log, 4096); strbuf_init(&rev_ctx.author, 4096); + strbuf_init(&node_ctx.src, 4096); + strbuf_init(&node_ctx.dst, 4096); reset_dump_ctx(NULL); reset_rev_ctx(0); reset_node_ctx(NULL); @@ -473,6 +475,8 @@ void svndump_deinit(void) reset_rev_ctx(0); reset_node_ctx(NULL); strbuf_release(&rev_ctx.log); + strbuf_release(&node_ctx.src); + strbuf_release(&node_ctx.dst); if (buffer_deinit(&input)) fprintf(stderr, "Input error\n"); if (ferror(stdout)) -- cgit v1.2.1 From 28c5d9ed2a2bc562bc8c50092f52f58b3aa08039 Mon Sep 17 00:00:00 2001 From: David Barr Date: Mon, 13 Dec 2010 21:17:36 +1100 Subject: vcs-svn: drop string_pool This reverts commit 1d73b52f5ba4184de6acf474f14668001304a10c (Add string-specific memory pool, 2010-08-09). Now that svn-fe does not need to maintain a growing collection of strings (paths) over a long period of time, the string_pool is not needed. Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- .gitignore | 1 - Makefile | 12 ++--- t/t0080-vcs-svn.sh | 16 ------- test-string-pool.c | 31 ------------- vcs-svn/string_pool.c | 113 ------------------------------------------------ vcs-svn/string_pool.h | 12 ----- vcs-svn/string_pool.txt | 43 ------------------ 7 files changed, 4 insertions(+), 224 deletions(-) delete mode 100644 test-string-pool.c delete mode 100644 vcs-svn/string_pool.c delete mode 100644 vcs-svn/string_pool.h delete mode 100644 vcs-svn/string_pool.txt diff --git a/.gitignore b/.gitignore index c460c66766..215e842457 100644 --- a/.gitignore +++ b/.gitignore @@ -177,7 +177,6 @@ /test-run-command /test-sha1 /test-sigchain -/test-string-pool /test-subprocess /test-svn-fe /test-treap diff --git a/Makefile b/Makefile index ade79232f4..f8182e56a1 100644 --- a/Makefile +++ b/Makefile @@ -430,7 +430,6 @@ TEST_PROGRAMS_NEED_X += test-path-utils TEST_PROGRAMS_NEED_X += test-run-command TEST_PROGRAMS_NEED_X += test-sha1 TEST_PROGRAMS_NEED_X += test-sigchain -TEST_PROGRAMS_NEED_X += test-string-pool TEST_PROGRAMS_NEED_X += test-subprocess TEST_PROGRAMS_NEED_X += test-svn-fe TEST_PROGRAMS_NEED_X += test-treap @@ -1838,10 +1837,9 @@ ifndef NO_CURL endif XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ xdiff/xmerge.o xdiff/xpatience.o -VCSSVN_OBJS = vcs-svn/string_pool.o vcs-svn/line_buffer.o \ - vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o -VCSSVN_TEST_OBJS = test-obj-pool.o test-string-pool.o \ - test-line-buffer.o test-treap.o +VCSSVN_OBJS = vcs-svn/line_buffer.o vcs-svn/repo_tree.o \ + vcs-svn/fast_export.o vcs-svn/svndump.o +VCSSVN_TEST_OBJS = test-obj-pool.o test-line-buffer.o test-treap.o OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS) dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) @@ -1965,7 +1963,7 @@ xdiff-interface.o $(XDIFF_OBJS): \ xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h $(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) \ - vcs-svn/obj_pool.h vcs-svn/trp.h vcs-svn/string_pool.h \ + vcs-svn/obj_pool.h vcs-svn/trp.h \ vcs-svn/line_buffer.h vcs-svn/repo_tree.h vcs-svn/fast_export.h \ vcs-svn/svndump.h @@ -2133,8 +2131,6 @@ test-line-buffer$X: vcs-svn/lib.a test-parse-options$X: parse-options.o -test-string-pool$X: vcs-svn/lib.a - test-svn-fe$X: vcs-svn/lib.a .PRECIOUS: $(TEST_OBJS) diff --git a/t/t0080-vcs-svn.sh b/t/t0080-vcs-svn.sh index 99a314b080..ce02c58e3e 100755 --- a/t/t0080-vcs-svn.sh +++ b/t/t0080-vcs-svn.sh @@ -76,22 +76,6 @@ test_expect_success 'obj pool: high-water mark' ' test_cmp expected actual ' -test_expect_success 'string pool' ' - echo a does not equal b >expected.differ && - echo a equals a >expected.match && - echo equals equals equals >expected.matchmore && - - test-string-pool "a,--b" >actual.differ && - test-string-pool "a,a" >actual.match && - test-string-pool "equals-equals" >actual.matchmore && - test_must_fail test-string-pool a,a,a && - test_must_fail test-string-pool a && - - test_cmp expected.differ actual.differ && - test_cmp expected.match actual.match && - test_cmp expected.matchmore actual.matchmore -' - test_expect_success 'treap sort' ' cat <<-\EOF >unsorted && 68 diff --git a/test-string-pool.c b/test-string-pool.c deleted file mode 100644 index c5782e6bce..0000000000 --- a/test-string-pool.c +++ /dev/null @@ -1,31 +0,0 @@ -/* - * test-string-pool.c: code to exercise the svn importer's string pool - */ - -#include "git-compat-util.h" -#include "vcs-svn/string_pool.h" - -int main(int argc, char *argv[]) -{ - const uint32_t unequal = pool_intern("does not equal"); - const uint32_t equal = pool_intern("equals"); - uint32_t buf[3]; - uint32_t n; - - if (argc != 2) - usage("test-string-pool ,"); - - n = pool_tok_seq(3, buf, ",-", argv[1]); - if (n >= 3) - die("too many strings"); - if (n <= 1) - die("too few strings"); - - buf[2] = buf[1]; - buf[1] = (buf[0] == buf[2]) ? equal : unequal; - pool_print_seq(3, buf, ' ', stdout); - fputc('\n', stdout); - - pool_reset(); - return 0; -} diff --git a/vcs-svn/string_pool.c b/vcs-svn/string_pool.c deleted file mode 100644 index be43598d5b..0000000000 --- a/vcs-svn/string_pool.c +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed under a two-clause BSD-style license. - * See LICENSE for details. - */ - -#include "git-compat-util.h" -#include "quote.h" -#include "trp.h" -#include "obj_pool.h" -#include "string_pool.h" - -static struct trp_root tree = { ~0 }; - -struct node { - uint32_t offset; - struct trp_node children; -}; - -/* Two memory pools: one for struct node, and another for strings */ -obj_pool_gen(node, struct node, 4096) -obj_pool_gen(string, char, 4096) - -static char *node_value(struct node *node) -{ - return node ? string_pointer(node->offset) : NULL; -} - -static int node_cmp(struct node *a, struct node *b) -{ - return strcmp(node_value(a), node_value(b)); -} - -/* Build a Treap from the node structure (a trp_node w/ offset) */ -trp_gen(static, tree_, struct node, children, node, node_cmp); - -const char *pool_fetch(uint32_t entry) -{ - return node_value(node_pointer(entry)); -} - -uint32_t pool_intern(const char *key) -{ - /* Canonicalize key */ - struct node *match = NULL, *node; - uint32_t key_len; - if (key == NULL) - return ~0; - key_len = strlen(key) + 1; - node = node_pointer(node_alloc(1)); - node->offset = string_alloc(key_len); - strcpy(node_value(node), key); - match = tree_search(&tree, node); - if (!match) { - tree_insert(&tree, node); - } else { - node_free(1); - string_free(key_len); - node = match; - } - return node_offset(node); -} - -uint32_t pool_tok_r(char *str, const char *delim, char **saveptr) -{ - char *token = strtok_r(str, delim, saveptr); - return token ? pool_intern(token) : ~0; -} - -void pool_print_seq(uint32_t len, const uint32_t *seq, char delim, FILE *stream) -{ - uint32_t i; - for (i = 0; i < len && ~seq[i]; i++) { - fputs(pool_fetch(seq[i]), stream); - if (i < len - 1 && ~seq[i + 1]) - fputc(delim, stream); - } -} - -void pool_print_seq_q(uint32_t len, const uint32_t *seq, char delim, FILE *stream) -{ - uint32_t i; - for (i = 0; i < len && ~seq[i]; i++) { - quote_c_style(pool_fetch(seq[i]), NULL, stream, 1); - if (i < len - 1 && ~seq[i + 1]) - fputc(delim, stream); - } -} - -uint32_t pool_tok_seq(uint32_t sz, uint32_t *seq, const char *delim, char *str) -{ - char *context = NULL; - uint32_t token = ~0; - uint32_t length; - - if (sz == 0) - return ~0; - if (str) - token = pool_tok_r(str, delim, &context); - for (length = 0; length < sz; length++) { - seq[length] = token; - if (token == ~0) - return length; - token = pool_tok_r(NULL, delim, &context); - } - seq[sz - 1] = ~0; - return sz; -} - -void pool_reset(void) -{ - node_reset(); - string_reset(); -} diff --git a/vcs-svn/string_pool.h b/vcs-svn/string_pool.h deleted file mode 100644 index 96e501dc53..0000000000 --- a/vcs-svn/string_pool.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef STRING_POOL_H_ -#define STRING_POOL_H_ - -uint32_t pool_intern(const char *key); -const char *pool_fetch(uint32_t entry); -uint32_t pool_tok_r(char *str, const char *delim, char **saveptr); -void pool_print_seq(uint32_t len, const uint32_t *seq, char delim, FILE *stream); -void pool_print_seq_q(uint32_t len, const uint32_t *seq, char delim, FILE *stream); -uint32_t pool_tok_seq(uint32_t sz, uint32_t *seq, const char *delim, char *str); -void pool_reset(void); - -#endif diff --git a/vcs-svn/string_pool.txt b/vcs-svn/string_pool.txt deleted file mode 100644 index 1b41f15628..0000000000 --- a/vcs-svn/string_pool.txt +++ /dev/null @@ -1,43 +0,0 @@ -string_pool API -=============== - -The string_pool API provides facilities for replacing strings -with integer keys that can be more easily compared and stored. -The facilities are designed so that one could teach Git without -too much trouble to store the information needed for these keys to -remain valid over multiple executions. - -Functions ---------- - -pool_intern:: - Include a string in the string pool and get its key. - If that string is already in the pool, retrieves its - existing key. - -pool_fetch:: - Retrieve the string associated to a given key. - -pool_tok_r:: - Extract the key of the next token from a string. - Interface mimics strtok_r. - -pool_print_seq:: - Print a sequence of strings named by key to a file, using the - specified delimiter to separate them. - - If NULL (key ~0) appears in the sequence, the sequence ends - early. - -pool_tok_seq:: - Split a string into tokens, storing the keys of segments - into a caller-provided array. - - Unless sz is 0, the array will always be ~0-terminated. - If there is not enough room for all the tokens, the - array holds as many tokens as fit in the entries before - the terminating ~0. Return value is the index after the - last token, or sz if the tokens did not fit. - -pool_reset:: - Deallocate storage for the string pool. -- cgit v1.2.1 From 5db348dbd51cdeac711521d1fa7258785e72d202 Mon Sep 17 00:00:00 2001 From: David Barr Date: Mon, 13 Dec 2010 21:23:17 +1100 Subject: vcs-svn: drop treap This reverts commit 951f316470acc7c785c460a4e40735b22822349f (Add treap implementation, 2010-08-09). The string_pool was trp.h's last user. Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- .gitignore | 1 - Makefile | 5 +- t/t0080-vcs-svn.sh | 22 ----- test-treap.c | 70 ---------------- vcs-svn/LICENSE | 3 - vcs-svn/trp.h | 237 ----------------------------------------------------- vcs-svn/trp.txt | 109 ------------------------ 7 files changed, 2 insertions(+), 445 deletions(-) delete mode 100644 test-treap.c delete mode 100644 vcs-svn/trp.h delete mode 100644 vcs-svn/trp.txt diff --git a/.gitignore b/.gitignore index 215e842457..aa94ff125a 100644 --- a/.gitignore +++ b/.gitignore @@ -179,7 +179,6 @@ /test-sigchain /test-subprocess /test-svn-fe -/test-treap /common-cmds.h *.tar.gz *.dsc diff --git a/Makefile b/Makefile index f8182e56a1..a2cadc5474 100644 --- a/Makefile +++ b/Makefile @@ -432,7 +432,6 @@ TEST_PROGRAMS_NEED_X += test-sha1 TEST_PROGRAMS_NEED_X += test-sigchain TEST_PROGRAMS_NEED_X += test-subprocess TEST_PROGRAMS_NEED_X += test-svn-fe -TEST_PROGRAMS_NEED_X += test-treap TEST_PROGRAMS_NEED_X += test-index-version TEST_PROGRAMS_NEED_X += test-mktemp @@ -1839,7 +1838,7 @@ XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ xdiff/xmerge.o xdiff/xpatience.o VCSSVN_OBJS = vcs-svn/line_buffer.o vcs-svn/repo_tree.o \ vcs-svn/fast_export.o vcs-svn/svndump.o -VCSSVN_TEST_OBJS = test-obj-pool.o test-line-buffer.o test-treap.o +VCSSVN_TEST_OBJS = test-obj-pool.o test-line-buffer.o OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS) dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) @@ -1963,7 +1962,7 @@ xdiff-interface.o $(XDIFF_OBJS): \ xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h $(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) \ - vcs-svn/obj_pool.h vcs-svn/trp.h \ + vcs-svn/obj_pool.h \ vcs-svn/line_buffer.h vcs-svn/repo_tree.h vcs-svn/fast_export.h \ vcs-svn/svndump.h diff --git a/t/t0080-vcs-svn.sh b/t/t0080-vcs-svn.sh index ce02c58e3e..3f29496bf8 100755 --- a/t/t0080-vcs-svn.sh +++ b/t/t0080-vcs-svn.sh @@ -76,26 +76,4 @@ test_expect_success 'obj pool: high-water mark' ' test_cmp expected actual ' -test_expect_success 'treap sort' ' - cat <<-\EOF >unsorted && - 68 - 12 - 13 - 13 - 68 - 13 - 13 - 21 - 10 - 11 - 12 - 13 - 13 - EOF - sort unsorted >expected && - - test-treap actual && - test_cmp expected actual -' - test_done diff --git a/test-treap.c b/test-treap.c deleted file mode 100644 index ab8c951c6e..0000000000 --- a/test-treap.c +++ /dev/null @@ -1,70 +0,0 @@ -/* - * test-treap.c: code to exercise the svn importer's treap structure - */ - -#include "cache.h" -#include "vcs-svn/obj_pool.h" -#include "vcs-svn/trp.h" - -struct int_node { - uintmax_t n; - struct trp_node children; -}; - -obj_pool_gen(node, struct int_node, 3) - -static int node_cmp(struct int_node *a, struct int_node *b) -{ - return (a->n > b->n) - (a->n < b->n); -} - -trp_gen(static, treap_, struct int_node, children, node, node_cmp) - -static void strtonode(struct int_node *item, const char *s) -{ - char *end; - item->n = strtoumax(s, &end, 10); - if (*s == '\0' || (*end != '\n' && *end != '\0')) - die("invalid integer: %s", s); -} - -int main(int argc, char *argv[]) -{ - struct strbuf sb = STRBUF_INIT; - struct trp_root root = { ~0 }; - uint32_t item; - - if (argc != 1) - usage("test-treap < ints"); - - while (strbuf_getline(&sb, stdin, '\n') != EOF) { - struct int_node *node = node_pointer(node_alloc(1)); - - item = node_offset(node); - strtonode(node, sb.buf); - node = treap_insert(&root, node_pointer(item)); - if (node_offset(node) != item) - die("inserted %"PRIu32" in place of %"PRIu32"", - node_offset(node), item); - } - - item = node_offset(treap_first(&root)); - while (~item) { - uint32_t next; - struct int_node *tmp = node_pointer(node_alloc(1)); - - tmp->n = node_pointer(item)->n; - next = node_offset(treap_next(&root, node_pointer(item))); - - treap_remove(&root, node_pointer(item)); - item = node_offset(treap_nsearch(&root, tmp)); - - if (item != next && (!~item || node_pointer(item)->n != tmp->n)) - die("found %"PRIuMAX" in place of %"PRIuMAX"", - ~item ? node_pointer(item)->n : ~(uintmax_t) 0, - ~next ? node_pointer(next)->n : ~(uintmax_t) 0); - printf("%"PRIuMAX"\n", tmp->n); - } - node_reset(); - return 0; -} diff --git a/vcs-svn/LICENSE b/vcs-svn/LICENSE index 0a5e3c43a0..533f585ebf 100644 --- a/vcs-svn/LICENSE +++ b/vcs-svn/LICENSE @@ -1,9 +1,6 @@ Copyright (C) 2010 David Barr . All rights reserved. -Copyright (C) 2008 Jason Evans . -All rights reserved. - Copyright (C) 2005 Stefan Hegny, hydrografix Consulting GmbH, Frankfurt/Main, Germany and others, see http://svn2cc.sarovar.org diff --git a/vcs-svn/trp.h b/vcs-svn/trp.h deleted file mode 100644 index c32b9184e9..0000000000 --- a/vcs-svn/trp.h +++ /dev/null @@ -1,237 +0,0 @@ -/* - * C macro implementation of treaps. - * - * Usage: - * #include - * #include "trp.h" - * trp_gen(...) - * - * Licensed under a two-clause BSD-style license. - * See LICENSE for details. - */ - -#ifndef TRP_H_ -#define TRP_H_ - -#define MAYBE_UNUSED __attribute__((__unused__)) - -/* Node structure. */ -struct trp_node { - uint32_t trpn_left; - uint32_t trpn_right; -}; - -/* Root structure. */ -struct trp_root { - uint32_t trp_root; -}; - -/* Pointer/Offset conversion. */ -#define trpn_pointer(a_base, a_offset) (a_base##_pointer(a_offset)) -#define trpn_offset(a_base, a_pointer) (a_base##_offset(a_pointer)) -#define trpn_modify(a_base, a_offset) \ - do { \ - if ((a_offset) < a_base##_pool.committed) { \ - uint32_t old_offset = (a_offset);\ - (a_offset) = a_base##_alloc(1); \ - *trpn_pointer(a_base, a_offset) = \ - *trpn_pointer(a_base, old_offset); \ - } \ - } while (0) - -/* Left accessors. */ -#define trp_left_get(a_base, a_field, a_node) \ - (trpn_pointer(a_base, a_node)->a_field.trpn_left) -#define trp_left_set(a_base, a_field, a_node, a_left) \ - do { \ - trpn_modify(a_base, a_node); \ - trp_left_get(a_base, a_field, a_node) = (a_left); \ - } while (0) - -/* Right accessors. */ -#define trp_right_get(a_base, a_field, a_node) \ - (trpn_pointer(a_base, a_node)->a_field.trpn_right) -#define trp_right_set(a_base, a_field, a_node, a_right) \ - do { \ - trpn_modify(a_base, a_node); \ - trp_right_get(a_base, a_field, a_node) = (a_right); \ - } while (0) - -/* - * Fibonacci hash function. - * The multiplier is the nearest prime to (2^32 times (√5 - 1)/2). - * See Knuth §6.4: volume 3, 3rd ed, p518. - */ -#define trpn_hash(a_node) (uint32_t) (2654435761u * (a_node)) - -/* Priority accessors. */ -#define trp_prio_get(a_node) trpn_hash(a_node) - -/* Node initializer. */ -#define trp_node_new(a_base, a_field, a_node) \ - do { \ - trp_left_set(a_base, a_field, (a_node), ~0); \ - trp_right_set(a_base, a_field, (a_node), ~0); \ - } while (0) - -/* Internal utility macros. */ -#define trpn_first(a_base, a_field, a_root, r_node) \ - do { \ - (r_node) = (a_root); \ - if ((r_node) == ~0) \ - return NULL; \ - while (~trp_left_get(a_base, a_field, (r_node))) \ - (r_node) = trp_left_get(a_base, a_field, (r_node)); \ - } while (0) - -#define trpn_rotate_left(a_base, a_field, a_node, r_node) \ - do { \ - (r_node) = trp_right_get(a_base, a_field, (a_node)); \ - trp_right_set(a_base, a_field, (a_node), \ - trp_left_get(a_base, a_field, (r_node))); \ - trp_left_set(a_base, a_field, (r_node), (a_node)); \ - } while (0) - -#define trpn_rotate_right(a_base, a_field, a_node, r_node) \ - do { \ - (r_node) = trp_left_get(a_base, a_field, (a_node)); \ - trp_left_set(a_base, a_field, (a_node), \ - trp_right_get(a_base, a_field, (r_node))); \ - trp_right_set(a_base, a_field, (r_node), (a_node)); \ - } while (0) - -#define trp_gen(a_attr, a_pre, a_type, a_field, a_base, a_cmp) \ -a_attr a_type MAYBE_UNUSED *a_pre##first(struct trp_root *treap) \ -{ \ - uint32_t ret; \ - trpn_first(a_base, a_field, treap->trp_root, ret); \ - return trpn_pointer(a_base, ret); \ -} \ -a_attr a_type MAYBE_UNUSED *a_pre##next(struct trp_root *treap, a_type *node) \ -{ \ - uint32_t ret; \ - uint32_t offset = trpn_offset(a_base, node); \ - if (~trp_right_get(a_base, a_field, offset)) { \ - trpn_first(a_base, a_field, \ - trp_right_get(a_base, a_field, offset), ret); \ - } else { \ - uint32_t tnode = treap->trp_root; \ - ret = ~0; \ - while (1) { \ - int cmp = (a_cmp)(trpn_pointer(a_base, offset), \ - trpn_pointer(a_base, tnode)); \ - if (cmp < 0) { \ - ret = tnode; \ - tnode = trp_left_get(a_base, a_field, tnode); \ - } else if (cmp > 0) { \ - tnode = trp_right_get(a_base, a_field, tnode); \ - } else { \ - break; \ - } \ - } \ - } \ - return trpn_pointer(a_base, ret); \ -} \ -a_attr a_type MAYBE_UNUSED *a_pre##search(struct trp_root *treap, a_type *key) \ -{ \ - int cmp; \ - uint32_t ret = treap->trp_root; \ - while (~ret && (cmp = (a_cmp)(key, trpn_pointer(a_base, ret)))) { \ - if (cmp < 0) { \ - ret = trp_left_get(a_base, a_field, ret); \ - } else { \ - ret = trp_right_get(a_base, a_field, ret); \ - } \ - } \ - return trpn_pointer(a_base, ret); \ -} \ -a_attr a_type MAYBE_UNUSED *a_pre##nsearch(struct trp_root *treap, a_type *key) \ -{ \ - int cmp; \ - uint32_t ret = treap->trp_root; \ - while (~ret && (cmp = (a_cmp)(key, trpn_pointer(a_base, ret)))) { \ - if (cmp < 0) { \ - if (!~trp_left_get(a_base, a_field, ret)) \ - break; \ - ret = trp_left_get(a_base, a_field, ret); \ - } else { \ - ret = trp_right_get(a_base, a_field, ret); \ - } \ - } \ - return trpn_pointer(a_base, ret); \ -} \ -a_attr uint32_t MAYBE_UNUSED a_pre##insert_recurse(uint32_t cur_node, uint32_t ins_node) \ -{ \ - if (cur_node == ~0) { \ - return ins_node; \ - } else { \ - uint32_t ret; \ - int cmp = (a_cmp)(trpn_pointer(a_base, ins_node), \ - trpn_pointer(a_base, cur_node)); \ - if (cmp < 0) { \ - uint32_t left = a_pre##insert_recurse( \ - trp_left_get(a_base, a_field, cur_node), ins_node); \ - trp_left_set(a_base, a_field, cur_node, left); \ - if (trp_prio_get(left) < trp_prio_get(cur_node)) \ - trpn_rotate_right(a_base, a_field, cur_node, ret); \ - else \ - ret = cur_node; \ - } else { \ - uint32_t right = a_pre##insert_recurse( \ - trp_right_get(a_base, a_field, cur_node), ins_node); \ - trp_right_set(a_base, a_field, cur_node, right); \ - if (trp_prio_get(right) < trp_prio_get(cur_node)) \ - trpn_rotate_left(a_base, a_field, cur_node, ret); \ - else \ - ret = cur_node; \ - } \ - return ret; \ - } \ -} \ -a_attr a_type *MAYBE_UNUSED a_pre##insert(struct trp_root *treap, a_type *node) \ -{ \ - uint32_t offset = trpn_offset(a_base, node); \ - trp_node_new(a_base, a_field, offset); \ - treap->trp_root = a_pre##insert_recurse(treap->trp_root, offset); \ - return trpn_pointer(a_base, offset); \ -} \ -a_attr uint32_t MAYBE_UNUSED a_pre##remove_recurse(uint32_t cur_node, uint32_t rem_node) \ -{ \ - int cmp = a_cmp(trpn_pointer(a_base, rem_node), \ - trpn_pointer(a_base, cur_node)); \ - if (cmp == 0) { \ - uint32_t ret; \ - uint32_t left = trp_left_get(a_base, a_field, cur_node); \ - uint32_t right = trp_right_get(a_base, a_field, cur_node); \ - if (left == ~0) { \ - if (right == ~0) \ - return ~0; \ - } else if (right == ~0 || trp_prio_get(left) < trp_prio_get(right)) { \ - trpn_rotate_right(a_base, a_field, cur_node, ret); \ - right = a_pre##remove_recurse(cur_node, rem_node); \ - trp_right_set(a_base, a_field, ret, right); \ - return ret; \ - } \ - trpn_rotate_left(a_base, a_field, cur_node, ret); \ - left = a_pre##remove_recurse(cur_node, rem_node); \ - trp_left_set(a_base, a_field, ret, left); \ - return ret; \ - } else if (cmp < 0) { \ - uint32_t left = a_pre##remove_recurse( \ - trp_left_get(a_base, a_field, cur_node), rem_node); \ - trp_left_set(a_base, a_field, cur_node, left); \ - return cur_node; \ - } else { \ - uint32_t right = a_pre##remove_recurse( \ - trp_right_get(a_base, a_field, cur_node), rem_node); \ - trp_right_set(a_base, a_field, cur_node, right); \ - return cur_node; \ - } \ -} \ -a_attr void MAYBE_UNUSED a_pre##remove(struct trp_root *treap, a_type *node) \ -{ \ - treap->trp_root = a_pre##remove_recurse(treap->trp_root, \ - trpn_offset(a_base, node)); \ -} \ - -#endif diff --git a/vcs-svn/trp.txt b/vcs-svn/trp.txt deleted file mode 100644 index 5ca6b42edb..0000000000 --- a/vcs-svn/trp.txt +++ /dev/null @@ -1,109 +0,0 @@ -Motivation -========== - -Treaps provide a memory-efficient binary search tree structure. -Insertion/deletion/search are about as about as fast in the average -case as red-black trees and the chances of worst-case behavior are -vanishingly small, thanks to (pseudo-)randomness. The bad worst-case -behavior is a small price to pay, given that treaps are much simpler -to implement. - -API -=== - -The trp API generates a data structure and functions to handle a -large growing set of objects stored in a pool. - -The caller: - -. Specifies parameters for the generated functions with the - trp_gen(static, foo_, ...) macro. - -. Allocates a `struct trp_root` variable and sets it to {~0}. - -. Adds new nodes to the set using `foo_insert`. Any pointers - to existing nodes cannot be relied upon any more, so the caller - might retrieve them anew with `foo_pointer`. - -. Can find a specific item in the set using `foo_search`. - -. Can iterate over items in the set using `foo_first` and `foo_next`. - -. Can remove an item from the set using `foo_remove`. - -Example: - ----- -struct ex_node { - const char *s; - struct trp_node ex_link; -}; -static struct trp_root ex_base = {~0}; -obj_pool_gen(ex, struct ex_node, 4096); -trp_gen(static, ex_, struct ex_node, ex_link, ex, strcmp) -struct ex_node *item; - -item = ex_pointer(ex_alloc(1)); -item->s = "hello"; -ex_insert(&ex_base, item); -item = ex_pointer(ex_alloc(1)); -item->s = "goodbye"; -ex_insert(&ex_base, item); -for (item = ex_first(&ex_base); item; item = ex_next(&ex_base, item)) - printf("%s\n", item->s); ----- - -Functions ---------- - -trp_gen(attr, foo_, node_type, link_field, pool, cmp):: - - Generate a type-specific treap implementation. -+ -. The storage class for generated functions will be 'attr' (e.g., `static`). -. Generated function names are prefixed with 'foo_' (e.g., `treap_`). -. Treap nodes will be of type 'node_type' (e.g., `struct treap_node`). - This type must be a struct with at least one `struct trp_node` field - to point to its children. -. The field used to access child nodes will be 'link_field'. -. All treap nodes must lie in the 'pool' object pool. -. Treap nodes must be totally ordered by the 'cmp' relation, with the - following prototype: -+ -int (*cmp)(node_type \*a, node_type \*b) -+ -and returning a value less than, equal to, or greater than zero -according to the result of comparison. - -node_type {asterisk}foo_insert(struct trp_root *treap, node_type \*node):: - - Insert node into treap. If inserted multiple times, - a node will appear in the treap multiple times. -+ -The return value is the address of the node within the treap, -which might differ from `node` if `pool_alloc` had to call -`realloc` to expand the pool. - -void foo_remove(struct trp_root *treap, node_type \*node):: - - Remove node from treap. Caller must ensure node is - present in treap before using this function. - -node_type *foo_search(struct trp_root \*treap, node_type \*key):: - - Search for a node that matches key. If no match is found, - result is NULL. - -node_type *foo_nsearch(struct trp_root \*treap, node_type \*key):: - - Like `foo_search`, but if if the key is missing return what - would be key's successor, were key in treap (NULL if no - successor). - -node_type *foo_first(struct trp_root \*treap):: - - Find the first item from the treap, in sorted order. - -node_type *foo_next(struct trp_root \*treap, node_type \*node):: - - Find the next item. -- cgit v1.2.1 From cba3546a43c64e2078664dbb6469aadf6bc473d3 Mon Sep 17 00:00:00 2001 From: David Barr Date: Mon, 13 Dec 2010 21:26:43 +1100 Subject: vcs-svn: drop obj_pool This reverts commit 4709455db3891f6cad9a96a574296b4926f70cbe (Add memory pool library, 2010-08-09). svn-fe uses strbufs to avoid memory allocation overhead nowadays. Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- .gitignore | 1 - Makefile | 4 +- t/t0080-vcs-svn.sh | 79 ------------------------------------ test-obj-pool.c | 116 ----------------------------------------------------- vcs-svn/obj_pool.h | 61 ---------------------------- 5 files changed, 1 insertion(+), 260 deletions(-) delete mode 100755 t/t0080-vcs-svn.sh delete mode 100644 test-obj-pool.c delete mode 100644 vcs-svn/obj_pool.h diff --git a/.gitignore b/.gitignore index aa94ff125a..789f922c36 100644 --- a/.gitignore +++ b/.gitignore @@ -171,7 +171,6 @@ /test-line-buffer /test-match-trees /test-mktemp -/test-obj-pool /test-parse-options /test-path-utils /test-run-command diff --git a/Makefile b/Makefile index a2cadc5474..b802ae97b2 100644 --- a/Makefile +++ b/Makefile @@ -424,7 +424,6 @@ TEST_PROGRAMS_NEED_X += test-dump-cache-tree TEST_PROGRAMS_NEED_X += test-genrandom TEST_PROGRAMS_NEED_X += test-line-buffer TEST_PROGRAMS_NEED_X += test-match-trees -TEST_PROGRAMS_NEED_X += test-obj-pool TEST_PROGRAMS_NEED_X += test-parse-options TEST_PROGRAMS_NEED_X += test-path-utils TEST_PROGRAMS_NEED_X += test-run-command @@ -1838,7 +1837,7 @@ XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ xdiff/xmerge.o xdiff/xpatience.o VCSSVN_OBJS = vcs-svn/line_buffer.o vcs-svn/repo_tree.o \ vcs-svn/fast_export.o vcs-svn/svndump.o -VCSSVN_TEST_OBJS = test-obj-pool.o test-line-buffer.o +VCSSVN_TEST_OBJS = test-line-buffer.o OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS) dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) @@ -1962,7 +1961,6 @@ xdiff-interface.o $(XDIFF_OBJS): \ xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h $(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) \ - vcs-svn/obj_pool.h \ vcs-svn/line_buffer.h vcs-svn/repo_tree.h vcs-svn/fast_export.h \ vcs-svn/svndump.h diff --git a/t/t0080-vcs-svn.sh b/t/t0080-vcs-svn.sh deleted file mode 100755 index 3f29496bf8..0000000000 --- a/t/t0080-vcs-svn.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/sh - -test_description='check infrastructure for svn importer' - -. ./test-lib.sh -uint32_max=4294967295 - -test_expect_success 'obj pool: store data' ' - cat <<-\EOF >expected && - 0 - 1 - EOF - - test-obj-pool <<-\EOF >actual && - alloc one 16 - set one 13 - test one 13 - reset one - EOF - test_cmp expected actual -' - -test_expect_success 'obj pool: NULL is offset ~0' ' - echo "$uint32_max" >expected && - echo null one | test-obj-pool >actual && - test_cmp expected actual -' - -test_expect_success 'obj pool: out-of-bounds access' ' - cat <<-EOF >expected && - 0 - 0 - $uint32_max - $uint32_max - 16 - 20 - $uint32_max - EOF - - test-obj-pool <<-\EOF >actual && - alloc one 16 - alloc two 16 - offset one 20 - offset two 20 - alloc one 5 - offset one 20 - free one 1 - offset one 20 - reset one - reset two - EOF - test_cmp expected actual -' - -test_expect_success 'obj pool: high-water mark' ' - cat <<-\EOF >expected && - 0 - 0 - 10 - 20 - 20 - 20 - EOF - - test-obj-pool <<-\EOF >actual && - alloc one 10 - committed one - alloc one 10 - commit one - committed one - alloc one 10 - free one 20 - committed one - reset one - EOF - test_cmp expected actual -' - -test_done diff --git a/test-obj-pool.c b/test-obj-pool.c deleted file mode 100644 index 5018863ef5..0000000000 --- a/test-obj-pool.c +++ /dev/null @@ -1,116 +0,0 @@ -/* - * test-obj-pool.c: code to exercise the svn importer's object pool - */ - -#include "cache.h" -#include "vcs-svn/obj_pool.h" - -enum pool { POOL_ONE, POOL_TWO }; -obj_pool_gen(one, int, 1) -obj_pool_gen(two, int, 4096) - -static uint32_t strtouint32(const char *s) -{ - char *end; - uintmax_t n = strtoumax(s, &end, 10); - if (*s == '\0' || (*end != '\n' && *end != '\0')) - die("invalid offset: %s", s); - return (uint32_t) n; -} - -static void handle_command(const char *command, enum pool pool, const char *arg) -{ - switch (*command) { - case 'a': - if (!prefixcmp(command, "alloc ")) { - uint32_t n = strtouint32(arg); - printf("%"PRIu32"\n", - pool == POOL_ONE ? - one_alloc(n) : two_alloc(n)); - return; - } - case 'c': - if (!prefixcmp(command, "commit ")) { - pool == POOL_ONE ? one_commit() : two_commit(); - return; - } - if (!prefixcmp(command, "committed ")) { - printf("%"PRIu32"\n", - pool == POOL_ONE ? - one_pool.committed : two_pool.committed); - return; - } - case 'f': - if (!prefixcmp(command, "free ")) { - uint32_t n = strtouint32(arg); - pool == POOL_ONE ? one_free(n) : two_free(n); - return; - } - case 'n': - if (!prefixcmp(command, "null ")) { - printf("%"PRIu32"\n", - pool == POOL_ONE ? - one_offset(NULL) : two_offset(NULL)); - return; - } - case 'o': - if (!prefixcmp(command, "offset ")) { - uint32_t n = strtouint32(arg); - printf("%"PRIu32"\n", - pool == POOL_ONE ? - one_offset(one_pointer(n)) : - two_offset(two_pointer(n))); - return; - } - case 'r': - if (!prefixcmp(command, "reset ")) { - pool == POOL_ONE ? one_reset() : two_reset(); - return; - } - case 's': - if (!prefixcmp(command, "set ")) { - uint32_t n = strtouint32(arg); - if (pool == POOL_ONE) - *one_pointer(n) = 1; - else - *two_pointer(n) = 1; - return; - } - case 't': - if (!prefixcmp(command, "test ")) { - uint32_t n = strtouint32(arg); - printf("%d\n", pool == POOL_ONE ? - *one_pointer(n) : *two_pointer(n)); - return; - } - default: - die("unrecognized command: %s", command); - } -} - -static void handle_line(const char *line) -{ - const char *arg = strchr(line, ' '); - enum pool pool; - - if (arg && !prefixcmp(arg + 1, "one")) - pool = POOL_ONE; - else if (arg && !prefixcmp(arg + 1, "two")) - pool = POOL_TWO; - else - die("no pool specified: %s", line); - - handle_command(line, pool, arg + strlen("one ")); -} - -int main(int argc, char *argv[]) -{ - struct strbuf sb = STRBUF_INIT; - if (argc != 1) - usage("test-obj-str < script"); - - while (strbuf_getline(&sb, stdin, '\n') != EOF) - handle_line(sb.buf); - strbuf_release(&sb); - return 0; -} diff --git a/vcs-svn/obj_pool.h b/vcs-svn/obj_pool.h deleted file mode 100644 index deb6eb8135..0000000000 --- a/vcs-svn/obj_pool.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed under a two-clause BSD-style license. - * See LICENSE for details. - */ - -#ifndef OBJ_POOL_H_ -#define OBJ_POOL_H_ - -#include "git-compat-util.h" - -#define MAYBE_UNUSED __attribute__((__unused__)) - -#define obj_pool_gen(pre, obj_t, initial_capacity) \ -static struct { \ - uint32_t committed; \ - uint32_t size; \ - uint32_t capacity; \ - obj_t *base; \ -} pre##_pool = {0, 0, 0, NULL}; \ -static MAYBE_UNUSED uint32_t pre##_alloc(uint32_t count) \ -{ \ - uint32_t offset; \ - if (pre##_pool.size + count > pre##_pool.capacity) { \ - while (pre##_pool.size + count > pre##_pool.capacity) \ - if (pre##_pool.capacity) \ - pre##_pool.capacity *= 2; \ - else \ - pre##_pool.capacity = initial_capacity; \ - pre##_pool.base = realloc(pre##_pool.base, \ - pre##_pool.capacity * sizeof(obj_t)); \ - } \ - offset = pre##_pool.size; \ - pre##_pool.size += count; \ - return offset; \ -} \ -static MAYBE_UNUSED void pre##_free(uint32_t count) \ -{ \ - pre##_pool.size -= count; \ -} \ -static MAYBE_UNUSED uint32_t pre##_offset(obj_t *obj) \ -{ \ - return obj == NULL ? ~0 : obj - pre##_pool.base; \ -} \ -static MAYBE_UNUSED obj_t *pre##_pointer(uint32_t offset) \ -{ \ - return offset >= pre##_pool.size ? NULL : &pre##_pool.base[offset]; \ -} \ -static MAYBE_UNUSED void pre##_commit(void) \ -{ \ - pre##_pool.committed = pre##_pool.size; \ -} \ -static MAYBE_UNUSED void pre##_reset(void) \ -{ \ - free(pre##_pool.base); \ - pre##_pool.base = NULL; \ - pre##_pool.size = 0; \ - pre##_pool.capacity = 0; \ - pre##_pool.committed = 0; \ -} - -#endif -- cgit v1.2.1 From 43155cfe1415f5547791613a5de6399112ba3560 Mon Sep 17 00:00:00 2001 From: David Barr Date: Mon, 13 Dec 2010 17:09:31 +1100 Subject: vcs-svn: avoid using ls command twice Currently there are two functions to retrieve the mode and content at a path: const char *repo_read_path(const uint32_t *path); uint32_t repo_read_mode(const uint32_t *path) Replace them with a single function with two return values. This means we can use one round-trip to get the same information from fast-import that previously took two. Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/repo_tree.c | 24 ++++-------------------- vcs-svn/repo_tree.h | 3 +-- vcs-svn/svndump.c | 3 +-- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/vcs-svn/repo_tree.c b/vcs-svn/repo_tree.c index e75f58087c..1681b654d1 100644 --- a/vcs-svn/repo_tree.c +++ b/vcs-svn/repo_tree.c @@ -8,39 +8,23 @@ #include "repo_tree.h" #include "fast_export.h" -const char *repo_read_path(const uint32_t *path) +const char *repo_read_path(const uint32_t *path, uint32_t *mode_out) { int err; - uint32_t dummy; static struct strbuf buf = STRBUF_INIT; strbuf_reset(&buf); - err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, &dummy, &buf); + err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, mode_out, &buf); if (err) { if (errno != ENOENT) die_errno("BUG: unexpected fast_export_ls error"); + /* Treat missing paths as directories. */ + *mode_out = REPO_MODE_DIR; return NULL; } return buf.buf; } -uint32_t repo_read_mode(const uint32_t *path) -{ - int err; - uint32_t result; - static struct strbuf dummy = STRBUF_INIT; - - strbuf_reset(&dummy); - err = fast_export_ls(REPO_MAX_PATH_DEPTH, path, &result, &dummy); - if (err) { - if (errno != ENOENT) - die_errno("BUG: unexpected fast_export_ls error"); - /* Treat missing paths as directories. */ - return REPO_MODE_DIR; - } - return result; -} - void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst) { int err; diff --git a/vcs-svn/repo_tree.h b/vcs-svn/repo_tree.h index d690784fbb..f506352dc2 100644 --- a/vcs-svn/repo_tree.h +++ b/vcs-svn/repo_tree.h @@ -14,8 +14,7 @@ uint32_t next_blob_mark(void); void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst); void repo_add(uint32_t *path, uint32_t mode, uint32_t blob_mark); -const char *repo_read_path(const uint32_t *path); -uint32_t repo_read_mode(const uint32_t *path); +const char *repo_read_path(const uint32_t *path, uint32_t *mode_out); void repo_delete(uint32_t *path); void repo_commit(uint32_t revision, uint32_t author, char *log, uint32_t uuid, uint32_t url, long unsigned timestamp); diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index 7ecb227a6d..99a5ba0d10 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -246,8 +246,7 @@ static void handle_node(void) old_data = NULL; } else if (node_ctx.action == NODEACT_CHANGE) { uint32_t mode; - old_data = repo_read_path(node_ctx.dst); - mode = repo_read_mode(node_ctx.dst); + old_data = repo_read_path(node_ctx.dst, &mode); if (mode == REPO_MODE_DIR && type != REPO_MODE_DIR) die("invalid dump: cannot modify a directory into a file"); if (mode != REPO_MODE_DIR && type == REPO_MODE_DIR) -- cgit v1.2.1 From 3371f9b3fb13a508c447e484618b9b5e2b9e45a7 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Sun, 27 Mar 2011 13:13:22 -0500 Subject: Makefile: list one vcs-svn/xdiff object or header per line As the svn import infrastructure evolves, it's getting to be a pain to tell by eye what files were added or removed from a dependency line like VCSSVN_OBJS = vcs-svn/string_pool.o vcs-svn/line_buffer.o \ vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o So use a style with one entry per line instead, like the existing BUILTIN_OBJS: # protect against environment VCSSVN_OBJS = ... VCSSVN_OBJS += vcs-svn/string_pool.o VCSSVN_OBJS += vcs-svn/line_buffer.o ... which is readable on its own and produces nice, clear diffs. Signed-off-by: Jonathan Nieder --- Makefile | 60 ++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index ade79232f4..dd1af362c7 100644 --- a/Makefile +++ b/Makefile @@ -341,6 +341,11 @@ BUILTIN_OBJS = BUILT_INS = COMPAT_CFLAGS = COMPAT_OBJS = +XDIFF_H = +XDIFF_OBJS = +VCSSVN_H = +VCSSVN_OBJS = +VCSSVN_TEST_OBJS = EXTRA_CPPFLAGS = LIB_H = LIB_OBJS = @@ -1836,12 +1841,25 @@ GIT_OBJS := $(LIB_OBJS) $(BUILTIN_OBJS) $(PROGRAM_OBJS) $(TEST_OBJS) \ ifndef NO_CURL GIT_OBJS += http.o http-walker.o remote-curl.o endif -XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ - xdiff/xmerge.o xdiff/xpatience.o -VCSSVN_OBJS = vcs-svn/string_pool.o vcs-svn/line_buffer.o \ - vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o -VCSSVN_TEST_OBJS = test-obj-pool.o test-string-pool.o \ - test-line-buffer.o test-treap.o + +XDIFF_OBJS += xdiff/xdiffi.o +XDIFF_OBJS += xdiff/xprepare.o +XDIFF_OBJS += xdiff/xutils.o +XDIFF_OBJS += xdiff/xemit.o +XDIFF_OBJS += xdiff/xmerge.o +XDIFF_OBJS += xdiff/xpatience.o + +VCSSVN_OBJS += vcs-svn/string_pool.o +VCSSVN_OBJS += vcs-svn/line_buffer.o +VCSSVN_OBJS += vcs-svn/repo_tree.o +VCSSVN_OBJS += vcs-svn/fast_export.o +VCSSVN_OBJS += vcs-svn/svndump.o + +VCSSVN_TEST_OBJS += test-obj-pool.o +VCSSVN_TEST_OBJS += test-string-pool.o +VCSSVN_TEST_OBJS += test-line-buffer.o +VCSSVN_TEST_OBJS += test-treap.o + OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS) dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) @@ -1960,16 +1978,26 @@ connect.o transport.o http-backend.o: url.h http-fetch.o http-walker.o remote-curl.o transport.o walker.o: walker.h http.o http-walker.o http-push.o http-fetch.o remote-curl.o: http.h url.h -xdiff-interface.o $(XDIFF_OBJS): \ - xdiff/xinclude.h xdiff/xmacros.h xdiff/xdiff.h xdiff/xtypes.h \ - xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h - -$(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) \ - vcs-svn/obj_pool.h vcs-svn/trp.h vcs-svn/string_pool.h \ - vcs-svn/line_buffer.h vcs-svn/repo_tree.h vcs-svn/fast_export.h \ - vcs-svn/svndump.h - -test-svn-fe.o: vcs-svn/svndump.h +XDIFF_H += xdiff/xinclude.h +XDIFF_H += xdiff/xmacros.h +XDIFF_H += xdiff/xdiff.h +XDIFF_H += xdiff/xtypes.h +XDIFF_H += xdiff/xutils.h +XDIFF_H += xdiff/xprepare.h +XDIFF_H += xdiff/xdiffi.h +XDIFF_H += xdiff/xemit.h + +xdiff-interface.o $(XDIFF_OBJS): $(XDIFF_H) + +VCSSVN_H += vcs-svn/obj_pool.h +VCSSVN_H += vcs-svn/trp.h +VCSSVN_H += vcs-svn/string_pool.h +VCSSVN_H += vcs-svn/line_buffer.h +VCSSVN_H += vcs-svn/repo_tree.h +VCSSVN_H += vcs-svn/fast_export.h +VCSSVN_H += vcs-svn/svndump.h + +$(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) $(VCSSVN_H) endif exec_cmd.s exec_cmd.o: EXTRA_CPPFLAGS = \ -- cgit v1.2.1 From 9d2f5ddfe56fcc228a36dd079f0897e0f474eb4e Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Sun, 2 Jan 2011 21:54:58 -0600 Subject: vcs-svn: learn to maintain a sliding view of a file Each section of a Subversion-format delta only requires examining (and keeping in random-access memory) a small portion of the preimage. At any moment, this portion starts at a certain file offset and has a well-defined length, and as the delta is applied, the portion advances from the beginning to the end of the preimage. Add a move_window function to keep track of this view into the preimage. You can use it like this: buffer_init(f, NULL); struct sliding_view window = SLIDING_VIEW_INIT(f); move_window(&window, 3, 7); /* (1) */ move_window(&window, 5, 5); /* (2) */ move_window(&window, 12, 2); /* (3) */ strbuf_release(&window.buf); buffer_deinit(f); The data structure is called sliding_view instead of _window to prevent confusion with svndiff0 Windows. In this example, (1) reads 10 bytes and discards the first 3; (2) discards the first 2, which are not needed any more; and (3) skips 2 bytes and reads 2 new bytes to work with. When move_window returns, the file position indicator is at position window->off + window->width and the data from positions window->off to the current file position are stored in window->buf. This function performs only sequential access from the input file and never seeks, so it can be safely used on pipes and sockets. On end-of-file, move_window silently reads less than the caller requested. On other errors, it prints a message and returns -1. Helped-by: David Barr Signed-off-by: Jonathan Nieder --- Makefile | 2 ++ vcs-svn/LICENSE | 2 ++ vcs-svn/sliding_window.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ vcs-svn/sliding_window.h | 17 +++++++++++ 4 files changed, 98 insertions(+) create mode 100644 vcs-svn/sliding_window.c create mode 100644 vcs-svn/sliding_window.h diff --git a/Makefile b/Makefile index dd1af362c7..5cd141d5af 100644 --- a/Makefile +++ b/Makefile @@ -1851,6 +1851,7 @@ XDIFF_OBJS += xdiff/xpatience.o VCSSVN_OBJS += vcs-svn/string_pool.o VCSSVN_OBJS += vcs-svn/line_buffer.o +VCSSVN_OBJS += vcs-svn/sliding_window.o VCSSVN_OBJS += vcs-svn/repo_tree.o VCSSVN_OBJS += vcs-svn/fast_export.o VCSSVN_OBJS += vcs-svn/svndump.o @@ -1993,6 +1994,7 @@ VCSSVN_H += vcs-svn/obj_pool.h VCSSVN_H += vcs-svn/trp.h VCSSVN_H += vcs-svn/string_pool.h VCSSVN_H += vcs-svn/line_buffer.h +VCSSVN_H += vcs-svn/sliding_window.h VCSSVN_H += vcs-svn/repo_tree.h VCSSVN_H += vcs-svn/fast_export.h VCSSVN_H += vcs-svn/svndump.h diff --git a/vcs-svn/LICENSE b/vcs-svn/LICENSE index 0a5e3c43a0..805882c838 100644 --- a/vcs-svn/LICENSE +++ b/vcs-svn/LICENSE @@ -1,6 +1,8 @@ Copyright (C) 2010 David Barr . All rights reserved. +Copyright (C) 2010 Jonathan Nieder . + Copyright (C) 2008 Jason Evans . All rights reserved. diff --git a/vcs-svn/sliding_window.c b/vcs-svn/sliding_window.c new file mode 100644 index 0000000000..1b8d9875ed --- /dev/null +++ b/vcs-svn/sliding_window.c @@ -0,0 +1,77 @@ +/* + * Licensed under a two-clause BSD-style license. + * See LICENSE for details. + */ + +#include "git-compat-util.h" +#include "sliding_window.h" +#include "line_buffer.h" +#include "strbuf.h" + +static int input_error(struct line_buffer *file) +{ + if (!buffer_ferror(file)) + return error("delta preimage ends early"); + return error("cannot read delta preimage: %s", strerror(errno)); +} + +static int skip_or_whine(struct line_buffer *file, off_t gap) +{ + if (buffer_skip_bytes(file, gap) != gap) + return input_error(file); + return 0; +} + +static int read_to_fill_or_whine(struct line_buffer *file, + struct strbuf *buf, size_t width) +{ + buffer_read_binary(file, buf, width - buf->len); + if (buf->len != width) + return input_error(file); + return 0; +} + +static int check_overflow(off_t a, size_t b) +{ + if (b > maximum_signed_value_of_type(off_t)) + return error("unrepresentable length in delta: " + "%"PRIuMAX" > OFF_MAX", (uintmax_t) b); + if (signed_add_overflows(a, (off_t) b)) + return error("unrepresentable offset in delta: " + "%"PRIuMAX" + %"PRIuMAX" > OFF_MAX", + (uintmax_t) a, (uintmax_t) b); + return 0; +} + +int move_window(struct sliding_view *view, off_t off, size_t width) +{ + off_t file_offset; + assert(view); + assert(view->width <= view->buf.len); + assert(!check_overflow(view->off, view->buf.len)); + + if (check_overflow(off, width)) + return -1; + if (off < view->off || off + width < view->off + view->width) + return error("invalid delta: window slides left"); + + file_offset = view->off + view->buf.len; + if (off < file_offset) { + /* Move the overlapping region into place. */ + strbuf_remove(&view->buf, 0, off - view->off); + } else { + /* Seek ahead to skip the gap. */ + if (skip_or_whine(view->file, off - file_offset)) + return -1; + strbuf_setlen(&view->buf, 0); + } + + if (view->buf.len > width) + ; /* Already read. */ + else if (read_to_fill_or_whine(view->file, &view->buf, width)) + return -1; + + view->off = off; + view->width = width; + return 0; +} diff --git a/vcs-svn/sliding_window.h b/vcs-svn/sliding_window.h new file mode 100644 index 0000000000..ed0bfdd65c --- /dev/null +++ b/vcs-svn/sliding_window.h @@ -0,0 +1,17 @@ +#ifndef SLIDING_WINDOW_H_ +#define SLIDING_WINDOW_H_ + +#include "strbuf.h" + +struct sliding_view { + struct line_buffer *file; + off_t off; + size_t width; + struct strbuf buf; +}; + +#define SLIDING_VIEW_INIT(input) { (input), 0, 0, STRBUF_INIT } + +extern int move_window(struct sliding_view *view, off_t off, size_t width); + +#endif -- cgit v1.2.1 From 896e4bfcec4f6b489aba2197f60a59bc7f45a8ac Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Sun, 2 Jan 2011 21:37:36 -0600 Subject: vcs-svn: make buffer_read_binary API more convenient buffer_read_binary is a thin wrapper around fread, but its signature is wrong: - fread can fill an arbitrary in-memory buffer. buffer_read_binary is limited to buffers whose size is representable by a 32-bit integer. - The result from fread is the number of bytes actually read. buffer_read_binary only reports the number of bytes read by incrementing sb->len by that amount and returns void. Fix both: let buffer_read_binary accept a size_t instead of uint32_t for the number of bytes to read and as a convenience return the number of bytes actually read. Signed-off-by: Jonathan Nieder Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- vcs-svn/line_buffer.c | 6 +++--- vcs-svn/line_buffer.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vcs-svn/line_buffer.c b/vcs-svn/line_buffer.c index c39038723e..01fcb842f1 100644 --- a/vcs-svn/line_buffer.c +++ b/vcs-svn/line_buffer.c @@ -91,10 +91,10 @@ char *buffer_read_line(struct line_buffer *buf) return buf->line_buffer; } -void buffer_read_binary(struct line_buffer *buf, - struct strbuf *sb, uint32_t size) +size_t buffer_read_binary(struct line_buffer *buf, + struct strbuf *sb, size_t size) { - strbuf_fread(sb, size, buf->infile); + return strbuf_fread(sb, size, buf->infile); } off_t buffer_copy_bytes(struct line_buffer *buf, off_t nbytes) diff --git a/vcs-svn/line_buffer.h b/vcs-svn/line_buffer.h index d0b22dda76..8901f214ba 100644 --- a/vcs-svn/line_buffer.h +++ b/vcs-svn/line_buffer.h @@ -23,7 +23,7 @@ long buffer_tmpfile_prepare_to_read(struct line_buffer *buf); int buffer_ferror(struct line_buffer *buf); char *buffer_read_line(struct line_buffer *buf); int buffer_read_char(struct line_buffer *buf); -void buffer_read_binary(struct line_buffer *buf, struct strbuf *sb, uint32_t len); +size_t buffer_read_binary(struct line_buffer *buf, struct strbuf *sb, size_t len); /* Returns number of bytes read (not necessarily written). */ off_t buffer_copy_bytes(struct line_buffer *buf, off_t len); off_t buffer_skip_bytes(struct line_buffer *buf, off_t len); -- cgit v1.2.1 From ddcc8c5b469d2564dbacd629a873e7703f2dbd83 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Sat, 25 Dec 2010 05:11:32 -0600 Subject: vcs-svn: skeleton of an svn delta parser A delta in the subversion delta (svndiff0) format consists of the magic bytes SVN\0 followed by a sequence of windows of a certain well specified format (starting with five integers). Add an svndiff0_apply function and test-svn-fe -d commandline tool to parse such a delta in the special case of not including any windows. Later patches will add features to turn this into a fully functional delta applier for svn-fe to use to parse the streams produced by "svnrdump dump" and "svnadmin dump --deltas". The content of symlinks starts with the word "link " in Subversion's worldview, so we need to be able to prepend that text to input for the sake of delta application. So initialization of the input state of the delta preimage is left to the calling program, giving callers a chance to seed the buffer with text of their choice. Improved-by: Ramkumar Ramachandra Improved-by: David Barr Signed-off-by: Jonathan Nieder --- Makefile | 2 ++ t/t9011-svn-da.sh | 35 +++++++++++++++++++++++++++++++++++ test-svn-fe.c | 42 ++++++++++++++++++++++++++++++++++-------- vcs-svn/svndiff.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ vcs-svn/svndiff.h | 10 ++++++++++ 5 files changed, 133 insertions(+), 8 deletions(-) create mode 100755 t/t9011-svn-da.sh create mode 100644 vcs-svn/svndiff.c create mode 100644 vcs-svn/svndiff.h diff --git a/Makefile b/Makefile index 5cd141d5af..cd7dd9723d 100644 --- a/Makefile +++ b/Makefile @@ -1854,6 +1854,7 @@ VCSSVN_OBJS += vcs-svn/line_buffer.o VCSSVN_OBJS += vcs-svn/sliding_window.o VCSSVN_OBJS += vcs-svn/repo_tree.o VCSSVN_OBJS += vcs-svn/fast_export.o +VCSSVN_OBJS += vcs-svn/svndiff.o VCSSVN_OBJS += vcs-svn/svndump.o VCSSVN_TEST_OBJS += test-obj-pool.o @@ -1997,6 +1998,7 @@ VCSSVN_H += vcs-svn/line_buffer.h VCSSVN_H += vcs-svn/sliding_window.h VCSSVN_H += vcs-svn/repo_tree.h VCSSVN_H += vcs-svn/fast_export.h +VCSSVN_H += vcs-svn/svndiff.h VCSSVN_H += vcs-svn/svndump.h $(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) $(VCSSVN_H) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh new file mode 100755 index 0000000000..ee0c1e2087 --- /dev/null +++ b/t/t9011-svn-da.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +test_description='test parsing of svndiff0 files + +Using the "test-svn-fe -d" helper, check that svn-fe correctly +interprets deltas using various facilities (some from the spec, +some only learned from practice). +' +. ./test-lib.sh + +>empty +printf foo >preimage + +test_expect_success 'reject empty delta' ' + test_must_fail test-svn-fe -d preimage empty 0 +' + +test_expect_success 'delta can empty file' ' + printf "SVNQ" | q_to_nul >clear.delta && + test-svn-fe -d preimage clear.delta 4 >actual && + test_cmp empty actual +' + +test_expect_success 'reject svndiff2' ' + printf "SVN\002" >bad.filetype && + test_must_fail test-svn-fe -d preimage bad.filetype 4 +' + +test_expect_failure 'one-window empty delta' ' + printf "SVNQ%s" "QQQQQ" | q_to_nul >clear.onewindow && + test-svn-fe -d preimage clear.onewindow 9 >actual && + test_cmp empty actual +' + +test_done diff --git a/test-svn-fe.c b/test-svn-fe.c index b42ba789b1..66bd04022d 100644 --- a/test-svn-fe.c +++ b/test-svn-fe.c @@ -4,15 +4,41 @@ #include "git-compat-util.h" #include "vcs-svn/svndump.h" +#include "vcs-svn/svndiff.h" +#include "vcs-svn/sliding_window.h" +#include "vcs-svn/line_buffer.h" int main(int argc, char *argv[]) { - if (argc != 2) - usage("test-svn-fe "); - if (svndump_init(argv[1])) - return 1; - svndump_read(NULL); - svndump_deinit(); - svndump_reset(); - return 0; + static const char test_svnfe_usage[] = + "test-svn-fe ( | [-d] )"; + if (argc == 2) { + if (svndump_init(argv[1])) + return 1; + svndump_read(NULL); + svndump_deinit(); + svndump_reset(); + return 0; + } + if (argc == 5 && !strcmp(argv[1], "-d")) { + struct line_buffer preimage = LINE_BUFFER_INIT; + struct line_buffer delta = LINE_BUFFER_INIT; + struct sliding_view preimage_view = SLIDING_VIEW_INIT(&preimage); + if (buffer_init(&preimage, argv[2])) + die_errno("cannot open preimage"); + if (buffer_init(&delta, argv[3])) + die_errno("cannot open delta"); + if (svndiff0_apply(&delta, (off_t) strtoull(argv[4], NULL, 0), + &preimage_view, stdout)) + return 1; + if (buffer_deinit(&preimage)) + die_errno("cannot close preimage"); + if (buffer_deinit(&delta)) + die_errno("cannot close delta"); + buffer_reset(&preimage); + strbuf_release(&preimage_view.buf); + buffer_reset(&delta); + return 0; + } + usage(test_svnfe_usage); } diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c new file mode 100644 index 0000000000..591603669c --- /dev/null +++ b/vcs-svn/svndiff.c @@ -0,0 +1,52 @@ +/* + * Licensed under a two-clause BSD-style license. + * See LICENSE for details. + */ + +#include "git-compat-util.h" +#include "line_buffer.h" +#include "svndiff.h" + +/* + * svndiff0 applier + * + * See http://svn.apache.org/repos/asf/subversion/trunk/notes/svndiff. + * + * svndiff0 ::= 'SVN\0' window* + */ + +static int error_short_read(struct line_buffer *input) +{ + if (buffer_ferror(input)) + return error("error reading delta: %s", strerror(errno)); + return error("invalid delta: unexpected end of file"); +} + +static int read_magic(struct line_buffer *in, off_t *len) +{ + static const char magic[] = {'S', 'V', 'N', '\0'}; + struct strbuf sb = STRBUF_INIT; + + if (*len < sizeof(magic) || + buffer_read_binary(in, &sb, sizeof(magic)) != sizeof(magic)) + return error_short_read(in); + + if (memcmp(sb.buf, magic, sizeof(magic))) + return error("invalid delta: unrecognized file type"); + + *len -= sizeof(magic); + strbuf_release(&sb); + return 0; +} + +int svndiff0_apply(struct line_buffer *delta, off_t delta_len, + struct sliding_view *preimage, FILE *postimage) +{ + assert(delta && preimage && postimage); + + if (read_magic(delta, &delta_len)) + return -1; + if (delta_len) + return error("What do you think I am? A delta applier?"); + return 0; +} diff --git a/vcs-svn/svndiff.h b/vcs-svn/svndiff.h new file mode 100644 index 0000000000..74eb464bab --- /dev/null +++ b/vcs-svn/svndiff.h @@ -0,0 +1,10 @@ +#ifndef SVNDIFF_H_ +#define SVNDIFF_H_ + +struct line_buffer; +struct sliding_view; + +extern int svndiff0_apply(struct line_buffer *delta, off_t delta_len, + struct sliding_view *preimage, FILE *postimage); + +#endif -- cgit v1.2.1 From 252712111fad127db365e3dd764309fe5658679a Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:21:43 -0500 Subject: vcs-svn: parse svndiff0 window header Each window in a subversion delta (svndiff0-format file) starts with a window header, consisting of five integers with variable-length representation: source view offset source view length output length instructions length auxiliary data length Parse it. The result is not usable for deltas with nonempty postimage yet; in fact, this only adds support for deltas without any instructions or auxiliary data. This is a good place to stop, though, since that little support lets us add some simple passing tests concerning error handling to the test suite. Improved-by: Ramkumar Ramachandra Improved-by: David Barr Signed-off-by: Jonathan Nieder --- t/t9011-svn-da.sh | 56 ++++++++++++++++++++++++++++++++- vcs-svn/svndiff.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index ee0c1e2087..be1234094f 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -26,10 +26,64 @@ test_expect_success 'reject svndiff2' ' test_must_fail test-svn-fe -d preimage bad.filetype 4 ' -test_expect_failure 'one-window empty delta' ' +test_expect_success 'one-window empty delta' ' printf "SVNQ%s" "QQQQQ" | q_to_nul >clear.onewindow && test-svn-fe -d preimage clear.onewindow 9 >actual && test_cmp empty actual ' +test_expect_success 'reject incomplete window header' ' + printf "SVNQ%s" "QQQQQ" | q_to_nul >clear.onewindow && + printf "SVNQ%s" "QQ" | q_to_nul >clear.partialwindow && + test_must_fail test-svn-fe -d preimage clear.onewindow 6 && + test_must_fail test-svn-fe -d preimage clear.partialwindow 6 +' + +test_expect_success 'reject declared delta longer than actual delta' ' + printf "SVNQ%s" "QQQQQ" | q_to_nul >clear.onewindow && + printf "SVNQ%s" "QQ" | q_to_nul >clear.partialwindow && + test_must_fail test-svn-fe -d preimage clear.onewindow 14 && + test_must_fail test-svn-fe -d preimage clear.partialwindow 9 +' + +test_expect_success 'two-window empty delta' ' + printf "SVNQ%s%s" "QQQQQ" "QQQQQ" | q_to_nul >clear.twowindow && + test-svn-fe -d preimage clear.twowindow 14 >actual && + test_must_fail test-svn-fe -d preimage clear.twowindow 13 && + test_cmp empty actual +' + +test_expect_success 'noisy zeroes' ' + printf "SVNQ%s" \ + "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRQQQQQ" | + tr R "\200" | + q_to_nul >clear.noisy && + len=$(wc -c clear.badmagic && + test_must_fail test-svn-fe -d preimage clear.badmagic 5 +' + +test_expect_success 'reject truncated integer' ' + printf "SVNQ%s%s" "QQQQQ" "QQQQRRQ" | + tr R "\200" | + q_to_nul >clear.fullint && + printf "SVNQ%s%s" "QQQQQ" "QQQQRR" | + tr RT "\201" | + q_to_nul >clear.partialint && + test_must_fail test-svn-fe -d preimage clear.fullint 15 && + test-svn-fe -d preimage clear.fullint 16 && + test_must_fail test-svn-fe -d preimage clear.partialint 15 +' + +test_expect_success 'nonempty (but unused) preimage view' ' + printf "SVNQ%b" "Q\003QQQ" | q_to_nul >clear.readpreimage && + test-svn-fe -d preimage clear.readpreimage 9 >actual && + test_cmp empty actual +' + test_done diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index 591603669c..249efb6eed 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -13,8 +13,16 @@ * See http://svn.apache.org/repos/asf/subversion/trunk/notes/svndiff. * * svndiff0 ::= 'SVN\0' window* + * window ::= int int int int int instructions inline_data; + * int ::= highdigit* lowdigit; + * highdigit ::= # binary 1000 0000 OR-ed with 7 bit value; + * lowdigit ::= # 7 bit value; */ +#define VLI_CONTINUE 0x80 +#define VLI_DIGIT_MASK 0x7f +#define VLI_BITS_PER_DIGIT 7 + static int error_short_read(struct line_buffer *input) { if (buffer_ferror(input)) @@ -28,17 +36,84 @@ static int read_magic(struct line_buffer *in, off_t *len) struct strbuf sb = STRBUF_INIT; if (*len < sizeof(magic) || - buffer_read_binary(in, &sb, sizeof(magic)) != sizeof(magic)) - return error_short_read(in); + buffer_read_binary(in, &sb, sizeof(magic)) != sizeof(magic)) { + error_short_read(in); + strbuf_release(&sb); + return -1; + } - if (memcmp(sb.buf, magic, sizeof(magic))) + if (memcmp(sb.buf, magic, sizeof(magic))) { + strbuf_release(&sb); return error("invalid delta: unrecognized file type"); + } *len -= sizeof(magic); strbuf_release(&sb); return 0; } +static int read_int(struct line_buffer *in, uintmax_t *result, off_t *len) +{ + uintmax_t rv = 0; + off_t sz; + for (sz = *len; sz; sz--) { + const int ch = buffer_read_char(in); + if (ch == EOF) + break; + + rv <<= VLI_BITS_PER_DIGIT; + rv += (ch & VLI_DIGIT_MASK); + if (ch & VLI_CONTINUE) + continue; + + *result = rv; + *len = sz - 1; + return 0; + } + return error_short_read(in); +} + +static int read_offset(struct line_buffer *in, off_t *result, off_t *len) +{ + uintmax_t val; + if (read_int(in, &val, len)) + return -1; + if (val > maximum_signed_value_of_type(off_t)) + return error("unrepresentable offset in delta: %"PRIuMAX"", val); + *result = val; + return 0; +} + +static int read_length(struct line_buffer *in, size_t *result, off_t *len) +{ + uintmax_t val; + if (read_int(in, &val, len)) + return -1; + if (val > SIZE_MAX) + return error("unrepresentable length in delta: %"PRIuMAX"", val); + *result = val; + return 0; +} + +static int apply_one_window(struct line_buffer *delta, off_t *delta_len) +{ + size_t out_len; + size_t instructions_len; + size_t data_len; + assert(delta_len); + + /* "source view" offset and length already handled; */ + if (read_length(delta, &out_len, delta_len) || + read_length(delta, &instructions_len, delta_len) || + read_length(delta, &data_len, delta_len)) + return -1; + if (instructions_len) + return error("What do you think I am? A delta applier?"); + if (data_len) + return error("No support for inline data yet"); + return 0; +} + int svndiff0_apply(struct line_buffer *delta, off_t delta_len, struct sliding_view *preimage, FILE *postimage) { @@ -46,7 +121,14 @@ int svndiff0_apply(struct line_buffer *delta, off_t delta_len, if (read_magic(delta, &delta_len)) return -1; - if (delta_len) - return error("What do you think I am? A delta applier?"); + while (delta_len) { /* For each window: */ + off_t pre_off; + size_t pre_len; + + if (read_offset(delta, &pre_off, &delta_len) || + read_length(delta, &pre_len, &delta_len) || + apply_one_window(delta, &delta_len)) + return -1; + } return 0; } -- cgit v1.2.1 From bcd254621f9a98794cdc32906db10af7135824c4 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:30:37 -0500 Subject: vcs-svn: read the preimage when applying deltas The source view offset heading each svndiff0 window represents a number of bytes past the beginning of the preimage. Together with the source view length, it dictates to the delta applier what portion of the preimage instructions will refer to. Read that portion right away using the sliding window code. Maybe some day we will use mmap to read data more lazily. Subversion's implementation tolerates source view offsets pointing past the end of the preimage file but we do not, for simplicity. This does not teach the delta applier to read instructions or copy data from the source view. Deltas that could produce nonempty output will still be rejected. Improved-by: Ramkumar Ramachandra Improved-by: David Barr Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 35 +++++++++++++++++++++++++++++++++++ vcs-svn/svndiff.c | 2 ++ 2 files changed, 37 insertions(+) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index be1234094f..90b6058ab6 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -86,4 +86,39 @@ test_expect_success 'nonempty (but unused) preimage view' ' test_cmp empty actual ' +test_expect_success 'preimage view: right endpoint cannot backtrack' ' + printf "SVNQ%b%b" "Q\003QQQ" "Q\002QQQ" | + q_to_nul >clear.backtrack && + test_must_fail test-svn-fe -d preimage clear.backtrack 14 +' + +test_expect_success 'preimage view: left endpoint can advance' ' + printf "SVNQ%b%b" "Q\003QQQ" "\001\002QQQ" | + q_to_nul >clear.preshrink && + printf "SVNQ%b%b" "Q\003QQQ" "\001\001QQQ" | + q_to_nul >clear.shrinkbacktrack && + test-svn-fe -d preimage clear.preshrink 14 >actual && + test_must_fail test-svn-fe -d preimage clear.shrinkbacktrack 14 && + test_cmp empty actual +' + +test_expect_success 'preimage view: offsets compared by value' ' + printf "SVNQ%b%b" "\001\001QQQ" "\0200Q\003QQQ" | + q_to_nul >clear.noisybacktrack && + printf "SVNQ%b%b" "\001\001QQQ" "\0200\001\002QQQ" | + q_to_nul >clear.noisyadvance && + test_must_fail test-svn-fe -d preimage clear.noisybacktrack 15 && + test-svn-fe -d preimage clear.noisyadvance 15 && + test_cmp empty actual +' + +test_expect_success 'preimage view: reject truncated preimage' ' + printf "SVNQ%b" "\010QQQQ" | q_to_nul >clear.lateemptyread && + printf "SVNQ%b" "\010\001QQQ" | q_to_nul >clear.latenonemptyread && + printf "SVNQ%b" "\001\010QQQ" | q_to_nul >clear.longread && + test_must_fail test-svn-fe -d preimage clear.lateemptyread 9 && + test_must_fail test-svn-fe -d preimage clear.latenonemptyread 9 && + test_must_fail test-svn-fe -d preimage clear.longread 9 +' + test_done diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index 249efb6eed..b7c2c8bf53 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -4,6 +4,7 @@ */ #include "git-compat-util.h" +#include "sliding_window.h" #include "line_buffer.h" #include "svndiff.h" @@ -127,6 +128,7 @@ int svndiff0_apply(struct line_buffer *delta, off_t delta_len, if (read_offset(delta, &pre_off, &delta_len) || read_length(delta, &pre_len, &delta_len) || + move_window(preimage, pre_off, pre_len) || apply_one_window(delta, &delta_len)) return -1; } -- cgit v1.2.1 From fc4ae43b2cbd53da6ac2a0047fb4e53175921696 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:35:59 -0500 Subject: vcs-svn: read inline data from deltas Each window of an svndiff0-format delta includes a section for novel text to be copied to the postimage (in the order it appears in the window, possibly interspersed with other data). Slurp in this data when encountering it. It is not actually necessary to do so --- it would be just as easy to copy from delta to output as part of interpreting the relevant instructions --- but this way, the code that interprets svndiff0 instructions can proceed very quickly because it does not require I/O. Subversion's svndiff0 parser rejects deltas that do not consume all the novel text that was provided. Omit that check for now so we can test the new functionality right away, rather than waiting to learn instructions that consume data. Do check for truncated data sections. Subversion's parser rejects deltas that end in the middle of a declared novel-text section, so it should be safe for us to reject them, too. Improved-by: Ramkumar Ramachandra Improved-by: David Barr Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 12 ++++++++++++ vcs-svn/svndiff.c | 46 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index 90b6058ab6..26a4a3694a 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -121,4 +121,16 @@ test_expect_success 'preimage view: reject truncated preimage' ' test_must_fail test-svn-fe -d preimage clear.longread 9 ' +test_expect_success 'inline data' ' + printf "SVNQ%b%s%b%s" "QQQQ\003" "bar" "QQQQ\001" "x" | + q_to_nul >inline.clear && + test-svn-fe -d preimage inline.clear 18 >actual && + test_cmp empty actual +' + +test_expect_success 'reject truncated inline data' ' + printf "SVNQ%b%s" "QQQQ\003" "b" | q_to_nul >inline.trunc && + test_must_fail test-svn-fe -d preimage inline.trunc 10 +' + test_done diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index b7c2c8bf53..175168f599 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -24,6 +24,17 @@ #define VLI_DIGIT_MASK 0x7f #define VLI_BITS_PER_DIGIT 7 +struct window { + struct strbuf data; +}; + +#define WINDOW_INIT { STRBUF_INIT } + +static void window_release(struct window *ctx) +{ + strbuf_release(&ctx->data); +} + static int error_short_read(struct line_buffer *input) { if (buffer_ferror(input)) @@ -31,24 +42,30 @@ static int error_short_read(struct line_buffer *input) return error("invalid delta: unexpected end of file"); } +static int read_chunk(struct line_buffer *delta, off_t *delta_len, + struct strbuf *buf, size_t len) +{ + strbuf_reset(buf); + if (len > *delta_len || + buffer_read_binary(delta, buf, len) != len) + return error_short_read(delta); + *delta_len -= buf->len; + return 0; +} + static int read_magic(struct line_buffer *in, off_t *len) { static const char magic[] = {'S', 'V', 'N', '\0'}; struct strbuf sb = STRBUF_INIT; - if (*len < sizeof(magic) || - buffer_read_binary(in, &sb, sizeof(magic)) != sizeof(magic)) { - error_short_read(in); + if (read_chunk(in, len, &sb, sizeof(magic))) { strbuf_release(&sb); return -1; } - if (memcmp(sb.buf, magic, sizeof(magic))) { strbuf_release(&sb); return error("invalid delta: unrecognized file type"); } - - *len -= sizeof(magic); strbuf_release(&sb); return 0; } @@ -98,6 +115,7 @@ static int read_length(struct line_buffer *in, size_t *result, off_t *len) static int apply_one_window(struct line_buffer *delta, off_t *delta_len) { + struct window ctx = WINDOW_INIT; size_t out_len; size_t instructions_len; size_t data_len; @@ -107,12 +125,18 @@ static int apply_one_window(struct line_buffer *delta, off_t *delta_len) if (read_length(delta, &out_len, delta_len) || read_length(delta, &instructions_len, delta_len) || read_length(delta, &data_len, delta_len)) - return -1; - if (instructions_len) - return error("What do you think I am? A delta applier?"); - if (data_len) - return error("No support for inline data yet"); + goto error_out; + if (instructions_len) { + error("What do you think I am? A delta applier?"); + goto error_out; + } + if (read_chunk(delta, delta_len, &ctx.data, data_len)) + goto error_out; + window_release(&ctx); return 0; +error_out: + window_release(&ctx); + return -1; } int svndiff0_apply(struct line_buffer *delta, off_t delta_len, -- cgit v1.2.1 From ef2ac77e9f8f4819f75cf52721567463e60a805c Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:38:01 -0500 Subject: vcs-svn: read instructions from deltas Buffer the instruction section upon encountering it for later interpretation. An alternative design would involve parsing the instructions at this point and buffering them in some processed form. Using the unprocessed form is simpler. Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 5 +++++ vcs-svn/svndiff.c | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index 26a4a3694a..d9acd0c8a6 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -133,4 +133,9 @@ test_expect_success 'reject truncated inline data' ' test_must_fail test-svn-fe -d preimage inline.trunc 10 ' +test_expect_success 'reject truncated inline data (after instruction section)' ' + printf "SVNQ%b%b%s" "QQ\001\001\003" "\0201" "b" | q_to_nul >insn.trunc && + test_must_fail test-svn-fe -d preimage insn.trunc 11 +' + test_done diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index 175168f599..8968fdb4eb 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -25,13 +25,15 @@ #define VLI_BITS_PER_DIGIT 7 struct window { + struct strbuf instructions; struct strbuf data; }; -#define WINDOW_INIT { STRBUF_INIT } +#define WINDOW_INIT { STRBUF_INIT, STRBUF_INIT } static void window_release(struct window *ctx) { + strbuf_release(&ctx->instructions); strbuf_release(&ctx->data); } @@ -124,7 +126,8 @@ static int apply_one_window(struct line_buffer *delta, off_t *delta_len) /* "source view" offset and length already handled; */ if (read_length(delta, &out_len, delta_len) || read_length(delta, &instructions_len, delta_len) || - read_length(delta, &data_len, delta_len)) + read_length(delta, &data_len, delta_len) || + read_chunk(delta, delta_len, &ctx.instructions, instructions_len)) goto error_out; if (instructions_len) { error("What do you think I am? A delta applier?"); -- cgit v1.2.1 From ec71aa2e1f229b90092e6678ac7c2dca3d15b5f3 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:39:44 -0500 Subject: vcs-svn: implement copyfrom_data delta instruction The copyfrom_data instruction copies a few bytes verbatim from the novel text section of a window to the postimage. [jn: with memory leak fix from David] Improved-by: David Barr Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 31 +++++++++++++++ vcs-svn/svndiff.c | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 139 insertions(+), 7 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index d9acd0c8a6..ba8ce052aa 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -138,4 +138,35 @@ test_expect_success 'reject truncated inline data (after instruction section)' ' test_must_fail test-svn-fe -d preimage insn.trunc 11 ' +test_expect_success 'copyfrom_data' ' + echo hi >expect && + printf "SVNQ%b%b%b" "QQ\003\001\003" "\0203" "hi\n" | q_to_nul >copydat && + test-svn-fe -d preimage copydat 13 >actual && + test_cmp expect actual +' + +test_expect_success 'multiple copyfrom_data' ' + echo hi >expect && + printf "SVNQ%b%b%b%b%b" "QQ\003\002\003" "\0201\0202" "hi\n" \ + "QQQ\002Q" "\0200Q" | q_to_nul >copy.multi && + len=$(wc -c actual && + test_cmp expect actual +' + +test_expect_success 'incomplete multiple insn' ' + printf "SVNQ%b%b%b" "QQ\003\002\003" "\0203\0200" "hi\n" | + q_to_nul >copy.partial && + len=$(wc -c copy.incomplete && + len=$(wc -c out); strbuf_release(&ctx->instructions); strbuf_release(&ctx->data); } +static int write_strbuf(struct strbuf *sb, FILE *out) +{ + if (fwrite(sb->buf, 1, sb->len, out) == sb->len) /* Success. */ + return 0; + return error("cannot write delta postimage: %s", strerror(errno)); +} + static int error_short_read(struct line_buffer *input) { if (buffer_ferror(input)) @@ -93,6 +114,25 @@ static int read_int(struct line_buffer *in, uintmax_t *result, off_t *len) return error_short_read(in); } +static int parse_int(const char **buf, size_t *result, const char *end) +{ + size_t rv = 0; + const char *pos; + for (pos = *buf; pos != end; pos++) { + unsigned char ch = *pos; + + rv <<= VLI_BITS_PER_DIGIT; + rv += (ch & VLI_DIGIT_MASK); + if (ch & VLI_CONTINUE) + continue; + + *result = rv; + *buf = pos + 1; + return 0; + } + return error("invalid delta: unexpected end of instructions section"); +} + static int read_offset(struct line_buffer *in, off_t *result, off_t *len) { uintmax_t val; @@ -115,7 +155,64 @@ static int read_length(struct line_buffer *in, size_t *result, off_t *len) return 0; } -static int apply_one_window(struct line_buffer *delta, off_t *delta_len) +static int copyfrom_data(struct window *ctx, size_t *data_pos, size_t nbytes) +{ + const size_t pos = *data_pos; + if (unsigned_add_overflows(pos, nbytes) || + pos + nbytes > ctx->data.len) + return error("invalid delta: copies unavailable inline data"); + strbuf_add(&ctx->out, ctx->data.buf + pos, nbytes); + *data_pos += nbytes; + return 0; +} + +static int parse_first_operand(const char **buf, size_t *out, const char *end) +{ + size_t result = (unsigned char) *(*buf)++ & OPERAND_MASK; + if (result) { /* immediate operand */ + *out = result; + return 0; + } + return parse_int(buf, out, end); +} + +static int execute_one_instruction(struct window *ctx, + const char **instructions, size_t *data_pos) +{ + unsigned int instruction; + const char *insns_end = ctx->instructions.buf + ctx->instructions.len; + size_t nbytes; + assert(ctx); + assert(instructions && *instructions); + assert(data_pos); + + instruction = (unsigned char) **instructions; + if (parse_first_operand(instructions, &nbytes, insns_end)) + return -1; + if ((instruction & INSN_MASK) != INSN_COPYFROM_DATA) + return error("Unknown instruction %x", instruction); + return copyfrom_data(ctx, data_pos, nbytes); +} + +static int apply_window_in_core(struct window *ctx) +{ + const char *instructions; + size_t data_pos = 0; + + /* + * Fill ctx->out.buf using data from the source, target, + * and inline data views. + */ + for (instructions = ctx->instructions.buf; + instructions != ctx->instructions.buf + ctx->instructions.len; + ) + if (execute_one_instruction(ctx, &instructions, &data_pos)) + return -1; + return 0; +} + +static int apply_one_window(struct line_buffer *delta, off_t *delta_len, + FILE *out) { struct window ctx = WINDOW_INIT; size_t out_len; @@ -127,13 +224,17 @@ static int apply_one_window(struct line_buffer *delta, off_t *delta_len) if (read_length(delta, &out_len, delta_len) || read_length(delta, &instructions_len, delta_len) || read_length(delta, &data_len, delta_len) || - read_chunk(delta, delta_len, &ctx.instructions, instructions_len)) + read_chunk(delta, delta_len, &ctx.instructions, instructions_len) || + read_chunk(delta, delta_len, &ctx.data, data_len)) + goto error_out; + strbuf_grow(&ctx.out, out_len); + if (apply_window_in_core(&ctx)) goto error_out; - if (instructions_len) { - error("What do you think I am? A delta applier?"); + if (ctx.out.len != out_len) { + error("invalid delta: incorrect postimage length"); goto error_out; } - if (read_chunk(delta, delta_len, &ctx.data, data_len)) + if (write_strbuf(&ctx.out, out)) goto error_out; window_release(&ctx); return 0; @@ -156,7 +257,7 @@ int svndiff0_apply(struct line_buffer *delta, off_t delta_len, if (read_offset(delta, &pre_off, &delta_len) || read_length(delta, &pre_len, &delta_len) || move_window(preimage, pre_off, pre_len) || - apply_one_window(delta, &delta_len)) + apply_one_window(delta, &delta_len, postimage)) return -1; } return 0; -- cgit v1.2.1 From 4c9b93ed7644a7a7c72bdd8105d88a9ebb8e3e74 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:48:07 -0500 Subject: vcs-svn: verify that deltas consume all inline data By constraining the format of deltas, we can more easily detect corruption and other breakage. Requiring deltas not to provide unconsumed data also opens the possibility of ignoring the declared amount of novel data and simply streaming the data as needed to fulfill copyfrom_data requests. Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 5 ++--- vcs-svn/svndiff.c | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index ba8ce052aa..72691b9379 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -121,11 +121,10 @@ test_expect_success 'preimage view: reject truncated preimage' ' test_must_fail test-svn-fe -d preimage clear.longread 9 ' -test_expect_success 'inline data' ' +test_expect_success 'forbid unconsumed inline data' ' printf "SVNQ%b%s%b%s" "QQQQ\003" "bar" "QQQQ\001" "x" | q_to_nul >inline.clear && - test-svn-fe -d preimage inline.clear 18 >actual && - test_cmp empty actual + test_must_fail test-svn-fe -d preimage inline.clear 18 >actual ' test_expect_success 'reject truncated inline data' ' diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index ed1d4a08be..fb7dc22f92 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -208,6 +208,8 @@ static int apply_window_in_core(struct window *ctx) ) if (execute_one_instruction(ctx, &instructions, &data_pos)) return -1; + if (data_pos != ctx->data.len) + return error("invalid delta: does not copy all inline data"); return 0; } -- cgit v1.2.1 From d3f131b57ec0e69a37bca882fa6bf39aa4c1c387 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:50:07 -0500 Subject: vcs-svn: let deltas use data from postimage The copyfrom_target instruction copies appends data that is already present in the current output view to the end of output. (The offset argument is relative to the beginning of output produced in the current window.) The region copied is allowed to run past the end of the existing output. To support that case, copy one character at a time rather than calling memcpy or memmove. This allows copyfrom_target to be used once to repeat a string many times. For example: COPYFROM_DATA 2 COPYFROM_OUTPUT 10, 0 DATA "ab" would produce the output "ababababababababababab". Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 42 ++++++++++++++++++++++++++++++++++++++++++ vcs-svn/svndiff.c | 28 ++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index 72691b9379..f9573a1c40 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -168,4 +168,46 @@ test_expect_success 'catch attempt to copy missing data' ' test_must_fail test-svn-fe -d preimage copy.incomplete $len ' +test_expect_success 'copyfrom target to repeat data' ' + printf foofoo >expect && + printf "SVNQ%b%b%s" "QQ\006\004\003" "\0203\0100\003Q" "foo" | + q_to_nul >copytarget.repeat && + len=$(wc -c actual && + test_cmp expect actual +' + +test_expect_success 'copyfrom target out of order' ' + printf foooof >expect && + printf "SVNQ%b%b%s" \ + "QQ\006\007\003" "\0203\0101\002\0101\001\0101Q" "foo" | + q_to_nul >copytarget.reverse && + len=$(wc -c actual && + test_cmp expect actual +' + +test_expect_success 'catch copyfrom future' ' + printf "SVNQ%b%b%s" "QQ\004\004\003" "\0202\0101\002\0201" "XYZ" | + q_to_nul >copytarget.infuture && + len=$(wc -c expect && + printf "SVNQ%b%b%s" "QQ\014\004\003" "\0202\0111Q\0201" "XYZ" | + q_to_nul >copytarget.sustain && + len=$(wc -c actual && + test_cmp expect actual +' + +test_expect_success 'catch copy that overflows' ' + printf "SVNQ%b%b%s" "QQ\003\003\001" "\0201\0177Q" X | + q_to_nul >copytarget.overflow && + len=$(wc -c = ctx->out.len) + return error("invalid delta: copies from the future"); + for (; nbytes > 0; nbytes--) + strbuf_addch(&ctx->out, ctx->out.buf[offset++]); + return 0; +} + static int copyfrom_data(struct window *ctx, size_t *data_pos, size_t nbytes) { const size_t pos = *data_pos; @@ -189,9 +208,14 @@ static int execute_one_instruction(struct window *ctx, instruction = (unsigned char) **instructions; if (parse_first_operand(instructions, &nbytes, insns_end)) return -1; - if ((instruction & INSN_MASK) != INSN_COPYFROM_DATA) + switch (instruction & INSN_MASK) { + case INSN_COPYFROM_TARGET: + return copyfrom_target(ctx, instructions, nbytes, insns_end); + case INSN_COPYFROM_DATA: + return copyfrom_data(ctx, data_pos, nbytes); + default: return error("Unknown instruction %x", instruction); - return copyfrom_data(ctx, data_pos, nbytes); + } } static int apply_window_in_core(struct window *ctx) -- cgit v1.2.1 From c846e4107876936bed7177a811559bd74a72dcd8 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 13 Oct 2010 04:58:30 -0500 Subject: vcs-svn: let deltas use data from preimage The copyfrom_source instruction appends data from the preimage buffer to the end of output. Its arguments are a length and an offset relative to the beginning of the source view. With this change, the delta applier is able to reproduce all 5,636,613 blobs in the early history of the ASF repository. Tested with mkfifo backflow svn-fe backflow with svn-asf-public-r0:940166 produced by whatever version of Subversion the dumps in /dump/ on svn.apache.org use (presumably 1.6.something). Improved-by: Ramkumar Ramachandra Improved-by: David Barr Signed-off-by: Jonathan Nieder Acked-by: Ramkumar Ramachandra --- t/t9011-svn-da.sh | 35 +++++++++++++++++++++++++++++++++++ vcs-svn/svndiff.c | 28 +++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/t/t9011-svn-da.sh b/t/t9011-svn-da.sh index f9573a1c40..b38d16f9db 100755 --- a/t/t9011-svn-da.sh +++ b/t/t9011-svn-da.sh @@ -210,4 +210,39 @@ test_expect_success 'catch copy that overflows' ' test_must_fail test-svn-fe -d preimage copytarget.overflow $len ' +test_expect_success 'copyfrom source' ' + printf foo >expect && + printf "SVNQ%b%b" "Q\003\003\002Q" "\003Q" | q_to_nul >copysource.all && + test-svn-fe -d preimage copysource.all 11 >actual && + test_cmp expect actual +' + +test_expect_success 'copy backwards' ' + printf oof >expect && + printf "SVNQ%b%b" "Q\003\003\006Q" "\001\002\001\001\001Q" | + q_to_nul >copysource.rev && + test-svn-fe -d preimage copysource.rev 15 >actual && + test_cmp expect actual +' + +test_expect_success 'offsets are relative to window' ' + printf fo >expect && + printf "SVNQ%b%b%b%b" "Q\003\001\002Q" "\001Q" \ + "\002\001\001\002Q" "\001Q" | + q_to_nul >copysource.two && + test-svn-fe -d preimage copysource.two 18 >actual && + test_cmp expect actual +' + +test_expect_success 'example from notes/svndiff' ' + printf aaaaccccdddddddd >expect && + printf aaaabbbbcccc >source && + printf "SVNQ%b%b%s" "Q\014\020\007\001" \ + "\004Q\004\010\0201\0107\010" d | + q_to_nul >delta.example && + len=$(wc -c actual && + test_cmp expect actual +' + test_done diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index a02eee0410..9ee41bbc90 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -24,6 +24,7 @@ * view_selector ::= copyfrom_source * | copyfrom_target * ; + * copyfrom_source ::= # binary 00 000000; * copyfrom_target ::= # binary 01 000000; * copyfrom_data ::= # binary 10 000000; * packed_view_selector ::= # view_selector OR-ed with 6 bit value; @@ -34,6 +35,7 @@ */ #define INSN_MASK 0xc0 +#define INSN_COPYFROM_SOURCE 0x00 #define INSN_COPYFROM_TARGET 0x40 #define INSN_COPYFROM_DATA 0x80 #define OPERAND_MASK 0x3f @@ -43,12 +45,13 @@ #define VLI_BITS_PER_DIGIT 7 struct window { + struct sliding_view *in; struct strbuf out; struct strbuf instructions; struct strbuf data; }; -#define WINDOW_INIT { STRBUF_INIT, STRBUF_INIT, STRBUF_INIT } +#define WINDOW_INIT(w) { (w), STRBUF_INIT, STRBUF_INIT, STRBUF_INIT } static void window_release(struct window *ctx) { @@ -161,6 +164,19 @@ static int read_length(struct line_buffer *in, size_t *result, off_t *len) return 0; } +static int copyfrom_source(struct window *ctx, const char **instructions, + size_t nbytes, const char *insns_end) +{ + size_t offset; + if (parse_int(instructions, &offset, insns_end)) + return -1; + if (unsigned_add_overflows(offset, nbytes) || + offset + nbytes > ctx->in->width) + return error("invalid delta: copies source data outside view"); + strbuf_add(&ctx->out, ctx->in->buf.buf + offset, nbytes); + return 0; +} + static int copyfrom_target(struct window *ctx, const char **instructions, size_t nbytes, const char *instructions_end) { @@ -209,12 +225,14 @@ static int execute_one_instruction(struct window *ctx, if (parse_first_operand(instructions, &nbytes, insns_end)) return -1; switch (instruction & INSN_MASK) { + case INSN_COPYFROM_SOURCE: + return copyfrom_source(ctx, instructions, nbytes, insns_end); case INSN_COPYFROM_TARGET: return copyfrom_target(ctx, instructions, nbytes, insns_end); case INSN_COPYFROM_DATA: return copyfrom_data(ctx, data_pos, nbytes); default: - return error("Unknown instruction %x", instruction); + return error("invalid delta: unrecognized instruction"); } } @@ -238,9 +256,9 @@ static int apply_window_in_core(struct window *ctx) } static int apply_one_window(struct line_buffer *delta, off_t *delta_len, - FILE *out) + struct sliding_view *preimage, FILE *out) { - struct window ctx = WINDOW_INIT; + struct window ctx = WINDOW_INIT(preimage); size_t out_len; size_t instructions_len; size_t data_len; @@ -283,7 +301,7 @@ int svndiff0_apply(struct line_buffer *delta, off_t delta_len, if (read_offset(delta, &pre_off, &delta_len) || read_length(delta, &pre_len, &delta_len) || move_window(preimage, pre_off, pre_len) || - apply_one_window(delta, &delta_len, postimage)) + apply_one_window(delta, &delta_len, preimage, postimage)) return -1; } return 0; -- cgit v1.2.1 From 7a75e661c5cef9fcd7c84fe0fb22672a57d6373e Mon Sep 17 00:00:00 2001 From: David Barr Date: Sat, 19 Mar 2011 18:20:54 +1100 Subject: vcs-svn: implement text-delta handling Handle input in Subversion's dumpfile format, version 3. This is the format produced by "svnrdump dump" and "svnadmin dump --deltas", and the main difference between v3 dumpfiles and the dumpfiles already handled is that these can include nodes whose properties and text are expressed relative to some other node. To handle such nodes, we find which node the text and properties are based on, handle its property changes, use the cat-blob command to request the basis blob from the fast-import backend, use the svndiff0_apply() helper to apply the text delta on the fly, writing output to a temporary file, and then measure that postimage file's length and write its content to the fast-import stream. The temporary postimage file is shared between delta-using nodes to avoid some file system overhead. The svn-fe interface needs to be more complicated to accomodate the backward flow of information from the fast-import backend to svn-fe. The backflow fd is not needed when parsing streams without deltas, though, so existing scripts using svn-fe on v2 dumps should continue to work. NEEDSWORK: generalize interface so caller sets the backflow fd, close temporary file before exiting Signed-off-by: David Barr Signed-off-by: Jonathan Nieder Signed-off-by: David Barr Signed-off-by: Jonathan Nieder --- contrib/svn-fe/svn-fe.txt | 5 +-- t/t9010-svn-fe.sh | 108 ++++++++++++++++++++++++++++++++++++++++++++- vcs-svn/fast_export.c | 109 +++++++++++++++++++++++++++++++++++++++++++++- vcs-svn/fast_export.h | 3 ++ vcs-svn/svndump.c | 13 ++++-- 5 files changed, 227 insertions(+), 11 deletions(-) diff --git a/contrib/svn-fe/svn-fe.txt b/contrib/svn-fe/svn-fe.txt index 85f7b83028..2dd27ceb0e 100644 --- a/contrib/svn-fe/svn-fe.txt +++ b/contrib/svn-fe/svn-fe.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] mkfifo backchannel && -svnadmin dump --incremental REPO | +svnadmin dump --deltas REPO | svn-fe [url] 3backchannel @@ -32,9 +32,6 @@ Subversion's repository dump format is documented in full in Files in this format can be generated using the 'svnadmin dump' or 'svk admin dump' command. -Dumps produced with 'svnadmin dump --deltas' (dumpfile format v3) -are not supported. - OUTPUT FORMAT ------------- The fast-import format is documented by the git-fast-import(1) diff --git a/t/t9010-svn-fe.sh b/t/t9010-svn-fe.sh index 003395c5f6..f24f004fd5 100755 --- a/t/t9010-svn-fe.sh +++ b/t/t9010-svn-fe.sh @@ -674,7 +674,7 @@ test_expect_success PIPE 'change file mode and reiterate content' ' test_cmp hello actual.target ' -test_expect_success PIPE 'deltas not supported' ' +test_expect_success PIPE 'deltas supported' ' reinit_git && { # (old) h + (inline) ello + (old) \n @@ -735,7 +735,7 @@ test_expect_success PIPE 'deltas not supported' ' echo PROPS-END && cat delta } >delta.dump && - test_must_fail try_dump delta.dump + try_dump delta.dump ' test_expect_success PIPE 'property deltas supported' ' @@ -942,6 +942,110 @@ test_expect_success PIPE 'deltas for typechange' ' test_cmp expect actual ' +test_expect_success PIPE 'deltas need not consume the whole preimage' ' + reinit_git && + cat >expect <<-\EOF && + OBJID + :120000 100644 OBJID OBJID T postimage + OBJID + :100644 120000 OBJID OBJID T postimage + OBJID + :000000 100644 OBJID OBJID A postimage + EOF + echo "first preimage" >expect.1 && + printf target >expect.2 && + printf lnk >expect.3 && + { + printf "SVNQ%b%b%b" "QQ\017\001\017" "\0217" "first preimage\n" | + q_to_nul + } >delta.1 && + { + properties svn:special "*" && + echo PROPS-END + } >symlink.props && + { + printf "SVNQ%b%b%b" "Q\002\013\004\012" "\0201\001\001\0211" "lnk target" | + q_to_nul + } >delta.2 && + { + printf "SVNQ%b%b" "Q\004\003\004Q" "\001Q\002\002" | + q_to_nul + } >delta.3 && + { + cat <<-\EOF && + SVN-fs-dump-format-version: 3 + + Revision-number: 1 + Prop-content-length: 10 + Content-length: 10 + + PROPS-END + + Node-path: postimage + Node-kind: file + Node-action: add + Text-delta: true + Prop-content-length: 10 + EOF + echo Text-content-length: $(wc -c deltapartial.dump && + try_dump deltapartial.dump && + { + git rev-list HEAD | + git diff-tree --root --stdin | + sed "s/$_x40/OBJID/g" + } >actual && + test_cmp expect actual && + git show HEAD:postimage >actual.3 && + git show HEAD^:postimage >actual.2 && + git show HEAD^^:postimage >actual.1 && + test_cmp expect.1 actual.1 && + test_cmp expect.2 actual.2 && + test_cmp expect.3 actual.3 +' test_expect_success 'set up svn repo' ' svnconf=$PWD/svnconf && diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 33e853d9cd..005674d8c1 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -7,15 +7,38 @@ #include "strbuf.h" #include "quote.h" #include "fast_export.h" -#include "line_buffer.h" #include "repo_tree.h" #include "strbuf.h" +#include "svndiff.h" +#include "sliding_window.h" +#include "line_buffer.h" #define MAX_GITSVN_LINE_LEN 4096 +#define REPORT_FILENO 3 static uint32_t first_commit_done; +static struct line_buffer postimage = LINE_BUFFER_INIT; static struct line_buffer report_buffer = LINE_BUFFER_INIT; +/* NEEDSWORK: move to fast_export_init() */ +static int init_postimage(void) +{ + static int postimage_initialized; + if (postimage_initialized) + return 0; + postimage_initialized = 1; + return buffer_tmpfile_init(&postimage); +} + +static int init_report_buffer(int fd) +{ + static int report_buffer_initialized; + if (report_buffer_initialized) + return 0; + report_buffer_initialized = 1; + return buffer_fdinit(&report_buffer, fd); +} + void fast_export_init(int fd) { if (buffer_fdinit(&report_buffer, fd)) @@ -132,6 +155,73 @@ static void die_short_read(struct line_buffer *input) die("invalid dump: unexpected end of file"); } +static int ends_with(const char *s, size_t len, const char *suffix) +{ + const size_t suffixlen = strlen(suffix); + if (len < suffixlen) + return 0; + return !memcmp(s + len - suffixlen, suffix, suffixlen); +} + +static int parse_cat_response_line(const char *header, off_t *len) +{ + size_t headerlen = strlen(header); + const char *type; + const char *end; + + if (ends_with(header, headerlen, " missing")) + return error("cat-blob reports missing blob: %s", header); + type = memmem(header, headerlen, " blob ", strlen(" blob ")); + if (!type) + return error("cat-blob header has wrong object type: %s", header); + *len = strtoumax(type + strlen(" blob "), (char **) &end, 10); + if (end == type + strlen(" blob ")) + return error("cat-blob header does not contain length: %s", header); + if (*end) + return error("cat-blob header contains garbage after length: %s", header); + return 0; +} + +static long apply_delta(off_t len, struct line_buffer *input, + const char *old_data, uint32_t old_mode) +{ + long ret; + off_t preimage_len = 0; + struct sliding_view preimage = SLIDING_VIEW_INIT(&report_buffer); + FILE *out; + + if (init_postimage() || !(out = buffer_tmpfile_rewind(&postimage))) + die("cannot open temporary file for blob retrieval"); + if (init_report_buffer(REPORT_FILENO)) + die("cannot open fd 3 for feedback from fast-import"); + if (old_data) { + const char *response; + printf("cat-blob %s\n", old_data); + fflush(stdout); + response = get_response_line(); + if (parse_cat_response_line(response, &preimage_len)) + die("invalid cat-blob response: %s", response); + } + if (old_mode == REPO_MODE_LNK) { + strbuf_addstr(&preimage.buf, "link "); + preimage_len += strlen("link "); + } + if (svndiff0_apply(input, len, &preimage, out)) + die("cannot apply delta"); + if (old_data) { + /* Read the remainder of preimage and trailing newline. */ + if (move_window(&preimage, preimage_len, 1)) + die("cannot seek to end of input"); + if (preimage.buf.buf[0] != '\n') + die("missing newline after cat-blob response"); + } + ret = buffer_tmpfile_prepare_to_read(&postimage); + if (ret < 0) + die("cannot read temporary file for blob retrieval"); + strbuf_release(&preimage.buf); + return ret; +} + void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input) { if (mode == REPO_MODE_LNK) { @@ -199,3 +289,20 @@ int fast_export_ls(const char *path, uint32_t *mode, struct strbuf *dataref) ls_from_active_commit(path); return parse_ls_response(get_response_line(), mode, dataref); } + +void fast_export_blob_delta(uint32_t mode, + uint32_t old_mode, const char *old_data, + uint32_t len, struct line_buffer *input) +{ + long postimage_len; + if (len > maximum_signed_value_of_type(off_t)) + die("enormous delta"); + postimage_len = apply_delta((off_t) len, input, old_data, old_mode); + if (mode == REPO_MODE_LNK) { + buffer_skip_bytes(&postimage, strlen("link ")); + postimage_len -= strlen("link "); + } + printf("data %ld\n", postimage_len); + buffer_copy_bytes(&postimage, postimage_len); + fputc('\n', stdout); +} diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index 2d392e370d..43d05b65ef 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -15,6 +15,9 @@ void fast_export_begin_commit(uint32_t revision, const char *author, const char *url, unsigned long timestamp); void fast_export_end_commit(uint32_t revision); void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input); +void fast_export_blob_delta(uint32_t mode, + uint32_t old_mode, const char *old_data, + uint32_t len, struct line_buffer *input); /* If there is no such file at that rev, returns -1, errno == ENOENT. */ int fast_export_ls_rev(uint32_t rev, const char *path, diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index 11c59f18bf..b1f4161068 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -217,9 +217,7 @@ static void handle_node(void) */ static const char *const empty_blob = "::empty::"; const char *old_data = NULL; - - if (node_ctx.text_delta) - die("text deltas not supported"); + uint32_t old_mode = REPO_MODE_BLB; if (node_ctx.action == NODEACT_DELETE) { if (have_text || have_props || node_ctx.srcRev) @@ -255,6 +253,7 @@ static void handle_node(void) if (mode != REPO_MODE_DIR && type == REPO_MODE_DIR) die("invalid dump: cannot modify a file into a directory"); node_ctx.type = mode; + old_mode = mode; } else if (node_ctx.action == NODEACT_ADD) { if (type == REPO_MODE_DIR) old_data = NULL; @@ -289,8 +288,14 @@ static void handle_node(void) fast_export_modify(node_ctx.dst.buf, node_ctx.type, old_data); return; } + if (!node_ctx.text_delta) { + fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); + fast_export_data(node_ctx.type, node_ctx.textLength, &input); + return; + } fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); - fast_export_data(node_ctx.type, node_ctx.textLength, &input); + fast_export_blob_delta(node_ctx.type, old_mode, old_data, + node_ctx.textLength, &input); } static void begin_revision(void) -- cgit v1.2.1 From b747e5675db5e26292c942146a25e1c26440c5f7 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Fri, 27 May 2011 04:28:46 -0500 Subject: test-svn-fe: split off "test-svn-fe -d" into a separate function The helper for testing the svndiff library is getting dangerously close to the right margin. Split it off into a separate function so it is easier to contemplate on its own. In the process, make the test_svnfe_usage[] string static so it can be shared by the two functions (and other future functions in this test program) without fuss. In other words, this just unindents the code a little. No functional change intended. Signed-off-by: Jonathan Nieder --- test-svn-fe.c | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/test-svn-fe.c b/test-svn-fe.c index 66bd04022d..a0276260eb 100644 --- a/test-svn-fe.c +++ b/test-svn-fe.c @@ -8,10 +8,37 @@ #include "vcs-svn/sliding_window.h" #include "vcs-svn/line_buffer.h" +static const char test_svnfe_usage[] = + "test-svn-fe ( | [-d] )"; + +static int apply_delta(int argc, char *argv[]) +{ + struct line_buffer preimage = LINE_BUFFER_INIT; + struct line_buffer delta = LINE_BUFFER_INIT; + struct sliding_view preimage_view = SLIDING_VIEW_INIT(&preimage); + + if (argc != 5) + usage(test_svnfe_usage); + + if (buffer_init(&preimage, argv[2])) + die_errno("cannot open preimage"); + if (buffer_init(&delta, argv[3])) + die_errno("cannot open delta"); + if (svndiff0_apply(&delta, (off_t) strtoull(argv[4], NULL, 0), + &preimage_view, stdout)) + return 1; + if (buffer_deinit(&preimage)) + die_errno("cannot close preimage"); + if (buffer_deinit(&delta)) + die_errno("cannot close delta"); + buffer_reset(&preimage); + strbuf_release(&preimage_view.buf); + buffer_reset(&delta); + return 0; +} + int main(int argc, char *argv[]) { - static const char test_svnfe_usage[] = - "test-svn-fe ( | [-d] )"; if (argc == 2) { if (svndump_init(argv[1])) return 1; @@ -20,25 +47,8 @@ int main(int argc, char *argv[]) svndump_reset(); return 0; } - if (argc == 5 && !strcmp(argv[1], "-d")) { - struct line_buffer preimage = LINE_BUFFER_INIT; - struct line_buffer delta = LINE_BUFFER_INIT; - struct sliding_view preimage_view = SLIDING_VIEW_INIT(&preimage); - if (buffer_init(&preimage, argv[2])) - die_errno("cannot open preimage"); - if (buffer_init(&delta, argv[3])) - die_errno("cannot open delta"); - if (svndiff0_apply(&delta, (off_t) strtoull(argv[4], NULL, 0), - &preimage_view, stdout)) - return 1; - if (buffer_deinit(&preimage)) - die_errno("cannot close preimage"); - if (buffer_deinit(&delta)) - die_errno("cannot close delta"); - buffer_reset(&preimage); - strbuf_release(&preimage_view.buf); - buffer_reset(&delta); - return 0; - } + + if (argc >= 2 && !strcmp(argv[1], "-d")) + return apply_delta(argc, argv); usage(test_svnfe_usage); } -- cgit v1.2.1 From fbdd4f6fb477885e4bf81658e02c3542a861c695 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Fri, 27 May 2011 04:07:44 -0500 Subject: vcs-svn: cap number of bytes read from sliding view Introduce a "max_off" field in struct sliding_view, roughly representing a maximum number of bytes that can be read from "file". If it is set to a nonnegative integer, a call to move_window() attempting to put the right endpoint beyond that offset will return an error instead. The idea is to use this when applying Subversion-format deltas to prevent reads past the end of the preimage (which has known length). Without such a check, corrupt deltas would cause svn-fe to block indefinitely when data in the input pipe is exhausted. Inspired-by: Ramkumar Ramachandra Signed-off-by: Jonathan Nieder --- test-svn-fe.c | 2 +- vcs-svn/sliding_window.c | 2 ++ vcs-svn/sliding_window.h | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test-svn-fe.c b/test-svn-fe.c index a0276260eb..332a5f711d 100644 --- a/test-svn-fe.c +++ b/test-svn-fe.c @@ -15,7 +15,7 @@ static int apply_delta(int argc, char *argv[]) { struct line_buffer preimage = LINE_BUFFER_INIT; struct line_buffer delta = LINE_BUFFER_INIT; - struct sliding_view preimage_view = SLIDING_VIEW_INIT(&preimage); + struct sliding_view preimage_view = SLIDING_VIEW_INIT(&preimage, -1); if (argc != 5) usage(test_svnfe_usage); diff --git a/vcs-svn/sliding_window.c b/vcs-svn/sliding_window.c index 1b8d9875ed..1bac7a4c7f 100644 --- a/vcs-svn/sliding_window.c +++ b/vcs-svn/sliding_window.c @@ -54,6 +54,8 @@ int move_window(struct sliding_view *view, off_t off, size_t width) return -1; if (off < view->off || off + width < view->off + view->width) return error("invalid delta: window slides left"); + if (view->max_off >= 0 && view->max_off < off + width) + return error("delta preimage ends early"); file_offset = view->off + view->buf.len; if (off < file_offset) { diff --git a/vcs-svn/sliding_window.h b/vcs-svn/sliding_window.h index ed0bfdd65c..b43a825cba 100644 --- a/vcs-svn/sliding_window.h +++ b/vcs-svn/sliding_window.h @@ -7,10 +7,11 @@ struct sliding_view { struct line_buffer *file; off_t off; size_t width; + off_t max_off; /* -1 means unlimited */ struct strbuf buf; }; -#define SLIDING_VIEW_INIT(input) { (input), 0, 0, STRBUF_INIT } +#define SLIDING_VIEW_INIT(input, len) { (input), 0, 0, (len), STRBUF_INIT } extern int move_window(struct sliding_view *view, off_t off, size_t width); -- cgit v1.2.1 From abe27c0cbd97bf6a693004ddb411392ed596a853 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Fri, 27 May 2011 05:18:33 -0500 Subject: vcs-svn: guard against overflow when computing preimage length Signed integer overflow produces undefined behavior in C and off_t is a signed type. For predictable behavior, add some checks to protect in advance against overflow. On 32-bit systems ftell as called by buffer_tmpfile_prepare_to_read is likely to fail with EOVERFLOW when reading the corresponding postimage, and this patch does not fix that. So it's more of a futureproofing measure than a complete fix. Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index edc658d4fe..96a75d51d1 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -166,6 +166,7 @@ static int ends_with(const char *s, size_t len, const char *suffix) static int parse_cat_response_line(const char *header, off_t *len) { size_t headerlen = strlen(header); + uintmax_t n; const char *type; const char *end; @@ -174,14 +175,25 @@ static int parse_cat_response_line(const char *header, off_t *len) type = memmem(header, headerlen, " blob ", strlen(" blob ")); if (!type) return error("cat-blob header has wrong object type: %s", header); - *len = strtoumax(type + strlen(" blob "), (char **) &end, 10); + n = strtoumax(type + strlen(" blob "), (char **) &end, 10); if (end == type + strlen(" blob ")) return error("cat-blob header does not contain length: %s", header); + if (memchr(type + strlen(" blob "), '-', end - type - strlen(" blob "))) + return error("cat-blob header contains negative length: %s", header); + if (n == UINTMAX_MAX || n > maximum_signed_value_of_type(off_t)) + return error("blob too large for current definition of off_t"); + *len = n; if (*end) return error("cat-blob header contains garbage after length: %s", header); return 0; } +static void check_preimage_overflow(off_t a, off_t b) +{ + if (signed_add_overflows(a, b)) + die("blob too large for current definition of off_t"); +} + static long apply_delta(off_t len, struct line_buffer *input, const char *old_data, uint32_t old_mode) { @@ -204,6 +216,7 @@ static long apply_delta(off_t len, struct line_buffer *input, } if (old_mode == REPO_MODE_LNK) { strbuf_addstr(&preimage.buf, "link "); + check_preimage_overflow(preimage_len, strlen("link ")); preimage_len += strlen("link "); } if (svndiff0_apply(input, len, &preimage, out)) -- cgit v1.2.1 From 3ac10b2e3fd6d858621f796160d251ad34affc20 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Fri, 27 May 2011 05:44:27 -0500 Subject: vcs-svn: avoid hangs from corrupt deltas A corrupt Subversion-format delta can request reads past the end of the preimage. Set sliding_view::max_off so such corruption is caught when it appears rather than blocking in an impossible-to-fulfill read() when input is coming from a socket or pipe. Inspired-by: Ramkumar Ramachandra Signed-off-by: Jonathan Nieder --- t/t9010-svn-fe.sh | 40 +++++++++++++++++++++++++++++++++++++--- vcs-svn/fast_export.c | 15 +++++++++------ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/t/t9010-svn-fe.sh b/t/t9010-svn-fe.sh index f24f004fd5..b7eed2489f 100755 --- a/t/t9010-svn-fe.sh +++ b/t/t9010-svn-fe.sh @@ -18,12 +18,13 @@ reinit_git () { try_dump () { input=$1 && - maybe_fail=${2:+test_$2} && + maybe_fail_svnfe=${2:+test_$2} && + maybe_fail_fi=${3:+test_$3} && { - $maybe_fail test-svn-fe "$input" >stream 3stream 3backflow && + $maybe_fail_fi git fast-import --cat-blob-fd=3 backflow && wait $! } @@ -1047,6 +1048,39 @@ test_expect_success PIPE 'deltas need not consume the whole preimage' ' test_cmp expect.3 actual.3 ' +test_expect_success PIPE 'no hang for delta trying to read past end of preimage' ' + reinit_git && + { + # COPY 1 + printf "SVNQ%b%b" "Q\001\001\002Q" "\001Q" | + q_to_nul + } >greedy.delta && + { + cat <<-\EOF && + SVN-fs-dump-format-version: 3 + + Revision-number: 1 + Prop-content-length: 10 + Content-length: 10 + + PROPS-END + + Node-path: bootstrap + Node-kind: file + Node-action: add + Text-delta: true + Prop-content-length: 10 + EOF + echo Text-content-length: $(wc -c greedydelta.dump && + try_dump greedydelta.dump must_fail might_fail +' + test_expect_success 'set up svn repo' ' svnconf=$PWD/svnconf && mkdir -p "$svnconf" && diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 96a75d51d1..97f5fdf489 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -198,8 +198,7 @@ static long apply_delta(off_t len, struct line_buffer *input, const char *old_data, uint32_t old_mode) { long ret; - off_t preimage_len = 0; - struct sliding_view preimage = SLIDING_VIEW_INIT(&report_buffer, -1); + struct sliding_view preimage = SLIDING_VIEW_INIT(&report_buffer, 0); FILE *out; if (init_postimage() || !(out = buffer_tmpfile_rewind(&postimage))) @@ -211,19 +210,23 @@ static long apply_delta(off_t len, struct line_buffer *input, printf("cat-blob %s\n", old_data); fflush(stdout); response = get_response_line(); - if (parse_cat_response_line(response, &preimage_len)) + if (parse_cat_response_line(response, &preimage.max_off)) die("invalid cat-blob response: %s", response); + check_preimage_overflow(preimage.max_off, 1); } if (old_mode == REPO_MODE_LNK) { strbuf_addstr(&preimage.buf, "link "); - check_preimage_overflow(preimage_len, strlen("link ")); - preimage_len += strlen("link "); + check_preimage_overflow(preimage.max_off, strlen("link ")); + preimage.max_off += strlen("link "); + check_preimage_overflow(preimage.max_off, 1); } if (svndiff0_apply(input, len, &preimage, out)) die("cannot apply delta"); if (old_data) { /* Read the remainder of preimage and trailing newline. */ - if (move_window(&preimage, preimage_len, 1)) + assert(!signed_add_overflows(preimage.max_off, 1)); + preimage.max_off++; /* room for newline */ + if (move_window(&preimage, preimage.max_off - 1, 1)) die("cannot seek to end of input"); if (preimage.buf.buf[0] != '\n') die("missing newline after cat-blob response"); -- cgit v1.2.1 From c5f1fbe7bc6b29f3343a168461ee70816ddebec2 Mon Sep 17 00:00:00 2001 From: Dmitry Ivankov Date: Mon, 20 Jun 2011 14:22:47 +0600 Subject: vcs-svn: do not initialize report_buffer twice When importing from a dump with deltas, first fast_export_init calls buffer_fdinit, and then init_report_buffer calls fdopen once again when processing the first delta. The second initialization is redundant and leaks a FILE *. Remove the redundant on-demand initialization to fix this. Initializing directly in fast_export_init is simpler and lets the caller pass an int specifying which fd to use instead of hard-coding REPORT_FILENO. Signed-off-by: Dmitry Ivankov Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 97f5fdf489..3efde20a0c 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -14,7 +14,6 @@ #include "line_buffer.h" #define MAX_GITSVN_LINE_LEN 4096 -#define REPORT_FILENO 3 static uint32_t first_commit_done; static struct line_buffer postimage = LINE_BUFFER_INIT; @@ -30,15 +29,6 @@ static int init_postimage(void) return buffer_tmpfile_init(&postimage); } -static int init_report_buffer(int fd) -{ - static int report_buffer_initialized; - if (report_buffer_initialized) - return 0; - report_buffer_initialized = 1; - return buffer_fdinit(&report_buffer, fd); -} - void fast_export_init(int fd) { if (buffer_fdinit(&report_buffer, fd)) @@ -203,8 +193,6 @@ static long apply_delta(off_t len, struct line_buffer *input, if (init_postimage() || !(out = buffer_tmpfile_rewind(&postimage))) die("cannot open temporary file for blob retrieval"); - if (init_report_buffer(REPORT_FILENO)) - die("cannot open fd 3 for feedback from fast-import"); if (old_data) { const char *response; printf("cat-blob %s\n", old_data); -- cgit v1.2.1 From c5bcbcdcfa1e2a1977497cb3a342c0365c8d78d6 Mon Sep 17 00:00:00 2001 From: Dmitry Ivankov Date: Thu, 23 Jun 2011 17:33:58 +0600 Subject: vcs-svn: reset first_commit_done in fast_export_init first_commit_done has zero as a default value, but it is not reset back to zero in fast_export_init. Reset it back to zero so that each export will have proper initial state. Signed-off-by: Dmitry Ivankov Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 1 + 1 file changed, 1 insertion(+) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 3efde20a0c..19d7c34c25 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -31,6 +31,7 @@ static int init_postimage(void) void fast_export_init(int fd) { + first_commit_done = 0; if (buffer_fdinit(&report_buffer, fd)) die_errno("cannot read from file descriptor %d", fd); } -- cgit v1.2.1 From 23cb5bf3b3b9699bf00fa38c4c08f32f8c60b529 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 22 Dec 2011 11:21:26 -0800 Subject: i18n of multi-line advice messages Advice messages are by definition meant for human end-users, and prime candidates for i18n/l10n. They tend to also be more verbose to be helpful, and need to be longer than just one line. Although we do not have parameterized multi-line advice messages yet, once we do, we cannot emit such a message like this: advise(_("Please rename %s to something else"), gostak); advise(_("so that we can avoid distimming %s unnecessarily."), doshes); because some translations may need to have the replacement of 'gostak' on the second line (or 'doshes' on the first line). Some languages may even need to use three lines in order to fit the same message within a reasonable width. Instead, it has to be a single advise() construct, like this: advise(_("Please rename %s to something else\n" "so that we can avoid distimming %s unnecessarily."), gostak, doshes); Update the advise() function and its existing callers to - take a format string that can be multi-line and translatable as a whole; - use the string and the parameters to form a localized message; and - show each line in the result with the localization of the "hint: ". Signed-off-by: Junio C Hamano --- advice.c | 23 ++++++++++++++++------- builtin/revert.c | 9 ++++----- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/advice.c b/advice.c index e02e632df3..65a07859f2 100644 --- a/advice.c +++ b/advice.c @@ -21,11 +21,21 @@ static struct { void advise(const char *advice, ...) { + struct strbuf buf = STRBUF_INIT; va_list params; + const char *cp, *np; va_start(params, advice); - vreportf("hint: ", advice, params); + strbuf_addf(&buf, advice, params); va_end(params); + + for (cp = buf.buf; *cp; cp = np) { + np = strchrnul(cp, '\n'); + fprintf(stderr, _("hint: %.*s\n"), (int)(np - cp), cp); + if (*np) + np++; + } + strbuf_release(&buf); } int git_default_advice_config(const char *var, const char *value) @@ -46,16 +56,15 @@ int git_default_advice_config(const char *var, const char *value) int error_resolve_conflict(const char *me) { error("'%s' is not possible because you have unmerged files.", me); - if (advice_resolve_conflict) { + if (advice_resolve_conflict) /* * Message used both when 'git commit' fails and when * other commands doing a merge do. */ - advise("Fix them up in the work tree,"); - advise("and then use 'git add/rm ' as"); - advise("appropriate to mark resolution and make a commit,"); - advise("or use 'git commit -a'."); - } + advise(_("Fix them up in the work tree,\n" + "and then use 'git add/rm ' as\n" + "appropriate to mark resolution and make a commit,\n" + "or use 'git commit -a'.")); return -1; } diff --git a/builtin/revert.c b/builtin/revert.c index 1ea525c10e..3ad14a1f98 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -332,11 +332,10 @@ static void print_advice(int show_hint) return; } - if (show_hint) { - advise("after resolving the conflicts, mark the corrected paths"); - advise("with 'git add ' or 'git rm '"); - advise("and commit the result with 'git commit'"); - } + if (show_hint) + advise(_("after resolving the conflicts, mark the corrected paths\n" + "with 'git add ' or 'git rm '\n" + "and commit the result with 'git commit'")); } static void write_message(struct strbuf *msgbuf, const char *filename) -- cgit v1.2.1 From ff7f2185d6e04b7bea66f39ee51d79919ab1279c Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Thu, 5 Jan 2012 21:26:48 +0100 Subject: gitweb: Fix file links in "grep" search There were two bugs in generating file links (links to "blob" view), one hidden by the other. The correct way of generating file link is href(action=>"blob", hash_base=>$co{'id'}, file_name=>$file); It was $co{'hash'} (this key does not exist, and therefore this is undef), and 'hash' instead of 'hash_base'. To have this fix applied in single place, this commit also reduces code duplication by saving file link (which is used for line links) in $file_href. Reported-by: Thomas Perl Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 4f0c3bd90c..1d2f046736 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5715,7 +5715,7 @@ sub git_search_files { my $lastfile = ''; while (my $line = <$fd>) { chomp $line; - my ($file, $lno, $ltext, $binary); + my ($file, $file_href, $lno, $ltext, $binary); last if ($matches++ > 1000); if ($line =~ /^Binary file (.+) matches$/) { $file = $1; @@ -5730,10 +5730,10 @@ sub git_search_files { } else { print "\n"; } + $file_href = href(action=>"blob", hash_base=>$co{'id'}, + file_name=>$file); print "". - $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, - file_name=>"$file"), - -class => "list"}, esc_path($file)); + $cgi->a({-href => $file_href, -class => "list"}, esc_path($file)); print "\n"; $lastfile = $file; } @@ -5751,10 +5751,9 @@ sub git_search_files { $ltext = esc_html($ltext, -nbsp=>1); } print "
" . - $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, - file_name=>"$file").'#l'.$lno, - -class => "linenr"}, sprintf('%4i', $lno)) - . ' ' . $ltext . "
\n"; + $cgi->a({-href => $file_href.'#l'.$lno, + -class => "linenr"}, sprintf('%4i', $lno)) . + ' ' . $ltext . "\n"; } } if ($lastfile) { -- cgit v1.2.1 From 8e09fd1a1e5ea8eaec960d47be51bde85df8870e Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Thu, 5 Jan 2012 21:32:56 +0100 Subject: gitweb: Harden "grep" search against filenames with ':' Run "git grep" in "grep" search with '-z' option, to be able to parse response also for files with filename containing ':' character. The ':' character is otherwise (without '-z') used to separate filename from line number and from matched line. Note that this does not protect files with filename containing embedded newline. This would be hard but doable for text files, and harder or even currently impossible with binary files: git does not quote filename in "Binary file matches" message, but new `--break` and/or `--header` options to git-grep could help here. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 1d2f046736..08020b0776 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5699,7 +5699,7 @@ sub git_search_files { my %co = @_; local $/ = "\n"; - open my $fd, "-|", git_cmd(), 'grep', '-n', + open my $fd, "-|", git_cmd(), 'grep', '-n', '-z', $search_use_regexp ? ('-E', '-i') : '-F', $searchtext, $co{'tree'} or die_error(500, "Open git-grep failed"); @@ -5721,7 +5721,8 @@ sub git_search_files { $file = $1; $binary = 1; } else { - (undef, $file, $lno, $ltext) = split(/:/, $line, 4); + ($file, $lno, $ltext) = split(/\0/, $line, 3); + $file =~ s/^$co{'tree'}://; } if ($file ne $lastfile) { $lastfile and print "\n"; -- cgit v1.2.1 From ccdc6037fee8761db82ccb8751d9c9442f4a9cc7 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 5 Jan 2012 16:00:01 -0500 Subject: parse_object: try internal cache before reading object db When parse_object is called, we do the following: 1. read the object data into a buffer via read_sha1_file 2. call parse_object_buffer, which then: a. calls the appropriate lookup_{commit,tree,blob,tag} to either create a new "struct object", or to find an existing one. We know the appropriate type from the lookup in step 1. b. calls the appropriate parse_{commit,tree,blob,tag} to parse the buffer for the new (or existing) object In step 2b, all of the called functions are no-ops for object "X" if "X->object.parsed" is set. I.e., when we have already parsed an object, we end up going to a lot of work just to find out at a low level that there is nothing left for us to do (and we throw away the data from read_sha1_file unread). We can optimize this by moving the check for "do we have an in-memory object" from 2a before the expensive call to read_sha1_file in step 1. This might seem circular, since step 2a uses the type information determined in step 1 to call the appropriate lookup function. However, we can notice that all of the lookup_* functions are backed by lookup_object. In other words, all of the objects are kept in a master hash table, and we don't actually need the type to do the "do we have it" part of the lookup, only to do the "and create it if it doesn't exist" part. This can save time whenever we call parse_object on the same sha1 twice in a single program. Some code paths already perform this optimization manually, with either: if (!obj->parsed) obj = parse_object(obj->sha1); if you already have a "struct object", or: struct object *obj = lookup_unknown_object(sha1); if (!obj || !obj->parsed) obj = parse_object(sha1); if you don't. This patch moves the optimization into parse_object itself. Most git operations won't notice any impact. Either they don't parse a lot of duplicate sha1s, or the calling code takes special care not to re-parse objects. I timed two code paths that do benefit (there may be more, but these two were immediately obvious and easy to time). The first is fast-export, which calls parse_object on each object it outputs, like this: object = parse_object(sha1); if (!object) die(...); if (object->flags & SHOWN) return; which means that just to realize we have already shown an object, we will read the whole object from disk! With this patch, my best-of-five time for "fast-export --all" on git.git dropped from 26.3s to 21.3s. The second case is upload-pack, which will call parse_object for each advertised ref (because it needs to peel tags to show "^{}" entries). This doesn't matter for most repositories, because they don't have a lot of refs pointing to the same objects. However, if you have a big alternates repository with a shared object db for a number of child repositories, then the alternates repository will have duplicated refs representing each of its children. For example, GitHub's alternates repository for git.git has ~120,000 refs, of which only ~3200 are unique. The time for upload-pack to print its list of advertised refs dropped from 3.4s to 0.76s. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- object.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/object.c b/object.c index d8d09f92aa..6b06297a5f 100644 --- a/object.c +++ b/object.c @@ -191,10 +191,15 @@ struct object *parse_object(const unsigned char *sha1) enum object_type type; int eaten; const unsigned char *repl = lookup_replace_object(sha1); - void *buffer = read_sha1_file(sha1, &type, &size); + void *buffer; + struct object *obj; + + obj = lookup_object(sha1); + if (obj && obj->parsed) + return obj; + buffer = read_sha1_file(sha1, &type, &size); if (buffer) { - struct object *obj; if (check_sha1_signature(repl, buffer, size, typename(type)) < 0) { free(buffer); error("sha1 mismatch %s\n", sha1_to_hex(repl)); -- cgit v1.2.1 From baf5aaa33383af656a34b7ba9039e9eb3c9e678c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Fri, 6 Jan 2012 18:13:00 +0100 Subject: xdiff: print post-image for common records instead of pre-image Normally it doesn't matter if we show the pre-image or th post-image for the common parts of a diff because they are the same. If white-space changes are ignored they can differ, though. The new text after applying the diff is more interesting in that case, so show that instead of the old contents. Note: GNU diff shows the pre-image. Suggested-by: Junio C Hamano Signed-off-by: Rene Scharfe Signed-off-by: Junio C Hamano --- t/t4015-diff-whitespace.sh | 14 +++++++------- xdiff/xemit.c | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 9059bcd69e..cc3db1304e 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -103,7 +103,7 @@ test_expect_success 'another test, with -w --ignore-space-at-eol' 'test_cmp expe git diff -w -b --ignore-space-at-eol > out test_expect_success 'another test, with -w -b --ignore-space-at-eol' 'test_cmp expect out' -tr 'Q' '\015' << EOF > expect +tr 'Q_' '\015 ' << EOF > expect diff --git a/x b/x index d99af23..8b32fb5 100644 --- a/x @@ -111,19 +111,19 @@ index d99af23..8b32fb5 100644 @@ -1,6 +1,6 @@ -whitespace at beginning + whitespace at beginning - whitespace change + whitespace change -whitespace in the middle +white space in the middle - whitespace at end + whitespace at end__ unchanged line - CR at endQ + CR at end EOF git diff -b > out test_expect_success 'another test, with -b' 'test_cmp expect out' git diff -b --ignore-space-at-eol > out test_expect_success 'another test, with -b --ignore-space-at-eol' 'test_cmp expect out' -tr 'Q' '\015' << EOF > expect +tr 'Q_' '\015 ' << EOF > expect diff --git a/x b/x index d99af23..8b32fb5 100644 --- a/x @@ -135,9 +135,9 @@ index d99af23..8b32fb5 100644 + whitespace at beginning +whitespace change +white space in the middle - whitespace at end + whitespace at end__ unchanged line - CR at endQ + CR at end EOF git diff --ignore-space-at-eol > out test_expect_success 'another test, with --ignore-space-at-eol' 'test_cmp expect out' diff --git a/xdiff/xemit.c b/xdiff/xemit.c index 277e2eec5b..1f188dc1f1 100644 --- a/xdiff/xemit.c +++ b/xdiff/xemit.c @@ -87,7 +87,7 @@ static long def_ff(const char *rec, long len, char *buf, long sz, void *priv) static int xdl_emit_common(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, xdemitconf_t const *xecfg) { - xdfile_t *xdf = &xe->xdf1; + xdfile_t *xdf = &xe->xdf2; const char *rchg = xdf->rchg; long ix; @@ -151,8 +151,8 @@ int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, /* * Emit pre-context. */ - for (; s1 < xch->i1; s1++) - if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0) + for (; s2 < xch->i2; s2++) + if (xdl_emit_record(&xe->xdf2, s2, " ", ecb) < 0) return -1; for (s1 = xch->i1, s2 = xch->i2;; xch = xch->next) { @@ -160,7 +160,7 @@ int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, * Merge previous with current change atom. */ for (; s1 < xch->i1 && s2 < xch->i2; s1++, s2++) - if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0) + if (xdl_emit_record(&xe->xdf2, s2, " ", ecb) < 0) return -1; /* @@ -186,8 +186,8 @@ int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, /* * Emit post-context. */ - for (s1 = xche->i1 + xche->chg1; s1 < e1; s1++) - if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0) + for (s2 = xche->i2 + xche->chg2; s2 < e2; s2++) + if (xdl_emit_record(&xe->xdf2, s2, " ", ecb) < 0) return -1; } -- cgit v1.2.1 From 926f1dd95486b596de1f6b8e108053a870da0e18 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Fri, 6 Jan 2012 14:17:40 -0500 Subject: upload-pack: avoid parsing objects during ref advertisement When we advertise a ref, the first thing we do is parse the pointed-to object. This gives us two things: 1. a "struct object" we can use to store flags 2. the type of the object, so we know whether we need to dereference it as a tag Instead, we can just use lookup_unknown_object to get an object struct, and then fill in just the type field using sha1_object_info (which, in the case of packed files, can find the information without actually inflating the object data). This can save time if you have a large number of refs, and the client isn't actually going to request those refs (e.g., because most of them are already up-to-date). The downside is that we are no longer verifying objects that we advertise by fully parsing them (however, we do still know we actually have them, because sha1_object_info must find them to get the type). While we might fail to detect a corrupt object here, if the client actually fetches the object, we will parse (and verify) it then. On a repository with 120K refs, the advertisement portion of upload-pack goes from ~3.4s to 3.2s (the failure to speed up more is largely due to the fact that most of these refs are tags, which need dereferenced to find the tag destination anyway). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- upload-pack.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/upload-pack.c b/upload-pack.c index 6f36f6255c..65cb0ff0fb 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -720,11 +720,14 @@ static int send_ref(const char *refname, const unsigned char *sha1, int flag, vo static const char *capabilities = "multi_ack thin-pack side-band" " side-band-64k ofs-delta shallow no-progress" " include-tag multi_ack_detailed"; - struct object *o = parse_object(sha1); + struct object *o = lookup_unknown_object(sha1); const char *refname_nons = strip_namespace(refname); - if (!o) - die("git upload-pack: cannot find object %s:", sha1_to_hex(sha1)); + if (o->type == OBJ_NONE) { + o->type = sha1_object_info(sha1, NULL); + if (o->type < 0) + die("git upload-pack: cannot find object %s:", sha1_to_hex(sha1)); + } if (capabilities) packet_write(1, "%s %s%c%s%s\n", sha1_to_hex(sha1), refname_nons, @@ -738,6 +741,7 @@ static int send_ref(const char *refname, const unsigned char *sha1, int flag, vo nr_our_refs++; } if (o->type == OBJ_TAG) { + o = parse_object(o->sha1); o = deref_tag(o, refname, 0); if (o) packet_write(1, "%s %s^{}\n", sha1_to_hex(o->sha1), refname_nons); -- cgit v1.2.1 From 90108a2441fb57f69c3f25d072d1952b306b77ab Mon Sep 17 00:00:00 2001 From: Jeff King Date: Fri, 6 Jan 2012 14:18:01 -0500 Subject: upload-pack: avoid parsing tag destinations When upload-pack advertises refs, it dereferences any tags it sees, and shows the resulting sha1 to the client. It does this by calling deref_tag. That function must load and parse each tag object to find the sha1 of the tagged object. However, it also ends up parsing the tagged object itself, which is not strictly necessary for upload-pack's use. Each tag produces two object loads (assuming it is not a recursive tag), when it could get away with only a single one. Dropping the second load halves the effort we spend. The downside is that we are no longer verifying the resulting object by loading it. In particular: 1. We never cross-check the "type" field given in the tag object with the type of the pointed-to object. If the tag says it points to a tag but doesn't, then we will keep peeling and realize the error. If the tag says it points to a non-tag but actually points to a tag, we will stop peeling and just advertise the pointed-to tag. 2. If we are missing the pointed-to object, we will not realize (because we never even look it up in the object db). However, both of these are errors in the object database, and both will be detected if a client actually requests the broken objects in question. So we are simply pushing the verification away from the advertising stage, and down to the actual fetching stage. On my test repo with 120K refs, this drops the time to advertise the refs from ~3.2s to ~2.0s. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- tag.c | 12 ++++++++++++ tag.h | 1 + upload-pack.c | 3 +-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tag.c b/tag.c index 3aa186df62..78d272b863 100644 --- a/tag.c +++ b/tag.c @@ -24,6 +24,18 @@ struct object *deref_tag(struct object *o, const char *warn, int warnlen) return o; } +struct object *deref_tag_noverify(struct object *o) +{ + while (o && o->type == OBJ_TAG) { + o = parse_object(o->sha1); + if (o && o->type == OBJ_TAG && ((struct tag *)o)->tagged) + o = ((struct tag *)o)->tagged; + else + o = NULL; + } + return o; +} + struct tag *lookup_tag(const unsigned char *sha1) { struct object *obj = lookup_object(sha1); diff --git a/tag.h b/tag.h index 5ee88e6550..bc8a1e40f0 100644 --- a/tag.h +++ b/tag.h @@ -16,6 +16,7 @@ extern struct tag *lookup_tag(const unsigned char *sha1); extern int parse_tag_buffer(struct tag *item, const void *data, unsigned long size); extern int parse_tag(struct tag *item); extern struct object *deref_tag(struct object *, const char *, int); +extern struct object *deref_tag_noverify(struct object *); extern size_t parse_signature(const char *buf, unsigned long size); #endif /* TAG_H */ diff --git a/upload-pack.c b/upload-pack.c index 65cb0ff0fb..c01e161a9d 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -741,8 +741,7 @@ static int send_ref(const char *refname, const unsigned char *sha1, int flag, vo nr_our_refs++; } if (o->type == OBJ_TAG) { - o = parse_object(o->sha1); - o = deref_tag(o, refname, 0); + o = deref_tag_noverify(o); if (o) packet_write(1, "%s %s^{}\n", sha1_to_hex(o->sha1), refname_nons); } -- cgit v1.2.1 From 3e6e0edde20f536b28cf1dac04c7376000bfc701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Sat, 7 Jan 2012 21:45:59 +0700 Subject: clone: add --single-branch to fetch only one branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When --single-branch is given, only one branch, either HEAD or one specified by --branch, will be fetched. Also only tags that point to the downloaded history are fetched. This helps most in shallow clones, where it can reduce the download to minimum and that is why it is enabled by default when --depth is given. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- Documentation/git-clone.txt | 11 ++++++- builtin/clone.c | 52 +++++++++++++++++++++++++++++--- t/t5500-fetch-pack.sh | 72 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 4b8b26b75e..0931a3e392 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -13,7 +13,8 @@ SYNOPSIS [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror] [-o ] [-b ] [-u ] [--reference ] [--separate-git-dir ] - [--depth ] [--recursive|--recurse-submodules] [--] + [--depth ] [--[no-]single-branch] + [--recursive|--recurse-submodules] [--] [] DESCRIPTION @@ -179,6 +180,14 @@ objects from the source repository into a pack in the cloned repository. with a long history, and would want to send in fixes as patches. +--single-branch:: + Clone only the history leading to the tip of a single branch, + either specified by the `--branch` option or the primary + branch remote's `HEAD` points at. When creating a shallow + clone with the `--depth` option, this is the default, unless + `--no-single-branch` is given to fetch the histories near the + tips of all branches. + --recursive:: --recurse-submodules:: After the clone is created, initialize all submodules within, diff --git a/builtin/clone.c b/builtin/clone.c index 86db954730..9dcc5fe775 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -37,7 +37,7 @@ static const char * const builtin_clone_usage[] = { NULL }; -static int option_no_checkout, option_bare, option_mirror; +static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1; static int option_local, option_no_hardlinks, option_shared, option_recursive; static char *option_template, *option_depth; static char *option_origin = NULL; @@ -48,6 +48,7 @@ static int option_verbosity; static int option_progress; static struct string_list option_config; static struct string_list option_reference; +static const char *src_ref_prefix = "refs/heads/"; static int opt_parse_reference(const struct option *opt, const char *arg, int unset) { @@ -92,6 +93,8 @@ static struct option builtin_clone_options[] = { "path to git-upload-pack on the remote"), OPT_STRING(0, "depth", &option_depth, "depth", "create a shallow clone of that depth"), + OPT_BOOL(0, "single-branch", &option_single_branch, + "clone only one branch, HEAD or --branch"), OPT_STRING(0, "separate-git-dir", &real_git_dir, "gitdir", "separate git dir from working tree"), OPT_STRING_LIST('c', "config", &option_config, "key=value", @@ -427,8 +430,28 @@ static struct ref *wanted_peer_refs(const struct ref *refs, struct ref *local_refs = head; struct ref **tail = head ? &head->next : &local_refs; - get_fetch_map(refs, refspec, &tail, 0); - if (!option_mirror) + if (option_single_branch) { + struct ref *remote_head = NULL; + + if (!option_branch) + remote_head = guess_remote_head(head, refs, 0); + else { + struct strbuf sb = STRBUF_INIT; + strbuf_addstr(&sb, src_ref_prefix); + strbuf_addstr(&sb, option_branch); + remote_head = find_ref_by_name(refs, sb.buf); + strbuf_release(&sb); + } + + if (!remote_head && option_branch) + warning(_("Could not find remote branch %s to clone."), + option_branch); + else + get_fetch_map(remote_head, refspec, &tail, 0); + } else + get_fetch_map(refs, refspec, &tail, 0); + + if (!option_mirror && !option_single_branch) get_fetch_map(refs, tag_refspec, &tail, 0); return local_refs; @@ -448,6 +471,21 @@ static void write_remote_refs(const struct ref *local_refs) clear_extra_refs(); } +static void write_followtags(const struct ref *refs, const char *msg) +{ + const struct ref *ref; + for (ref = refs; ref; ref = ref->next) { + if (prefixcmp(ref->name, "refs/tags/")) + continue; + if (!suffixcmp(ref->name, "^{}")) + continue; + if (!has_sha1_file(ref->old_sha1)) + continue; + update_ref(msg, ref->name, ref->old_sha1, + NULL, 0, DIE_ON_ERR); + } +} + static int write_one_config(const char *key, const char *value, void *data) { return git_config_set_multivar(key, value ? value : "true", "^$", 0); @@ -478,7 +516,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix) struct strbuf key = STRBUF_INIT, value = STRBUF_INIT; struct strbuf branch_top = STRBUF_INIT, reflog_msg = STRBUF_INIT; struct transport *transport = NULL; - char *src_ref_prefix = "refs/heads/"; int err = 0; struct refspec *refspec; @@ -498,6 +535,9 @@ int cmd_clone(int argc, const char **argv, const char *prefix) usage_msg_opt(_("You must specify a repository to clone."), builtin_clone_usage, builtin_clone_options); + if (option_single_branch == -1) + option_single_branch = option_depth ? 1 : 0; + if (option_mirror) option_bare = 1; @@ -645,6 +685,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix) if (option_depth) transport_set_option(transport, TRANS_OPT_DEPTH, option_depth); + if (option_single_branch) + transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1"); transport_set_verbosity(transport, option_verbosity, option_progress); @@ -663,6 +705,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix) clear_extra_refs(); write_remote_refs(mapped_refs); + if (option_single_branch) + write_followtags(refs, reflog_msg.buf); remote_head = find_ref_by_name(refs, "HEAD"); remote_head_points_at = diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 9bf69e9a0f..7e85c71ad1 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -114,8 +114,19 @@ pull_to_client 2nd "refs/heads/B" $((64*3)) pull_to_client 3rd "refs/heads/A" $((1*3)) +test_expect_success 'single branch clone' ' + git clone --single-branch "file://$(pwd)/." singlebranch +' + +test_expect_success 'single branch object count' ' + GIT_DIR=singlebranch/.git git count-objects -v | + grep "^in-pack:" > count.singlebranch && + echo "in-pack: 198" >expected && + test_cmp expected count.singlebranch +' + test_expect_success 'clone shallow' ' - git clone --depth 2 "file://$(pwd)/." shallow + git clone --no-single-branch --depth 2 "file://$(pwd)/." shallow ' test_expect_success 'clone shallow object count' ' @@ -248,4 +259,63 @@ test_expect_success 'clone shallow object count' ' grep "^count: 52" count.shallow ' +test_expect_success 'clone shallow without --no-single-branch' ' + git clone --depth 1 "file://$(pwd)/." shallow2 +' + +test_expect_success 'clone shallow object count' ' + ( + cd shallow2 && + git count-objects -v + ) > count.shallow2 && + grep "^in-pack: 6" count.shallow2 +' + +test_expect_success 'clone shallow with --branch' ' + git clone --depth 1 --branch A "file://$(pwd)/." shallow3 +' + +test_expect_success 'clone shallow object count' ' + echo "in-pack: 12" > count3.expected && + GIT_DIR=shallow3/.git git count-objects -v | + grep "^in-pack" > count3.actual && + test_cmp count3.expected count3.actual +' + +test_expect_success 'clone shallow with nonexistent --branch' ' + git clone --depth 1 --branch Z "file://$(pwd)/." shallow4 && + GIT_DIR=shallow4/.git git rev-parse HEAD >actual && + git rev-parse HEAD >expected && + test_cmp expected actual +' + +test_expect_success 'clone shallow with detached HEAD' ' + git checkout HEAD^ && + git clone --depth 1 "file://$(pwd)/." shallow5 && + git checkout - && + GIT_DIR=shallow5/.git git rev-parse HEAD >actual && + git rev-parse HEAD^ >expected && + test_cmp expected actual +' + +test_expect_success 'shallow clone pulling tags' ' + git tag -a -m A TAGA1 A && + git tag -a -m B TAGB1 B && + git tag TAGA2 A && + git tag TAGB2 B && + git clone --depth 1 "file://$(pwd)/." shallow6 && + + cat >taglist.expected <<\EOF && +TAGB1 +TAGB2 +EOF + GIT_DIR=shallow6/.git git tag -l >taglist.actual && + test_cmp taglist.expected taglist.actual && + + echo "in-pack: 7" > count6.expected && + GIT_DIR=shallow6/.git git count-objects -v | + grep "^in-pack" > count6.actual && + test_cmp count6.expected count6.actual +' + test_done -- cgit v1.2.1 From f47182c852601c045f62bd7f669360a920516311 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 8 Jan 2012 22:06:19 +0100 Subject: server_supports(): parse feature list more carefully We have been carefully choosing feature names used in the protocol extensions so that the vocabulary does not contain a word that is a substring of another word, so it is not a real problem, but we have recently added "quiet" feature word, which would mean we cannot later add some other word with "quiet" (e.g. "quiet-push"), which is awkward. Let's make sure that we can eventually be able to do so by teaching the clients and servers that feature words consist of non whitespace letters. This parser also allows us to later add features with parameters e.g. "feature=1.5" (parameter values need to be quoted for whitespaces, but we will worry about the detauls when we do introduce them). Signed-off-by: Junio C Hamano Signed-off-by: Clemens Buchacher Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 5 +++-- cache.h | 1 + connect.c | 23 +++++++++++++++++++++-- upload-pack.c | 22 +++++++++++++--------- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index d2dcb7e4af..d8ddcaa0c9 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -731,9 +731,10 @@ static struct command *read_head_info(void) refname = line + 82; reflen = strlen(refname); if (reflen + 82 < len) { - if (strstr(refname + reflen + 1, "report-status")) + const char *feature_list = refname + reflen + 1; + if (parse_feature_request(feature_list, "report-status")) report_status = 1; - if (strstr(refname + reflen + 1, "side-band-64k")) + if (parse_feature_request(feature_list, "side-band-64k")) use_sideband = LARGE_PACKET_MAX; } cmd = xcalloc(1, sizeof(struct command) + len - 80); diff --git a/cache.h b/cache.h index 10afd71d43..9bd8c2d06c 100644 --- a/cache.h +++ b/cache.h @@ -1037,6 +1037,7 @@ struct extra_have_objects { }; extern struct ref **get_remote_heads(int in, struct ref **list, unsigned int flags, struct extra_have_objects *); extern int server_supports(const char *feature); +extern const char *parse_feature_request(const char *features, const char *feature); extern struct packed_git *parse_pack_index(unsigned char *sha1, const char *idx_path); diff --git a/connect.c b/connect.c index 2ea5c3c0fb..912cddeea8 100644 --- a/connect.c +++ b/connect.c @@ -101,8 +101,27 @@ struct ref **get_remote_heads(int in, struct ref **list, int server_supports(const char *feature) { - return server_capabilities && - strstr(server_capabilities, feature) != NULL; + return !!parse_feature_request(server_capabilities, feature); +} + +const char *parse_feature_request(const char *feature_list, const char *feature) +{ + int len; + + if (!feature_list) + return NULL; + + len = strlen(feature); + while (*feature_list) { + const char *found = strstr(feature_list, feature); + if (!found) + return NULL; + if ((feature_list == found || isspace(found[-1])) && + (!found[len] || isspace(found[len]) || found[len] == '=')) + return found; + feature_list = found + 1; + } + return NULL; } enum protocol { diff --git a/upload-pack.c b/upload-pack.c index 6f36f6255c..0d11e21a68 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -585,6 +585,7 @@ static void receive_needs(void) write_str_in_full(debug_fd, "#S\n"); for (;;) { struct object *o; + const char *features; unsigned char sha1_buf[20]; len = packet_read_line(0, line, sizeof(line)); reset_timeout(); @@ -616,23 +617,26 @@ static void receive_needs(void) get_sha1_hex(line+5, sha1_buf)) die("git upload-pack: protocol error, " "expected to get sha, not '%s'", line); - if (strstr(line+45, "multi_ack_detailed")) + + features = line + 45; + + if (parse_feature_request(features, "multi_ack_detailed")) multi_ack = 2; - else if (strstr(line+45, "multi_ack")) + else if (parse_feature_request(features, "multi_ack")) multi_ack = 1; - if (strstr(line+45, "no-done")) + if (parse_feature_request(features, "no-done")) no_done = 1; - if (strstr(line+45, "thin-pack")) + if (parse_feature_request(features, "thin-pack")) use_thin_pack = 1; - if (strstr(line+45, "ofs-delta")) + if (parse_feature_request(features, "ofs-delta")) use_ofs_delta = 1; - if (strstr(line+45, "side-band-64k")) + if (parse_feature_request(features, "side-band-64k")) use_sideband = LARGE_PACKET_MAX; - else if (strstr(line+45, "side-band")) + else if (parse_feature_request(features, "side-band")) use_sideband = DEFAULT_PACKET_MAX; - if (strstr(line+45, "no-progress")) + if (parse_feature_request(features, "no-progress")) no_progress = 1; - if (strstr(line+45, "include-tag")) + if (parse_feature_request(features, "include-tag")) use_include_tag = 1; o = lookup_object(sha1_buf); -- cgit v1.2.1 From c207e34f7733df04342e1c0f449f6d3cae501e33 Mon Sep 17 00:00:00 2001 From: Clemens Buchacher Date: Sun, 8 Jan 2012 22:06:20 +0100 Subject: fix push --quiet: add 'quiet' capability to receive-pack Currently, git push --quiet produces some non-error output, e.g.: $ git push --quiet Unpacking objects: 100% (3/3), done. This fixes a bug reported for the fedora git package: https://bugzilla.redhat.com/show_bug.cgi?id=725593 Reported-by: Jesse Keating Cc: Todd Zullinger Commit 90a6c7d4 (propagate --quiet to send-pack/receive-pack) introduced the --quiet option to receive-pack and made send-pack pass that option. Older versions of receive-pack do not recognize the option, however, and terminate immediately. The commit was therefore reverted. This change instead adds a 'quiet' capability to receive-pack, which is a backwards compatible. In addition, this fixes push --quiet via http: A verbosity of 0 means quiet for remote helpers. Reported-by: Tobias Ulmer Signed-off-by: Clemens Buchacher Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 14 ++++++++++++-- builtin/send-pack.c | 13 ++++++++++--- remote-curl.c | 4 +++- t/t5523-push-upstream.sh | 7 +++++++ t/t5541-http-push.sh | 8 ++++++++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index d8ddcaa0c9..31d17cf198 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -33,6 +33,7 @@ static int transfer_unpack_limit = -1; static int unpack_limit = 100; static int report_status; static int use_sideband; +static int quiet; static int prefer_ofs_delta = 1; static int auto_update_server_info; static int auto_gc = 1; @@ -122,7 +123,7 @@ static int show_ref(const char *path, const unsigned char *sha1, int flag, void else packet_write(1, "%s %s%c%s%s\n", sha1_to_hex(sha1), path, 0, - " report-status delete-refs side-band-64k", + " report-status delete-refs side-band-64k quiet", prefer_ofs_delta ? " ofs-delta" : ""); sent_capabilities = 1; return 0; @@ -736,6 +737,8 @@ static struct command *read_head_info(void) report_status = 1; if (parse_feature_request(feature_list, "side-band-64k")) use_sideband = LARGE_PACKET_MAX; + if (parse_feature_request(feature_list, "quiet")) + quiet = 1; } cmd = xcalloc(1, sizeof(struct command) + len - 80); hashcpy(cmd->old_sha1, old_sha1); @@ -789,8 +792,10 @@ static const char *unpack(void) if (ntohl(hdr.hdr_entries) < unpack_limit) { int code, i = 0; - const char *unpacker[4]; + const char *unpacker[5]; unpacker[i++] = "unpack-objects"; + if (quiet) + unpacker[i++] = "-q"; if (fsck_objects) unpacker[i++] = "--strict"; unpacker[i++] = hdr_arg; @@ -904,6 +909,11 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) const char *arg = *argv++; if (*arg == '-') { + if (!strcmp(arg, "--quiet")) { + quiet = 1; + continue; + } + if (!strcmp(arg, "--advertise-refs")) { advertise_refs = 1; continue; diff --git a/builtin/send-pack.c b/builtin/send-pack.c index cd1115ffc6..71f258ef6e 100644 --- a/builtin/send-pack.c +++ b/builtin/send-pack.c @@ -263,6 +263,8 @@ int send_pack(struct send_pack_args *args, args->use_ofs_delta = 1; if (server_supports("side-band-64k")) use_sideband = 1; + if (!server_supports("quiet")) + args->quiet = 0; if (!remote_refs) { fprintf(stderr, "No refs in common and none specified; doing nothing.\n" @@ -301,11 +303,12 @@ int send_pack(struct send_pack_args *args, char *old_hex = sha1_to_hex(ref->old_sha1); char *new_hex = sha1_to_hex(ref->new_sha1); - if (!cmds_sent && (status_report || use_sideband)) { - packet_buf_write(&req_buf, "%s %s %s%c%s%s", + if (!cmds_sent && (status_report || use_sideband || args->quiet)) { + packet_buf_write(&req_buf, "%s %s %s%c%s%s%s", old_hex, new_hex, ref->name, 0, status_report ? " report-status" : "", - use_sideband ? " side-band-64k" : ""); + use_sideband ? " side-band-64k" : "", + args->quiet ? " quiet" : ""); } else packet_buf_write(&req_buf, "%s %s %s", @@ -439,6 +442,10 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix) args.force_update = 1; continue; } + if (!strcmp(arg, "--quiet")) { + args.quiet = 1; + continue; + } if (!strcmp(arg, "--verbose")) { args.verbose = 1; continue; diff --git a/remote-curl.c b/remote-curl.c index 48c20b86f3..bcbc7fba4e 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -770,7 +770,9 @@ static int push_git(struct discovery *heads, int nr_spec, char **specs) argv[argc++] = "--thin"; if (options.dry_run) argv[argc++] = "--dry-run"; - if (options.verbosity > 1) + if (options.verbosity == 0) + argv[argc++] = "--quiet"; + else if (options.verbosity > 1) argv[argc++] = "--verbose"; argv[argc++] = url; for (i = 0; i < nr_spec; i++) diff --git a/t/t5523-push-upstream.sh b/t/t5523-push-upstream.sh index c229fe68f1..9ee52cfc45 100755 --- a/t/t5523-push-upstream.sh +++ b/t/t5523-push-upstream.sh @@ -108,4 +108,11 @@ test_expect_failure TTY 'push --no-progress suppresses progress' ' ! grep "Writing objects" err ' +test_expect_success TTY 'quiet push' ' + ensure_fresh_upstream && + + test_terminal git push --quiet --no-progress upstream master 2>&1 | tee output && + test_cmp /dev/null output +' + test_done diff --git a/t/t5541-http-push.sh b/t/t5541-http-push.sh index 9b85d420c3..0c3cd3b4dd 100755 --- a/t/t5541-http-push.sh +++ b/t/t5541-http-push.sh @@ -5,6 +5,7 @@ test_description='test smart pushing over http via http-backend' . ./test-lib.sh +. "$TEST_DIRECTORY"/lib-terminal.sh if test -n "$NO_CURL"; then skip_all='skipping test, git built without http support' @@ -186,5 +187,12 @@ test_expect_success 'push --mirror to repo with alternates' ' git push --mirror "$HTTPD_URL"/smart/alternates-mirror.git ' +test_expect_success TTY 'quiet push' ' + cd "$ROOT_PATH"/test_repo_clone && + test_commit quiet && + test_terminal git push --quiet --no-progress 2>&1 | tee output && + test_cmp /dev/null output +' + stop_httpd test_done -- cgit v1.2.1 From d336572f57e398318a0f6c51cc760d5be9872cc2 Mon Sep 17 00:00:00 2001 From: Michael J Gruber Date: Sun, 8 Jan 2012 22:06:21 +0100 Subject: t5541: avoid TAP test miscounting lib-terminal.sh runs a test and thus increases the test count, but the output is lost so that TAP produces a "no plan found error". Move the lib-terminal call after the lib-httpd and make TAP happy (though still leave me clueless). Signed-off-by: Michael J Gruber Signed-off-by: Clemens Buchacher Signed-off-by: Junio C Hamano --- t/t5541-http-push.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t5541-http-push.sh b/t/t5541-http-push.sh index 0c3cd3b4dd..6c9ec6f117 100755 --- a/t/t5541-http-push.sh +++ b/t/t5541-http-push.sh @@ -5,7 +5,6 @@ test_description='test smart pushing over http via http-backend' . ./test-lib.sh -. "$TEST_DIRECTORY"/lib-terminal.sh if test -n "$NO_CURL"; then skip_all='skipping test, git built without http support' @@ -15,6 +14,7 @@ fi ROOT_PATH="$PWD" LIB_HTTPD_PORT=${LIB_HTTPD_PORT-'5541'} . "$TEST_DIRECTORY"/lib-httpd.sh +. "$TEST_DIRECTORY"/lib-terminal.sh start_httpd test_expect_success 'setup remote repository' ' -- cgit v1.2.1 From afe19ff7b55129d988e421ae1e0df4ec9659787a Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sat, 7 Jan 2012 12:42:43 +0100 Subject: run-command: optionally kill children on exit When we spawn a helper process, it should generally be done and finish_command called before we exit. However, if we exit abnormally due to an early return or a signal, the helper may continue to run in our absence. In the best case, this may simply be wasted CPU cycles or a few stray messages on a terminal. But it could also mean a process that the user thought was aborted continues to run to completion (e.g., a push's pack-objects helper will complete the push, even though you killed the push process). This patch provides infrastructure for run-command to keep track of PIDs to be killed, and clean them on signal reception or input, just as we do with tempfiles. PIDs can be added in two ways: 1. If NO_PTHREADS is defined, async helper processes are automatically marked. By definition this code must be ready to die when the parent dies, since it may be implemented as a thread of the parent process. 2. If the run-command caller specifies the "clean_on_exit" option. This is not the default, as there are cases where it is OK for the child to outlive us (e.g., when spawning a pager). PIDs are cleared from the kill-list automatically during wait_or_whine, which is called from finish_command and finish_async. Signed-off-by: Jeff King Signed-off-by: Clemens Buchacher Signed-off-by: Junio C Hamano --- run-command.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ run-command.h | 1 + 2 files changed, 69 insertions(+) diff --git a/run-command.c b/run-command.c index 1c51043884..0204aaf7e8 100644 --- a/run-command.c +++ b/run-command.c @@ -1,8 +1,66 @@ #include "cache.h" #include "run-command.h" #include "exec_cmd.h" +#include "sigchain.h" #include "argv-array.h" +struct child_to_clean { + pid_t pid; + struct child_to_clean *next; +}; +static struct child_to_clean *children_to_clean; +static int installed_child_cleanup_handler; + +static void cleanup_children(int sig) +{ + while (children_to_clean) { + struct child_to_clean *p = children_to_clean; + children_to_clean = p->next; + kill(p->pid, sig); + free(p); + } +} + +static void cleanup_children_on_signal(int sig) +{ + cleanup_children(sig); + sigchain_pop(sig); + raise(sig); +} + +static void cleanup_children_on_exit(void) +{ + cleanup_children(SIGTERM); +} + +static void mark_child_for_cleanup(pid_t pid) +{ + struct child_to_clean *p = xmalloc(sizeof(*p)); + p->pid = pid; + p->next = children_to_clean; + children_to_clean = p; + + if (!installed_child_cleanup_handler) { + atexit(cleanup_children_on_exit); + sigchain_push_common(cleanup_children_on_signal); + installed_child_cleanup_handler = 1; + } +} + +static void clear_child_for_cleanup(pid_t pid) +{ + struct child_to_clean **last, *p; + + last = &children_to_clean; + for (p = children_to_clean; p; p = p->next) { + if (p->pid == pid) { + *last = p->next; + free(p); + return; + } + } +} + static inline void close_pair(int fd[2]) { close(fd[0]); @@ -130,6 +188,9 @@ static int wait_or_whine(pid_t pid, const char *argv0, int silent_exec_failure) } else { error("waitpid is confused (%s)", argv0); } + + clear_child_for_cleanup(pid); + errno = failed_errno; return code; } @@ -292,6 +353,8 @@ fail_pipe: if (cmd->pid < 0) error("cannot fork() for %s: %s", cmd->argv[0], strerror(failed_errno = errno)); + else if (cmd->clean_on_exit) + mark_child_for_cleanup(cmd->pid); /* * Wait for child's execvp. If the execvp succeeds (or if fork() @@ -312,6 +375,7 @@ fail_pipe: cmd->pid = -1; } close(notify_pipe[0]); + } #else { @@ -356,6 +420,8 @@ fail_pipe: failed_errno = errno; if (cmd->pid < 0 && (!cmd->silent_exec_failure || errno != ENOENT)) error("cannot spawn %s: %s", cmd->argv[0], strerror(errno)); + if (cmd->clean_on_exit && cmd->pid >= 0) + mark_child_for_cleanup(cmd->pid); if (cmd->env) free_environ(env); @@ -540,6 +606,8 @@ int start_async(struct async *async) exit(!!async->proc(proc_in, proc_out, async->data)); } + mark_child_for_cleanup(async->pid); + if (need_in) close(fdin[0]); else if (async->in) diff --git a/run-command.h b/run-command.h index 56491b9f23..2a6946668b 100644 --- a/run-command.h +++ b/run-command.h @@ -38,6 +38,7 @@ struct child_process { unsigned silent_exec_failure:1; unsigned stdout_to_stderr:1; unsigned use_shell:1; + unsigned clean_on_exit:1; void (*preexec_cb)(void); }; -- cgit v1.2.1 From 10c6cddd928b24ac6030a172c6c7b46efb32aedc Mon Sep 17 00:00:00 2001 From: Clemens Buchacher Date: Sun, 8 Jan 2012 21:41:09 +0100 Subject: dashed externals: kill children on exit Several git commands are so-called dashed externals, that is commands executed as a child process of the git wrapper command. If the git wrapper is killed by a signal, the child process will continue to run. This is different from internal commands, which always die with the git wrapper command. Enable the recently introduced cleanup mechanism for child processes in order to make dashed externals act more in line with internal commands. Signed-off-by: Clemens Buchacher Acked-by: Jeff King Signed-off-by: Junio C Hamano --- git.c | 2 +- run-command.c | 1 + run-command.h | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/git.c b/git.c index fb9029cbf1..3805616630 100644 --- a/git.c +++ b/git.c @@ -495,7 +495,7 @@ static void execv_dashed_external(const char **argv) * if we fail because the command is not found, it is * OK to return. Otherwise, we just pass along the status code. */ - status = run_command_v_opt(argv, RUN_SILENT_EXEC_FAILURE); + status = run_command_v_opt(argv, RUN_SILENT_EXEC_FAILURE | RUN_CLEAN_ON_EXIT); if (status >= 0 || errno != ENOENT) exit(status); diff --git a/run-command.c b/run-command.c index 0204aaf7e8..1db8abf984 100644 --- a/run-command.c +++ b/run-command.c @@ -497,6 +497,7 @@ static void prepare_run_command_v_opt(struct child_process *cmd, cmd->stdout_to_stderr = opt & RUN_COMMAND_STDOUT_TO_STDERR ? 1 : 0; cmd->silent_exec_failure = opt & RUN_SILENT_EXEC_FAILURE ? 1 : 0; cmd->use_shell = opt & RUN_USING_SHELL ? 1 : 0; + cmd->clean_on_exit = opt & RUN_CLEAN_ON_EXIT ? 1 : 0; } int run_command_v_opt(const char **argv, int opt) diff --git a/run-command.h b/run-command.h index 2a6946668b..44f7d2bd42 100644 --- a/run-command.h +++ b/run-command.h @@ -53,6 +53,7 @@ extern int run_hook(const char *index_file, const char *name, ...); #define RUN_COMMAND_STDOUT_TO_STDERR 4 #define RUN_SILENT_EXEC_FAILURE 8 #define RUN_USING_SHELL 16 +#define RUN_CLEAN_ON_EXIT 32 int run_command_v_opt(const char **argv, int opt); /* -- cgit v1.2.1 From 71039fb9d562731ed700ef072b6fcb18e2478361 Mon Sep 17 00:00:00 2001 From: Clemens Buchacher Date: Sat, 7 Jan 2012 12:42:45 +0100 Subject: git-daemon: add tests The semantics of the git daemon tests are similar to the http transport tests. In fact, they are only a slightly modified copy of t5550, plus the newly added remote error tests. All git-daemon tests will be skipped unless the environment variable GIT_TEST_GIT_DAEMON is set. Signed-off-by: Clemens Buchacher Helped-by: Jeff King Signed-off-by: Junio C Hamano --- t/lib-git-daemon.sh | 53 ++++++++++++++++++ t/t5570-git-daemon.sh | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 t/lib-git-daemon.sh create mode 100755 t/t5570-git-daemon.sh diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh new file mode 100644 index 0000000000..5e81a25942 --- /dev/null +++ b/t/lib-git-daemon.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +if test -z "$GIT_TEST_GIT_DAEMON" +then + skip_all="git-daemon testing disabled (define GIT_TEST_GIT_DAEMON to enable)" + test_done +fi + +LIB_GIT_DAEMON_PORT=${LIB_GIT_DAEMON_PORT-'8121'} + +GIT_DAEMON_PID= +GIT_DAEMON_DOCUMENT_ROOT_PATH="$PWD"/repo +GIT_DAEMON_URL=git://127.0.0.1:$LIB_GIT_DAEMON_PORT + +start_git_daemon() { + if test -n "$GIT_DAEMON_PID" + then + error "start_git_daemon already called" + fi + + mkdir -p "$GIT_DAEMON_DOCUMENT_ROOT_PATH" + + trap 'code=$?; stop_git_daemon; (exit $code); die' EXIT + + say >&3 "Starting git daemon ..." + git daemon --listen=127.0.0.1 --port="$LIB_GIT_DAEMON_PORT" \ + --reuseaddr --verbose \ + --base-path="$GIT_DAEMON_DOCUMENT_ROOT_PATH" \ + "$@" "$GIT_DAEMON_DOCUMENT_ROOT_PATH" \ + >&3 2>&4 & + GIT_DAEMON_PID=$! +} + +stop_git_daemon() { + if test -z "$GIT_DAEMON_PID" + then + return + fi + + trap 'die' EXIT + + # kill git-daemon child of git + say >&3 "Stopping git daemon ..." + kill "$GIT_DAEMON_PID" + wait "$GIT_DAEMON_PID" >&3 2>&4 + ret=$? + # expect exit with status 143 = 128+15 for signal TERM=15 + if test $ret -ne 143 + then + error "git daemon exited with status: $ret" + fi + GIT_DAEMON_PID= +} diff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh new file mode 100755 index 0000000000..7cbc9994a3 --- /dev/null +++ b/t/t5570-git-daemon.sh @@ -0,0 +1,148 @@ +#!/bin/sh + +test_description='test fetching over git protocol' +. ./test-lib.sh + +LIB_GIT_DAEMON_PORT=${LIB_GIT_DAEMON_PORT-5570} +. "$TEST_DIRECTORY"/lib-git-daemon.sh +start_git_daemon + +test_expect_success 'setup repository' ' + echo content >file && + git add file && + git commit -m one +' + +test_expect_success 'create git-accessible bare repository' ' + mkdir "$GIT_DAEMON_DOCUMENT_ROOT_PATH/repo.git" && + (cd "$GIT_DAEMON_DOCUMENT_ROOT_PATH/repo.git" && + git --bare init && + : >git-daemon-export-ok + ) && + git remote add public "$GIT_DAEMON_DOCUMENT_ROOT_PATH/repo.git" && + git push public master:master +' + +test_expect_success 'clone git repository' ' + git clone "$GIT_DAEMON_URL/repo.git" clone && + test_cmp file clone/file +' + +test_expect_success 'fetch changes via git protocol' ' + echo content >>file && + git commit -a -m two && + git push public && + (cd clone && git pull) && + test_cmp file clone/file +' + +test_expect_failure 'remote detects correct HEAD' ' + git push public master:other && + (cd clone && + git remote set-head -d origin && + git remote set-head -a origin && + git symbolic-ref refs/remotes/origin/HEAD > output && + echo refs/remotes/origin/master > expect && + test_cmp expect output + ) +' + +test_expect_success 'prepare pack objects' ' + cp -R "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo.git "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_pack.git && + (cd "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_pack.git && + git --bare repack -a -d + ) +' + +test_expect_success 'fetch notices corrupt pack' ' + cp -R "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_pack.git "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_bad1.git && + (cd "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_bad1.git && + p=`ls objects/pack/pack-*.pack` && + chmod u+w $p && + printf %0256d 0 | dd of=$p bs=256 count=1 seek=1 conv=notrunc + ) && + mkdir repo_bad1.git && + (cd repo_bad1.git && + git --bare init && + test_must_fail git --bare fetch "$GIT_DAEMON_URL/repo_bad1.git" && + test 0 = `ls objects/pack/pack-*.pack | wc -l` + ) +' + +test_expect_success 'fetch notices corrupt idx' ' + cp -R "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_pack.git "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_bad2.git && + (cd "$GIT_DAEMON_DOCUMENT_ROOT_PATH"/repo_bad2.git && + p=`ls objects/pack/pack-*.idx` && + chmod u+w $p && + printf %0256d 0 | dd of=$p bs=256 count=1 seek=1 conv=notrunc + ) && + mkdir repo_bad2.git && + (cd repo_bad2.git && + git --bare init && + test_must_fail git --bare fetch "$GIT_DAEMON_URL/repo_bad2.git" && + test 0 = `ls objects/pack | wc -l` + ) +' + +test_remote_error() +{ + do_export=YesPlease + while test $# -gt 0 + do + case $1 in + -x) + shift + chmod -x "$GIT_DAEMON_DOCUMENT_ROOT_PATH/repo.git" + ;; + -n) + shift + do_export= + ;; + *) + break + esac + done + + if test $# -ne 3 + then + error "invalid number of arguments" + fi + + cmd=$1 + repo=$2 + msg=$3 + + if test -x "$GIT_DAEMON_DOCUMENT_ROOT_PATH/$repo" + then + if test -n "$do_export" + then + : >"$GIT_DAEMON_DOCUMENT_ROOT_PATH/$repo/git-daemon-export-ok" + else + rm -f "$GIT_DAEMON_DOCUMENT_ROOT_PATH/$repo/git-daemon-export-ok" + fi + fi + + test_must_fail git "$cmd" "$GIT_DAEMON_URL/$repo" 2>output && + echo "fatal: remote error: $msg: /$repo" >expect && + test_cmp expect output + ret=$? + chmod +x "$GIT_DAEMON_DOCUMENT_ROOT_PATH/repo.git" + (exit $ret) +} + +msg="access denied or repository not exported" +test_expect_success 'clone non-existent' "test_remote_error clone nowhere.git '$msg'" +test_expect_success 'push disabled' "test_remote_error push repo.git '$msg'" +test_expect_success 'read access denied' "test_remote_error -x fetch repo.git '$msg'" +test_expect_success 'not exported' "test_remote_error -n fetch repo.git '$msg'" + +stop_git_daemon +start_git_daemon --informative-errors + +test_expect_success 'clone non-existent' "test_remote_error clone nowhere.git 'no such repository'" +test_expect_success 'push disabled' "test_remote_error push repo.git 'service not enabled'" +test_expect_success 'read access denied' "test_remote_error -x fetch repo.git 'no such repository'" +test_expect_success 'not exported' "test_remote_error -n fetch repo.git 'repository not exported'" + +stop_git_daemon +test_done -- cgit v1.2.1 From f6a34cfbb4314105c4dadd88eb42da26aef44dfd Mon Sep 17 00:00:00 2001 From: Clemens Buchacher Date: Sat, 7 Jan 2012 12:42:46 +0100 Subject: git-daemon: produce output when ready If a client tries to connect after git-daemon starts, but before it opens a listening socket, the connection will fail. Output "[PID] Ready to rumble]" after opening the socket successfully in order to inform the user that the daemon is now ready to receive connections. Signed-off-by: Clemens Buchacher Signed-off-by: Junio C Hamano --- daemon.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon.c b/daemon.c index 15ce918a21..ab21e66b2f 100644 --- a/daemon.c +++ b/daemon.c @@ -1086,6 +1086,8 @@ static int serve(struct string_list *listen_addr, int listen_port, drop_privileges(cred); + loginfo("Ready to rumble"); + return service_loop(&socklist); } @@ -1270,10 +1272,8 @@ int main(int argc, char **argv) if (inetd_mode || serve_mode) return execute(); - if (detach) { + if (detach) daemonize(); - loginfo("Ready to rumble"); - } else sanitize_stdfds(); -- cgit v1.2.1 From 561b133c2c29ccf5e2aeaa5d6b1da3835e660db8 Mon Sep 17 00:00:00 2001 From: Clemens Buchacher Date: Sat, 7 Jan 2012 12:42:47 +0100 Subject: git-daemon tests: wait until daemon is ready In start_daemon, git-daemon is started as a background process. In theory, the tests may try to connect before the daemon had a chance to open a listening socket. Avoid this race condition by waiting for it to output "Ready to rumble". Any other output is considered an error and the test is aborted. Should git-daemon produce no output at all, lib-git-daemon would block forever. This could be fixed by introducing a timeout. On the other hand, we have no timeout for other git commands which could suffer from the same problem. Since such a mechanism adds some complexity, I have decided against it. Signed-off-by: Clemens Buchacher Signed-off-by: Junio C Hamano --- t/lib-git-daemon.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh index 5e81a25942..ef2d01f369 100644 --- a/t/lib-git-daemon.sh +++ b/t/lib-git-daemon.sh @@ -23,12 +23,27 @@ start_git_daemon() { trap 'code=$?; stop_git_daemon; (exit $code); die' EXIT say >&3 "Starting git daemon ..." + mkfifo git_daemon_output git daemon --listen=127.0.0.1 --port="$LIB_GIT_DAEMON_PORT" \ --reuseaddr --verbose \ --base-path="$GIT_DAEMON_DOCUMENT_ROOT_PATH" \ "$@" "$GIT_DAEMON_DOCUMENT_ROOT_PATH" \ - >&3 2>&4 & + >&3 2>git_daemon_output & GIT_DAEMON_PID=$! + { + read line + echo >&4 "$line" + cat >&4 & + + # Check expected output + if test x"$(expr "$line" : "\[[0-9]*\] \(.*\)")" != x"Ready to rumble" + then + kill "$GIT_DAEMON_PID" + wait "$GIT_DAEMON_PID" + trap 'die' EXIT + error "git daemon failed to start" + fi + } Date: Sat, 7 Jan 2012 11:47:38 +0100 Subject: gitweb: Fix actionless dispatch for non-existent objects When gitweb URL does not provide action explicitly, e.g. http://git.example.org/repo.git/branch dispatch() tries to guess action (view to be used) based on remaining parameters. Among others it is based on the type of requested object, which gave problems when asking for non-existent branch or file (for example misspelt name). Now undefined $action from dispatch() should not result in problems. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 4 +++- t/t9500-gitweb-standalone-no-errors.sh | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 874023a33e..6cf38853b5 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -1122,8 +1122,10 @@ sub dispatch { if (!defined $action) { if (defined $hash) { $action = git_get_type($hash); + $action or die_error(404, "Object does not exist"); } elsif (defined $hash_base && defined $file_name) { $action = git_get_type("$hash_base:$file_name"); + $action or die_error(404, "File or directory does not exist"); } elsif (defined $project) { $action = 'summary'; } else { @@ -2364,7 +2366,7 @@ sub get_feed_info { return unless (defined $project); # some views should link to OPML, or to generic project feed, # or don't have specific feed yet (so they should use generic) - return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x); + return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x); my $branch; # branches refs uses 'refs/heads/' prefix (fullname) to differentiate diff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh index 53297156a3..94365bb006 100755 --- a/t/t9500-gitweb-standalone-no-errors.sh +++ b/t/t9500-gitweb-standalone-no-errors.sh @@ -403,6 +403,14 @@ test_expect_success \ 'path_info: project/branch:dir/' \ 'gitweb_run "" "/.git/master:foo/"' +test_expect_success \ + 'path_info: project/branch (non-existent)' \ + 'gitweb_run "" "/.git/non-existent"' + +test_expect_success \ + 'path_info: project/branch:filename (non-existent branch)' \ + 'gitweb_run "" "/.git/non-existent:non-existent"' + test_expect_success \ 'path_info: project/branch:file (non-existent)' \ 'gitweb_run "" "/.git/master:non-existent"' -- cgit v1.2.1 From 1eb10f4091931d6b89ff10edad63ce9c01ed17fd Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 9 Jan 2012 23:44:30 -0500 Subject: unix-socket: handle long socket pathnames On many systems, the sockaddr_un.sun_path field is quite small. Even on Linux, it is only 108 characters. A user of the credential-cache daemon can easily surpass this, especially if their home directory is in a deep directory tree (since the default location expands ~/.git-credentials). We can hack around this in the unix-socket.[ch] code by doing a chdir() to the enclosing directory, feeding the relative basename to the socket functions, and then restoring the working directory. This introduces several new possible error cases for creating a socket, including an irrecoverable one in the case that we can't restore the working directory. In the case of the credential-cache code, we could perhaps get away with simply chdir()-ing to the socket directory and never coming back. However, I'd rather do it at the lower level for a few reasons: 1. It keeps the hackery behind an opaque interface instead of polluting the main program logic. 2. A hack in credential-cache won't help any unix-socket users who come along later. 3. The chdir trickery isn't that likely to fail (basically it's only a problem if your cwd is missing or goes away while you're running). And because we only enable the hack when we get a too-long name, it can only fail in cases that would have failed under the previous code anyway. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- unix-socket.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/unix-socket.c b/unix-socket.c index 84b15099f2..7d8bec6158 100644 --- a/unix-socket.c +++ b/unix-socket.c @@ -9,27 +9,83 @@ static int unix_stream_socket(void) return fd; } -static void unix_sockaddr_init(struct sockaddr_un *sa, const char *path) +static int chdir_len(const char *orig, int len) +{ + char *path = xmemdupz(orig, len); + int r = chdir(path); + free(path); + return r; +} + +struct unix_sockaddr_context { + char orig_dir[PATH_MAX]; +}; + +static void unix_sockaddr_cleanup(struct unix_sockaddr_context *ctx) +{ + if (!ctx->orig_dir[0]) + return; + /* + * If we fail, we can't just return an error, since we have + * moved the cwd of the whole process, which could confuse calling + * code. We are better off to just die. + */ + if (chdir(ctx->orig_dir) < 0) + die("unable to restore original working directory"); +} + +static int unix_sockaddr_init(struct sockaddr_un *sa, const char *path, + struct unix_sockaddr_context *ctx) { int size = strlen(path) + 1; - if (size > sizeof(sa->sun_path)) - die("socket path is too long to fit in sockaddr"); + + ctx->orig_dir[0] = '\0'; + if (size > sizeof(sa->sun_path)) { + const char *slash = find_last_dir_sep(path); + const char *dir; + + if (!slash) { + errno = ENAMETOOLONG; + return -1; + } + + dir = path; + path = slash + 1; + size = strlen(path) + 1; + if (size > sizeof(sa->sun_path)) { + errno = ENAMETOOLONG; + return -1; + } + + if (!getcwd(ctx->orig_dir, sizeof(ctx->orig_dir))) { + errno = ENAMETOOLONG; + return -1; + } + if (chdir_len(dir, slash - dir) < 0) + return -1; + } + memset(sa, 0, sizeof(*sa)); sa->sun_family = AF_UNIX; memcpy(sa->sun_path, path, size); + return 0; } int unix_stream_connect(const char *path) { int fd; struct sockaddr_un sa; + struct unix_sockaddr_context ctx; - unix_sockaddr_init(&sa, path); + if (unix_sockaddr_init(&sa, path, &ctx) < 0) + return -1; fd = unix_stream_socket(); if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + unix_sockaddr_cleanup(&ctx); close(fd); return -1; } + unix_sockaddr_cleanup(&ctx); return fd; } @@ -37,20 +93,25 @@ int unix_stream_listen(const char *path) { int fd; struct sockaddr_un sa; + struct unix_sockaddr_context ctx; - unix_sockaddr_init(&sa, path); + if (unix_sockaddr_init(&sa, path, &ctx) < 0) + return -1; fd = unix_stream_socket(); unlink(path); if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + unix_sockaddr_cleanup(&ctx); close(fd); return -1; } if (listen(fd, 5) < 0) { + unix_sockaddr_cleanup(&ctx); close(fd); return -1; } + unix_sockaddr_cleanup(&ctx); return fd; } -- cgit v1.2.1 From 8ec6c8d79567a71ca3c6f1ec73eb453d371b1ade Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 9 Jan 2012 23:57:33 -0500 Subject: credential-cache: report more daemon connection errors Originally, this code remained relatively silent when we failed to connect to the cache. The idea was that it was simply a cache, and we didn't want to bother the user with temporary failures (the worst case is that we would simply ask their password again). However, if you have a configuration failure or other problem, it is helpful for the daemon to report those problems. Git will happily ignore the failed error code, but the extra information to stderr can help the user diagnose the problem. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- credential-cache.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/credential-cache.c b/credential-cache.c index b15a9a7449..193301877f 100644 --- a/credential-cache.c +++ b/credential-cache.c @@ -71,10 +71,14 @@ static void do_cache(const char *socket, const char *action, int timeout, die_errno("unable to relay credential"); } - if (send_request(socket, &buf) < 0 && (flags & FLAG_SPAWN)) { - spawn_daemon(socket); - if (send_request(socket, &buf) < 0) + if (send_request(socket, &buf) < 0) { + if (errno != ENOENT) die_errno("unable to connect to cache daemon"); + if (flags & FLAG_SPAWN) { + spawn_daemon(socket); + if (send_request(socket, &buf) < 0) + die_errno("unable to connect to cache daemon"); + } } strbuf_release(&buf); } -- cgit v1.2.1 From b7e642ecec4347b170d206c84850ae0a46b5f46e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 10 Jan 2012 21:45:52 -0800 Subject: request-pull: use the real fork point when preparing the message The command takes the "start" argument and computes the merge base between it and the commit to be pulled so that we can show the diffstat, but uses the "start" argument as-is when composing the message The following changes since commit $X are available to tell the integrator which commit the work is based on. Giving "origin" (most of the time it resolves to refs/remotes/origin/master) as the start argument is often convenient, but it is usually not the fork point, and does not help the integrator at all. Use the real fork point, which is the merge base we already compute, when composing that part of the message. Suggested-by: Linus Torvalds Signed-off-by: Junio C Hamano --- git-request-pull.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-request-pull.sh b/git-request-pull.sh index d7ba1178ae..64960d65a1 100755 --- a/git-request-pull.sh +++ b/git-request-pull.sh @@ -96,7 +96,7 @@ git show -s --format='The following changes since commit %H: %s (%ci) are available in the git repository at: -' $baserev && +' $merge_base && echo " $url${ref+ $ref}" && git show -s --format=' for you to fetch changes up to %H: -- cgit v1.2.1 From 592ed5673e3074a0d7e0a6d6626faa783c327c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Wed, 11 Jan 2012 10:21:38 +0700 Subject: t2203: fix wrong commit command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add commit message to avoid commit's aborting due to the lack of commit message, not because there are INTENT_TO_ADD entries in index. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- t/t2203-add-intent.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh index 58a329961e..25435290a7 100755 --- a/t/t2203-add-intent.sh +++ b/t/t2203-add-intent.sh @@ -41,7 +41,7 @@ test_expect_success 'cannot commit with i-t-a entry' ' echo frotz >nitfol && git add rezrov && git add -N nitfol && - test_must_fail git commit + test_must_fail git commit -m initial ' test_expect_success 'can commit with an unrelated i-t-a entry in index' ' -- cgit v1.2.1 From 97ba642bcf918de028b2e77165c5210cf3f462e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Thu, 5 Jan 2012 19:39:40 +0700 Subject: Fix incorrect ref namespace check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reason why the trailing slash is needed is obvious. refs/stash and HEAD are not namespace, but complete refs. Do full string compare on them. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/fetch.c | 2 +- builtin/remote.c | 2 +- log-tree.c | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 8761a33b49..ea45cb0e8e 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -574,7 +574,7 @@ static void find_non_local_tags(struct transport *transport, for_each_ref(add_existing, &existing_refs); for (ref = transport_get_remote_refs(transport); ref; ref = ref->next) { - if (prefixcmp(ref->name, "refs/tags")) + if (prefixcmp(ref->name, "refs/tags/")) continue; /* diff --git a/builtin/remote.c b/builtin/remote.c index c810643815..164626b1d1 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -535,7 +535,7 @@ static int add_branch_for_removal(const char *refname, } /* don't delete non-remote-tracking refs */ - if (prefixcmp(refname, "refs/remotes")) { + if (prefixcmp(refname, "refs/remotes/")) { /* advise user how to delete local branches */ if (!prefixcmp(refname, "refs/heads/")) string_list_append(branches->skipped, diff --git a/log-tree.c b/log-tree.c index e7694a3a4c..21ac9047c0 100644 --- a/log-tree.c +++ b/log-tree.c @@ -119,9 +119,9 @@ static int add_ref_decoration(const char *refname, const unsigned char *sha1, in type = DECORATION_REF_REMOTE; else if (!prefixcmp(refname, "refs/tags/")) type = DECORATION_REF_TAG; - else if (!prefixcmp(refname, "refs/stash")) + else if (!strcmp(refname, "refs/stash")) type = DECORATION_REF_STASH; - else if (!prefixcmp(refname, "HEAD")) + else if (!strcmp(refname, "HEAD")) type = DECORATION_REF_HEAD; if (!cb_data || *(int *)cb_data == DECORATE_SHORT_REFS) -- cgit v1.2.1 From 43b82bd9c3a5ad597bbfc0c12a519d4053df01b8 Mon Sep 17 00:00:00 2001 From: Pete Wyckoff Date: Wed, 11 Jan 2012 18:31:06 -0500 Subject: git-p4: only a single ... wildcard is supported Catch the case where a ... exists at the end, and also elsehwere. Reported-by: Gary Gibbons Signed-off-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 4 ++-- t/t9809-git-p4-client-view.sh | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 3e1aa276cf..20208bfbc0 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -1207,8 +1207,8 @@ class View(object): die("Can't handle * wildcards in view: %s" % self.path) triple_dot_index = self.path.find("...") if triple_dot_index >= 0: - if not self.path.endswith("..."): - die("Can handle ... wildcard only at end of path: %s" % + if triple_dot_index != len(self.path) - 3: + die("Can handle only single ... wildcard, at end: %s" % self.path) self.ends_triple_dot = True diff --git a/t/t9809-git-p4-client-view.sh b/t/t9809-git-p4-client-view.sh index c9471d562d..54204af8fb 100755 --- a/t/t9809-git-p4-client-view.sh +++ b/t/t9809-git-p4-client-view.sh @@ -101,12 +101,18 @@ test_expect_success 'unsupported view wildcard *' ' test_must_fail "$GITP4" clone --use-client-spec --dest="$git" //depot ' -test_expect_success 'wildcard ... only supported at end of spec' ' +test_expect_success 'wildcard ... only supported at end of spec 1' ' client_view "//depot/.../file11 //client/.../file11" && test_when_finished cleanup_git && test_must_fail "$GITP4" clone --use-client-spec --dest="$git" //depot ' +test_expect_success 'wildcard ... only supported at end of spec 2' ' + client_view "//depot/.../a/... //client/.../a/..." && + test_when_finished cleanup_git && + test_must_fail "$GITP4" clone --use-client-spec --dest="$git" //depot +' + test_expect_success 'basic map' ' client_view "//depot/dir1/... //client/cli1/..." && files="cli1/file11 cli1/file12" && -- cgit v1.2.1 From 329afb8e976b2605bbc92a6ac8ef63f9a6c98ef1 Mon Sep 17 00:00:00 2001 From: Pete Wyckoff Date: Wed, 11 Jan 2012 18:31:07 -0500 Subject: git-p4: fix verbose comment typo Signed-off-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 20208bfbc0..e267f3115d 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -1263,7 +1263,7 @@ class View(object): if self.exclude: c = "-" return "View.Mapping: %s%s -> %s" % \ - (c, self.depot_side, self.client_side) + (c, self.depot_side.path, self.client_side.path) def map_depot_to_client(self, depot_path): """Calculate the client path if using this mapping on the -- cgit v1.2.1 From 6ee9a9993fcbe5dae356013c51cfed1e39eeaf01 Mon Sep 17 00:00:00 2001 From: Pete Wyckoff Date: Wed, 11 Jan 2012 18:31:08 -0500 Subject: git-p4: clarify comment Signed-off-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index e267f3115d..e11e15bad0 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -1363,7 +1363,8 @@ class View(object): else: # This mapping matched; no need to search any further. # But, the mapping could be rejected if the client path - # has already been claimed by an earlier mapping. + # has already been claimed by an earlier mapping (i.e. + # one later in the list, which we are walking backwards). already_mapped_in_client = False for f in paths_filled: # this is View.Path.match -- cgit v1.2.1 From 2ea09b5ace46bf8292afe9c2b83b4a6580218bdf Mon Sep 17 00:00:00 2001 From: Pete Wyckoff Date: Wed, 11 Jan 2012 18:31:09 -0500 Subject: git-p4: adjust test to adhere to stricter useClientSpec This test relied on what now is seen as broken behavior in --use-client-spec. Change it to make sure it works according to the new behavior as described in ecb7cf9 (git-p4: rewrite view handling, 2012-01-02) and c700b68 (git-p4: test client view handling, 2012-01-02). Signed-off-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- t/t9806-git-p4-options.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t9806-git-p4-options.sh b/t/t9806-git-p4-options.sh index 1f1952a657..0571602129 100755 --- a/t/t9806-git-p4-options.sh +++ b/t/t9806-git-p4-options.sh @@ -146,7 +146,7 @@ test_expect_success 'clone --use-client-spec' ' ( cd "$git" && test_path_is_file bus/dir/f4 && - test_path_is_file file1 + test_path_is_missing file1 ) && cleanup_git && @@ -159,7 +159,7 @@ test_expect_success 'clone --use-client-spec' ' "$GITP4" sync //depot/... && git checkout -b master p4/master && test_path_is_file bus/dir/f4 && - test_path_is_file file1 + test_path_is_missing file1 ) ' -- cgit v1.2.1 From 42d8c2799062bafc538d511a0262f76e23c99421 Mon Sep 17 00:00:00 2001 From: Pete Wyckoff Date: Wed, 11 Jan 2012 18:31:10 -0500 Subject: git-p4: add tests demonstrating spec overlay ambiguities Introduce new tests that look more closely at overlay situations when there are conflicting files. Five of these are broken. Document the brokenness. This is a fundamental problem with how git-p4 only "borrows" a client spec. At some sync operation, a new change can contain a file which is already in the repo or explicitly deleted through another mapping. To sort this out would involve listing all the files in the client spec to find one with a higher priority. While this is not too hard for the initial import, subsequent sync operations would be very costly. Signed-off-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- Documentation/git-p4.txt | 5 + t/t9809-git-p4-client-view.sh | 387 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+) diff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt index 78938b2930..8b92cc0f8d 100644 --- a/Documentation/git-p4.txt +++ b/Documentation/git-p4.txt @@ -314,6 +314,11 @@ around whitespace. Of the possible wildcards, git-p4 only handles '...', and only when it is at the end of the path. Git-p4 will complain if it encounters an unhandled wildcard. +Bugs in the implementation of overlap mappings exist. If multiple depot +paths map through overlays to the same location in the repository, +git-p4 can choose the wrong one. This is hard to solve without +dedicating a client spec just for git-p4. + The name of the client can be given to git-p4 in multiple ways. The variable 'git-p4.client' takes precedence if it exists. Otherwise, normal p4 mechanisms of determining the client are used: environment diff --git a/t/t9809-git-p4-client-view.sh b/t/t9809-git-p4-client-view.sh index 54204af8fb..ae9145e307 100755 --- a/t/t9809-git-p4-client-view.sh +++ b/t/t9809-git-p4-client-view.sh @@ -246,6 +246,393 @@ test_expect_success 'quotes on rhs only' ' git_verify "cdir 1/file11" "cdir 1/file12" ' +# +# What happens when two files of the same name are overlayed together? +# The last-listed file should take preference. +# +# //depot +# - dir1 +# - file11 +# - file12 +# - filecollide +# - dir2 +# - file21 +# - file22 +# - filecollide +# +test_expect_success 'overlay collision setup' ' + client_view "//depot/... //client/..." && + ( + cd "$cli" && + p4 sync && + echo dir1/filecollide >dir1/filecollide && + p4 add dir1/filecollide && + p4 submit -d dir1/filecollide && + echo dir2/filecollide >dir2/filecollide && + p4 add dir2/filecollide && + p4 submit -d dir2/filecollide + ) +' + +test_expect_success 'overlay collision 1 to 2' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22 filecollide" && + echo dir2/filecollide >actual && + client_verify $files && + test_cmp actual "$cli"/filecollide && + test_when_finished cleanup_git && + "$GITP4" clone --use-client-spec --dest="$git" //depot && + git_verify $files && + test_cmp actual "$git"/filecollide +' + +test_expect_failure 'overlay collision 2 to 1' ' + client_view "//depot/dir2/... //client/..." \ + "+//depot/dir1/... //client/..." && + files="file11 file12 file21 file22 filecollide" && + echo dir1/filecollide >actual && + client_verify $files && + test_cmp actual "$cli"/filecollide && + test_when_finished cleanup_git && + "$GITP4" clone --use-client-spec --dest="$git" //depot && + git_verify $files && + test_cmp actual "$git"/filecollide +' + +test_expect_success 'overlay collision delete 2' ' + client_view "//depot/... //client/..." && + ( + cd "$cli" && + p4 sync && + p4 delete dir2/filecollide && + p4 submit -d "remove dir2/filecollide" + ) +' + +# no filecollide, got deleted with dir2 +test_expect_failure 'overlay collision 1 to 2, but 2 deleted' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22" && + client_verify $files && + test_when_finished cleanup_git && + "$GITP4" clone --use-client-spec --dest="$git" //depot && + git_verify $files +' + +test_expect_success 'overlay collision update 1' ' + client_view "//depot/dir1/... //client/dir1/..." && + ( + cd "$cli" && + p4 sync && + p4 open dir1/filecollide && + echo dir1/filecollide update >dir1/filecollide && + p4 submit -d "update dir1/filecollide" + ) +' + +# still no filecollide, dir2 still wins with the deletion even though the +# change to dir1 is more recent +test_expect_failure 'overlay collision 1 to 2, but 2 deleted, then 1 updated' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22" && + client_verify $files && + test_when_finished cleanup_git && + "$GITP4" clone --use-client-spec --dest="$git" //depot && + git_verify $files +' + +test_expect_success 'overlay collision delete filecollides' ' + client_view "//depot/... //client/..." && + ( + cd "$cli" && + p4 sync && + p4 delete dir1/filecollide dir2/filecollide && + p4 submit -d "remove filecollides" + ) +' + +# +# Overlays as part of sync, rather than initial checkout: +# 1. add a file in dir1 +# 2. sync to include it +# 3. add same file in dir2 +# 4. sync, make sure content switches as dir2 has priority +# 5. add another file in dir1 +# 6. sync +# 7. add/delete same file in dir2 +# 8. sync, make sure it disappears, again dir2 wins +# 9. cleanup +# +# //depot +# - dir1 +# - file11 +# - file12 +# - colA +# - colB +# - dir2 +# - file21 +# - file22 +# - colA +# - colB +# +test_expect_success 'overlay sync: add colA in dir1' ' + client_view "//depot/dir1/... //client/dir1/..." && + ( + cd "$cli" && + p4 sync && + echo dir1/colA >dir1/colA && + p4 add dir1/colA && + p4 submit -d dir1/colA + ) +' + +test_expect_success 'overlay sync: initial git checkout' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22 colA" && + echo dir1/colA >actual && + client_verify $files && + test_cmp actual "$cli"/colA && + "$GITP4" clone --use-client-spec --dest="$git" //depot && + git_verify $files && + test_cmp actual "$git"/colA +' + +test_expect_success 'overlay sync: add colA in dir2' ' + client_view "//depot/dir2/... //client/dir2/..." && + ( + cd "$cli" && + p4 sync && + echo dir2/colA >dir2/colA && + p4 add dir2/colA && + p4 submit -d dir2/colA + ) +' + +test_expect_success 'overlay sync: colA content switch' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22 colA" && + echo dir2/colA >actual && + client_verify $files && + test_cmp actual "$cli"/colA && + ( + cd "$git" && + "$GITP4" sync --use-client-spec && + git merge --ff-only p4/master + ) && + git_verify $files && + test_cmp actual "$git"/colA +' + +test_expect_success 'overlay sync: add colB in dir1' ' + client_view "//depot/dir1/... //client/dir1/..." && + ( + cd "$cli" && + p4 sync && + echo dir1/colB >dir1/colB && + p4 add dir1/colB && + p4 submit -d dir1/colB + ) +' + +test_expect_success 'overlay sync: colB appears' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22 colA colB" && + echo dir1/colB >actual && + client_verify $files && + test_cmp actual "$cli"/colB && + ( + cd "$git" && + "$GITP4" sync --use-client-spec && + git merge --ff-only p4/master + ) && + git_verify $files && + test_cmp actual "$git"/colB +' + +test_expect_success 'overlay sync: add/delete colB in dir2' ' + client_view "//depot/dir2/... //client/dir2/..." && + ( + cd "$cli" && + p4 sync && + echo dir2/colB >dir2/colB && + p4 add dir2/colB && + p4 submit -d dir2/colB && + p4 delete dir2/colB && + p4 submit -d "delete dir2/colB" + ) +' + +test_expect_success 'overlay sync: colB disappears' ' + client_view "//depot/dir1/... //client/..." \ + "+//depot/dir2/... //client/..." && + files="file11 file12 file21 file22 colA" && + client_verify $files && + test_when_finished cleanup_git && + ( + cd "$git" && + "$GITP4" sync --use-client-spec && + git merge --ff-only p4/master + ) && + git_verify $files +' + +test_expect_success 'overlay sync: cleanup' ' + client_view "//depot/... //client/..." && + ( + cd "$cli" && + p4 sync && + p4 delete dir1/colA dir2/colA dir1/colB && + p4 submit -d "remove overlay sync files" + ) +' + +# +# Overlay tests again, but swapped so dir1 has priority. +# 1. add a file in dir1 +# 2. sync to include it +# 3. add same file in dir2 +# 4. sync, make sure content does not switch +# 5. add another file in dir1 +# 6. sync +# 7. add/delete same file in dir2 +# 8. sync, make sure it is still there +# 9. cleanup +# +# //depot +# - dir1 +# - file11 +# - file12 +# - colA +# - colB +# - dir2 +# - file21 +# - file22 +# - colA +# - colB +# +test_expect_success 'overlay sync swap: add colA in dir1' ' + client_view "//depot/dir1/... //client/dir1/..." && + ( + cd "$cli" && + p4 sync && + echo dir1/colA >dir1/colA && + p4 add dir1/colA && + p4 submit -d dir1/colA + ) +' + +test_expect_success 'overlay sync swap: initial git checkout' ' + client_view "//depot/dir2/... //client/..." \ + "+//depot/dir1/... //client/..." && + files="file11 file12 file21 file22 colA" && + echo dir1/colA >actual && + client_verify $files && + test_cmp actual "$cli"/colA && + "$GITP4" clone --use-client-spec --dest="$git" //depot && + git_verify $files && + test_cmp actual "$git"/colA +' + +test_expect_success 'overlay sync swap: add colA in dir2' ' + client_view "//depot/dir2/... //client/dir2/..." && + ( + cd "$cli" && + p4 sync && + echo dir2/colA >dir2/colA && + p4 add dir2/colA && + p4 submit -d dir2/colA + ) +' + +test_expect_failure 'overlay sync swap: colA no content switch' ' + client_view "//depot/dir2/... //client/..." \ + "+//depot/dir1/... //client/..." && + files="file11 file12 file21 file22 colA" && + echo dir1/colA >actual && + client_verify $files && + test_cmp actual "$cli"/colA && + ( + cd "$git" && + "$GITP4" sync --use-client-spec && + git merge --ff-only p4/master + ) && + git_verify $files && + test_cmp actual "$git"/colA +' + +test_expect_success 'overlay sync swap: add colB in dir1' ' + client_view "//depot/dir1/... //client/dir1/..." && + ( + cd "$cli" && + p4 sync && + echo dir1/colB >dir1/colB && + p4 add dir1/colB && + p4 submit -d dir1/colB + ) +' + +test_expect_success 'overlay sync swap: colB appears' ' + client_view "//depot/dir2/... //client/..." \ + "+//depot/dir1/... //client/..." && + files="file11 file12 file21 file22 colA colB" && + echo dir1/colB >actual && + client_verify $files && + test_cmp actual "$cli"/colB && + ( + cd "$git" && + "$GITP4" sync --use-client-spec && + git merge --ff-only p4/master + ) && + git_verify $files && + test_cmp actual "$git"/colB +' + +test_expect_success 'overlay sync swap: add/delete colB in dir2' ' + client_view "//depot/dir2/... //client/dir2/..." && + ( + cd "$cli" && + p4 sync && + echo dir2/colB >dir2/colB && + p4 add dir2/colB && + p4 submit -d dir2/colB && + p4 delete dir2/colB && + p4 submit -d "delete dir2/colB" + ) +' + +test_expect_failure 'overlay sync swap: colB no change' ' + client_view "//depot/dir2/... //client/..." \ + "+//depot/dir1/... //client/..." && + files="file11 file12 file21 file22 colA colB" && + echo dir1/colB >actual && + client_verify $files && + test_cmp actual "$cli"/colB && + test_when_finished cleanup_git && + ( + cd "$git" && + "$GITP4" sync --use-client-spec && + git merge --ff-only p4/master + ) && + git_verify $files && + test_cmp actual "$cli"/colB +' + +test_expect_success 'overlay sync swap: cleanup' ' + client_view "//depot/... //client/..." && + ( + cd "$cli" && + p4 sync && + p4 delete dir1/colA dir2/colA dir1/colB && + p4 submit -d "remove overlay sync files" + ) +' + # # Rename directories to test quoting in depot-side mappings # //depot -- cgit v1.2.1 From 82553cbb08b791aa0bed920ee58494268c0f579f Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Wed, 11 Jan 2012 21:13:42 +0100 Subject: mailinfo documentation: accurately describe non -k case Since its very first description of -k, the documentation for git-mailinfo claimed that (in the case without -k) after cleaning up bracketed strings [blah], it would insert [PATCH]. It doesn't; on the contrary, one of the important jobs of mailinfo is to remove those strings. Since we're already there, rewrite the paragraph to give a complete enumeration of all the transformations. Specifically, it was missing the whitespace normalization (run of isspace(c) -> ' ') and the removal of leading ':'. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- Documentation/git-mailinfo.txt | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Documentation/git-mailinfo.txt b/Documentation/git-mailinfo.txt index ed45662cc9..36d0400207 100644 --- a/Documentation/git-mailinfo.txt +++ b/Documentation/git-mailinfo.txt @@ -24,13 +24,24 @@ command directly. See linkgit:git-am[1] instead. OPTIONS ------- -k:: - Usually the program 'cleans up' the Subject: header line - to extract the title line for the commit log message, - among which (1) remove 'Re:' or 're:', (2) leading - whitespaces, (3) '[' up to ']', typically '[PATCH]', and - then prepends "[PATCH] ". This flag forbids this - munging, and is most useful when used to read back - 'git format-patch -k' output. + Usually the program removes email cruft from the Subject: + header line to extract the title line for the commit log + message. This option prevents this munging, and is most + useful when used to read back 'git format-patch -k' output. ++ +Specifically, the following are removed until none of them remain: ++ +-- +* Leading and trailing whitespace. + +* Leading `Re:`, `re:`, and `:`. + +* Leading bracketed strings (between `[` and `]`, usually + `[PATCH]`). +-- ++ +Finally, runs of whitespace are normalized to a single ASCII space +character. -b:: When -k is not in effect, all leading strings bracketed with '[' -- cgit v1.2.1 From 06121a0a8328c8aaa7a023cf6ebb142e9dc2b45c Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Wed, 11 Jan 2012 17:50:10 -0600 Subject: unix-socket: do not let close() or chdir() clobber errno during cleanup unix_stream_connect and unix_stream_listen return -1 on error, with errno set by the failing underlying call to allow the caller to write a useful diagnosis. Unfortunately the error path involves a few system calls itself, such as close(), that can themselves touch errno. This is not as worrisome as it might sound. If close() fails, this just means substituting one meaningful error message for another, which is perfectly fine. However, when the call _succeeds_, it is allowed to (and sometimes might) clobber errno along the way with some undefined value, so it is good higiene to save errno and restore it immediately before returning to the caller. Do so. Signed-off-by: Jonathan Nieder Signed-off-by: Junio C Hamano --- unix-socket.c | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/unix-socket.c b/unix-socket.c index 7d8bec6158..01f119f970 100644 --- a/unix-socket.c +++ b/unix-socket.c @@ -73,25 +73,29 @@ static int unix_sockaddr_init(struct sockaddr_un *sa, const char *path, int unix_stream_connect(const char *path) { - int fd; + int fd, saved_errno; struct sockaddr_un sa; struct unix_sockaddr_context ctx; if (unix_sockaddr_init(&sa, path, &ctx) < 0) return -1; fd = unix_stream_socket(); - if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { - unix_sockaddr_cleanup(&ctx); - close(fd); - return -1; - } + if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) + goto fail; unix_sockaddr_cleanup(&ctx); return fd; + +fail: + saved_errno = errno; + unix_sockaddr_cleanup(&ctx); + close(fd); + errno = saved_errno; + return -1; } int unix_stream_listen(const char *path) { - int fd; + int fd, saved_errno; struct sockaddr_un sa; struct unix_sockaddr_context ctx; @@ -100,18 +104,19 @@ int unix_stream_listen(const char *path) fd = unix_stream_socket(); unlink(path); - if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { - unix_sockaddr_cleanup(&ctx); - close(fd); - return -1; - } + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) + goto fail; - if (listen(fd, 5) < 0) { - unix_sockaddr_cleanup(&ctx); - close(fd); - return -1; - } + if (listen(fd, 5) < 0) + goto fail; unix_sockaddr_cleanup(&ctx); return fd; + +fail: + saved_errno = errno; + unix_sockaddr_cleanup(&ctx); + close(fd); + errno = saved_errno; + return -1; } -- cgit v1.2.1 From 4397c6535ea314b3412d6a0658ecf44241d07df1 Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Wed, 11 Jan 2012 10:20:14 +0100 Subject: t9200: On MSYS, do not pass Windows-style paths to CVS For details, see the commit message of 4114156ae9. Note that while using $PWD as part of GIT_DIR is not required here, it does no harm and it is more consistent. In addition, on MSYS using an environment variable should be slightly faster than spawning an external executable. Signed-off-by: Sebastian Schuberth Signed-off-by: Junio C Hamano --- t/t9200-git-cvsexportcommit.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh index 41db05cb4a..518358aa64 100755 --- a/t/t9200-git-cvsexportcommit.sh +++ b/t/t9200-git-cvsexportcommit.sh @@ -19,9 +19,9 @@ then test_done fi -CVSROOT=$(pwd)/cvsroot -CVSWORK=$(pwd)/cvswork -GIT_DIR=$(pwd)/.git +CVSROOT=$PWD/cvsroot +CVSWORK=$PWD/cvswork +GIT_DIR=$PWD/.git export CVSROOT CVSWORK GIT_DIR rm -rf "$CVSROOT" "$CVSWORK" -- cgit v1.2.1 From 37495eef4c6071fa124911753cfca3690ea4bf96 Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Wed, 11 Jan 2012 10:21:10 +0100 Subject: git-cvsexportcommit: Fix calling Perl's rel2abs() on MSYS Due to MSYS path mangling GIT_DIR contains a Windows-style path when checked inside a Perl script even if GIT_DIR was previously set to an MSYS-style path in a shell script. So explicitly convert to an MSYS-style path before calling Perl's rel2abs() to make it work. This fix was inspired by a very similar patch in WebKit: http://trac.webkit.org/changeset/76255/trunk/Tools/Scripts/commit-log-editor Signed-off-by: Sebastian Schuberth Tested-by: Pat Thoyts Signed-off-by: Junio C Hamano --- git-cvsexportcommit.perl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/git-cvsexportcommit.perl b/git-cvsexportcommit.perl index 39a426e067..e6bf25232c 100755 --- a/git-cvsexportcommit.perl +++ b/git-cvsexportcommit.perl @@ -30,6 +30,13 @@ if ($opt_w || $opt_W) { chomp($gd); $ENV{GIT_DIR} = $gd; } + + # On MSYS, convert a Windows-style path to an MSYS-style path + # so that rel2abs() below works correctly. + if ($^O eq 'msys') { + $ENV{GIT_DIR} =~ s#^([[:alpha:]]):/#/$1/#; + } + # Make sure GIT_DIR is absolute $ENV{GIT_DIR} = File::Spec->rel2abs($ENV{GIT_DIR}); } -- cgit v1.2.1 From 644a36908d6b52cb8c0c4d1ac69fd0b451e38f13 Mon Sep 17 00:00:00 2001 From: Ramkumar Ramachandra Date: Wed, 11 Jan 2012 23:45:56 +0530 Subject: revert: prepare to move replay_action to header REVERT and CHERRY_PICK and are unsuitable names for an enumerator in a public interface, because they are generic enough to be likely to clash with identifiers with other meanings. Rename to REPLAY_REVERT and REPLAY_PICK as preparation for exposing them. Helped-by: Jonathan Nieder Signed-off-by: Ramkumar Ramachandra Signed-off-by: Junio C Hamano --- builtin/revert.c | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/builtin/revert.c b/builtin/revert.c index 0d8020cf64..2739405766 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -39,7 +39,11 @@ static const char * const cherry_pick_usage[] = { NULL }; -enum replay_action { REVERT, CHERRY_PICK }; +enum replay_action { + REPLAY_REVERT, + REPLAY_PICK +}; + enum replay_subcommand { REPLAY_NONE, REPLAY_REMOVE_STATE, @@ -74,14 +78,14 @@ struct replay_opts { static const char *action_name(const struct replay_opts *opts) { - return opts->action == REVERT ? "revert" : "cherry-pick"; + return opts->action == REPLAY_REVERT ? "revert" : "cherry-pick"; } static char *get_encoding(const char *message); static const char * const *revert_or_cherry_pick_usage(struct replay_opts *opts) { - return opts->action == REVERT ? revert_usage : cherry_pick_usage; + return opts->action == REPLAY_REVERT ? revert_usage : cherry_pick_usage; } static int option_parse_x(const struct option *opt, @@ -160,7 +164,7 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts) OPT_END(), }; - if (opts->action == CHERRY_PICK) { + if (opts->action == REPLAY_PICK) { struct option cp_extra[] = { OPT_BOOLEAN('x', NULL, &opts->record_origin, "append commit name"), OPT_BOOLEAN(0, "ff", &opts->allow_ff, "allow fast-forward"), @@ -374,7 +378,7 @@ static int error_dirty_index(struct replay_opts *opts) return error_resolve_conflict(action_name(opts)); /* Different translation strings for cherry-pick and revert */ - if (opts->action == CHERRY_PICK) + if (opts->action == REPLAY_PICK) error(_("Your local changes would be overwritten by cherry-pick.")); else error(_("Your local changes would be overwritten by revert.")); @@ -553,7 +557,7 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) defmsg = git_pathdup("MERGE_MSG"); - if (opts->action == REVERT) { + if (opts->action == REPLAY_REVERT) { base = commit; base_label = msg.label; next = parent; @@ -594,7 +598,7 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) } } - if (!opts->strategy || !strcmp(opts->strategy, "recursive") || opts->action == REVERT) { + if (!opts->strategy || !strcmp(opts->strategy, "recursive") || opts->action == REPLAY_REVERT) { res = do_recursive_merge(base, next, base_label, next_label, head, &msgbuf, opts); write_message(&msgbuf, defmsg); @@ -618,13 +622,13 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) * However, if the merge did not even start, then we don't want to * write it at all. */ - if (opts->action == CHERRY_PICK && !opts->no_commit && (res == 0 || res == 1)) + if (opts->action == REPLAY_PICK && !opts->no_commit && (res == 0 || res == 1)) write_cherry_pick_head(commit, "CHERRY_PICK_HEAD"); - if (opts->action == REVERT && ((opts->no_commit && res == 0) || res == 1)) + if (opts->action == REPLAY_REVERT && ((opts->no_commit && res == 0) || res == 1)) write_cherry_pick_head(commit, "REVERT_HEAD"); if (res) { - error(opts->action == REVERT + error(opts->action == REPLAY_REVERT ? _("could not revert %s... %s") : _("could not apply %s... %s"), find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV), @@ -644,7 +648,7 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts) static void prepare_revs(struct replay_opts *opts) { - if (opts->action != REVERT) + if (opts->action != REPLAY_REVERT) opts->revs->reverse ^= 1; if (prepare_revision_walk(opts->revs)) @@ -701,7 +705,7 @@ static int format_todo(struct strbuf *buf, struct commit_list *todo_list, { struct commit_list *cur = NULL; const char *sha1_abbrev = NULL; - const char *action_str = opts->action == REVERT ? "revert" : "pick"; + const char *action_str = opts->action == REPLAY_REVERT ? "revert" : "pick"; const char *subject; int subject_len; @@ -722,10 +726,10 @@ static struct commit *parse_insn_line(char *bol, char *eol, struct replay_opts * int saved, status, padding; if (!prefixcmp(bol, "pick")) { - action = CHERRY_PICK; + action = REPLAY_PICK; bol += strlen("pick"); } else if (!prefixcmp(bol, "revert")) { - action = REVERT; + action = REPLAY_REVERT; bol += strlen("revert"); } else return NULL; @@ -748,7 +752,7 @@ static struct commit *parse_insn_line(char *bol, char *eol, struct replay_opts * */ if (action != opts->action) { const char *action_str; - action_str = action == REVERT ? "revert" : "cherry-pick"; + action_str = action == REPLAY_REVERT ? "revert" : "cherry-pick"; error(_("Cannot %s during a %s"), action_str, action_name(opts)); return NULL; } @@ -1124,7 +1128,7 @@ static int pick_revisions(struct replay_opts *opts) if (create_seq_dir() < 0) return -1; if (get_sha1("HEAD", sha1)) { - if (opts->action == REVERT) + if (opts->action == REPLAY_REVERT) return error(_("Can't revert as initial commit")); return error(_("Can't cherry-pick into empty head")); } @@ -1141,7 +1145,7 @@ int cmd_revert(int argc, const char **argv, const char *prefix) memset(&opts, 0, sizeof(opts)); if (isatty(0)) opts.edit = 1; - opts.action = REVERT; + opts.action = REPLAY_REVERT; git_config(git_default_config, NULL); parse_args(argc, argv, &opts); res = pick_revisions(&opts); @@ -1156,7 +1160,7 @@ int cmd_cherry_pick(int argc, const char **argv, const char *prefix) int res; memset(&opts, 0, sizeof(opts)); - opts.action = CHERRY_PICK; + opts.action = REPLAY_PICK; git_config(git_default_config, NULL); parse_args(argc, argv, &opts); res = pick_revisions(&opts); -- cgit v1.2.1 From 043a4492b3b7da6496617201c369cff6ab7c26f2 Mon Sep 17 00:00:00 2001 From: Ramkumar Ramachandra Date: Wed, 11 Jan 2012 23:45:57 +0530 Subject: sequencer: factor code out of revert builtin Expose the cherry-picking machinery through a public sequencer_pick_revisions() (renamed from pick_revisions() in builtin/revert.c), so that cherry-picking and reverting are special cases of a general sequencer operation. The cherry-pick builtin is now a thin wrapper that does command-line argument parsing before calling into sequencer_pick_revisions(). In the future, we can write a new "foo" builtin that calls into the sequencer like: memset(&opts, 0, sizeof(opts)); opts.action = REPLAY_FOO; opts.revisions = xmalloc(sizeof(*opts.revs)); parse_args_populate_opts(argc, argv, &opts); init_revisions(opts.revs); sequencer_pick_revisions(&opts); This patch does not intend to make any functional changes. Check with: $ git blame -s -C HEAD^..HEAD -- sequencer.c | grep -C3 '^[^^]' Signed-off-by: Ramkumar Ramachandra Reviewed-by: Jonathan Nieder Signed-off-by: Junio C Hamano --- builtin/revert.c | 948 +------------------------------------------------------ sequencer.c | 918 ++++++++++++++++++++++++++++++++++++++++++++++++++++- sequencer.h | 37 +++ 3 files changed, 956 insertions(+), 947 deletions(-) diff --git a/builtin/revert.c b/builtin/revert.c index 2739405766..e6840f23dc 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -1,18 +1,9 @@ #include "cache.h" #include "builtin.h" -#include "object.h" -#include "commit.h" -#include "tag.h" -#include "run-command.h" -#include "exec_cmd.h" -#include "utf8.h" #include "parse-options.h" -#include "cache-tree.h" #include "diff.h" #include "revision.h" #include "rerere.h" -#include "merge-recursive.h" -#include "refs.h" #include "dir.h" #include "sequencer.h" @@ -39,50 +30,11 @@ static const char * const cherry_pick_usage[] = { NULL }; -enum replay_action { - REPLAY_REVERT, - REPLAY_PICK -}; - -enum replay_subcommand { - REPLAY_NONE, - REPLAY_REMOVE_STATE, - REPLAY_CONTINUE, - REPLAY_ROLLBACK -}; - -struct replay_opts { - enum replay_action action; - enum replay_subcommand subcommand; - - /* Boolean options */ - int edit; - int record_origin; - int no_commit; - int signoff; - int allow_ff; - int allow_rerere_auto; - - int mainline; - - /* Merge strategy */ - const char *strategy; - const char **xopts; - size_t xopts_nr, xopts_alloc; - - /* Only used by REPLAY_NONE */ - struct rev_info *revs; -}; - -#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" - static const char *action_name(const struct replay_opts *opts) { return opts->action == REPLAY_REVERT ? "revert" : "cherry-pick"; } -static char *get_encoding(const char *message); - static const char * const *revert_or_cherry_pick_usage(struct replay_opts *opts) { return opts->action == REPLAY_REVERT ? revert_usage : cherry_pick_usage; @@ -241,902 +193,6 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts) usage_with_options(usage_str, options); } -struct commit_message { - char *parent_label; - const char *label; - const char *subject; - char *reencoded_message; - const char *message; -}; - -static int get_message(struct commit *commit, struct commit_message *out) -{ - const char *encoding; - const char *abbrev, *subject; - int abbrev_len, subject_len; - char *q; - - if (!commit->buffer) - return -1; - encoding = get_encoding(commit->buffer); - if (!encoding) - encoding = "UTF-8"; - if (!git_commit_encoding) - git_commit_encoding = "UTF-8"; - - out->reencoded_message = NULL; - out->message = commit->buffer; - if (strcmp(encoding, git_commit_encoding)) - out->reencoded_message = reencode_string(commit->buffer, - git_commit_encoding, encoding); - if (out->reencoded_message) - out->message = out->reencoded_message; - - abbrev = find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV); - abbrev_len = strlen(abbrev); - - subject_len = find_commit_subject(out->message, &subject); - - out->parent_label = xmalloc(strlen("parent of ") + abbrev_len + - strlen("... ") + subject_len + 1); - q = out->parent_label; - q = mempcpy(q, "parent of ", strlen("parent of ")); - out->label = q; - q = mempcpy(q, abbrev, abbrev_len); - q = mempcpy(q, "... ", strlen("... ")); - out->subject = q; - q = mempcpy(q, subject, subject_len); - *q = '\0'; - return 0; -} - -static void free_message(struct commit_message *msg) -{ - free(msg->parent_label); - free(msg->reencoded_message); -} - -static char *get_encoding(const char *message) -{ - const char *p = message, *eol; - - while (*p && *p != '\n') { - for (eol = p + 1; *eol && *eol != '\n'; eol++) - ; /* do nothing */ - if (!prefixcmp(p, "encoding ")) { - char *result = xmalloc(eol - 8 - p); - strlcpy(result, p + 9, eol - 8 - p); - return result; - } - p = eol; - if (*p == '\n') - p++; - } - return NULL; -} - -static void write_cherry_pick_head(struct commit *commit, const char *pseudoref) -{ - const char *filename; - int fd; - struct strbuf buf = STRBUF_INIT; - - strbuf_addf(&buf, "%s\n", sha1_to_hex(commit->object.sha1)); - - filename = git_path("%s", pseudoref); - fd = open(filename, O_WRONLY | O_CREAT, 0666); - if (fd < 0) - die_errno(_("Could not open '%s' for writing"), filename); - if (write_in_full(fd, buf.buf, buf.len) != buf.len || close(fd)) - die_errno(_("Could not write to '%s'"), filename); - strbuf_release(&buf); -} - -static void print_advice(int show_hint) -{ - char *msg = getenv("GIT_CHERRY_PICK_HELP"); - - if (msg) { - fprintf(stderr, "%s\n", msg); - /* - * A conflict has occured but the porcelain - * (typically rebase --interactive) wants to take care - * of the commit itself so remove CHERRY_PICK_HEAD - */ - unlink(git_path("CHERRY_PICK_HEAD")); - return; - } - - if (show_hint) { - advise("after resolving the conflicts, mark the corrected paths"); - advise("with 'git add ' or 'git rm '"); - advise("and commit the result with 'git commit'"); - } -} - -static void write_message(struct strbuf *msgbuf, const char *filename) -{ - static struct lock_file msg_file; - - int msg_fd = hold_lock_file_for_update(&msg_file, filename, - LOCK_DIE_ON_ERROR); - if (write_in_full(msg_fd, msgbuf->buf, msgbuf->len) < 0) - die_errno(_("Could not write to %s"), filename); - strbuf_release(msgbuf); - if (commit_lock_file(&msg_file) < 0) - die(_("Error wrapping up %s"), filename); -} - -static struct tree *empty_tree(void) -{ - return lookup_tree((const unsigned char *)EMPTY_TREE_SHA1_BIN); -} - -static int error_dirty_index(struct replay_opts *opts) -{ - if (read_cache_unmerged()) - return error_resolve_conflict(action_name(opts)); - - /* Different translation strings for cherry-pick and revert */ - if (opts->action == REPLAY_PICK) - error(_("Your local changes would be overwritten by cherry-pick.")); - else - error(_("Your local changes would be overwritten by revert.")); - - if (advice_commit_before_merge) - advise(_("Commit your changes or stash them to proceed.")); - return -1; -} - -static int fast_forward_to(const unsigned char *to, const unsigned char *from) -{ - struct ref_lock *ref_lock; - - read_cache(); - if (checkout_fast_forward(from, to)) - exit(1); /* the callee should have complained already */ - ref_lock = lock_any_ref_for_update("HEAD", from, 0); - return write_ref_sha1(ref_lock, to, "cherry-pick"); -} - -static int do_recursive_merge(struct commit *base, struct commit *next, - const char *base_label, const char *next_label, - unsigned char *head, struct strbuf *msgbuf, - struct replay_opts *opts) -{ - struct merge_options o; - struct tree *result, *next_tree, *base_tree, *head_tree; - int clean, index_fd; - const char **xopt; - static struct lock_file index_lock; - - index_fd = hold_locked_index(&index_lock, 1); - - read_cache(); - - init_merge_options(&o); - o.ancestor = base ? base_label : "(empty tree)"; - o.branch1 = "HEAD"; - o.branch2 = next ? next_label : "(empty tree)"; - - head_tree = parse_tree_indirect(head); - next_tree = next ? next->tree : empty_tree(); - base_tree = base ? base->tree : empty_tree(); - - for (xopt = opts->xopts; xopt != opts->xopts + opts->xopts_nr; xopt++) - parse_merge_opt(&o, *xopt); - - clean = merge_trees(&o, - head_tree, - next_tree, base_tree, &result); - - if (active_cache_changed && - (write_cache(index_fd, active_cache, active_nr) || - commit_locked_index(&index_lock))) - /* TRANSLATORS: %s will be "revert" or "cherry-pick" */ - die(_("%s: Unable to write new index file"), action_name(opts)); - rollback_lock_file(&index_lock); - - if (!clean) { - int i; - strbuf_addstr(msgbuf, "\nConflicts:\n\n"); - for (i = 0; i < active_nr;) { - struct cache_entry *ce = active_cache[i++]; - if (ce_stage(ce)) { - strbuf_addch(msgbuf, '\t'); - strbuf_addstr(msgbuf, ce->name); - strbuf_addch(msgbuf, '\n'); - while (i < active_nr && !strcmp(ce->name, - active_cache[i]->name)) - i++; - } - } - } - - return !clean; -} - -/* - * If we are cherry-pick, and if the merge did not result in - * hand-editing, we will hit this commit and inherit the original - * author date and name. - * If we are revert, or if our cherry-pick results in a hand merge, - * we had better say that the current user is responsible for that. - */ -static int run_git_commit(const char *defmsg, struct replay_opts *opts) -{ - /* 6 is max possible length of our args array including NULL */ - const char *args[6]; - int i = 0; - - args[i++] = "commit"; - args[i++] = "-n"; - if (opts->signoff) - args[i++] = "-s"; - if (!opts->edit) { - args[i++] = "-F"; - args[i++] = defmsg; - } - args[i] = NULL; - - return run_command_v_opt(args, RUN_GIT_CMD); -} - -static int do_pick_commit(struct commit *commit, struct replay_opts *opts) -{ - unsigned char head[20]; - struct commit *base, *next, *parent; - const char *base_label, *next_label; - struct commit_message msg = { NULL, NULL, NULL, NULL, NULL }; - char *defmsg = NULL; - struct strbuf msgbuf = STRBUF_INIT; - int res; - - if (opts->no_commit) { - /* - * We do not intend to commit immediately. We just want to - * merge the differences in, so let's compute the tree - * that represents the "current" state for merge-recursive - * to work on. - */ - if (write_cache_as_tree(head, 0, NULL)) - die (_("Your index file is unmerged.")); - } else { - if (get_sha1("HEAD", head)) - return error(_("You do not have a valid HEAD")); - if (index_differs_from("HEAD", 0)) - return error_dirty_index(opts); - } - discard_cache(); - - if (!commit->parents) { - parent = NULL; - } - else if (commit->parents->next) { - /* Reverting or cherry-picking a merge commit */ - int cnt; - struct commit_list *p; - - if (!opts->mainline) - return error(_("Commit %s is a merge but no -m option was given."), - sha1_to_hex(commit->object.sha1)); - - for (cnt = 1, p = commit->parents; - cnt != opts->mainline && p; - cnt++) - p = p->next; - if (cnt != opts->mainline || !p) - return error(_("Commit %s does not have parent %d"), - sha1_to_hex(commit->object.sha1), opts->mainline); - parent = p->item; - } else if (0 < opts->mainline) - return error(_("Mainline was specified but commit %s is not a merge."), - sha1_to_hex(commit->object.sha1)); - else - parent = commit->parents->item; - - if (opts->allow_ff && parent && !hashcmp(parent->object.sha1, head)) - return fast_forward_to(commit->object.sha1, head); - - if (parent && parse_commit(parent) < 0) - /* TRANSLATORS: The first %s will be "revert" or - "cherry-pick", the second %s a SHA1 */ - return error(_("%s: cannot parse parent commit %s"), - action_name(opts), sha1_to_hex(parent->object.sha1)); - - if (get_message(commit, &msg) != 0) - return error(_("Cannot get commit message for %s"), - sha1_to_hex(commit->object.sha1)); - - /* - * "commit" is an existing commit. We would want to apply - * the difference it introduces since its first parent "prev" - * on top of the current HEAD if we are cherry-pick. Or the - * reverse of it if we are revert. - */ - - defmsg = git_pathdup("MERGE_MSG"); - - if (opts->action == REPLAY_REVERT) { - base = commit; - base_label = msg.label; - next = parent; - next_label = msg.parent_label; - strbuf_addstr(&msgbuf, "Revert \""); - strbuf_addstr(&msgbuf, msg.subject); - strbuf_addstr(&msgbuf, "\"\n\nThis reverts commit "); - strbuf_addstr(&msgbuf, sha1_to_hex(commit->object.sha1)); - - if (commit->parents && commit->parents->next) { - strbuf_addstr(&msgbuf, ", reversing\nchanges made to "); - strbuf_addstr(&msgbuf, sha1_to_hex(parent->object.sha1)); - } - strbuf_addstr(&msgbuf, ".\n"); - } else { - const char *p; - - base = parent; - base_label = msg.parent_label; - next = commit; - next_label = msg.label; - - /* - * Append the commit log message to msgbuf; it starts - * after the tree, parent, author, committer - * information followed by "\n\n". - */ - p = strstr(msg.message, "\n\n"); - if (p) { - p += 2; - strbuf_addstr(&msgbuf, p); - } - - if (opts->record_origin) { - strbuf_addstr(&msgbuf, "(cherry picked from commit "); - strbuf_addstr(&msgbuf, sha1_to_hex(commit->object.sha1)); - strbuf_addstr(&msgbuf, ")\n"); - } - } - - if (!opts->strategy || !strcmp(opts->strategy, "recursive") || opts->action == REPLAY_REVERT) { - res = do_recursive_merge(base, next, base_label, next_label, - head, &msgbuf, opts); - write_message(&msgbuf, defmsg); - } else { - struct commit_list *common = NULL; - struct commit_list *remotes = NULL; - - write_message(&msgbuf, defmsg); - - commit_list_insert(base, &common); - commit_list_insert(next, &remotes); - res = try_merge_command(opts->strategy, opts->xopts_nr, opts->xopts, - common, sha1_to_hex(head), remotes); - free_commit_list(common); - free_commit_list(remotes); - } - - /* - * If the merge was clean or if it failed due to conflict, we write - * CHERRY_PICK_HEAD for the subsequent invocation of commit to use. - * However, if the merge did not even start, then we don't want to - * write it at all. - */ - if (opts->action == REPLAY_PICK && !opts->no_commit && (res == 0 || res == 1)) - write_cherry_pick_head(commit, "CHERRY_PICK_HEAD"); - if (opts->action == REPLAY_REVERT && ((opts->no_commit && res == 0) || res == 1)) - write_cherry_pick_head(commit, "REVERT_HEAD"); - - if (res) { - error(opts->action == REPLAY_REVERT - ? _("could not revert %s... %s") - : _("could not apply %s... %s"), - find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV), - msg.subject); - print_advice(res == 1); - rerere(opts->allow_rerere_auto); - } else { - if (!opts->no_commit) - res = run_git_commit(defmsg, opts); - } - - free_message(&msg); - free(defmsg); - - return res; -} - -static void prepare_revs(struct replay_opts *opts) -{ - if (opts->action != REPLAY_REVERT) - opts->revs->reverse ^= 1; - - if (prepare_revision_walk(opts->revs)) - die(_("revision walk setup failed")); - - if (!opts->revs->commits) - die(_("empty commit set passed")); -} - -static void read_and_refresh_cache(struct replay_opts *opts) -{ - static struct lock_file index_lock; - int index_fd = hold_locked_index(&index_lock, 0); - if (read_index_preload(&the_index, NULL) < 0) - die(_("git %s: failed to read the index"), action_name(opts)); - refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, NULL); - if (the_index.cache_changed) { - if (write_index(&the_index, index_fd) || - commit_locked_index(&index_lock)) - die(_("git %s: failed to refresh the index"), action_name(opts)); - } - rollback_lock_file(&index_lock); -} - -/* - * Append a commit to the end of the commit_list. - * - * next starts by pointing to the variable that holds the head of an - * empty commit_list, and is updated to point to the "next" field of - * the last item on the list as new commits are appended. - * - * Usage example: - * - * struct commit_list *list; - * struct commit_list **next = &list; - * - * next = commit_list_append(c1, next); - * next = commit_list_append(c2, next); - * assert(commit_list_count(list) == 2); - * return list; - */ -static struct commit_list **commit_list_append(struct commit *commit, - struct commit_list **next) -{ - struct commit_list *new = xmalloc(sizeof(struct commit_list)); - new->item = commit; - *next = new; - new->next = NULL; - return &new->next; -} - -static int format_todo(struct strbuf *buf, struct commit_list *todo_list, - struct replay_opts *opts) -{ - struct commit_list *cur = NULL; - const char *sha1_abbrev = NULL; - const char *action_str = opts->action == REPLAY_REVERT ? "revert" : "pick"; - const char *subject; - int subject_len; - - for (cur = todo_list; cur; cur = cur->next) { - sha1_abbrev = find_unique_abbrev(cur->item->object.sha1, DEFAULT_ABBREV); - subject_len = find_commit_subject(cur->item->buffer, &subject); - strbuf_addf(buf, "%s %s %.*s\n", action_str, sha1_abbrev, - subject_len, subject); - } - return 0; -} - -static struct commit *parse_insn_line(char *bol, char *eol, struct replay_opts *opts) -{ - unsigned char commit_sha1[20]; - enum replay_action action; - char *end_of_object_name; - int saved, status, padding; - - if (!prefixcmp(bol, "pick")) { - action = REPLAY_PICK; - bol += strlen("pick"); - } else if (!prefixcmp(bol, "revert")) { - action = REPLAY_REVERT; - bol += strlen("revert"); - } else - return NULL; - - /* Eat up extra spaces/ tabs before object name */ - padding = strspn(bol, " \t"); - if (!padding) - return NULL; - bol += padding; - - end_of_object_name = bol + strcspn(bol, " \t\n"); - saved = *end_of_object_name; - *end_of_object_name = '\0'; - status = get_sha1(bol, commit_sha1); - *end_of_object_name = saved; - - /* - * Verify that the action matches up with the one in - * opts; we don't support arbitrary instructions - */ - if (action != opts->action) { - const char *action_str; - action_str = action == REPLAY_REVERT ? "revert" : "cherry-pick"; - error(_("Cannot %s during a %s"), action_str, action_name(opts)); - return NULL; - } - - if (status < 0) - return NULL; - - return lookup_commit_reference(commit_sha1); -} - -static int parse_insn_buffer(char *buf, struct commit_list **todo_list, - struct replay_opts *opts) -{ - struct commit_list **next = todo_list; - struct commit *commit; - char *p = buf; - int i; - - for (i = 1; *p; i++) { - char *eol = strchrnul(p, '\n'); - commit = parse_insn_line(p, eol, opts); - if (!commit) - return error(_("Could not parse line %d."), i); - next = commit_list_append(commit, next); - p = *eol ? eol + 1 : eol; - } - if (!*todo_list) - return error(_("No commits parsed.")); - return 0; -} - -static void read_populate_todo(struct commit_list **todo_list, - struct replay_opts *opts) -{ - const char *todo_file = git_path(SEQ_TODO_FILE); - struct strbuf buf = STRBUF_INIT; - int fd, res; - - fd = open(todo_file, O_RDONLY); - if (fd < 0) - die_errno(_("Could not open %s"), todo_file); - if (strbuf_read(&buf, fd, 0) < 0) { - close(fd); - strbuf_release(&buf); - die(_("Could not read %s."), todo_file); - } - close(fd); - - res = parse_insn_buffer(buf.buf, todo_list, opts); - strbuf_release(&buf); - if (res) - die(_("Unusable instruction sheet: %s"), todo_file); -} - -static int populate_opts_cb(const char *key, const char *value, void *data) -{ - struct replay_opts *opts = data; - int error_flag = 1; - - if (!value) - error_flag = 0; - else if (!strcmp(key, "options.no-commit")) - opts->no_commit = git_config_bool_or_int(key, value, &error_flag); - else if (!strcmp(key, "options.edit")) - opts->edit = git_config_bool_or_int(key, value, &error_flag); - else if (!strcmp(key, "options.signoff")) - opts->signoff = git_config_bool_or_int(key, value, &error_flag); - else if (!strcmp(key, "options.record-origin")) - opts->record_origin = git_config_bool_or_int(key, value, &error_flag); - else if (!strcmp(key, "options.allow-ff")) - opts->allow_ff = git_config_bool_or_int(key, value, &error_flag); - else if (!strcmp(key, "options.mainline")) - opts->mainline = git_config_int(key, value); - else if (!strcmp(key, "options.strategy")) - git_config_string(&opts->strategy, key, value); - else if (!strcmp(key, "options.strategy-option")) { - ALLOC_GROW(opts->xopts, opts->xopts_nr + 1, opts->xopts_alloc); - opts->xopts[opts->xopts_nr++] = xstrdup(value); - } else - return error(_("Invalid key: %s"), key); - - if (!error_flag) - return error(_("Invalid value for %s: %s"), key, value); - - return 0; -} - -static void read_populate_opts(struct replay_opts **opts_ptr) -{ - const char *opts_file = git_path(SEQ_OPTS_FILE); - - if (!file_exists(opts_file)) - return; - if (git_config_from_file(populate_opts_cb, opts_file, *opts_ptr) < 0) - die(_("Malformed options sheet: %s"), opts_file); -} - -static void walk_revs_populate_todo(struct commit_list **todo_list, - struct replay_opts *opts) -{ - struct commit *commit; - struct commit_list **next; - - prepare_revs(opts); - - next = todo_list; - while ((commit = get_revision(opts->revs))) - next = commit_list_append(commit, next); -} - -static int create_seq_dir(void) -{ - const char *seq_dir = git_path(SEQ_DIR); - - if (file_exists(seq_dir)) { - error(_("a cherry-pick or revert is already in progress")); - advise(_("try \"git cherry-pick (--continue | --quit | --abort)\"")); - return -1; - } - else if (mkdir(seq_dir, 0777) < 0) - die_errno(_("Could not create sequencer directory %s"), seq_dir); - return 0; -} - -static void save_head(const char *head) -{ - const char *head_file = git_path(SEQ_HEAD_FILE); - static struct lock_file head_lock; - struct strbuf buf = STRBUF_INIT; - int fd; - - fd = hold_lock_file_for_update(&head_lock, head_file, LOCK_DIE_ON_ERROR); - strbuf_addf(&buf, "%s\n", head); - if (write_in_full(fd, buf.buf, buf.len) < 0) - die_errno(_("Could not write to %s"), head_file); - if (commit_lock_file(&head_lock) < 0) - die(_("Error wrapping up %s."), head_file); -} - -static int reset_for_rollback(const unsigned char *sha1) -{ - const char *argv[4]; /* reset --merge + NULL */ - argv[0] = "reset"; - argv[1] = "--merge"; - argv[2] = sha1_to_hex(sha1); - argv[3] = NULL; - return run_command_v_opt(argv, RUN_GIT_CMD); -} - -static int rollback_single_pick(void) -{ - unsigned char head_sha1[20]; - - if (!file_exists(git_path("CHERRY_PICK_HEAD")) && - !file_exists(git_path("REVERT_HEAD"))) - return error(_("no cherry-pick or revert in progress")); - if (read_ref_full("HEAD", head_sha1, 0, NULL)) - return error(_("cannot resolve HEAD")); - if (is_null_sha1(head_sha1)) - return error(_("cannot abort from a branch yet to be born")); - return reset_for_rollback(head_sha1); -} - -static int sequencer_rollback(struct replay_opts *opts) -{ - const char *filename; - FILE *f; - unsigned char sha1[20]; - struct strbuf buf = STRBUF_INIT; - - filename = git_path(SEQ_HEAD_FILE); - f = fopen(filename, "r"); - if (!f && errno == ENOENT) { - /* - * There is no multiple-cherry-pick in progress. - * If CHERRY_PICK_HEAD or REVERT_HEAD indicates - * a single-cherry-pick in progress, abort that. - */ - return rollback_single_pick(); - } - if (!f) - return error(_("cannot open %s: %s"), filename, - strerror(errno)); - if (strbuf_getline(&buf, f, '\n')) { - error(_("cannot read %s: %s"), filename, ferror(f) ? - strerror(errno) : _("unexpected end of file")); - fclose(f); - goto fail; - } - fclose(f); - if (get_sha1_hex(buf.buf, sha1) || buf.buf[40] != '\0') { - error(_("stored pre-cherry-pick HEAD file '%s' is corrupt"), - filename); - goto fail; - } - if (reset_for_rollback(sha1)) - goto fail; - remove_sequencer_state(); - strbuf_release(&buf); - return 0; -fail: - strbuf_release(&buf); - return -1; -} - -static void save_todo(struct commit_list *todo_list, struct replay_opts *opts) -{ - const char *todo_file = git_path(SEQ_TODO_FILE); - static struct lock_file todo_lock; - struct strbuf buf = STRBUF_INIT; - int fd; - - fd = hold_lock_file_for_update(&todo_lock, todo_file, LOCK_DIE_ON_ERROR); - if (format_todo(&buf, todo_list, opts) < 0) - die(_("Could not format %s."), todo_file); - if (write_in_full(fd, buf.buf, buf.len) < 0) { - strbuf_release(&buf); - die_errno(_("Could not write to %s"), todo_file); - } - if (commit_lock_file(&todo_lock) < 0) { - strbuf_release(&buf); - die(_("Error wrapping up %s."), todo_file); - } - strbuf_release(&buf); -} - -static void save_opts(struct replay_opts *opts) -{ - const char *opts_file = git_path(SEQ_OPTS_FILE); - - if (opts->no_commit) - git_config_set_in_file(opts_file, "options.no-commit", "true"); - if (opts->edit) - git_config_set_in_file(opts_file, "options.edit", "true"); - if (opts->signoff) - git_config_set_in_file(opts_file, "options.signoff", "true"); - if (opts->record_origin) - git_config_set_in_file(opts_file, "options.record-origin", "true"); - if (opts->allow_ff) - git_config_set_in_file(opts_file, "options.allow-ff", "true"); - if (opts->mainline) { - struct strbuf buf = STRBUF_INIT; - strbuf_addf(&buf, "%d", opts->mainline); - git_config_set_in_file(opts_file, "options.mainline", buf.buf); - strbuf_release(&buf); - } - if (opts->strategy) - git_config_set_in_file(opts_file, "options.strategy", opts->strategy); - if (opts->xopts) { - int i; - for (i = 0; i < opts->xopts_nr; i++) - git_config_set_multivar_in_file(opts_file, - "options.strategy-option", - opts->xopts[i], "^$", 0); - } -} - -static int pick_commits(struct commit_list *todo_list, struct replay_opts *opts) -{ - struct commit_list *cur; - int res; - - setenv(GIT_REFLOG_ACTION, action_name(opts), 0); - if (opts->allow_ff) - assert(!(opts->signoff || opts->no_commit || - opts->record_origin || opts->edit)); - read_and_refresh_cache(opts); - - for (cur = todo_list; cur; cur = cur->next) { - save_todo(cur, opts); - res = do_pick_commit(cur->item, opts); - if (res) - return res; - } - - /* - * Sequence of picks finished successfully; cleanup by - * removing the .git/sequencer directory - */ - remove_sequencer_state(); - return 0; -} - -static int continue_single_pick(void) -{ - const char *argv[] = { "commit", NULL }; - - if (!file_exists(git_path("CHERRY_PICK_HEAD")) && - !file_exists(git_path("REVERT_HEAD"))) - return error(_("no cherry-pick or revert in progress")); - return run_command_v_opt(argv, RUN_GIT_CMD); -} - -static int sequencer_continue(struct replay_opts *opts) -{ - struct commit_list *todo_list = NULL; - - if (!file_exists(git_path(SEQ_TODO_FILE))) - return continue_single_pick(); - read_populate_opts(&opts); - read_populate_todo(&todo_list, opts); - - /* Verify that the conflict has been resolved */ - if (file_exists(git_path("CHERRY_PICK_HEAD")) || - file_exists(git_path("REVERT_HEAD"))) { - int ret = continue_single_pick(); - if (ret) - return ret; - } - if (index_differs_from("HEAD", 0)) - return error_dirty_index(opts); - todo_list = todo_list->next; - return pick_commits(todo_list, opts); -} - -static int single_pick(struct commit *cmit, struct replay_opts *opts) -{ - setenv(GIT_REFLOG_ACTION, action_name(opts), 0); - return do_pick_commit(cmit, opts); -} - -static int pick_revisions(struct replay_opts *opts) -{ - struct commit_list *todo_list = NULL; - unsigned char sha1[20]; - - if (opts->subcommand == REPLAY_NONE) - assert(opts->revs); - - read_and_refresh_cache(opts); - - /* - * Decide what to do depending on the arguments; a fresh - * cherry-pick should be handled differently from an existing - * one that is being continued - */ - if (opts->subcommand == REPLAY_REMOVE_STATE) { - remove_sequencer_state(); - return 0; - } - if (opts->subcommand == REPLAY_ROLLBACK) - return sequencer_rollback(opts); - if (opts->subcommand == REPLAY_CONTINUE) - return sequencer_continue(opts); - - /* - * If we were called as "git cherry-pick ", just - * cherry-pick/revert it, set CHERRY_PICK_HEAD / - * REVERT_HEAD, and don't touch the sequencer state. - * This means it is possible to cherry-pick in the middle - * of a cherry-pick sequence. - */ - if (opts->revs->cmdline.nr == 1 && - opts->revs->cmdline.rev->whence == REV_CMD_REV && - opts->revs->no_walk && - !opts->revs->cmdline.rev->flags) { - struct commit *cmit; - if (prepare_revision_walk(opts->revs)) - die(_("revision walk setup failed")); - cmit = get_revision(opts->revs); - if (!cmit || get_revision(opts->revs)) - die("BUG: expected exactly one commit from walk"); - return single_pick(cmit, opts); - } - - /* - * Start a new cherry-pick/ revert sequence; but - * first, make sure that an existing one isn't in - * progress - */ - - walk_revs_populate_todo(&todo_list, opts); - if (create_seq_dir() < 0) - return -1; - if (get_sha1("HEAD", sha1)) { - if (opts->action == REPLAY_REVERT) - return error(_("Can't revert as initial commit")); - return error(_("Can't cherry-pick into empty head")); - } - save_head(sha1_to_hex(sha1)); - save_opts(opts); - return pick_commits(todo_list, opts); -} - int cmd_revert(int argc, const char **argv, const char *prefix) { struct replay_opts opts; @@ -1148,7 +204,7 @@ int cmd_revert(int argc, const char **argv, const char *prefix) opts.action = REPLAY_REVERT; git_config(git_default_config, NULL); parse_args(argc, argv, &opts); - res = pick_revisions(&opts); + res = sequencer_pick_revisions(&opts); if (res < 0) die(_("revert failed")); return res; @@ -1163,7 +219,7 @@ int cmd_cherry_pick(int argc, const char **argv, const char *prefix) opts.action = REPLAY_PICK; git_config(git_default_config, NULL); parse_args(argc, argv, &opts); - res = pick_revisions(&opts); + res = sequencer_pick_revisions(&opts); if (res < 0) die(_("cherry-pick failed")); return res; diff --git a/sequencer.c b/sequencer.c index d1f28a6945..54771196c3 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1,7 +1,20 @@ #include "cache.h" #include "sequencer.h" -#include "strbuf.h" #include "dir.h" +#include "object.h" +#include "commit.h" +#include "tag.h" +#include "run-command.h" +#include "exec_cmd.h" +#include "utf8.h" +#include "cache-tree.h" +#include "diff.h" +#include "revision.h" +#include "rerere.h" +#include "merge-recursive.h" +#include "refs.h" + +#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION" void remove_sequencer_state(void) { @@ -11,3 +24,906 @@ void remove_sequencer_state(void) remove_dir_recursively(&seq_dir, 0); strbuf_release(&seq_dir); } + +static const char *action_name(const struct replay_opts *opts) +{ + return opts->action == REPLAY_REVERT ? "revert" : "cherry-pick"; +} + +static char *get_encoding(const char *message); + +struct commit_message { + char *parent_label; + const char *label; + const char *subject; + char *reencoded_message; + const char *message; +}; + +static int get_message(struct commit *commit, struct commit_message *out) +{ + const char *encoding; + const char *abbrev, *subject; + int abbrev_len, subject_len; + char *q; + + if (!commit->buffer) + return -1; + encoding = get_encoding(commit->buffer); + if (!encoding) + encoding = "UTF-8"; + if (!git_commit_encoding) + git_commit_encoding = "UTF-8"; + + out->reencoded_message = NULL; + out->message = commit->buffer; + if (strcmp(encoding, git_commit_encoding)) + out->reencoded_message = reencode_string(commit->buffer, + git_commit_encoding, encoding); + if (out->reencoded_message) + out->message = out->reencoded_message; + + abbrev = find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV); + abbrev_len = strlen(abbrev); + + subject_len = find_commit_subject(out->message, &subject); + + out->parent_label = xmalloc(strlen("parent of ") + abbrev_len + + strlen("... ") + subject_len + 1); + q = out->parent_label; + q = mempcpy(q, "parent of ", strlen("parent of ")); + out->label = q; + q = mempcpy(q, abbrev, abbrev_len); + q = mempcpy(q, "... ", strlen("... ")); + out->subject = q; + q = mempcpy(q, subject, subject_len); + *q = '\0'; + return 0; +} + +static void free_message(struct commit_message *msg) +{ + free(msg->parent_label); + free(msg->reencoded_message); +} + +static char *get_encoding(const char *message) +{ + const char *p = message, *eol; + + while (*p && *p != '\n') { + for (eol = p + 1; *eol && *eol != '\n'; eol++) + ; /* do nothing */ + if (!prefixcmp(p, "encoding ")) { + char *result = xmalloc(eol - 8 - p); + strlcpy(result, p + 9, eol - 8 - p); + return result; + } + p = eol; + if (*p == '\n') + p++; + } + return NULL; +} + +static void write_cherry_pick_head(struct commit *commit, const char *pseudoref) +{ + const char *filename; + int fd; + struct strbuf buf = STRBUF_INIT; + + strbuf_addf(&buf, "%s\n", sha1_to_hex(commit->object.sha1)); + + filename = git_path("%s", pseudoref); + fd = open(filename, O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die_errno(_("Could not open '%s' for writing"), filename); + if (write_in_full(fd, buf.buf, buf.len) != buf.len || close(fd)) + die_errno(_("Could not write to '%s'"), filename); + strbuf_release(&buf); +} + +static void print_advice(int show_hint) +{ + char *msg = getenv("GIT_CHERRY_PICK_HELP"); + + if (msg) { + fprintf(stderr, "%s\n", msg); + /* + * A conflict has occured but the porcelain + * (typically rebase --interactive) wants to take care + * of the commit itself so remove CHERRY_PICK_HEAD + */ + unlink(git_path("CHERRY_PICK_HEAD")); + return; + } + + if (show_hint) { + advise("after resolving the conflicts, mark the corrected paths"); + advise("with 'git add ' or 'git rm '"); + advise("and commit the result with 'git commit'"); + } +} + +static void write_message(struct strbuf *msgbuf, const char *filename) +{ + static struct lock_file msg_file; + + int msg_fd = hold_lock_file_for_update(&msg_file, filename, + LOCK_DIE_ON_ERROR); + if (write_in_full(msg_fd, msgbuf->buf, msgbuf->len) < 0) + die_errno(_("Could not write to %s"), filename); + strbuf_release(msgbuf); + if (commit_lock_file(&msg_file) < 0) + die(_("Error wrapping up %s"), filename); +} + +static struct tree *empty_tree(void) +{ + return lookup_tree((const unsigned char *)EMPTY_TREE_SHA1_BIN); +} + +static int error_dirty_index(struct replay_opts *opts) +{ + if (read_cache_unmerged()) + return error_resolve_conflict(action_name(opts)); + + /* Different translation strings for cherry-pick and revert */ + if (opts->action == REPLAY_PICK) + error(_("Your local changes would be overwritten by cherry-pick.")); + else + error(_("Your local changes would be overwritten by revert.")); + + if (advice_commit_before_merge) + advise(_("Commit your changes or stash them to proceed.")); + return -1; +} + +static int fast_forward_to(const unsigned char *to, const unsigned char *from) +{ + struct ref_lock *ref_lock; + + read_cache(); + if (checkout_fast_forward(from, to)) + exit(1); /* the callee should have complained already */ + ref_lock = lock_any_ref_for_update("HEAD", from, 0); + return write_ref_sha1(ref_lock, to, "cherry-pick"); +} + +static int do_recursive_merge(struct commit *base, struct commit *next, + const char *base_label, const char *next_label, + unsigned char *head, struct strbuf *msgbuf, + struct replay_opts *opts) +{ + struct merge_options o; + struct tree *result, *next_tree, *base_tree, *head_tree; + int clean, index_fd; + const char **xopt; + static struct lock_file index_lock; + + index_fd = hold_locked_index(&index_lock, 1); + + read_cache(); + + init_merge_options(&o); + o.ancestor = base ? base_label : "(empty tree)"; + o.branch1 = "HEAD"; + o.branch2 = next ? next_label : "(empty tree)"; + + head_tree = parse_tree_indirect(head); + next_tree = next ? next->tree : empty_tree(); + base_tree = base ? base->tree : empty_tree(); + + for (xopt = opts->xopts; xopt != opts->xopts + opts->xopts_nr; xopt++) + parse_merge_opt(&o, *xopt); + + clean = merge_trees(&o, + head_tree, + next_tree, base_tree, &result); + + if (active_cache_changed && + (write_cache(index_fd, active_cache, active_nr) || + commit_locked_index(&index_lock))) + /* TRANSLATORS: %s will be "revert" or "cherry-pick" */ + die(_("%s: Unable to write new index file"), action_name(opts)); + rollback_lock_file(&index_lock); + + if (!clean) { + int i; + strbuf_addstr(msgbuf, "\nConflicts:\n\n"); + for (i = 0; i < active_nr;) { + struct cache_entry *ce = active_cache[i++]; + if (ce_stage(ce)) { + strbuf_addch(msgbuf, '\t'); + strbuf_addstr(msgbuf, ce->name); + strbuf_addch(msgbuf, '\n'); + while (i < active_nr && !strcmp(ce->name, + active_cache[i]->name)) + i++; + } + } + } + + return !clean; +} + +/* + * If we are cherry-pick, and if the merge did not result in + * hand-editing, we will hit this commit and inherit the original + * author date and name. + * If we are revert, or if our cherry-pick results in a hand merge, + * we had better say that the current user is responsible for that. + */ +static int run_git_commit(const char *defmsg, struct replay_opts *opts) +{ + /* 6 is max possible length of our args array including NULL */ + const char *args[6]; + int i = 0; + + args[i++] = "commit"; + args[i++] = "-n"; + if (opts->signoff) + args[i++] = "-s"; + if (!opts->edit) { + args[i++] = "-F"; + args[i++] = defmsg; + } + args[i] = NULL; + + return run_command_v_opt(args, RUN_GIT_CMD); +} + +static int do_pick_commit(struct commit *commit, struct replay_opts *opts) +{ + unsigned char head[20]; + struct commit *base, *next, *parent; + const char *base_label, *next_label; + struct commit_message msg = { NULL, NULL, NULL, NULL, NULL }; + char *defmsg = NULL; + struct strbuf msgbuf = STRBUF_INIT; + int res; + + if (opts->no_commit) { + /* + * We do not intend to commit immediately. We just want to + * merge the differences in, so let's compute the tree + * that represents the "current" state for merge-recursive + * to work on. + */ + if (write_cache_as_tree(head, 0, NULL)) + die (_("Your index file is unmerged.")); + } else { + if (get_sha1("HEAD", head)) + return error(_("You do not have a valid HEAD")); + if (index_differs_from("HEAD", 0)) + return error_dirty_index(opts); + } + discard_cache(); + + if (!commit->parents) { + parent = NULL; + } + else if (commit->parents->next) { + /* Reverting or cherry-picking a merge commit */ + int cnt; + struct commit_list *p; + + if (!opts->mainline) + return error(_("Commit %s is a merge but no -m option was given."), + sha1_to_hex(commit->object.sha1)); + + for (cnt = 1, p = commit->parents; + cnt != opts->mainline && p; + cnt++) + p = p->next; + if (cnt != opts->mainline || !p) + return error(_("Commit %s does not have parent %d"), + sha1_to_hex(commit->object.sha1), opts->mainline); + parent = p->item; + } else if (0 < opts->mainline) + return error(_("Mainline was specified but commit %s is not a merge."), + sha1_to_hex(commit->object.sha1)); + else + parent = commit->parents->item; + + if (opts->allow_ff && parent && !hashcmp(parent->object.sha1, head)) + return fast_forward_to(commit->object.sha1, head); + + if (parent && parse_commit(parent) < 0) + /* TRANSLATORS: The first %s will be "revert" or + "cherry-pick", the second %s a SHA1 */ + return error(_("%s: cannot parse parent commit %s"), + action_name(opts), sha1_to_hex(parent->object.sha1)); + + if (get_message(commit, &msg) != 0) + return error(_("Cannot get commit message for %s"), + sha1_to_hex(commit->object.sha1)); + + /* + * "commit" is an existing commit. We would want to apply + * the difference it introduces since its first parent "prev" + * on top of the current HEAD if we are cherry-pick. Or the + * reverse of it if we are revert. + */ + + defmsg = git_pathdup("MERGE_MSG"); + + if (opts->action == REPLAY_REVERT) { + base = commit; + base_label = msg.label; + next = parent; + next_label = msg.parent_label; + strbuf_addstr(&msgbuf, "Revert \""); + strbuf_addstr(&msgbuf, msg.subject); + strbuf_addstr(&msgbuf, "\"\n\nThis reverts commit "); + strbuf_addstr(&msgbuf, sha1_to_hex(commit->object.sha1)); + + if (commit->parents && commit->parents->next) { + strbuf_addstr(&msgbuf, ", reversing\nchanges made to "); + strbuf_addstr(&msgbuf, sha1_to_hex(parent->object.sha1)); + } + strbuf_addstr(&msgbuf, ".\n"); + } else { + const char *p; + + base = parent; + base_label = msg.parent_label; + next = commit; + next_label = msg.label; + + /* + * Append the commit log message to msgbuf; it starts + * after the tree, parent, author, committer + * information followed by "\n\n". + */ + p = strstr(msg.message, "\n\n"); + if (p) { + p += 2; + strbuf_addstr(&msgbuf, p); + } + + if (opts->record_origin) { + strbuf_addstr(&msgbuf, "(cherry picked from commit "); + strbuf_addstr(&msgbuf, sha1_to_hex(commit->object.sha1)); + strbuf_addstr(&msgbuf, ")\n"); + } + } + + if (!opts->strategy || !strcmp(opts->strategy, "recursive") || opts->action == REPLAY_REVERT) { + res = do_recursive_merge(base, next, base_label, next_label, + head, &msgbuf, opts); + write_message(&msgbuf, defmsg); + } else { + struct commit_list *common = NULL; + struct commit_list *remotes = NULL; + + write_message(&msgbuf, defmsg); + + commit_list_insert(base, &common); + commit_list_insert(next, &remotes); + res = try_merge_command(opts->strategy, opts->xopts_nr, opts->xopts, + common, sha1_to_hex(head), remotes); + free_commit_list(common); + free_commit_list(remotes); + } + + /* + * If the merge was clean or if it failed due to conflict, we write + * CHERRY_PICK_HEAD for the subsequent invocation of commit to use. + * However, if the merge did not even start, then we don't want to + * write it at all. + */ + if (opts->action == REPLAY_PICK && !opts->no_commit && (res == 0 || res == 1)) + write_cherry_pick_head(commit, "CHERRY_PICK_HEAD"); + if (opts->action == REPLAY_REVERT && ((opts->no_commit && res == 0) || res == 1)) + write_cherry_pick_head(commit, "REVERT_HEAD"); + + if (res) { + error(opts->action == REPLAY_REVERT + ? _("could not revert %s... %s") + : _("could not apply %s... %s"), + find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV), + msg.subject); + print_advice(res == 1); + rerere(opts->allow_rerere_auto); + } else { + if (!opts->no_commit) + res = run_git_commit(defmsg, opts); + } + + free_message(&msg); + free(defmsg); + + return res; +} + +static void prepare_revs(struct replay_opts *opts) +{ + if (opts->action != REPLAY_REVERT) + opts->revs->reverse ^= 1; + + if (prepare_revision_walk(opts->revs)) + die(_("revision walk setup failed")); + + if (!opts->revs->commits) + die(_("empty commit set passed")); +} + +static void read_and_refresh_cache(struct replay_opts *opts) +{ + static struct lock_file index_lock; + int index_fd = hold_locked_index(&index_lock, 0); + if (read_index_preload(&the_index, NULL) < 0) + die(_("git %s: failed to read the index"), action_name(opts)); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, NULL); + if (the_index.cache_changed) { + if (write_index(&the_index, index_fd) || + commit_locked_index(&index_lock)) + die(_("git %s: failed to refresh the index"), action_name(opts)); + } + rollback_lock_file(&index_lock); +} + +/* + * Append a commit to the end of the commit_list. + * + * next starts by pointing to the variable that holds the head of an + * empty commit_list, and is updated to point to the "next" field of + * the last item on the list as new commits are appended. + * + * Usage example: + * + * struct commit_list *list; + * struct commit_list **next = &list; + * + * next = commit_list_append(c1, next); + * next = commit_list_append(c2, next); + * assert(commit_list_count(list) == 2); + * return list; + */ +static struct commit_list **commit_list_append(struct commit *commit, + struct commit_list **next) +{ + struct commit_list *new = xmalloc(sizeof(struct commit_list)); + new->item = commit; + *next = new; + new->next = NULL; + return &new->next; +} + +static int format_todo(struct strbuf *buf, struct commit_list *todo_list, + struct replay_opts *opts) +{ + struct commit_list *cur = NULL; + const char *sha1_abbrev = NULL; + const char *action_str = opts->action == REPLAY_REVERT ? "revert" : "pick"; + const char *subject; + int subject_len; + + for (cur = todo_list; cur; cur = cur->next) { + sha1_abbrev = find_unique_abbrev(cur->item->object.sha1, DEFAULT_ABBREV); + subject_len = find_commit_subject(cur->item->buffer, &subject); + strbuf_addf(buf, "%s %s %.*s\n", action_str, sha1_abbrev, + subject_len, subject); + } + return 0; +} + +static struct commit *parse_insn_line(char *bol, char *eol, struct replay_opts *opts) +{ + unsigned char commit_sha1[20]; + enum replay_action action; + char *end_of_object_name; + int saved, status, padding; + + if (!prefixcmp(bol, "pick")) { + action = REPLAY_PICK; + bol += strlen("pick"); + } else if (!prefixcmp(bol, "revert")) { + action = REPLAY_REVERT; + bol += strlen("revert"); + } else + return NULL; + + /* Eat up extra spaces/ tabs before object name */ + padding = strspn(bol, " \t"); + if (!padding) + return NULL; + bol += padding; + + end_of_object_name = bol + strcspn(bol, " \t\n"); + saved = *end_of_object_name; + *end_of_object_name = '\0'; + status = get_sha1(bol, commit_sha1); + *end_of_object_name = saved; + + /* + * Verify that the action matches up with the one in + * opts; we don't support arbitrary instructions + */ + if (action != opts->action) { + const char *action_str; + action_str = action == REPLAY_REVERT ? "revert" : "cherry-pick"; + error(_("Cannot %s during a %s"), action_str, action_name(opts)); + return NULL; + } + + if (status < 0) + return NULL; + + return lookup_commit_reference(commit_sha1); +} + +static int parse_insn_buffer(char *buf, struct commit_list **todo_list, + struct replay_opts *opts) +{ + struct commit_list **next = todo_list; + struct commit *commit; + char *p = buf; + int i; + + for (i = 1; *p; i++) { + char *eol = strchrnul(p, '\n'); + commit = parse_insn_line(p, eol, opts); + if (!commit) + return error(_("Could not parse line %d."), i); + next = commit_list_append(commit, next); + p = *eol ? eol + 1 : eol; + } + if (!*todo_list) + return error(_("No commits parsed.")); + return 0; +} + +static void read_populate_todo(struct commit_list **todo_list, + struct replay_opts *opts) +{ + const char *todo_file = git_path(SEQ_TODO_FILE); + struct strbuf buf = STRBUF_INIT; + int fd, res; + + fd = open(todo_file, O_RDONLY); + if (fd < 0) + die_errno(_("Could not open %s"), todo_file); + if (strbuf_read(&buf, fd, 0) < 0) { + close(fd); + strbuf_release(&buf); + die(_("Could not read %s."), todo_file); + } + close(fd); + + res = parse_insn_buffer(buf.buf, todo_list, opts); + strbuf_release(&buf); + if (res) + die(_("Unusable instruction sheet: %s"), todo_file); +} + +static int populate_opts_cb(const char *key, const char *value, void *data) +{ + struct replay_opts *opts = data; + int error_flag = 1; + + if (!value) + error_flag = 0; + else if (!strcmp(key, "options.no-commit")) + opts->no_commit = git_config_bool_or_int(key, value, &error_flag); + else if (!strcmp(key, "options.edit")) + opts->edit = git_config_bool_or_int(key, value, &error_flag); + else if (!strcmp(key, "options.signoff")) + opts->signoff = git_config_bool_or_int(key, value, &error_flag); + else if (!strcmp(key, "options.record-origin")) + opts->record_origin = git_config_bool_or_int(key, value, &error_flag); + else if (!strcmp(key, "options.allow-ff")) + opts->allow_ff = git_config_bool_or_int(key, value, &error_flag); + else if (!strcmp(key, "options.mainline")) + opts->mainline = git_config_int(key, value); + else if (!strcmp(key, "options.strategy")) + git_config_string(&opts->strategy, key, value); + else if (!strcmp(key, "options.strategy-option")) { + ALLOC_GROW(opts->xopts, opts->xopts_nr + 1, opts->xopts_alloc); + opts->xopts[opts->xopts_nr++] = xstrdup(value); + } else + return error(_("Invalid key: %s"), key); + + if (!error_flag) + return error(_("Invalid value for %s: %s"), key, value); + + return 0; +} + +static void read_populate_opts(struct replay_opts **opts_ptr) +{ + const char *opts_file = git_path(SEQ_OPTS_FILE); + + if (!file_exists(opts_file)) + return; + if (git_config_from_file(populate_opts_cb, opts_file, *opts_ptr) < 0) + die(_("Malformed options sheet: %s"), opts_file); +} + +static void walk_revs_populate_todo(struct commit_list **todo_list, + struct replay_opts *opts) +{ + struct commit *commit; + struct commit_list **next; + + prepare_revs(opts); + + next = todo_list; + while ((commit = get_revision(opts->revs))) + next = commit_list_append(commit, next); +} + +static int create_seq_dir(void) +{ + const char *seq_dir = git_path(SEQ_DIR); + + if (file_exists(seq_dir)) { + error(_("a cherry-pick or revert is already in progress")); + advise(_("try \"git cherry-pick (--continue | --quit | --abort)\"")); + return -1; + } + else if (mkdir(seq_dir, 0777) < 0) + die_errno(_("Could not create sequencer directory %s"), seq_dir); + return 0; +} + +static void save_head(const char *head) +{ + const char *head_file = git_path(SEQ_HEAD_FILE); + static struct lock_file head_lock; + struct strbuf buf = STRBUF_INIT; + int fd; + + fd = hold_lock_file_for_update(&head_lock, head_file, LOCK_DIE_ON_ERROR); + strbuf_addf(&buf, "%s\n", head); + if (write_in_full(fd, buf.buf, buf.len) < 0) + die_errno(_("Could not write to %s"), head_file); + if (commit_lock_file(&head_lock) < 0) + die(_("Error wrapping up %s."), head_file); +} + +static int reset_for_rollback(const unsigned char *sha1) +{ + const char *argv[4]; /* reset --merge + NULL */ + argv[0] = "reset"; + argv[1] = "--merge"; + argv[2] = sha1_to_hex(sha1); + argv[3] = NULL; + return run_command_v_opt(argv, RUN_GIT_CMD); +} + +static int rollback_single_pick(void) +{ + unsigned char head_sha1[20]; + + if (!file_exists(git_path("CHERRY_PICK_HEAD")) && + !file_exists(git_path("REVERT_HEAD"))) + return error(_("no cherry-pick or revert in progress")); + if (read_ref_full("HEAD", head_sha1, 0, NULL)) + return error(_("cannot resolve HEAD")); + if (is_null_sha1(head_sha1)) + return error(_("cannot abort from a branch yet to be born")); + return reset_for_rollback(head_sha1); +} + +static int sequencer_rollback(struct replay_opts *opts) +{ + const char *filename; + FILE *f; + unsigned char sha1[20]; + struct strbuf buf = STRBUF_INIT; + + filename = git_path(SEQ_HEAD_FILE); + f = fopen(filename, "r"); + if (!f && errno == ENOENT) { + /* + * There is no multiple-cherry-pick in progress. + * If CHERRY_PICK_HEAD or REVERT_HEAD indicates + * a single-cherry-pick in progress, abort that. + */ + return rollback_single_pick(); + } + if (!f) + return error(_("cannot open %s: %s"), filename, + strerror(errno)); + if (strbuf_getline(&buf, f, '\n')) { + error(_("cannot read %s: %s"), filename, ferror(f) ? + strerror(errno) : _("unexpected end of file")); + fclose(f); + goto fail; + } + fclose(f); + if (get_sha1_hex(buf.buf, sha1) || buf.buf[40] != '\0') { + error(_("stored pre-cherry-pick HEAD file '%s' is corrupt"), + filename); + goto fail; + } + if (reset_for_rollback(sha1)) + goto fail; + remove_sequencer_state(); + strbuf_release(&buf); + return 0; +fail: + strbuf_release(&buf); + return -1; +} + +static void save_todo(struct commit_list *todo_list, struct replay_opts *opts) +{ + const char *todo_file = git_path(SEQ_TODO_FILE); + static struct lock_file todo_lock; + struct strbuf buf = STRBUF_INIT; + int fd; + + fd = hold_lock_file_for_update(&todo_lock, todo_file, LOCK_DIE_ON_ERROR); + if (format_todo(&buf, todo_list, opts) < 0) + die(_("Could not format %s."), todo_file); + if (write_in_full(fd, buf.buf, buf.len) < 0) { + strbuf_release(&buf); + die_errno(_("Could not write to %s"), todo_file); + } + if (commit_lock_file(&todo_lock) < 0) { + strbuf_release(&buf); + die(_("Error wrapping up %s."), todo_file); + } + strbuf_release(&buf); +} + +static void save_opts(struct replay_opts *opts) +{ + const char *opts_file = git_path(SEQ_OPTS_FILE); + + if (opts->no_commit) + git_config_set_in_file(opts_file, "options.no-commit", "true"); + if (opts->edit) + git_config_set_in_file(opts_file, "options.edit", "true"); + if (opts->signoff) + git_config_set_in_file(opts_file, "options.signoff", "true"); + if (opts->record_origin) + git_config_set_in_file(opts_file, "options.record-origin", "true"); + if (opts->allow_ff) + git_config_set_in_file(opts_file, "options.allow-ff", "true"); + if (opts->mainline) { + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "%d", opts->mainline); + git_config_set_in_file(opts_file, "options.mainline", buf.buf); + strbuf_release(&buf); + } + if (opts->strategy) + git_config_set_in_file(opts_file, "options.strategy", opts->strategy); + if (opts->xopts) { + int i; + for (i = 0; i < opts->xopts_nr; i++) + git_config_set_multivar_in_file(opts_file, + "options.strategy-option", + opts->xopts[i], "^$", 0); + } +} + +static int pick_commits(struct commit_list *todo_list, struct replay_opts *opts) +{ + struct commit_list *cur; + int res; + + setenv(GIT_REFLOG_ACTION, action_name(opts), 0); + if (opts->allow_ff) + assert(!(opts->signoff || opts->no_commit || + opts->record_origin || opts->edit)); + read_and_refresh_cache(opts); + + for (cur = todo_list; cur; cur = cur->next) { + save_todo(cur, opts); + res = do_pick_commit(cur->item, opts); + if (res) + return res; + } + + /* + * Sequence of picks finished successfully; cleanup by + * removing the .git/sequencer directory + */ + remove_sequencer_state(); + return 0; +} + +static int continue_single_pick(void) +{ + const char *argv[] = { "commit", NULL }; + + if (!file_exists(git_path("CHERRY_PICK_HEAD")) && + !file_exists(git_path("REVERT_HEAD"))) + return error(_("no cherry-pick or revert in progress")); + return run_command_v_opt(argv, RUN_GIT_CMD); +} + +static int sequencer_continue(struct replay_opts *opts) +{ + struct commit_list *todo_list = NULL; + + if (!file_exists(git_path(SEQ_TODO_FILE))) + return continue_single_pick(); + read_populate_opts(&opts); + read_populate_todo(&todo_list, opts); + + /* Verify that the conflict has been resolved */ + if (file_exists(git_path("CHERRY_PICK_HEAD")) || + file_exists(git_path("REVERT_HEAD"))) { + int ret = continue_single_pick(); + if (ret) + return ret; + } + if (index_differs_from("HEAD", 0)) + return error_dirty_index(opts); + todo_list = todo_list->next; + return pick_commits(todo_list, opts); +} + +static int single_pick(struct commit *cmit, struct replay_opts *opts) +{ + setenv(GIT_REFLOG_ACTION, action_name(opts), 0); + return do_pick_commit(cmit, opts); +} + +int sequencer_pick_revisions(struct replay_opts *opts) +{ + struct commit_list *todo_list = NULL; + unsigned char sha1[20]; + + if (opts->subcommand == REPLAY_NONE) + assert(opts->revs); + + read_and_refresh_cache(opts); + + /* + * Decide what to do depending on the arguments; a fresh + * cherry-pick should be handled differently from an existing + * one that is being continued + */ + if (opts->subcommand == REPLAY_REMOVE_STATE) { + remove_sequencer_state(); + return 0; + } + if (opts->subcommand == REPLAY_ROLLBACK) + return sequencer_rollback(opts); + if (opts->subcommand == REPLAY_CONTINUE) + return sequencer_continue(opts); + + /* + * If we were called as "git cherry-pick ", just + * cherry-pick/revert it, set CHERRY_PICK_HEAD / + * REVERT_HEAD, and don't touch the sequencer state. + * This means it is possible to cherry-pick in the middle + * of a cherry-pick sequence. + */ + if (opts->revs->cmdline.nr == 1 && + opts->revs->cmdline.rev->whence == REV_CMD_REV && + opts->revs->no_walk && + !opts->revs->cmdline.rev->flags) { + struct commit *cmit; + if (prepare_revision_walk(opts->revs)) + die(_("revision walk setup failed")); + cmit = get_revision(opts->revs); + if (!cmit || get_revision(opts->revs)) + die("BUG: expected exactly one commit from walk"); + return single_pick(cmit, opts); + } + + /* + * Start a new cherry-pick/ revert sequence; but + * first, make sure that an existing one isn't in + * progress + */ + + walk_revs_populate_todo(&todo_list, opts); + if (create_seq_dir() < 0) + return -1; + if (get_sha1("HEAD", sha1)) { + if (opts->action == REPLAY_REVERT) + return error(_("Can't revert as initial commit")); + return error(_("Can't cherry-pick into empty head")); + } + save_head(sha1_to_hex(sha1)); + save_opts(opts); + return pick_commits(todo_list, opts); +} diff --git a/sequencer.h b/sequencer.h index 2d4528f292..bb4b13830e 100644 --- a/sequencer.h +++ b/sequencer.h @@ -6,7 +6,44 @@ #define SEQ_TODO_FILE "sequencer/todo" #define SEQ_OPTS_FILE "sequencer/opts" +enum replay_action { + REPLAY_REVERT, + REPLAY_PICK +}; + +enum replay_subcommand { + REPLAY_NONE, + REPLAY_REMOVE_STATE, + REPLAY_CONTINUE, + REPLAY_ROLLBACK +}; + +struct replay_opts { + enum replay_action action; + enum replay_subcommand subcommand; + + /* Boolean options */ + int edit; + int record_origin; + int no_commit; + int signoff; + int allow_ff; + int allow_rerere_auto; + + int mainline; + + /* Merge strategy */ + const char *strategy; + const char **xopts; + size_t xopts_nr, xopts_alloc; + + /* Only used by REPLAY_NONE */ + struct rev_info *revs; +}; + /* Removes SEQ_DIR. */ extern void remove_sequencer_state(void); +int sequencer_pick_revisions(struct replay_opts *opts); + #endif -- cgit v1.2.1 From 37475f97d1fbab0842c17b175263eeba6cd5e318 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 11 Jan 2012 22:05:03 -0500 Subject: attr: fix leak in free_attr_elem This function frees the individual "struct match_attr"s we have allocated, but forgot to free the array holding their pointers, leading to a minor memory leak (but it can add up after checking attributes for paths in many directories). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- attr.c | 1 + 1 file changed, 1 insertion(+) diff --git a/attr.c b/attr.c index 2ce7365138..af4083582d 100644 --- a/attr.c +++ b/attr.c @@ -286,6 +286,7 @@ static void free_attr_elem(struct attr_stack *e) } free(a); } + free(e->attrs); free(e); } -- cgit v1.2.1 From 0f544ee897ac2cc760f29d3568cc4295ca69fd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 11 Jan 2012 13:12:38 +0100 Subject: archive: re-allow HEAD:Documentation on a remote invocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tightening done in (ee27ca4a: archive: don't let remote clients get unreachable commits, 2011-11-17) went too far and disallowed HEAD:Documentation as it would try to find "HEAD:Documentation" as a ref. Only DWIM the "HEAD" part to see if it exists as a ref. Once we're sure that we've been given a valid ref, we follow the normal code path. This still disallows attempts to access commits which are not branch tips. Signed-off-by: Carlos Martín Nieto Signed-off-by: Junio C Hamano --- archive.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/archive.c b/archive.c index 5f0a3fc6d2..a348480b26 100644 --- a/archive.c +++ b/archive.c @@ -260,14 +260,23 @@ static void parse_treeish_arg(const char **argv, /* Remotes are only allowed to fetch actual refs */ if (remote) { char *ref = NULL; - if (!dwim_ref(name, strlen(name), sha1, &ref)) - die("no such ref: %s", name); + const char *refname, *colon = NULL; + + colon = strchr(name, ':'); + if (colon) + refname = xstrndup(name, colon - name); + else + refname = name; + + if (!dwim_ref(refname, strlen(refname), sha1, &ref)) + die("no such ref: %s", refname); + if (refname != name) + free((void *)refname); free(ref); } - else { - if (get_sha1(name, sha1)) - die("Not a valid object name"); - } + + if (get_sha1(name, sha1)) + die("Not a valid object name"); commit = lookup_commit_reference_gently(sha1, 1); if (commit) { -- cgit v1.2.1 From c7c2bc0ac9e7f077771db53960d4917fda4b27a7 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 12 Jan 2012 12:15:33 +0100 Subject: word-diff: ignore '\ No newline at eof' marker The word-diff logic accumulates + and - lines until another line type appears (normally [ @\]), at which point it generates the word diff. This is usually correct, but it breaks when the preimage does not have a newline at EOF: $ printf "%s" "a a a" >a $ printf "%s\n" "a ab a" >b $ git diff --no-index --word-diff a b diff --git 1/a 2/b index 9f68e94..6a7c02f 100644 --- 1/a +++ 2/b @@ -1 +1 @@ [-a a a-] No newline at end of file {+a ab a+} Because of the order of the lines in a unified diff @@ -1 +1 @@ -a a a \ No newline at end of file +a ab a the '\' line flushed the buffers, and the - and + lines were never matched with each other. A proper fix would defer such markers until the end of the hunk. However, word-diff is inherently whitespace-ignoring, so as a cheap fix simply ignore the marker (and hide it from the output). We use a prefix match for '\ ' to parallel the logic in apply.c:parse_fragment(). We currently do not localize this string (just accept other variants of it in git-apply), but this should be future-proof. Noticed-by: Ivan Shirokoff Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- diff.c | 9 +++++++++ t/t4034-diff-words.sh | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/diff.c b/diff.c index 9038f190ec..2e6965e84b 100644 --- a/diff.c +++ b/diff.c @@ -1111,6 +1111,15 @@ static void fn_out_consume(void *priv, char *line, unsigned long len) diff_words_append(line, len, &ecbdata->diff_words->plus); return; + } else if (!prefixcmp(line, "\\ ")) { + /* + * Eat the "no newline at eof" marker as if we + * saw a "+" or "-" line with nothing on it, + * and return without diff_words_flush() to + * defer processing. If this is the end of + * preimage, more "+" lines may come after it. + */ + return; } diff_words_flush(ecbdata); if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) { diff --git a/t/t4034-diff-words.sh b/t/t4034-diff-words.sh index c374aa4c1c..219b45e9a4 100755 --- a/t/t4034-diff-words.sh +++ b/t/t4034-diff-words.sh @@ -333,4 +333,18 @@ test_expect_success 'word-diff with diff.sbe' ' word_diff --word-diff=plain ' +test_expect_success 'word-diff with no newline at EOF' ' + cat >expect <<-\EOF && + diff --git a/pre b/post + index 7bf316e..3dd0303 100644 + --- a/pre + +++ b/post + @@ -1 +1 @@ + a a [-a-]{+ab+} a a + EOF + printf "%s" "a a a a a" >pre && + printf "%s" "a a ab a a" >post && + word_diff --word-diff=plain +' + test_done -- cgit v1.2.1 From 15f07e061e272079229d1ab2799d8e7a4f65213f Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 12 Jan 2012 17:32:34 -0500 Subject: thin-pack: try harder to use preferred base objects as base When creating a pack using objects that reside in existing packs, we try to avoid recomputing futile delta between an object (trg) and a candidate for its base object (src) if they are stored in the same packfile, and trg is not recorded as a delta already. This heuristics makes sense because it is likely that we tried to express trg as a delta based on src but it did not produce a good delta when we created the existing pack. As the pack heuristics prefer producing delta to remove data, and Linus's law dictates that the size of a file grows over time, we tend to record the newest version of the file as inflated, and older ones as delta against it. When creating a thin-pack to transfer recent history, it is likely that we will try to send an object that is recorded in full, as it is newer. But the heuristics to avoid recomputing futile delta effectively forbids us from attempting to express such an object as a delta based on another object. Sending an object in full is often more expensive than sending a suboptimal delta based on other objects, and it is even more so if we could use an object we know the receiving end already has (i.e. preferred base object) as the delta base. Tweak the recomputation avoidance logic, so that we do not punt on computing delta against a preferred base object. The effect of this change can be seen on two simulated upload-pack workloads. The first is based on 44 reflog entries from my git.git origin/master reflog, and represents the packs that kernel.org sent me git updates for the past month or two. The second workload represents much larger fetches, going from git's v1.0.0 tag to v1.1.0, then v1.1.0 to v1.2.0, and so on. The table below shows the average generated pack size and the average CPU time consumed for each dataset, both before and after the patch: dataset | reflog | tags --------------------------------- before | 53358 | 2750977 size after | 32398 | 2668479 change | -39% | -3% --------------------------------- before | 0.18 | 1.12 CPU after | 0.18 | 1.15 change | +0% | +3% This patch makes a much bigger difference for packs with a shorter slice of history (since its effect is seen at the boundaries of the pack) though it has some benefit even for larger packs. Signed-off-by: Jeff King Acked-by: Nicolas Pitre Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index c6e2d8766b..8bfe3a6ffb 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1248,11 +1248,16 @@ static int try_delta(struct unpacked *trg, struct unpacked *src, return -1; /* - * We do not bother to try a delta that we discarded - * on an earlier try, but only when reusing delta data. + * We do not bother to try a delta that we discarded on an + * earlier try, but only when reusing delta data. Note that + * src_entry that is marked as the preferred_base should always + * be considered, as even if we produce a suboptimal delta against + * it, we will still save the transfer cost, as we already know + * the other side has it and we won't send src_entry at all. */ if (reuse_delta && trg_entry->in_pack && trg_entry->in_pack == src_entry->in_pack && + !src_entry->preferred_base && trg_entry->in_pack_type != OBJ_REF_DELTA && trg_entry->in_pack_type != OBJ_OFS_DELTA) return 0; -- cgit v1.2.1 From 04f6785a089e552585ba022f9d9054eca385ca67 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 12 Jan 2012 23:30:53 -0800 Subject: Update draft release notes to 1.7.6.6 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.6.6.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/RelNotes/1.7.6.6.txt b/Documentation/RelNotes/1.7.6.6.txt index 13ce2dc2d7..5343e00400 100644 --- a/Documentation/RelNotes/1.7.6.6.txt +++ b/Documentation/RelNotes/1.7.6.6.txt @@ -8,4 +8,9 @@ Fixes since v1.7.6.5 directory when two paths in question are in adjacent directories and the name of the one directory is a prefix of the other. + * When producing a "thin pack" (primarily used in bundles and smart + HTTP transfers) out of a fully packed repository, we unnecessarily + avoided sending recent objects as a delta against objects we know + the other side has. + Also contains minor fixes and documentation updates. -- cgit v1.2.1 From 8f83acf77cd14567dfdeff0e15f2da086109df70 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 12 Jan 2012 23:31:41 -0800 Subject: Update draft release notes to 1.7.7.6 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.7.6.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/RelNotes/1.7.7.6.txt b/Documentation/RelNotes/1.7.7.6.txt index 065ed2ad6c..b8b86ebc61 100644 --- a/Documentation/RelNotes/1.7.7.6.txt +++ b/Documentation/RelNotes/1.7.7.6.txt @@ -8,4 +8,9 @@ Fixes since v1.7.7.5 directory when two paths in question are in adjacent directories and the name of the one directory is a prefix of the other. + * When producing a "thin pack" (primarily used in bundles and smart + HTTP transfers) out of a fully packed repository, we unnecessarily + avoided sending recent objects as a delta against objects we know + the other side has. + Also contains minor fixes and documentation updates. -- cgit v1.2.1 From ab8a78084b7159d99dae2905e1a3ff04c1500a6d Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 12 Jan 2012 23:33:29 -0800 Subject: Update draft release notes to 1.7.8.4 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.8.4.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/RelNotes/1.7.8.4.txt b/Documentation/RelNotes/1.7.8.4.txt index 3172f1cb31..c21fc99bdb 100644 --- a/Documentation/RelNotes/1.7.8.4.txt +++ b/Documentation/RelNotes/1.7.8.4.txt @@ -8,6 +8,11 @@ Fixes since v1.7.8.3 directory when two paths in question are in adjacent directories and the name of the one directory is a prefix of the other. + * When producing a "thin pack" (primarily used in bundles and smart + HTTP transfers) out of a fully packed repository, we unnecessarily + avoided sending recent objects as a delta against objects we know + the other side has. + * "git send-email" did not properly treat sendemail.multiedit as a boolean (e.g. setting it to "false" did not turn it off). -- cgit v1.2.1 From 6db5c6e43dccb380ca6e9947777985eb11248c31 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 12 Jan 2012 23:43:28 -0800 Subject: Git 1.7.9-rc1 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.9.txt | 6 +++++- GIT-VERSION-GEN | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Documentation/RelNotes/1.7.9.txt b/Documentation/RelNotes/1.7.9.txt index 79397eb6c7..5eb88b899a 100644 --- a/Documentation/RelNotes/1.7.9.txt +++ b/Documentation/RelNotes/1.7.9.txt @@ -28,6 +28,10 @@ Updates since v1.7.8 building more generic "sequencer" on top of the implementation that drives them. + * "git rev-parse FETCH_HEAD" after "git fetch" without specifying + what to fetch from the command line will now show the commit that + would be merged if the command were "git pull". + * "git add" learned to stream large files directly into a packfile instead of writing them into individual loose object files. @@ -108,7 +112,7 @@ details). -- exec >/var/tmp/1 -O=v1.7.8.2-301-g48de656 +O=v1.7.9-rc0-44-g478c446 echo O=$(git describe master) git log --first-parent --oneline --reverse ^$O master echo diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 4e83354d0b..15d06cfaab 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.7.9-rc0 +DEF_VER=v1.7.9-rc1 LF=' ' -- cgit v1.2.1 From 6ab260809b61b622e833771fbe08874f14a7e544 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 13 Jan 2012 17:39:15 +0100 Subject: git-show-ref: fix escaping in asciidoc source Two "^" characters were incorrectly being interpreted as markup for superscripting. Fix them by writing them as attribute references "{caret}". Although a single "^" character in a paragraph cannot be misinterpreted in this way, also write other "^" characters as "{caret}" in the interest of good hygiene (unless they are in literal paragraphs, of course, in which context attribute references are not recognized). Spell "{}" consistently, namely *not* quoted as "\{\}". Since the braces are empty, they cannot be interpreted as an attribute reference, and either spelling is OK. So arbitrarily choose one variation and use it consistently. Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- Documentation/git-show-ref.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/git-show-ref.txt b/Documentation/git-show-ref.txt index 3c45895299..8dfcbe3fd4 100644 --- a/Documentation/git-show-ref.txt +++ b/Documentation/git-show-ref.txt @@ -44,7 +44,7 @@ OPTIONS -d:: --dereference:: - Dereference tags into object IDs as well. They will be shown with "^{}" + Dereference tags into object IDs as well. They will be shown with "{caret}{}" appended. -s:: @@ -73,9 +73,9 @@ OPTIONS --exclude-existing[=]:: Make 'git show-ref' act as a filter that reads refs from stdin of the - form "^(?:\s)?(?:{backslash}{caret}\{\})?$" + form "{caret}(?:\s)?(?:{backslash}{caret}{})?$" and performs the following actions on each: - (1) strip "^{}" at the end of line if any; + (1) strip "{caret}{}" at the end of line if any; (2) ignore if pattern is provided and does not head-match refname; (3) warn if refname is not a well-formed refname and skip; (4) ignore if refname is a ref that exists in the local repository; -- cgit v1.2.1 From 87b340b9677c898136b84015d8119972bb7d260b Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 13 Jan 2012 17:39:16 +0100 Subject: git-show-ref doc: typeset regexp in fixed width font Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- Documentation/git-show-ref.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/git-show-ref.txt b/Documentation/git-show-ref.txt index 8dfcbe3fd4..fcee0008a9 100644 --- a/Documentation/git-show-ref.txt +++ b/Documentation/git-show-ref.txt @@ -73,7 +73,7 @@ OPTIONS --exclude-existing[=]:: Make 'git show-ref' act as a filter that reads refs from stdin of the - form "{caret}(?:\s)?(?:{backslash}{caret}{})?$" + form "`{caret}(?:\s)?(?:{backslash}{caret}{})?$`" and performs the following actions on each: (1) strip "{caret}{}" at the end of line if any; (2) ignore if pattern is provided and does not head-match refname; -- cgit v1.2.1 From 8c69c1f92eb79a597225814781fdf1ab4be26758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Sat, 14 Jan 2012 16:23:22 +0700 Subject: Document limited recursion pathspec matching with wildcards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's actually unlimited recursion if wildcards are active regardless --max-depth Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- Documentation/git-grep.txt | 3 +++ tree-walk.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt index 15d6711d46..6a8b1e3a7d 100644 --- a/Documentation/git-grep.txt +++ b/Documentation/git-grep.txt @@ -79,6 +79,9 @@ OPTIONS --max-depth :: For each given on command line, descend at most levels of directories. A negative value means no limit. + This option is ignored if contains active wildcards. + In other words if "a*" matches a directory named "a*", + "*" is matched literally so --max-depth is still effective. -w:: --word-regexp:: diff --git a/tree-walk.c b/tree-walk.c index f82dba6a1f..492c7cd744 100644 --- a/tree-walk.c +++ b/tree-walk.c @@ -661,6 +661,9 @@ match_wildcards: /* * Match all directories. We'll try to match files * later on. + * max_depth is ignored but we may consider support it + * in future, see + * http://thread.gmane.org/gmane.comp.version-control.git/163757/focus=163840 */ if (ps->recursive && S_ISDIR(entry->mode)) return entry_interesting; -- cgit v1.2.1 From 4838237cb73a13d38a7e4348b71be96b60eed21e Mon Sep 17 00:00:00 2001 From: Nguyen Thai Ngoc Duy Date: Sun, 15 Jan 2012 17:03:27 +0700 Subject: diff-index: enable recursive pathspec matching in unpack_trees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pathspec structure has a few bits of data to drive various operation modes after we unified the pathspec matching logic in various codepaths. For example, max_depth field is there so that "git grep" can limit the output for files found in limited depth of tree traversal. Also in order to show just the surface level differences in "git diff-tree", recursive field stops us from descending into deeper level of the tree structure when it is set to false, and this also affects pathspec matching when we have wildcards in the pathspec. The diff-index has always wanted the recursive behaviour, and wanted to match pathspecs without any depth limit. But we forgot to do so when we updated tree_entry_interesting() logic to unify the pathspec matching logic. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- diff-lib.c | 2 ++ t/t4010-diff-pathspec.sh | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/diff-lib.c b/diff-lib.c index 62f4cd94cf..fc0dff31b5 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -469,6 +469,8 @@ static int diff_cache(struct rev_info *revs, opts.src_index = &the_index; opts.dst_index = NULL; opts.pathspec = &revs->diffopt.pathspec; + opts.pathspec->recursive = 1; + opts.pathspec->max_depth = -1; init_tree_desc(&t, tree->buffer, tree->size); return unpack_trees(1, &t, &opts); diff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh index fbc8cd8f05..af5134b70c 100755 --- a/t/t4010-diff-pathspec.sh +++ b/t/t4010-diff-pathspec.sh @@ -47,6 +47,14 @@ test_expect_success \ 'git diff-index --cached $tree -- path1/ >current && compare_diff_raw current expected' +cat >expected <<\EOF +:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M path1/file1 +EOF +test_expect_success \ + '"*file1" should show path1/file1' \ + 'git diff-index --cached $tree -- "*file1" >current && + compare_diff_raw current expected' + cat >expected <<\EOF :100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M file0 EOF -- cgit v1.2.1 From 941ba8db57f2d075aee48b002ee30686288cb502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Sat, 14 Jan 2012 19:19:53 +0700 Subject: Eliminate recursion in setting/clearing marks in commit list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recursion in a DAG is generally a bad idea because it could be very deep. Be defensive and avoid recursion in mark_parents_uninteresting() and clear_commit_marks(). mark_parents_uninteresting() learns a trick from clear_commit_marks() to avoid malloc() in (dominant) single-parent case. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- commit.c | 13 +++++++++++-- revision.c | 45 +++++++++++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/commit.c b/commit.c index 44bc96d44d..c7aefbfd28 100644 --- a/commit.c +++ b/commit.c @@ -421,7 +421,8 @@ struct commit *pop_most_recent_commit(struct commit_list **list, return ret; } -void clear_commit_marks(struct commit *commit, unsigned int mark) +static void clear_commit_marks_1(struct commit_list **plist, + struct commit *commit, unsigned int mark) { while (commit) { struct commit_list *parents; @@ -436,12 +437,20 @@ void clear_commit_marks(struct commit *commit, unsigned int mark) return; while ((parents = parents->next)) - clear_commit_marks(parents->item, mark); + commit_list_insert(parents->item, plist); commit = commit->parents->item; } } +void clear_commit_marks(struct commit *commit, unsigned int mark) +{ + struct commit_list *list = NULL; + commit_list_insert(commit, &list); + while (list) + clear_commit_marks_1(&list, pop_commit(&list), mark); +} + void clear_commit_marks_for_object_array(struct object_array *a, unsigned mark) { struct object *object; diff --git a/revision.c b/revision.c index 8764dde381..7cc72fc02d 100644 --- a/revision.c +++ b/revision.c @@ -139,11 +139,32 @@ void mark_tree_uninteresting(struct tree *tree) void mark_parents_uninteresting(struct commit *commit) { - struct commit_list *parents = commit->parents; + struct commit_list *parents = NULL, *l; + + for (l = commit->parents; l; l = l->next) + commit_list_insert(l->item, &parents); while (parents) { struct commit *commit = parents->item; - if (!(commit->object.flags & UNINTERESTING)) { + l = parents; + parents = parents->next; + free(l); + + while (commit) { + /* + * A missing commit is ok iff its parent is marked + * uninteresting. + * + * We just mark such a thing parsed, so that when + * it is popped next time around, we won't be trying + * to parse it and get an error. + */ + if (!has_sha1_file(commit->object.sha1)) + commit->object.parsed = 1; + + if (commit->object.flags & UNINTERESTING) + break; + commit->object.flags |= UNINTERESTING; /* @@ -154,21 +175,13 @@ void mark_parents_uninteresting(struct commit *commit) * wasn't uninteresting), in which case we need * to mark its parents recursively too.. */ - if (commit->parents) - mark_parents_uninteresting(commit); - } + if (!commit->parents) + break; - /* - * A missing commit is ok iff its parent is marked - * uninteresting. - * - * We just mark such a thing parsed, so that when - * it is popped next time around, we won't be trying - * to parse it and get an error. - */ - if (!has_sha1_file(commit->object.sha1)) - commit->object.parsed = 1; - parents = parents->next; + for (l = commit->parents->next; l; l = l->next) + commit_list_insert(l->item, &parents); + commit = commit->parents->item; + } } } -- cgit v1.2.1 From 2baad220138b10582c55ef942466f2b8df18944f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Sat, 14 Jan 2012 19:19:54 +0700 Subject: index-pack: eliminate recursion in find_unresolved_deltas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current find_unresolved_deltas() links all bases together in a form of tree, using struct base_data, with prev_base pointer to point to parent node. Then it traverses down from parent to children in recursive manner with all base_data allocated on stack. To eliminate recursion, we simply need to put all on heap (parse_pack_objects and fix_unresolved_deltas). After that, it's simple non-recursive depth-first traversal loop. Each node also maintains its own state (ofs and ref indices) to iterate over all children nodes. So we process one node: - if it returns a new (child) node (a parent base), we link it to our tree, then process the new node. - if it returns nothing, the node is done, free it. We go back to parent node and resume whatever it's doing. and do it until we have no nodes to process. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/index-pack.c | 111 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/builtin/index-pack.c b/builtin/index-pack.c index af7dc37a44..38ff03a5cd 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -34,6 +34,8 @@ struct base_data { struct object_entry *obj; void *data; unsigned long size; + int ref_first, ref_last; + int ofs_first, ofs_last; }; /* @@ -221,6 +223,15 @@ static NORETURN void bad_object(unsigned long offset, const char *format, ...) die("pack has bad object at offset %lu: %s", offset, buf); } +static struct base_data *alloc_base_data(void) +{ + struct base_data *base = xmalloc(sizeof(struct base_data)); + memset(base, 0, sizeof(*base)); + base->ref_last = -1; + base->ofs_last = -1; + return base; +} + static void free_base_data(struct base_data *c) { if (c->data) { @@ -553,58 +564,76 @@ static void resolve_delta(struct object_entry *delta_obj, nr_resolved_deltas++; } -static void find_unresolved_deltas(struct base_data *base, - struct base_data *prev_base) +static struct base_data *find_unresolved_deltas_1(struct base_data *base, + struct base_data *prev_base) { - int i, ref_first, ref_last, ofs_first, ofs_last; - - /* - * This is a recursive function. Those brackets should help reducing - * stack usage by limiting the scope of the delta_base union. - */ - { + if (base->ref_last == -1 && base->ofs_last == -1) { union delta_base base_spec; hashcpy(base_spec.sha1, base->obj->idx.sha1); find_delta_children(&base_spec, - &ref_first, &ref_last, OBJ_REF_DELTA); + &base->ref_first, &base->ref_last, OBJ_REF_DELTA); memset(&base_spec, 0, sizeof(base_spec)); base_spec.offset = base->obj->idx.offset; find_delta_children(&base_spec, - &ofs_first, &ofs_last, OBJ_OFS_DELTA); - } + &base->ofs_first, &base->ofs_last, OBJ_OFS_DELTA); - if (ref_last == -1 && ofs_last == -1) { - free(base->data); - return; - } + if (base->ref_last == -1 && base->ofs_last == -1) { + free(base->data); + return NULL; + } - link_base_data(prev_base, base); + link_base_data(prev_base, base); + } - for (i = ref_first; i <= ref_last; i++) { - struct object_entry *child = objects + deltas[i].obj_no; - struct base_data result; + if (base->ref_first <= base->ref_last) { + struct object_entry *child = objects + deltas[base->ref_first].obj_no; + struct base_data *result = alloc_base_data(); assert(child->real_type == OBJ_REF_DELTA); - resolve_delta(child, base, &result); - if (i == ref_last && ofs_last == -1) + resolve_delta(child, base, result); + if (base->ref_first == base->ref_last && base->ofs_last == -1) free_base_data(base); - find_unresolved_deltas(&result, base); + + base->ref_first++; + return result; } - for (i = ofs_first; i <= ofs_last; i++) { - struct object_entry *child = objects + deltas[i].obj_no; - struct base_data result; + if (base->ofs_first <= base->ofs_last) { + struct object_entry *child = objects + deltas[base->ofs_first].obj_no; + struct base_data *result = alloc_base_data(); assert(child->real_type == OBJ_OFS_DELTA); - resolve_delta(child, base, &result); - if (i == ofs_last) + resolve_delta(child, base, result); + if (base->ofs_first == base->ofs_last) free_base_data(base); - find_unresolved_deltas(&result, base); + + base->ofs_first++; + return result; } unlink_base_data(base); + return NULL; +} + +static void find_unresolved_deltas(struct base_data *base) +{ + struct base_data *new_base, *prev_base = NULL; + for (;;) { + new_base = find_unresolved_deltas_1(base, prev_base); + + if (new_base) { + prev_base = base; + base = new_base; + } else { + free(base); + base = prev_base; + if (!base) + return; + prev_base = base->base; + } + } } static int compare_delta_entry(const void *a, const void *b) @@ -684,13 +713,13 @@ static void parse_pack_objects(unsigned char *sha1) progress = start_progress("Resolving deltas", nr_deltas); for (i = 0; i < nr_objects; i++) { struct object_entry *obj = &objects[i]; - struct base_data base_obj; + struct base_data *base_obj = alloc_base_data(); if (is_delta_type(obj->type)) continue; - base_obj.obj = obj; - base_obj.data = NULL; - find_unresolved_deltas(&base_obj, NULL); + base_obj->obj = obj; + base_obj->data = NULL; + find_unresolved_deltas(base_obj); display_progress(progress, nr_resolved_deltas); } } @@ -783,20 +812,20 @@ static void fix_unresolved_deltas(struct sha1file *f, int nr_unresolved) for (i = 0; i < n; i++) { struct delta_entry *d = sorted_by_pos[i]; enum object_type type; - struct base_data base_obj; + struct base_data *base_obj = alloc_base_data(); if (objects[d->obj_no].real_type != OBJ_REF_DELTA) continue; - base_obj.data = read_sha1_file(d->base.sha1, &type, &base_obj.size); - if (!base_obj.data) + base_obj->data = read_sha1_file(d->base.sha1, &type, &base_obj->size); + if (!base_obj->data) continue; - if (check_sha1_signature(d->base.sha1, base_obj.data, - base_obj.size, typename(type))) + if (check_sha1_signature(d->base.sha1, base_obj->data, + base_obj->size, typename(type))) die("local object %s is corrupt", sha1_to_hex(d->base.sha1)); - base_obj.obj = append_obj_to_pack(f, d->base.sha1, - base_obj.data, base_obj.size, type); - find_unresolved_deltas(&base_obj, NULL); + base_obj->obj = append_obj_to_pack(f, d->base.sha1, + base_obj->data, base_obj->size, type); + find_unresolved_deltas(base_obj); display_progress(progress, nr_resolved_deltas); } free(sorted_by_pos); -- cgit v1.2.1 From 20e95d0a8475ce467903e1aaad5c2fd47777fcc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Sat, 14 Jan 2012 19:19:55 +0700 Subject: index-pack: eliminate unlimited recursion in get_base_data() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revese the order of delta applying so that by the time a delta is applied, its base is either non-delta or already inflated. get_base_data() is still recursive, but because base's data is always ready, the inner get_base_data() call never has any chance to call itself again. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/index-pack.c | 53 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/builtin/index-pack.c b/builtin/index-pack.c index 38ff03a5cd..dd1c5c961d 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -515,14 +515,52 @@ static int is_delta_type(enum object_type type) return (type == OBJ_REF_DELTA || type == OBJ_OFS_DELTA); } +/* + * This function is part of find_unresolved_deltas(). There are two + * walkers going in the opposite ways. + * + * The first one in find_unresolved_deltas() traverses down from + * parent node to children, deflating nodes along the way. However, + * memory for deflated nodes is limited by delta_base_cache_limit, so + * at some point parent node's deflated content may be freed. + * + * The second walker is this function, which goes from current node up + * to top parent if necessary to deflate the node. In normal + * situation, its parent node would be already deflated, so it just + * needs to apply delta. + * + * In the worst case scenario, parent node is no longer deflated because + * we're running out of delta_base_cache_limit; we need to re-deflate + * parents, possibly up to the top base. + * + * All deflated objects here are subject to be freed if we exceed + * delta_base_cache_limit, just like in find_unresolved_deltas(), we + * just need to make sure the last node is not freed. + */ static void *get_base_data(struct base_data *c) { if (!c->data) { struct object_entry *obj = c->obj; + struct base_data **delta = NULL; + int delta_nr = 0, delta_alloc = 0; - if (is_delta_type(obj->type)) { - void *base = get_base_data(c->base); - void *raw = get_data_from_pack(obj); + while (is_delta_type(c->obj->type) && !c->data) { + ALLOC_GROW(delta, delta_nr + 1, delta_alloc); + delta[delta_nr++] = c; + c = c->base; + } + if (!delta_nr) { + c->data = get_data_from_pack(obj); + c->size = obj->size; + base_cache_used += c->size; + prune_base_data(c); + } + for (; delta_nr > 0; delta_nr--) { + void *base, *raw; + c = delta[delta_nr - 1]; + obj = c->obj; + base = get_base_data(c->base); + raw = get_data_from_pack(obj); c->data = patch_delta( base, c->base->size, raw, obj->size, @@ -530,13 +568,10 @@ static void *get_base_data(struct base_data *c) free(raw); if (!c->data) bad_object(obj->idx.offset, "failed to apply delta"); - } else { - c->data = get_data_from_pack(obj); - c->size = obj->size; + base_cache_used += c->size; + prune_base_data(c); } - - base_cache_used += c->size; - prune_base_data(c); + free(delta); } return c->data; } -- cgit v1.2.1 From f7e5ea171b693bf5fc88e059d9d4af3753c09143 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Mon, 16 Jan 2012 11:53:00 +0100 Subject: am: learn passing -b to mailinfo git-am could pass -k to mailinfo, but not -b. Introduce an option that does so. We change the meaning of the 'keep' state file, but are careful not to cause a problem unless you downgrade in the middle of an 'am' run. This uncovers a bug in mailinfo -b, hence the failing test. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- Documentation/git-am.txt | 3 +++ git-am.sh | 20 ++++++++++++++++---- t/t4150-am.sh | 14 ++++++++++++-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index 6b1b5af64e..123f7c9e4a 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -39,6 +39,9 @@ OPTIONS --keep:: Pass `-k` flag to 'git mailinfo' (see linkgit:git-mailinfo[1]). +--keep-non-patch:: + Pass `-b` flag to 'git mailinfo' (see linkgit:git-mailinfo[1]). + --keep-cr:: --no-keep-cr:: With `--keep-cr`, call 'git mailsplit' (see linkgit:git-mailsplit[1]) diff --git a/git-am.sh b/git-am.sh index 6cdd5910db..8b755d93ba 100755 --- a/git-am.sh +++ b/git-am.sh @@ -15,6 +15,7 @@ q,quiet be quiet s,signoff add a Signed-off-by line to the commit message u,utf8 recode into utf8 (default) k,keep pass -k flag to git-mailinfo +keep-non-patch pass -b flag to git-mailinfo keep-cr pass --keep-cr flag to git-mailsplit for mbox format no-keep-cr do not pass --keep-cr flag to git-mailsplit independent of am.keepcr c,scissors strip everything before a scissors line @@ -345,6 +346,8 @@ do utf8= ;; -k|--keep) keep=t ;; + --keep-non-patch) + keep=b ;; -c|--scissors) scissors=t ;; --no-scissors) @@ -522,16 +525,25 @@ case "$resolved" in fi esac +# Now, decide what command line options we will give to the git +# commands we invoke, based on the result of parsing command line +# options and previous invocation state stored in $dotest/ files. + if test "$(cat "$dotest/utf8")" = t then utf8=-u else utf8=-n fi -if test "$(cat "$dotest/keep")" = t -then - keep=-k -fi +keep=$(cat "$dotest/keep") +case "$keep" in +t) + keep=-k ;; +b) + keep=-b ;; +*) + keep= ;; +esac case "$(cat "$dotest/keepcr")" in t) keepcr=--keep-cr ;; diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 850fc96d1f..efcff3c51d 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -219,7 +219,7 @@ test_expect_success 'am stays in branch' ' test_expect_success 'am --signoff does not add Signed-off-by: line if already there' ' git format-patch --stdout HEAD^ >patch3 && - sed -e "/^Subject/ s,\[PATCH,Re: Re: Re: & 1/5 v2," patch3 >patch4 && + sed -e "/^Subject/ s,\[PATCH,Re: Re: Re: & 1/5 v2] [foo," patch3 >patch4 && rm -fr .git/rebase-apply && git reset --hard && git checkout HEAD^ && @@ -241,7 +241,17 @@ test_expect_success 'am --keep really keeps the subject' ' git am --keep patch4 && ! test -d .git/rebase-apply && git cat-file commit HEAD >actual && - grep "Re: Re: Re: \[PATCH 1/5 v2\] third" actual + grep "Re: Re: Re: \[PATCH 1/5 v2\] \[foo\] third" actual +' + +test_expect_failure 'am --keep-non-patch really keeps the non-patch part' ' + rm -fr .git/rebase-apply && + git reset --hard && + git checkout HEAD^ && + git am --keep-non-patch patch4 && + ! test -d .git/rebase-apply && + git cat-file commit HEAD >actual && + grep "^\[foo\] third" actual ' test_expect_success 'am -3 falls back to 3-way merge' ' -- cgit v1.2.1 From ee2d1cb402a7ccda0028b221f2252801b3cb7eef Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Mon, 16 Jan 2012 11:53:01 +0100 Subject: mailinfo: with -b, keep space after [foo] The logic for the -b mode, where [PATCH] is dropped but [foo] is not, silently ate all spaces after the ]. Fix this by keeping the next isspace() character, if there is any. Being more thorough is pointless, as the later cleanup_space() call will normalize any sequence of whitespace to a single ' '. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- builtin/mailinfo.c | 11 ++++++++++- t/t4150-am.sh | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/builtin/mailinfo.c b/builtin/mailinfo.c index bfb32b7233..eaf9e157a3 100644 --- a/builtin/mailinfo.c +++ b/builtin/mailinfo.c @@ -250,8 +250,17 @@ static void cleanup_subject(struct strbuf *subject) (7 <= remove && memmem(subject->buf + at, remove, "PATCH", 5))) strbuf_remove(subject, at, remove); - else + else { at += remove; + /* + * If the input had a space after the ], keep + * it. We don't bother with finding the end of + * the space, since we later normalize it + * anyway. + */ + if (isspace(subject->buf[at])) + at += 1; + } continue; } break; diff --git a/t/t4150-am.sh b/t/t4150-am.sh index efcff3c51d..a087254f64 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -244,7 +244,7 @@ test_expect_success 'am --keep really keeps the subject' ' grep "Re: Re: Re: \[PATCH 1/5 v2\] \[foo\] third" actual ' -test_expect_failure 'am --keep-non-patch really keeps the non-patch part' ' +test_expect_success 'am --keep-non-patch really keeps the non-patch part' ' rm -fr .git/rebase-apply && git reset --hard && git checkout HEAD^ && -- cgit v1.2.1 From bafe763197cb6535e5b4956ffeaa4e26ebb10651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:07 +0700 Subject: t5601: add missing && cascade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- t/t5601-clone.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 87ee01662c..49821eb467 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -9,9 +9,9 @@ test_expect_success setup ' rm -fr .git && test_create_repo src && ( - cd src - >file - git add file + cd src && + >file && + git add file && git commit -m initial ) -- cgit v1.2.1 From 7f08c6858ecfdf29f9713cf3a0b3699192f01a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:08 +0700 Subject: clone: write detached HEAD in bare repositories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we don't write, HEAD is still at refs/heads/master as initialized by init-db, which may or may not match remote's HEAD. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 9 +++------ t/t5601-clone.sh | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 9dcc5fe775..91862f7963 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -764,12 +764,9 @@ int cmd_clone(int argc, const char **argv, const char *prefix) } } else if (remote_head) { /* Source had detached HEAD pointing somewhere. */ - if (!option_bare) { - update_ref(reflog_msg.buf, "HEAD", - remote_head->old_sha1, - NULL, REF_NODEREF, DIE_ON_ERR); - our_head_points_at = remote_head; - } + update_ref(reflog_msg.buf, "HEAD", remote_head->old_sha1, + NULL, REF_NODEREF, DIE_ON_ERR); + our_head_points_at = remote_head; } else { /* Nothing to checkout out */ if (!option_no_checkout) diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 49821eb467..e0b8db6c53 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -12,7 +12,10 @@ test_expect_success setup ' cd src && >file && git add file && - git commit -m initial + git commit -m initial && + echo 1 >file && + git add file && + git commit -m updated ) ' @@ -88,6 +91,26 @@ test_expect_success 'clone --mirror' ' ' +test_expect_success 'clone --mirror with detached HEAD' ' + + ( cd src && git checkout HEAD^ && git rev-parse HEAD >../expected ) && + git clone --mirror src mirror.detached && + ( cd src && git checkout - ) && + GIT_DIR=mirror.detached git rev-parse HEAD >actual && + test_cmp expected actual + +' + +test_expect_success 'clone --bare with detached HEAD' ' + + ( cd src && git checkout HEAD^ && git rev-parse HEAD >../expected ) && + git clone --bare src bare.detached && + ( cd src && git checkout - ) && + GIT_DIR=bare.detached git rev-parse HEAD >actual && + test_cmp expected actual + +' + test_expect_success 'clone --bare names the local repository .git' ' git clone --bare src && -- cgit v1.2.1 From c39852c18dc7efa794bbe20b10ecfe7038409727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:09 +0700 Subject: clone: factor out checkout code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read HEAD from disk instead of relying on local variable our_head_points_at, so that if earlier code fails to make HEAD properly, it'll be detected. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 101 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 91862f7963..98e354242b 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -486,6 +486,63 @@ static void write_followtags(const struct ref *refs, const char *msg) } } +static int checkout(void) +{ + unsigned char sha1[20]; + char *head; + struct lock_file *lock_file; + struct unpack_trees_options opts; + struct tree *tree; + struct tree_desc t; + int err = 0, fd; + + if (option_no_checkout) + return 0; + + head = resolve_refdup("HEAD", sha1, 1, NULL); + if (!head) { + warning(_("remote HEAD refers to nonexistent ref, " + "unable to checkout.\n")); + return 0; + } + if (strcmp(head, "HEAD")) { + if (prefixcmp(head, "refs/heads/")) + die(_("HEAD not found below refs/heads!")); + } + free(head); + + /* We need to be in the new work tree for the checkout */ + setup_work_tree(); + + lock_file = xcalloc(1, sizeof(struct lock_file)); + fd = hold_locked_index(lock_file, 1); + + memset(&opts, 0, sizeof opts); + opts.update = 1; + opts.merge = 1; + opts.fn = oneway_merge; + opts.verbose_update = (option_verbosity > 0); + opts.src_index = &the_index; + opts.dst_index = &the_index; + + tree = parse_tree_indirect(sha1); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + unpack_trees(1, &t, &opts); + + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die(_("unable to write new index file")); + + err |= run_hook(NULL, "post-checkout", sha1_to_hex(null_sha1), + sha1_to_hex(sha1), "1", NULL); + + if (!err && option_recursive) + err = run_command_v_opt(argv_submodule, RUN_GIT_CMD); + + return err; +} + static int write_one_config(const char *key, const char *value, void *data) { return git_config_set_multivar(key, value ? value : "true", "^$", 0); @@ -766,13 +823,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix) /* Source had detached HEAD pointing somewhere. */ update_ref(reflog_msg.buf, "HEAD", remote_head->old_sha1, NULL, REF_NODEREF, DIE_ON_ERR); - our_head_points_at = remote_head; - } else { - /* Nothing to checkout out */ - if (!option_no_checkout) - warning(_("remote HEAD refers to nonexistent ref, " - "unable to checkout.\n")); - option_no_checkout = 1; } if (transport) { @@ -780,42 +830,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) transport_disconnect(transport); } - if (!option_no_checkout) { - struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); - struct unpack_trees_options opts; - struct tree *tree; - struct tree_desc t; - int fd; - - /* We need to be in the new work tree for the checkout */ - setup_work_tree(); - - fd = hold_locked_index(lock_file, 1); - - memset(&opts, 0, sizeof opts); - opts.update = 1; - opts.merge = 1; - opts.fn = oneway_merge; - opts.verbose_update = (option_verbosity > 0); - opts.src_index = &the_index; - opts.dst_index = &the_index; - - tree = parse_tree_indirect(our_head_points_at->old_sha1); - parse_tree(tree); - init_tree_desc(&t, tree->buffer, tree->size); - unpack_trees(1, &t, &opts); - - if (write_cache(fd, active_cache, active_nr) || - commit_locked_index(lock_file)) - die(_("unable to write new index file")); - - err |= run_hook(NULL, "post-checkout", sha1_to_hex(null_sha1), - sha1_to_hex(our_head_points_at->old_sha1), "1", - NULL); - - if (!err && option_recursive) - err = run_command_v_opt(argv_submodule, RUN_GIT_CMD); - } + err = checkout(); strbuf_release(&reflog_msg); strbuf_release(&branch_top); -- cgit v1.2.1 From f034d3549fa34e65ca09ee9ce721dcf43faefd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:10 +0700 Subject: clone: factor out HEAD update code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While at it, update the comment at "if (remote_head)" Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 98e354242b..3b68014998 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -486,6 +486,29 @@ static void write_followtags(const struct ref *refs, const char *msg) } } +static void update_head(const struct ref *our, const struct ref *remote, + const char *msg) +{ + if (our) { + /* Local default branch link */ + create_symref("HEAD", our->name, NULL); + if (!option_bare) { + const char *head = skip_prefix(our->name, "refs/heads/"); + update_ref(msg, "HEAD", our->old_sha1, NULL, 0, DIE_ON_ERR); + install_branch_config(0, head, option_origin, our->name); + } + } else if (remote) { + /* + * We know remote HEAD points to a non-branch, or + * HEAD points to a branch but we don't know which one, or + * we asked for a specific branch but it did not exist. + * Detach HEAD in all these cases. + */ + update_ref(msg, "HEAD", remote->old_sha1, + NULL, REF_NODEREF, DIE_ON_ERR); + } +} + static int checkout(void) { unsigned char sha1[20]; @@ -807,23 +830,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) reflog_msg.buf); } - if (our_head_points_at) { - /* Local default branch link */ - create_symref("HEAD", our_head_points_at->name, NULL); - if (!option_bare) { - const char *head = skip_prefix(our_head_points_at->name, - "refs/heads/"); - update_ref(reflog_msg.buf, "HEAD", - our_head_points_at->old_sha1, - NULL, 0, DIE_ON_ERR); - install_branch_config(0, head, option_origin, - our_head_points_at->name); - } - } else if (remote_head) { - /* Source had detached HEAD pointing somewhere. */ - update_ref(reflog_msg.buf, "HEAD", remote_head->old_sha1, - NULL, REF_NODEREF, DIE_ON_ERR); - } + update_head(our_head_points_at, remote_head, reflog_msg.buf); if (transport) { transport_unlock_pack(transport); -- cgit v1.2.1 From 960b7d1c62bb8010954890b090546382161ae731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:11 +0700 Subject: clone: factor out remote ref writing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 3b68014998..2733fa47ef 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -486,6 +486,29 @@ static void write_followtags(const struct ref *refs, const char *msg) } } +static void update_remote_refs(const struct ref *refs, + const struct ref *mapped_refs, + const struct ref *remote_head_points_at, + const char *branch_top, + const char *msg) +{ + if (refs) { + clear_extra_refs(); + write_remote_refs(mapped_refs); + if (option_single_branch) + write_followtags(refs, msg); + } + + if (remote_head_points_at && !option_bare) { + struct strbuf head_ref = STRBUF_INIT; + strbuf_addstr(&head_ref, branch_top); + strbuf_addstr(&head_ref, "HEAD"); + create_symref(head_ref.buf, + remote_head_points_at->peer_ref->name, + msg); + } +} + static void update_head(const struct ref *our, const struct ref *remote, const char *msg) { @@ -782,12 +805,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix) } if (refs) { - clear_extra_refs(); - - write_remote_refs(mapped_refs); - if (option_single_branch) - write_followtags(refs, reflog_msg.buf); - remote_head = find_ref_by_name(refs, "HEAD"); remote_head_points_at = guess_remote_head(remote_head, mapped_refs, 0); @@ -821,14 +838,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix) "refs/heads/master"); } - if (remote_head_points_at && !option_bare) { - struct strbuf head_ref = STRBUF_INIT; - strbuf_addstr(&head_ref, branch_top.buf); - strbuf_addstr(&head_ref, "HEAD"); - create_symref(head_ref.buf, - remote_head_points_at->peer_ref->name, - reflog_msg.buf); - } + update_remote_refs(refs, mapped_refs, remote_head_points_at, + branch_top.buf, reflog_msg.buf); update_head(our_head_points_at, remote_head, reflog_msg.buf); -- cgit v1.2.1 From 6f48d39fa4d3e364c81d3cbbbd656c804b9ec873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:12 +0700 Subject: clone: delay cloning until after remote HEAD checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This gives us an opportunity to abort the command during remote HEAD check without wasting much bandwidth. Cloning with remote-helper remains before the check because the remote helper updates mapped_refs, which is necessary for remote ref checks. foreign_vcs field is used to indicate the transport is handled by remote helper. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 54 +++++++++++++++++++++++++++--------------------------- transport.c | 5 ++++- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 2733fa47ef..a1fbb3b90c 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -364,13 +364,8 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, closedir(dir); } -static const struct ref *clone_local(const char *src_repo, - const char *dest_repo) +static void clone_local(const char *src_repo, const char *dest_repo) { - const struct ref *ret; - struct remote *remote; - struct transport *transport; - if (option_shared) { struct strbuf alt = STRBUF_INIT; strbuf_addf(&alt, "%s/objects", src_repo); @@ -386,13 +381,8 @@ static const struct ref *clone_local(const char *src_repo, strbuf_release(&dest); } - remote = remote_get(src_repo); - transport = transport_get(remote, src_repo); - ret = transport_get_remote_refs(transport); - transport_disconnect(transport); if (0 <= option_verbosity) printf(_("done.\n")); - return ret; } static const char *junk_work_tree; @@ -619,6 +609,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) struct strbuf key = STRBUF_INIT, value = STRBUF_INIT; struct strbuf branch_top = STRBUF_INIT, reflog_msg = STRBUF_INIT; struct transport *transport = NULL; + struct remote *remote; int err = 0; struct refspec *refspec; @@ -773,13 +764,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix) strbuf_reset(&value); - if (is_local) { - refs = clone_local(path, git_dir); - mapped_refs = wanted_peer_refs(refs, refspec); - } else { - struct remote *remote = remote_get(option_origin); - transport = transport_get(remote, remote->url[0]); + remote = remote_get(option_origin); + transport = transport_get(remote, remote->url[0]); + if (!is_local) { if (!transport->get_refs_list || !transport->fetch) die(_("Don't know how to clone %s"), transport->url); @@ -796,14 +784,23 @@ int cmd_clone(int argc, const char **argv, const char *prefix) if (option_upload_pack) transport_set_option(transport, TRANS_OPT_UPLOADPACK, option_upload_pack); - - refs = transport_get_remote_refs(transport); - if (refs) { - mapped_refs = wanted_peer_refs(refs, refspec); - transport_fetch_refs(transport, mapped_refs); - } } + refs = transport_get_remote_refs(transport); + mapped_refs = refs ? wanted_peer_refs(refs, refspec) : NULL; + + /* + * mapped_refs may be updated if transport-helper is used so + * we need fetch it early because remote_head code below + * relies on it. + * + * for normal clones, transport_get_remote_refs() should + * return reliable ref set, we can delay cloning until after + * remote HEAD check. + */ + if (!is_local && remote->foreign_vcs && refs) + transport_fetch_refs(transport, mapped_refs); + if (refs) { remote_head = find_ref_by_name(refs, "HEAD"); remote_head_points_at = @@ -838,15 +835,18 @@ int cmd_clone(int argc, const char **argv, const char *prefix) "refs/heads/master"); } + if (is_local) + clone_local(path, git_dir); + else if (refs && !remote->foreign_vcs) + transport_fetch_refs(transport, mapped_refs); + update_remote_refs(refs, mapped_refs, remote_head_points_at, branch_top.buf, reflog_msg.buf); update_head(our_head_points_at, remote_head, reflog_msg.buf); - if (transport) { - transport_unlock_pack(transport); - transport_disconnect(transport); - } + transport_unlock_pack(transport); + transport_disconnect(transport); err = checkout(); diff --git a/transport.c b/transport.c index a99b7c9c45..43666394df 100644 --- a/transport.c +++ b/transport.c @@ -895,8 +895,10 @@ struct transport *transport_get(struct remote *remote, const char *url) while (is_urlschemechar(p == url, *p)) p++; - if (!prefixcmp(p, "::")) + if (!prefixcmp(p, "::")) { helper = xstrndup(url, p - url); + remote->foreign_vcs = helper; + } } if (helper) { @@ -938,6 +940,7 @@ struct transport *transport_get(struct remote *remote, const char *url) char *handler = xmalloc(len + 1); handler[len] = 0; strncpy(handler, url, len); + remote->foreign_vcs = handler; transport_helper_init(ret, handler); } -- cgit v1.2.1 From 9e5850460164f49dc9ae47569838084f5572846d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:13 +0700 Subject: clone: --branch= always means refs/heads/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It does not make sense to look outside refs/heads for HEAD's target (src_ref_prefix can be set to "refs/" if --mirror is used) because ref code only allows symref in form refs/heads/... Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index a1fbb3b90c..253a7946ce 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -48,7 +48,6 @@ static int option_verbosity; static int option_progress; static struct string_list option_config; static struct string_list option_reference; -static const char *src_ref_prefix = "refs/heads/"; static int opt_parse_reference(const struct option *opt, const char *arg, int unset) { @@ -413,6 +412,17 @@ static void remove_junk_on_signal(int signo) raise(signo); } +static struct ref *find_remote_branch(const struct ref *refs, const char *branch) +{ + struct ref *ref; + struct strbuf head = STRBUF_INIT; + strbuf_addstr(&head, "refs/heads/"); + strbuf_addstr(&head, branch); + ref = find_ref_by_name(refs, head.buf); + strbuf_release(&head); + return ref; +} + static struct ref *wanted_peer_refs(const struct ref *refs, struct refspec *refspec) { @@ -425,13 +435,8 @@ static struct ref *wanted_peer_refs(const struct ref *refs, if (!option_branch) remote_head = guess_remote_head(head, refs, 0); - else { - struct strbuf sb = STRBUF_INIT; - strbuf_addstr(&sb, src_ref_prefix); - strbuf_addstr(&sb, option_branch); - remote_head = find_ref_by_name(refs, sb.buf); - strbuf_release(&sb); - } + else + remote_head = find_remote_branch(refs, option_branch); if (!remote_head && option_branch) warning(_("Could not find remote branch %s to clone."), @@ -502,7 +507,7 @@ static void update_remote_refs(const struct ref *refs, static void update_head(const struct ref *our, const struct ref *remote, const char *msg) { - if (our) { + if (our && !prefixcmp(our->name, "refs/heads/")) { /* Local default branch link */ create_symref("HEAD", our->name, NULL); if (!option_bare) { @@ -609,6 +614,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) struct strbuf key = STRBUF_INIT, value = STRBUF_INIT; struct strbuf branch_top = STRBUF_INIT, reflog_msg = STRBUF_INIT; struct transport *transport = NULL; + const char *src_ref_prefix = "refs/heads/"; struct remote *remote; int err = 0; @@ -807,12 +813,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix) guess_remote_head(remote_head, mapped_refs, 0); if (option_branch) { - struct strbuf head = STRBUF_INIT; - strbuf_addstr(&head, src_ref_prefix); - strbuf_addstr(&head, option_branch); our_head_points_at = - find_ref_by_name(mapped_refs, head.buf); - strbuf_release(&head); + find_remote_branch(mapped_refs, option_branch); if (!our_head_points_at) { warning(_("Remote branch %s not found in " -- cgit v1.2.1 From 920b691fe4da8115f9b79901411c0cc5fff17efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:14 +0700 Subject: clone: refuse to clone if --branch points to bogus ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's possible that users make a typo in the branch name. Stop and let users recheck. Falling back to remote's HEAD is not documented any way. Except when using remote helper, the pack has not been transferred at this stage yet so we don't waste much bandwidth. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 12 ++++-------- t/t5500-fetch-pack.sh | 7 ------- t/t5706-clone-branch.sh | 8 ++------ 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 253a7946ce..3cfedb3a93 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -518,8 +518,7 @@ static void update_head(const struct ref *our, const struct ref *remote, } else if (remote) { /* * We know remote HEAD points to a non-branch, or - * HEAD points to a branch but we don't know which one, or - * we asked for a specific branch but it did not exist. + * HEAD points to a branch but we don't know which one. * Detach HEAD in all these cases. */ update_ref(msg, "HEAD", remote->old_sha1, @@ -816,12 +815,9 @@ int cmd_clone(int argc, const char **argv, const char *prefix) our_head_points_at = find_remote_branch(mapped_refs, option_branch); - if (!our_head_points_at) { - warning(_("Remote branch %s not found in " - "upstream %s, using HEAD instead"), - option_branch, option_origin); - our_head_points_at = remote_head_points_at; - } + if (!our_head_points_at) + die(_("Remote branch %s not found in upstream %s"), + option_branch, option_origin); } else our_head_points_at = remote_head_points_at; diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 7e85c71ad1..5237066140 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -282,13 +282,6 @@ test_expect_success 'clone shallow object count' ' test_cmp count3.expected count3.actual ' -test_expect_success 'clone shallow with nonexistent --branch' ' - git clone --depth 1 --branch Z "file://$(pwd)/." shallow4 && - GIT_DIR=shallow4/.git git rev-parse HEAD >actual && - git rev-parse HEAD >expected && - test_cmp expected actual -' - test_expect_success 'clone shallow with detached HEAD' ' git checkout HEAD^ && git clone --depth 1 "file://$(pwd)/." shallow5 && diff --git a/t/t5706-clone-branch.sh b/t/t5706-clone-branch.sh index f3f9a76056..56be67e07e 100755 --- a/t/t5706-clone-branch.sh +++ b/t/t5706-clone-branch.sh @@ -57,12 +57,8 @@ test_expect_success 'clone -b does not munge remotes/origin/HEAD' ' ) ' -test_expect_success 'clone -b with bogus branch chooses HEAD' ' - git clone -b bogus parent clone-bogus && - (cd clone-bogus && - check_HEAD master && - check_file one - ) +test_expect_success 'clone -b with bogus branch' ' + test_must_fail git clone -b bogus parent clone-bogus ' test_done -- cgit v1.2.1 From 5a7d5b683f869d3e3884a89775241afa515da9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:15 +0700 Subject: clone: allow --branch to take a tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because a tag ref cannot be put to HEAD, HEAD will become detached. This is consistent with "git checkout ". This is mostly useful in shallow clone, where it allows you to clone a tag in addtion to branches. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- Documentation/git-clone.txt | 5 +++-- builtin/clone.c | 20 +++++++++++++++++++- t/t5500-fetch-pack.sh | 15 +++++++++++++++ t/t5601-clone.sh | 9 +++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 0931a3e392..6e22522c4f 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -147,8 +147,9 @@ objects from the source repository into a pack in the cloned repository. -b :: Instead of pointing the newly created HEAD to the branch pointed to by the cloned repository's HEAD, point to `` branch - instead. In a non-bare repository, this is the branch that will - be checked out. + instead. `--branch` can also take tags and treat them like + detached HEAD. In a non-bare repository, this is the branch + that will be checked out. --upload-pack :: -u :: diff --git a/builtin/clone.c b/builtin/clone.c index 3cfedb3a93..651b4cc20b 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -420,6 +420,15 @@ static struct ref *find_remote_branch(const struct ref *refs, const char *branch strbuf_addstr(&head, branch); ref = find_ref_by_name(refs, head.buf); strbuf_release(&head); + + if (ref) + return ref; + + strbuf_addstr(&head, "refs/tags/"); + strbuf_addstr(&head, branch); + ref = find_ref_by_name(refs, head.buf); + strbuf_release(&head); + return ref; } @@ -441,8 +450,12 @@ static struct ref *wanted_peer_refs(const struct ref *refs, if (!remote_head && option_branch) warning(_("Could not find remote branch %s to clone."), option_branch); - else + else { get_fetch_map(remote_head, refspec, &tail, 0); + + /* if --branch=tag, pull the requested tag explicitly */ + get_fetch_map(remote_head, tag_refspec, &tail, 0); + } } else get_fetch_map(refs, refspec, &tail, 0); @@ -515,6 +528,11 @@ static void update_head(const struct ref *our, const struct ref *remote, update_ref(msg, "HEAD", our->old_sha1, NULL, 0, DIE_ON_ERR); install_branch_config(0, head, option_origin, our->name); } + } else if (our) { + struct commit *c = lookup_commit_reference(our->old_sha1); + /* --branch specifies a non-branch (i.e. tags), detach HEAD */ + update_ref(msg, "HEAD", c->object.sha1, + NULL, REF_NODEREF, DIE_ON_ERR); } else if (remote) { /* * We know remote HEAD points to a non-branch, or diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 5237066140..ce51692bb2 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -311,4 +311,19 @@ EOF test_cmp count6.expected count6.actual ' +test_expect_success 'shallow cloning single tag' ' + git clone --depth 1 --branch=TAGB1 "file://$(pwd)/." shallow7 && + cat >taglist.expected <<\EOF && +TAGB1 +TAGB2 +EOF + GIT_DIR=shallow7/.git git tag -l >taglist.actual && + test_cmp taglist.expected taglist.actual && + + echo "in-pack: 7" > count7.expected && + GIT_DIR=shallow7/.git git count-objects -v | + grep "^in-pack" > count7.actual && + test_cmp count7.expected count7.actual +' + test_done diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index e0b8db6c53..67869b4813 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -271,4 +271,13 @@ test_expect_success 'clone from original with relative alternate' ' grep /src/\\.git/objects target-10/objects/info/alternates ' +test_expect_success 'clone checking out a tag' ' + git clone --branch=some-tag src dst.tag && + GIT_DIR=src/.git git rev-parse some-tag >expected && + test_cmp expected dst.tag/.git/HEAD && + GIT_DIR=dst.tag/.git git config remote.origin.fetch >fetch.actual && + echo "+refs/heads/*:refs/remotes/origin/*" >fetch.expected && + test_cmp fetch.expected fetch.actual +' + test_done -- cgit v1.2.1 From 2857093ba15982d21ff0d5a9fd65294ac895cb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Mon, 16 Jan 2012 16:46:16 +0700 Subject: clone: print advice on checking out detached HEAD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- advice.c | 14 ++++++++++++++ advice.h | 1 + builtin/checkout.c | 16 +--------------- builtin/clone.c | 5 ++++- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/advice.c b/advice.c index e02e632df3..3e1a145bf0 100644 --- a/advice.c +++ b/advice.c @@ -64,3 +64,17 @@ void NORETURN die_resolve_conflict(const char *me) error_resolve_conflict(me); die("Exiting because of an unresolved conflict."); } + +void detach_advice(const char *new_name) +{ + const char fmt[] = + "Note: checking out '%s'.\n\n" + "You are in 'detached HEAD' state. You can look around, make experimental\n" + "changes and commit them, and you can discard any commits you make in this\n" + "state without impacting any branches by performing another checkout.\n\n" + "If you want to create a new branch to retain commits you create, you may\n" + "do so (now or later) by using -b with the checkout command again. Example:\n\n" + " git checkout -b new_branch_name\n\n"; + + fprintf(stderr, fmt, new_name); +} diff --git a/advice.h b/advice.h index e5d0af782b..7bda45b83e 100644 --- a/advice.h +++ b/advice.h @@ -14,5 +14,6 @@ int git_default_advice_config(const char *var, const char *value); void advise(const char *advice, ...); int error_resolve_conflict(const char *me); extern void NORETURN die_resolve_conflict(const char *me); +void detach_advice(const char *new_name); #endif /* ADVICE_H */ diff --git a/builtin/checkout.c b/builtin/checkout.c index f1984d9933..5bf96ba4d4 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -514,20 +514,6 @@ static void report_tracking(struct branch_info *new) strbuf_release(&sb); } -static void detach_advice(const char *old_path, const char *new_name) -{ - const char fmt[] = - "Note: checking out '%s'.\n\n" - "You are in 'detached HEAD' state. You can look around, make experimental\n" - "changes and commit them, and you can discard any commits you make in this\n" - "state without impacting any branches by performing another checkout.\n\n" - "If you want to create a new branch to retain commits you create, you may\n" - "do so (now or later) by using -b with the checkout command again. Example:\n\n" - " git checkout -b new_branch_name\n\n"; - - fprintf(stderr, fmt, new_name); -} - static void update_refs_for_switch(struct checkout_opts *opts, struct branch_info *old, struct branch_info *new) @@ -575,7 +561,7 @@ static void update_refs_for_switch(struct checkout_opts *opts, REF_NODEREF, DIE_ON_ERR); if (!opts->quiet) { if (old->path && advice_detached_head) - detach_advice(old->path, new->name); + detach_advice(new->name); describe_detached_head(_("HEAD is now at"), new->commit); } } else if (new->path) { /* Switch branches. */ diff --git a/builtin/clone.c b/builtin/clone.c index 651b4cc20b..72eebca535 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -563,7 +563,10 @@ static int checkout(void) "unable to checkout.\n")); return 0; } - if (strcmp(head, "HEAD")) { + if (!strcmp(head, "HEAD")) { + if (advice_detached_head) + detach_advice(sha1_to_hex(sha1)); + } else { if (prefixcmp(head, "refs/heads/")) die(_("HEAD not found below refs/heads!")); } -- cgit v1.2.1 From 35a71f1402b40b580d985a9d7e5fb1c9ec4d0232 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 17 Jan 2012 01:02:32 -0500 Subject: credential-cache: ignore "connection refused" errors The credential-cache helper will try to connect to its daemon over a unix socket. Originally, a failure to do so was silently ignored, and we would either give up (if performing a "get" or "erase" operation), or spawn a new daemon (for a "store" operation). But since 8ec6c8d, we try to report more errors. We detect a missing daemon by checking for ENOENT on our connection attempt. If the daemon is missing, we continue as before (giving up or spawning a new daemon). For any other error, we die and report the problem. However, checking for ENOENT is not sufficient for a missing daemon. We might also get ECONNREFUSED if a dead daemon process left a stale socket. This generally shouldn't happen, as the daemon cleans up after itself, but the daemon may not always be given a chance to do so (e.g., power loss, "kill -9"). The resulting state is annoying not just because the helper outputs an extra useless message, but because it actually blocks the helper from spawning a new daemon to replace the stale socket. Fix it by checking for ECONNREFUSED. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- credential-cache.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/credential-cache.c b/credential-cache.c index 193301877f..9a03792c7d 100644 --- a/credential-cache.c +++ b/credential-cache.c @@ -72,7 +72,7 @@ static void do_cache(const char *socket, const char *action, int timeout, } if (send_request(socket, &buf) < 0) { - if (errno != ENOENT) + if (errno != ENOENT && errno != ECONNREFUSED) die_errno("unable to connect to cache daemon"); if (flags & FLAG_SPAWN) { spawn_daemon(socket); -- cgit v1.2.1 From e45a59955ec78bca12930bcf6aa9642fd94c9e7c Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 17 Jan 2012 06:50:31 +0100 Subject: pack_refs(): remove redundant check handle_one_ref() only adds refs to the cbdata.ref_to_prune list if (cbdata.flags & PACK_REFS_PRUNE) is set. So any references in this list at the end of pack_refs() can be pruned unconditionally. Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- pack-refs.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pack-refs.c b/pack-refs.c index 23bbd00e3e..f09a054228 100644 --- a/pack-refs.c +++ b/pack-refs.c @@ -143,7 +143,6 @@ int pack_refs(unsigned int flags) packed.fd = -1; if (commit_lock_file(&packed) < 0) die_errno("unable to overwrite old ref-pack file"); - if (cbdata.flags & PACK_REFS_PRUNE) - prune_refs(cbdata.ref_to_prune); + prune_refs(cbdata.ref_to_prune); return 0; } -- cgit v1.2.1 From e6ed3ca651fac702f9d9a9ef64a14c7efadf7365 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 17 Jan 2012 06:50:32 +0100 Subject: ref_array: keep track of whether references are sorted Keep track of how many entries at the beginning of a ref_array are already sorted. In sort_ref_array(), return early if the the array is already sorted (i.e., if no new references has been appended to the end of the list since the last call to sort_ref_array()). Sort ref_arrays only when needed, namely in search_ref_array() and in do_for_each_ref(). However, never call sort_ref_array() on the extra_refs, because extra_refs can contain multiple entries with the same name and because sort_ref_array() not only sorts, but de-dups its contents. This change is currently not useful, because entries are not added to ref_arrays after they are created. But in a moment they will be... Implementation note: we could store a binary "sorted" value instead of an integer, but storing the number of sorted entries leaves the way open for a couple of possible future optimizations: * In sort_ref_array(), sort *only* the unsorted entries, then merge them with the sorted entries. This should be faster if most of the entries are already sorted. * Teach search_ref_array() to do a binary search of any sorted entries, and if unsuccessful do a linear search of any unsorted entries. This would avoid the need to sort the list every time that search_ref_array() is called, and (given some intelligence about how often to sort) could significantly improve the speed in certain hypothetical usage patterns. Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- refs.c | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/refs.c b/refs.c index 6f436f1cb0..3785cc200c 100644 --- a/refs.c +++ b/refs.c @@ -17,6 +17,15 @@ struct ref_entry { struct ref_array { int nr, alloc; + + /* + * Entries with index 0 <= i < sorted are sorted by name. New + * entries are appended to the list unsorted, and are sorted + * only when required; thus we avoid the need to sort the list + * after the addition of every reference. + */ + int sorted; + struct ref_entry **refs; }; @@ -105,12 +114,18 @@ static int is_dup_ref(const struct ref_entry *ref1, const struct ref_entry *ref2 } } +/* + * Sort the entries in array (if they are not already sorted). + */ static void sort_ref_array(struct ref_array *array) { int i, j; - /* Nothing to sort unless there are at least two entries */ - if (array->nr < 2) + /* + * This check also prevents passing a zero-length array to qsort(), + * which is a problem on some platforms. + */ + if (array->sorted == array->nr) return; qsort(array->refs, array->nr, sizeof(*array->refs), ref_entry_cmp); @@ -124,7 +139,7 @@ static void sort_ref_array(struct ref_array *array) } array->refs[++i] = array->refs[j]; } - array->nr = i + 1; + array->sorted = array->nr = i + 1; } static struct ref_entry *search_ref_array(struct ref_array *array, const char *refname) @@ -137,7 +152,7 @@ static struct ref_entry *search_ref_array(struct ref_array *array, const char *r if (!array->nr) return NULL; - + sort_ref_array(array); len = strlen(refname) + 1; e = xmalloc(sizeof(struct ref_entry) + len); memcpy(e->name, refname, len); @@ -168,6 +183,10 @@ static struct ref_cache { static struct ref_entry *current_ref; +/* + * Never call sort_ref_array() on the extra_refs, because it is + * allowed to contain entries with duplicate names. + */ static struct ref_array extra_refs; static void clear_ref_array(struct ref_array *array) @@ -176,7 +195,7 @@ static void clear_ref_array(struct ref_array *array) for (i = 0; i < array->nr; i++) free(array->refs[i]); free(array->refs); - array->nr = array->alloc = 0; + array->sorted = array->nr = array->alloc = 0; array->refs = NULL; } @@ -268,7 +287,6 @@ static void read_packed_refs(FILE *f, struct ref_array *array) !get_sha1_hex(refline + 1, sha1)) hashcpy(last->peeled, sha1); } - sort_ref_array(array); } void add_extra_ref(const char *refname, const unsigned char *sha1, int flag) @@ -404,7 +422,6 @@ static struct ref_array *get_loose_refs(struct ref_cache *refs) { if (!refs->did_loose) { get_ref_dir(refs, "refs", &refs->loose); - sort_ref_array(&refs->loose); refs->did_loose = 1; } return &refs->loose; @@ -720,6 +737,8 @@ static int do_for_each_ref(const char *submodule, const char *base, each_ref_fn for (i = 0; i < extra->nr; i++) retval = do_one_ref(base, fn, trim, flags, cb_data, extra->refs[i]); + sort_ref_array(packed); + sort_ref_array(loose); while (p < packed->nr && l < loose->nr) { struct ref_entry *entry; int cmp = strcmp(packed->refs[p]->name, loose->refs[l]->name); -- cgit v1.2.1 From 30249ee68fa5fa63bfb9bb417987b0547253b8e7 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 17 Jan 2012 06:50:33 +0100 Subject: add_packed_ref(): new function in the refs API. Add a new function add_packed_ref() that adds a reference directly to the in-memory packed reference cache. This will be useful for creating local references while cloning. Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- refs.c | 6 ++++++ refs.h | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/refs.c b/refs.c index 3785cc200c..b8843bb476 100644 --- a/refs.c +++ b/refs.c @@ -319,6 +319,12 @@ static struct ref_array *get_packed_refs(struct ref_cache *refs) return &refs->packed; } +void add_packed_ref(const char *refname, const unsigned char *sha1) +{ + add_ref(get_packed_refs(get_ref_cache(NULL)), + create_ref_entry(refname, sha1, REF_ISPACKED, 1)); +} + static void get_ref_dir(struct ref_cache *refs, const char *base, struct ref_array *array) { diff --git a/refs.h b/refs.h index d4982915c5..00ba1e2813 100644 --- a/refs.h +++ b/refs.h @@ -50,6 +50,12 @@ extern int for_each_rawref(each_ref_fn, void *); extern void warn_dangling_symref(FILE *fp, const char *msg_fmt, const char *refname); +/* + * Add a reference to the in-memory packed reference cache. To actually + * write the reference to the packed-refs file, call pack_refs(). + */ +extern void add_packed_ref(const char *refname, const unsigned char *sha1); + /* * Extra refs will be listed by for_each_ref() before any actual refs * for the duration of this process or until clear_extra_refs() is -- cgit v1.2.1 From 39ef7fae9a398ad4523a211bc87aff599c3d3869 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 17 Jan 2012 06:50:34 +0100 Subject: write_remote_refs(): create packed (rather than extra) refs write_remote_refs() creates new packed refs from references obtained from the remote repository, which is "out of thin air" as far as the local repository is concerned. Previously it did this by creating "extra" refs, then calling pack_refs() to bake them into the packed-refs file. Instead, create packed refs (in the packed reference cache) directly, then call pack_refs(). Aside from being more logical, this is another step towards removing extra refs entirely. Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- builtin/clone.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 86db954730..9413537a8e 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -441,11 +441,10 @@ static void write_remote_refs(const struct ref *local_refs) for (r = local_refs; r; r = r->next) { if (!r->peer_ref) continue; - add_extra_ref(r->peer_ref->name, r->old_sha1, 0); + add_packed_ref(r->peer_ref->name, r->old_sha1); } pack_refs(PACK_REFS_ALL); - clear_extra_refs(); } static int write_one_config(const char *key, const char *value, void *data) -- cgit v1.2.1 From c4d2539af751ed394ee68c02fe688e75c647c5fe Mon Sep 17 00:00:00 2001 From: Jens Lehmann Date: Tue, 17 Jan 2012 22:04:31 +0100 Subject: test-lib: add the test_pause convenience function Since 781f76b15 (test-lib: redirect stdin of tests) you can't simply put a "bash &&" into a test for debugging purposes anymore. Instead you'll have to use "bash <&6 >&3 2>&4". As that invocation is not that easy to remember add the test_pause convenience function. It invokes "$SHELL_PATH" to provide a sane shell for the user. This function also checks if the -v flag is given and will error out if that is not the case instead of letting the test hang until ^D is pressed. Signed-off-by: Jens Lehmann Signed-off-by: Junio C Hamano --- t/README | 13 +++++++++++++ t/test-lib.sh | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/t/README b/t/README index c85abaffb3..c09c582c16 100644 --- a/t/README +++ b/t/README @@ -548,6 +548,19 @@ library for your script to use. ... ' + - test_pause + + This command is useful for writing and debugging tests and must be + removed before submitting. It halts the execution of the test and + spawns a shell in the trash directory. Exit the shell to continue + the test. Example: + + test_expect_success 'test' ' + git do-something >actual && + test_pause && + test_cmp expected actual + ' + Prerequisites ------------- diff --git a/t/test-lib.sh b/t/test-lib.sh index a65dfc7ea9..709a30067e 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -329,6 +329,19 @@ test_tick () { export GIT_COMMITTER_DATE GIT_AUTHOR_DATE } +# Stop execution and start a shell. This is useful for debugging tests and +# only makes sense together with "-v". +# +# Be sure to remove all invocations of this command before submitting. + +test_pause () { + if test "$verbose" = t; then + "$SHELL_PATH" <&6 >&3 2>&4 + else + error >&5 "test_pause requires --verbose" + fi +} + # Call test_commit with the arguments " [ []]" # # This will commit a file with the given contents and the given commit -- cgit v1.2.1 From 11b17afc933446c3afdbed72afaa440348f2b0ff Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 17 Jan 2012 14:52:24 -0800 Subject: pulling signed tag: add howto document Signed-off-by: Junio C Hamano --- Documentation/Makefile | 5 +- .../howto/using-signed-tag-in-pull-request.txt | 217 +++++++++++++++++++++ 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 Documentation/howto/using-signed-tag-in-pull-request.txt diff --git a/Documentation/Makefile b/Documentation/Makefile index 116f17587e..d40e211f22 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -20,7 +20,10 @@ ARTICLES += everyday ARTICLES += git-tools ARTICLES += git-bisect-lk2009 # with their own formatting rules. -SP_ARTICLES = howto/revert-branch-rebase howto/using-merge-subtree user-manual +SP_ARTICLES = user-manual +SP_ARTICLES += howto/revert-branch-rebase +SP_ARTICLES += howto/using-merge-subtree +SP_ARTICLES += howto/using-signed-tag-in-pull-request API_DOCS = $(patsubst %.txt,%,$(filter-out technical/api-index-skel.txt technical/api-index.txt, $(wildcard technical/api-*.txt))) SP_ARTICLES += $(API_DOCS) SP_ARTICLES += technical/api-index diff --git a/Documentation/howto/using-signed-tag-in-pull-request.txt b/Documentation/howto/using-signed-tag-in-pull-request.txt new file mode 100644 index 0000000000..a1351c5bb8 --- /dev/null +++ b/Documentation/howto/using-signed-tag-in-pull-request.txt @@ -0,0 +1,217 @@ +From: Junio C Hamano +Date: Tue, 17 Jan 2011 13:00:00 -0800 +Subject: Using signed tag in pull requests +Abstract: Beginning v1.7.9, a contributor can push a signed tag to her + publishing repository and ask her integrator to pull it. This assures the + integrator that the pulled history is authentic and allows others to + later validate it. +Content-type: text/asciidoc + +Using signed tag in pull requests +================================= + +A typical distributed workflow using Git is for a contributor to fork a +project, build on it, publish the result to her public repository, and ask +the "upstream" person (often the owner of the project where she forked +from) to pull from her public repository. Requesting such a "pull" is made +easy by the `git request-pull` command. + +Earlier, a typical pull request may have started like this: + +------------ + The following changes since commit 406da78032179...: + + Froboz 3.2 (2011-09-30 14:20:57 -0700) + + are available in the git repository at: + + example.com:/git/froboz.git for-xyzzy +------------ + +followed by a shortlog of the changes and a diffstat. + +The request was for a branch name (e.g. `for-xyzzy`) in the public +repository of the contributor, and even though it stated where the +contributor forked her work from, the message did not say anything about +the commit to expect at the tip of the for-xyzzy branch. If the site that +hosts the public repository of the contributor cannot be fully trusted, it +was unnecessarily hard to make sure what was pulled by the integrator was +genuinely what the contributor had produced for the project. Also there +was no easy way for third-party auditors to later verify the resulting +history. + +Starting from Git release v1.7.9, a contributor can add a signed tag to +the commit at the tip of the history and ask the integrator to pull that +signed tag. When the integrator runs `git pull`, the signed tag is +automatically verified to assure that the history is not tampered with. +In addition, the resulting merge commit records the content of the signed +tag, so that other people can verify that the branch merged by the +integrator was signed by the contributor, without fetching the signed tag +used to validate the pull request separately and keeping it in the refs +namespace. + +This document describes the workflow between the contributor and the +integrator, using Git v1.7.9 or later. + + +A contributor or a lieutenant +----------------------------- + +After preparing her work to be pulled, the contributor uses `git tag -s` +to create a signed tag: + +------------ + $ git checkout work + $ ... "git pull" from sublieutenants, "git commit" your own work ... + $ git tag -s -m "Completed frotz feature" frotz-for-xyzzy work +------------ + +Note that this example uses the `-m` option to create a signed tag with +just a one-liner message, but this is for illustration purposes only. It +is advisable to compose a well-written explanation of what the topic does +to justify why it is worthwhile for the integrator to pull it, as this +message will eventually become part of the final history after the +integrator responds to the pull request (as we will see later). + +Then she pushes the tag out to her public repository: + +------------ + $ git push example.com:/git/froboz.git/ +frotz-for-xyzzy +------------ + +There is no need to push the `work` branch or anything else. + +Note that the above command line used a plus sign at the beginning of +`+frotz-for-xyzzy` to allow forcing the update of a tag, as the same +contributor may want to reuse a signed tag with the same name after the +previous pull request has already been responded to. + +The contributor then prepares a message to request a "pull": + +------------ + $ git request-pull v3.2 example.com:/git/froboz.git/ frotz-for-xyzzy >msg.txt +------------ + +The arguments are: + +. the version of the integrator's commit the contributor based her work on; +. the URL of the repository, to which the contributor has pushed what she + wants to get pulled; and +. the name of the tag the contributor wants to get pulled (earlier, she could + write only a branch name here). + +The resulting msg.txt file begins like so: + +------------ + The following changes since commit 406da78032179...: + + Froboz 3.2 (2011-09-30 14:20:57 -0700) + + are available in the git repository at: + + example.com:/git/froboz.git frotz-for-xyzzy + + for you to fetch changes up to 703f05ad5835c...: + + Add tests and documentation for frotz (2011-12-02 10:02:52 -0800) + + ----------------------------------------------- + Completed frotz feature + ----------------------------------------------- +------------ + +followed by a shortlog of the changes and a diffstat. Comparing this with +the earlier illustration of the output from the traditional `git request-pull` +command, the reader should notice that: + +. The tip commit to expect is shown to the integrator; and +. The signed tag message is shown prominently between the dashed lines + before the shortlog. + +The latter is why the contributor would want to justify why pulling her +work is worthwhile when creating the signed tag. The contributor then +opens her favorite MUA, reads msg.txt, edits and sends it to her upstream +integrator. + + +Integrator +---------- + +After receiving such a pull request message, the integrator fetches and +integrates the tag named in the request, with: + +------------ + $ git pull example.com:/git/froboz.git/ frotz-for-xyzzy +------------ + +This operation will always open an editor to allow the integrator to fine +tune the commit log message when merging a signed tag. Also, pulling a +signed tag will always create a merge commit even when the integrator does +not have any new commit since the contributor's work forked (i.e. 'fast +forward'), so that the integrator can properly explain what the merge is +about and why it was made. + +In the editor, the integrator will see something like this: + +------------ + Merge tag 'frotz-for-xyzzy' of example.com:/git/froboz.git/ + + Completed frotz feature + # gpg: Signature made Fri 02 Dec 2011 10:03:01 AM PST using RSA key ID 96AFE6CB + # gpg: Good signature from "Con Tributor " +------------ + +Notice that the message recorded in the signed tag "Completed frotz +feature" appears here, and again that is why it is important for the +contributor to explain her work well when creating the signed tag. + +As usual, the lines commented with `#` are stripped out. The resulting +commit records the signed tag used for this validation in a hidden field +so that it can later be used by others to audit the history. There is no +need for the integrator to keep a separate copy of the tag in his +repository (i.e. `git tag -l` won't list the `frotz-for-xyzzy` tag in the +above example), and there is no need to publish the tag to his public +repository, either. + +After the integrator responds to the pull request and her work becomes +part of the permanent history, the contributor can remove the tag from +her public repository, if she chooses, in order to keep the tag namespace +of her public repository clean, with: + +------------ + $ git push example.com:/git/froboz.git :frotz-for-xyzzy +------------ + + +Auditors +-------- + +The `--show-signature` option can be given to `git log` or `git show` and +shows the verification status of the embedded signed tag in merge commits +created when the integrator responded to a pull request of a signed tag. + +A typical output from `git show --show-signature` may look like this: + +------------ + $ git show --show-signature + commit 02306ef6a3498a39118aef9df7975bdb50091585 + merged tag 'frotz-for-xyzzy' + gpg: Signature made Fri 06 Jan 2012 12:41:49 PM PST using RSA key ID 96AFE6CB + gpg: Good signature from "Con Tributor " + Merge: 406da78 703f05a + Author: Inte Grator + Date: Tue Jan 17 13:49:41 2012 -0800 + + Merge tag 'frotz-for-xyzzy' of example.com:/git/froboz.git/ + + Completed frotz feature + + * tag 'frotz-for-xyzzy' (100 commits) + Add tests and documentation for frotz + ... +------------ + +There is no need for the auditor to explicitly fetch the contributor's +signature, or to even be aware of what tag(s) the contributor and integrator +used to communicate the signature. All the required information is recorded +as part of the merge commit. -- cgit v1.2.1 From 5c8eeb83dbba11d7e41758b7a8dcb2da6a2025ba Mon Sep 17 00:00:00 2001 From: Nguyen Thai Ngoc Duy Date: Sun, 15 Jan 2012 17:03:27 +0700 Subject: diff-index: enable recursive pathspec matching in unpack_trees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pathspec structure has a few bits of data to drive various operation modes after we unified the pathspec matching logic in various codepaths. For example, max_depth field is there so that "git grep" can limit the output for files found in limited depth of tree traversal. Also in order to show just the surface level differences in "git diff-tree", recursive field stops us from descending into deeper level of the tree structure when it is set to false, and this also affects pathspec matching when we have wildcards in the pathspec. The diff-index has always wanted the recursive behaviour, and wanted to match pathspecs without any depth limit. But we forgot to do so when we updated tree_entry_interesting() logic to unify the pathspec matching logic. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- diff-lib.c | 2 ++ t/t4010-diff-pathspec.sh | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/diff-lib.c b/diff-lib.c index 62f4cd94cf..fc0dff31b5 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -469,6 +469,8 @@ static int diff_cache(struct rev_info *revs, opts.src_index = &the_index; opts.dst_index = NULL; opts.pathspec = &revs->diffopt.pathspec; + opts.pathspec->recursive = 1; + opts.pathspec->max_depth = -1; init_tree_desc(&t, tree->buffer, tree->size); return unpack_trees(1, &t, &opts); diff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh index fbc8cd8f05..af5134b70c 100755 --- a/t/t4010-diff-pathspec.sh +++ b/t/t4010-diff-pathspec.sh @@ -47,6 +47,14 @@ test_expect_success \ 'git diff-index --cached $tree -- path1/ >current && compare_diff_raw current expected' +cat >expected <<\EOF +:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M path1/file1 +EOF +test_expect_success \ + '"*file1" should show path1/file1' \ + 'git diff-index --cached $tree -- "*file1" >current && + compare_diff_raw current expected' + cat >expected <<\EOF :100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M file0 EOF -- cgit v1.2.1 From 00653435488f5ed86c5d6dc411fa14fa56f5e3bc Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 18 Jan 2012 15:46:31 -0800 Subject: Git 1.7.7.6 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.7.6.txt | 4 ++++ GIT-VERSION-GEN | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Documentation/RelNotes/1.7.7.6.txt b/Documentation/RelNotes/1.7.7.6.txt index b8b86ebc61..8df606d452 100644 --- a/Documentation/RelNotes/1.7.7.6.txt +++ b/Documentation/RelNotes/1.7.7.6.txt @@ -8,6 +8,10 @@ Fixes since v1.7.7.5 directory when two paths in question are in adjacent directories and the name of the one directory is a prefix of the other. + * A wildcard that matches deeper hierarchy given to the "diff-index" command, + e.g. "git diff-index HEAD -- '*.txt'", incorrectly reported additions of + matching files even when there is no change. + * When producing a "thin pack" (primarily used in bundles and smart HTTP transfers) out of a fully packed repository, we unnecessarily avoided sending recent objects as a delta against objects we know diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 7b7ac91f80..2feae58821 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.7.7.5 +DEF_VER=v1.7.7.6 LF=' ' -- cgit v1.2.1 From c572f491e545f3fe76a966e418139fc9755a910f Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 18 Jan 2012 15:51:00 -0800 Subject: Git 1.7.8.4 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.8.4.txt | 4 ++++ Documentation/git.txt | 6 ++++-- GIT-VERSION-GEN | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Documentation/RelNotes/1.7.8.4.txt b/Documentation/RelNotes/1.7.8.4.txt index c21fc99bdb..9bebdbf13d 100644 --- a/Documentation/RelNotes/1.7.8.4.txt +++ b/Documentation/RelNotes/1.7.8.4.txt @@ -8,6 +8,10 @@ Fixes since v1.7.8.3 directory when two paths in question are in adjacent directories and the name of the one directory is a prefix of the other. + * A wildcard that matches deeper hierarchy given to the "diff-index" command, + e.g. "git diff-index HEAD -- '*.txt'", incorrectly reported additions of + matching files even when there is no change. + * When producing a "thin pack" (primarily used in bundles and smart HTTP transfers) out of a fully packed repository, we unnecessarily avoided sending recent objects as a delta against objects we know diff --git a/Documentation/git.txt b/Documentation/git.txt index 8a77fa47ad..db5d5570cf 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -44,17 +44,19 @@ unreleased) version of git, that is available from 'master' branch of the `git.git` repository. Documentation for older releases are available here: -* link:v1.7.8.3/git.html[documentation for release 1.7.8.3] +* link:v1.7.8.4/git.html[documentation for release 1.7.8.4] * release notes for + link:RelNotes/1.7.8.4.txt[1.7.8.4], link:RelNotes/1.7.8.3.txt[1.7.8.3], link:RelNotes/1.7.8.2.txt[1.7.8.2], link:RelNotes/1.7.8.1.txt[1.7.8.1], link:RelNotes/1.7.8.txt[1.7.8]. -* link:v1.7.7.5/git.html[documentation for release 1.7.7.5] +* link:v1.7.7.6/git.html[documentation for release 1.7.7.6] * release notes for + link:RelNotes/1.7.7.6.txt[1.7.7.6], link:RelNotes/1.7.7.5.txt[1.7.7.5], link:RelNotes/1.7.7.4.txt[1.7.7.4], link:RelNotes/1.7.7.3.txt[1.7.7.3], diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 3d2a089add..445190db7a 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.7.8.3 +DEF_VER=v1.7.8.4 LF=' ' -- cgit v1.2.1 From bddcefc6380bd6629f3f12b5ffd856ec436c6abd Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 18 Jan 2012 15:53:35 -0800 Subject: Git 1.7.9-rc2 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.9.txt | 10 +--------- GIT-VERSION-GEN | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Documentation/RelNotes/1.7.9.txt b/Documentation/RelNotes/1.7.9.txt index 5eb88b899a..9b0a6ebf15 100644 --- a/Documentation/RelNotes/1.7.9.txt +++ b/Documentation/RelNotes/1.7.9.txt @@ -1,4 +1,4 @@ -Git v1.7.9 Release Notes (draft) +Git v1.7.9 Release Notes ======================== Updates since v1.7.8 @@ -109,11 +109,3 @@ Fixes since v1.7.8 Unless otherwise noted, all the fixes since v1.7.8 in the maintenance releases are contained in this release (see release notes to them for details). - --- -exec >/var/tmp/1 -O=v1.7.9-rc0-44-g478c446 -echo O=$(git describe master) -git log --first-parent --oneline --reverse ^$O master -echo -git shortlog --no-merges ^$O ^maint master diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 15d06cfaab..ba8a07a2ea 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.7.9-rc1 +DEF_VER=v1.7.9-rc2 LF=' ' -- cgit v1.2.1 From 36ed1913e1d5de0930e59db6eeec3ccb2bd58bd9 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 19 Jan 2012 11:58:45 -0800 Subject: Making pathspec limited log play nicer with --first-parent In a topic branch workflow, you often want to find the latest commit that merged a side branch that touched a particular area of the system, so that a new topic branch to work on that area can be forked from that commit. For example, I wanted to find an appropriate fork-point to queue Luke's changes related to git-p4 in contrib/fast-import/. "git log --first-parent" traverses the first-parent chain, and "-m --stat" shows the list of paths touched by commits including merge commits. We could ask the question this way: # What is the latest commit that touched that path? $ git log --first-parent --oneline -m --stat master | sed -e '/^ contrib\/fast-import\/git-p4 /q' | tail The above finds that 8cbfc11 (Merge branch 'pw/p4-view-updates', 2012-01-06) was such a commit. But a more natural way to spell this question is this: $ git log --first-parent --oneline -m --stat -1 master -- \ contrib/fast-import/git-p4 Unfortunately, this does not work. It finds ecb7cf9 (git-p4: rewrite view handling, 2012-01-02). This commit is a part of the merged topic branch and is _not_ on the first-parent path from the 'master': $ git show-branch 8cbfc11 ecb7cf9 ! [8cbfc11] Merge branch 'pw/p4-view-updates' ! [ecb7cf9] git-p4: rewrite view handling -- - [8cbfc11] Merge branch 'pw/p4-view-updates' + [8cbfc11^2] git-p4: view spec documentation ++ [ecb7cf9] git-p4: rewrite view handling The problem is caused by the merge simplification logic when it inspects the merge commit 8cbfc11. In this case, the history leading to the tip of 'master' did not touch git-p4 since 'pw/p4-view-updates' topic forked, and the result of the merge is simply a copy from the tip of the topic branch in the view limited by the given pathspec. The merge simplification logic discards the history on the mainline side of the merge, and pretends as if the sole parent of the merge is its second parent, i.e. the tip of the topic. While this simplification is correct in the general case, it is at least surprising if not outright wrong when the user explicitly asked to show the first-parent history. Here is an attempt to fix this issue, by not allowing us to compare the merge result with anything but the first parent when --first-parent is in effect, to avoid the history traversal veering off to the side branch. Signed-off-by: Junio C Hamano --- revision.c | 10 +++++++++- t/t6012-rev-list-simplify.sh | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/revision.c b/revision.c index c46cfaa3e4..f03b7031e5 100644 --- a/revision.c +++ b/revision.c @@ -368,7 +368,7 @@ static int rev_same_tree_as_empty(struct rev_info *revs, struct commit *commit) static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) { struct commit_list **pp, *parent; - int tree_changed = 0, tree_same = 0; + int tree_changed = 0, tree_same = 0, nth_parent = 0; /* * If we don't do pruning, everything is interesting @@ -396,6 +396,14 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) while ((parent = *pp) != NULL) { struct commit *p = parent->item; + /* + * Do not compare with later parents when we care only about + * the first parent chain, in order to avoid derailing the + * traversal to follow a side branch that brought everything + * in the path we are limited to by the pathspec. + */ + if (revs->first_parent_only && nth_parent++) + break; if (parse_commit(p) < 0) die("cannot simplify commit %s (because of %s)", sha1_to_hex(commit->object.sha1), diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh index af34a1e817..839ad97b79 100755 --- a/t/t6012-rev-list-simplify.sh +++ b/t/t6012-rev-list-simplify.sh @@ -86,5 +86,6 @@ check_result 'I H E C B A' --full-history --date-order -- file check_result 'I E C B A' --simplify-merges -- file check_result 'I B A' -- file check_result 'I B A' --topo-order -- file +check_result 'H' --first-parent -- another-file test_done -- cgit v1.2.1 From 5238cbf65638d7a097bdb5ca8226f5acbe31f143 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Thu, 19 Jan 2012 19:12:09 -0800 Subject: remote-curl: Fix push status report when all branches fail The protocol between transport-helper.c and remote-curl requires remote-curl to always print a blank line after the push command has run. If the blank line is ommitted, transport-helper kills its container process (the git push the user started) with exit(128) and no message indicating a problem, assuming the helper already printed reasonable error text to the console. However if the remote rejects all branches with "ng" commands in the report-status reply, send-pack terminates with non-zero status, and in turn remote-curl exited with non-zero status before outputting the blank line after the helper status printed by send-pack. No error messages reach the user. This caused users to see the following from git push over HTTP when the remote side's update hook rejected the branch: $ git push http://... master Counting objects: 4, done. Delta compression using up to 6 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 301 bytes, done. Total 3 (delta 0), reused 0 (delta 0) $ Always print a blank line after the send-pack process terminates, ensuring the helper status report (if it was output) will be correctly parsed by the calling transport-helper.c. This ensures the helper doesn't abort before the status report can be shown to the user. Signed-off-by: Shawn O. Pearce Signed-off-by: Junio C Hamano --- remote-curl.c | 9 +++++---- t/t5541-http-push.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/remote-curl.c b/remote-curl.c index 69831e931a..f48485931f 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -797,7 +797,7 @@ static int push(int nr_spec, char **specs) static void parse_push(struct strbuf *buf) { char **specs = NULL; - int alloc_spec = 0, nr_spec = 0, i; + int alloc_spec = 0, nr_spec = 0, i, ret; do { if (!prefixcmp(buf->buf, "push ")) { @@ -814,12 +814,13 @@ static void parse_push(struct strbuf *buf) break; } while (1); - if (push(nr_spec, specs)) - exit(128); /* error already reported */ - + ret = push(nr_spec, specs); printf("\n"); fflush(stdout); + if (ret) + exit(128); /* error already reported */ + free_specs: for (i = 0; i < nr_spec; i++) free(specs[i]); diff --git a/t/t5541-http-push.sh b/t/t5541-http-push.sh index a73c82635f..b8f4c2ac3c 100755 --- a/t/t5541-http-push.sh +++ b/t/t5541-http-push.sh @@ -95,6 +95,32 @@ test_expect_success 'create and delete remote branch' ' test_must_fail git show-ref --verify refs/remotes/origin/dev ' +cat >"$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git/hooks/update" <exp < dev2 (hook declined) +error: failed to push some refs to 'http://127.0.0.1:5541/smart/test_repo.git' +EOF + +test_expect_success 'rejected update prints status' ' + cd "$ROOT_PATH"/test_repo_clone && + git checkout -b dev2 && + : >path4 && + git add path4 && + test_tick && + git commit -m dev2 && + test_must_fail git push origin dev2 2>act && + sed -e "/^remote: /s/ *$//" cmp && + test_cmp exp cmp +' +rm -f "$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git/hooks/update" + cat >exp < Date: Thu, 19 Jan 2012 09:52:25 +0000 Subject: git-p4: handle p4 branches and labels containing shell chars Don't use shell expansion when detecting branches, as it will fail if the branch name contains a shell metachar. Similarly for labels. Add additional test for branches with shell metachars. Signed-off-by: Luke Diamand Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 8 +++---- t/t9803-git-p4-shell-metachars.sh | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 3e1aa276cf..e8c586e242 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -799,7 +799,7 @@ class P4Submit(Command, P4UserMap): def canChangeChangelists(self): # check to see if we have p4 admin or super-user permissions, either of # which are required to modify changelists. - results = p4CmdList("protects %s" % self.depotPath) + results = p4CmdList(["protects", self.depotPath]) for r in results: if r.has_key('perm'): if r['perm'] == 'admin': @@ -1758,7 +1758,7 @@ class P4Sync(Command, P4UserMap): def getLabels(self): self.labels = {} - l = p4CmdList("labels %s..." % ' '.join (self.depotPaths)) + l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths]) if len(l) > 0 and not self.silent: print "Finding files belonging to labels in %s" % `self.depotPaths` @@ -1800,7 +1800,7 @@ class P4Sync(Command, P4UserMap): command = "branches" for info in p4CmdList(command): - details = p4Cmd("branch -o %s" % info["branch"]) + details = p4Cmd(["branch", "-o", info["branch"]]) viewIdx = 0 while details.has_key("View%s" % viewIdx): paths = details["View%s" % viewIdx].split(" ") @@ -1938,7 +1938,7 @@ class P4Sync(Command, P4UserMap): sourceRef = self.gitRefForBranch(sourceBranch) #print "source " + sourceBranch - branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"]) + branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"]) #print "branch parent: %s" % branchParentChange gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange) if len(gitParent) > 0: diff --git a/t/t9803-git-p4-shell-metachars.sh b/t/t9803-git-p4-shell-metachars.sh index db04375a13..db670207bd 100755 --- a/t/t9803-git-p4-shell-metachars.sh +++ b/t/t9803-git-p4-shell-metachars.sh @@ -57,6 +57,54 @@ test_expect_success 'deleting with shell metachars' ' ) ' +# Create a branch with a shell metachar in its name +# +# 1. //depot/main +# 2. //depot/branch$3 + +test_expect_success 'branch with shell char' ' + test_when_finished cleanup_git && + test_create_repo "$git" && + ( + cd "$cli" && + + mkdir -p main && + + echo f1 >main/f1 && + p4 add main/f1 && + p4 submit -d "main/f1" && + + p4 integrate //depot/main/... //depot/branch\$3/... && + p4 submit -d "integrate main to branch\$3" && + + echo f1 >branch\$3/shell_char_branch_file && + p4 add branch\$3/shell_char_branch_file && + p4 submit -d "branch\$3/shell_char_branch_file" && + + p4 branch -i <<-EOF && + Branch: branch\$3 + View: //depot/main/... //depot/branch\$3/... + EOF + + p4 edit main/f1 && + echo "a change" >> main/f1 && + p4 submit -d "a change" main/f1 && + + p4 integrate -b branch\$3 && + p4 resolve -am branch\$3/... && + p4 submit -d "integrate main to branch\$3" && + + cd "$git" && + + git config git-p4.branchList main:branch\$3 && + "$GITP4" clone --dest=. --detect-branches //depot@all && + git log --all --graph --decorate --stat && + git reset --hard p4/depot/branch\$3 && + test -f shell_char_branch_file && + test -f f1 + ) +' + test_expect_success 'kill p4d' ' kill_p4d ' -- cgit v1.2.1 From a37a8de8d61f5af3a2d347c6d6668f4316f48383 Mon Sep 17 00:00:00 2001 From: Luke Diamand Date: Thu, 19 Jan 2012 09:52:26 +0000 Subject: git-p4: cope with labels with empty descriptions Use an explicit length for the data in a label, rather than EOT, so that labels with empty descriptions are passed through correctly. Signed-off-by: Luke Diamand Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index e8c586e242..8aa84f2caf 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -1741,9 +1741,11 @@ class P4Sync(Command, P4UserMap): else: tagger = "%s %s %s" % (owner, epoch, self.tz) self.gitStream.write("tagger %s\n" % tagger) - self.gitStream.write("data < Date: Thu, 19 Jan 2012 09:52:27 +0000 Subject: git-p4: importing labels should cope with missing owner In p4, the Owner field is optional. If it is missing, construct something sensible rather than crashing. Signed-off-by: Luke Diamand Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 63 +++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 8aa84f2caf..bcb0e6cd23 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -563,6 +563,26 @@ class Command: class P4UserMap: def __init__(self): self.userMapFromPerforceServer = False + self.myP4UserId = None + + def p4UserId(self): + if self.myP4UserId: + return self.myP4UserId + + results = p4CmdList("user -o") + for r in results: + if r.has_key('User'): + self.myP4UserId = r['User'] + return r['User'] + die("Could not find your p4 user id") + + def p4UserIsMe(self, p4User): + # return True if the given p4 user is actually me + me = self.p4UserId() + if not p4User or p4User != me: + return False + else: + return True def getUserCacheFilename(self): home = os.environ.get("HOME", os.environ.get("USERPROFILE")) @@ -700,7 +720,6 @@ class P4Submit(Command, P4UserMap): self.verbose = False self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true" self.isWindows = (platform.system() == "Windows") - self.myP4UserId = None def check(self): if len(p4CmdList("opened ...")) > 0: @@ -808,25 +827,6 @@ class P4Submit(Command, P4UserMap): return 1 return 0 - def p4UserId(self): - if self.myP4UserId: - return self.myP4UserId - - results = p4CmdList("user -o") - for r in results: - if r.has_key('User'): - self.myP4UserId = r['User'] - return r['User'] - die("Could not find your p4 user id") - - def p4UserIsMe(self, p4User): - # return True if the given p4 user is actually me - me = self.p4UserId() - if not p4User or p4User != me: - return False - else: - return True - def prepareSubmitTemplate(self): # remove lines in the Files section that show changes to files outside the depot path we're committing into template = "" @@ -1664,6 +1664,12 @@ class P4Sync(Command, P4UserMap): if self.stream_file.has_key('depotFile'): self.streamOneP4File(self.stream_file, self.stream_contents) + def make_email(self, userid): + if userid in self.users: + return self.users[userid] + else: + return "%s " % userid + def commit(self, details, files, branch, branchPrefixes, parent = ""): epoch = details["time"] author = details["user"] @@ -1687,10 +1693,7 @@ class P4Sync(Command, P4UserMap): committer = "" if author not in self.users: self.getUserMapFromPerforceServer() - if author in self.users: - committer = "%s %s %s" % (self.users[author], epoch, self.tz) - else: - committer = "%s %s %s" % (author, epoch, self.tz) + committer = "%s %s %s" % (self.make_email(author), epoch, self.tz) self.gitStream.write("committer %s\n" % committer) @@ -1735,11 +1738,15 @@ class P4Sync(Command, P4UserMap): self.gitStream.write("from %s\n" % branch) owner = labelDetails["Owner"] - tagger = "" - if author in self.users: - tagger = "%s %s %s" % (self.users[owner], epoch, self.tz) + + # Try to use the owner of the p4 label, or failing that, + # the current p4 user id. + if owner: + email = self.make_email(owner) else: - tagger = "%s %s %s" % (owner, epoch, self.tz) + email = self.make_email(self.p4UserId()) + tagger = "%s %s %s" % (email, epoch, self.tz) + self.gitStream.write("tagger %s\n" % tagger) description = labelDetails["Description"] -- cgit v1.2.1 From 4139ecc2f0d18eef54666c660285136488b90c03 Mon Sep 17 00:00:00 2001 From: Luke Diamand Date: Thu, 19 Jan 2012 09:52:28 +0000 Subject: git-p4: add test for p4 labels Add basic test of p4 label import. Checks label import and import with shell metachars; labels with different length descriptions. Signed-off-by: Luke Diamand Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- t/t9804-git-p4-label.sh | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t9804-git-p4-label.sh diff --git a/t/t9804-git-p4-label.sh b/t/t9804-git-p4-label.sh new file mode 100755 index 0000000000..5fa2bcfa97 --- /dev/null +++ b/t/t9804-git-p4-label.sh @@ -0,0 +1,72 @@ +test_description='git-p4 p4 label tests' + +. ./lib-git-p4.sh + +test_expect_success 'start p4d' ' + start_p4d +' + +# Basic p4 label tests. +# +# Note: can't have more than one label per commit - others +# are silently discarded. +# +test_expect_success 'basic p4 labels' ' + test_when_finished cleanup_git && + ( + cd "$cli" && + mkdir -p main && + + echo f1 >main/f1 && + p4 add main/f1 && + p4 submit -d "main/f1" && + + echo f2 >main/f2 && + p4 add main/f2 && + p4 submit -d "main/f2" && + + echo f3 >main/file_with_\$metachar && + p4 add main/file_with_\$metachar && + p4 submit -d "file with metachar" && + + p4 tag -l tag_f1_only main/f1 && + p4 tag -l tag_with\$_shell_char main/... && + + echo f4 >main/f4 && + p4 add main/f4 && + p4 submit -d "main/f4" && + + p4 label -i <<-EOF && + Label: long_label + Description: + A Label first line + A Label second line + View: //depot/... + EOF + + p4 tag -l long_label ... && + + p4 labels ... && + + "$GITP4" clone --dest="$git" --detect-labels //depot@all && + cd "$git" && + + git tag && + git tag >taglist && + test_line_count = 3 taglist && + + cd main && + git checkout tag_tag_f1_only && + ! test -f f2 && + git checkout tag_tag_with\$_shell_char && + test -f f1 && test -f f2 && test -f file_with_\$metachar && + + git show tag_long_label | grep -q "A Label second line" + ) +' + +test_expect_success 'kill p4d' ' + kill_p4d +' + +test_done -- cgit v1.2.1 From a080558ed77952b2394499531edc324eb2885d67 Mon Sep 17 00:00:00 2001 From: Luke Diamand Date: Thu, 19 Jan 2012 09:52:29 +0000 Subject: git-p4: label import fails with multiple labels at the same changelist git-p4 has an array of changelists with one label per changelist. But you can have multiple labels on a single changelist and so this code fails. Add a test case demonstrating the problem. Signed-off-by: Luke Diamand Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- t/t9804-git-p4-label.sh | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/t/t9804-git-p4-label.sh b/t/t9804-git-p4-label.sh index 5fa2bcfa97..80d01ea438 100755 --- a/t/t9804-git-p4-label.sh +++ b/t/t9804-git-p4-label.sh @@ -65,6 +65,47 @@ test_expect_success 'basic p4 labels' ' ) ' +# Test some label corner cases: +# +# - two tags on the same file; both should be available +# - a tag that is only on one file; this kind of tag +# cannot be imported (at least not easily). + +test_expect_failure 'two labels on the same changelist' ' + test_when_finished cleanup_git && + ( + cd "$cli" && + mkdir -p main && + + p4 edit main/f1 main/f2 && + echo "hello world" >main/f1 && + echo "not in the tag" >main/f2 && + p4 submit -d "main/f[12]: testing two labels" && + + p4 tag -l tag_f1_1 main/... && + p4 tag -l tag_f1_2 main/... && + + p4 labels ... && + + "$GITP4" clone --dest="$git" --detect-labels //depot@all && + cd "$git" && + + git tag | grep tag_f1 && + git tag | grep -q tag_f1_1 && + git tag | grep -q tag_f1_2 && + + cd main && + + git checkout tag_tag_f1_1 && + ls && + test -f f1 && + + git checkout tag_tag_f1_2 && + ls && + test -f f1 + ) +' + test_expect_success 'kill p4d' ' kill_p4d ' -- cgit v1.2.1 From aae5239be2b41477e8dc515f4fa372be2025e70a Mon Sep 17 00:00:00 2001 From: Kirill Smelkov Date: Sun, 4 Sep 2011 00:41:21 +0400 Subject: t/Makefile: Use $(sort ...) explicitly where needed Starting from GNU Make 3.82 $(wildcard ...) no longer sorts the result (from NEWS): * WARNING: Backward-incompatibility! Wildcards were not documented as returning sorted values, but the results have been sorted up until this release.. If your makefiles require sorted results from wildcard expansions, use the $(sort ...) function to request it explicitly. http://repo.or.cz/w/make.git/commitdiff/2a59dc32aaf0681dec569f32a9d7ab88a379d34f I usually watch test progress visually, and if tests are sorted, even with make -j4 they go more or less incrementally by their t number. On the other side, without sorting, tests are executed in seemingly random order even for -j1. Let's please maintain sane tests order for perceived prettyness. Another note is that in GNU Make sort also works as uniq, so after sort being removed, we might expect e.g. $(wildcard *.sh a.*) to produce duplicates for e.g. "a.sh". From this point of view, adding sort could be seen as hardening t/Makefile from accidentally introduced dups. It turned out that prevous releases of GNU Make did not perform full sort in $(wildcard), only sorting results for each pattern, that's why explicit sort-as-uniq is relevant even for older makes. Signed-off-by: Kirill Smelkov Signed-off-by: Junio C Hamano --- t/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/t/Makefile b/t/Makefile index 9046ec9816..66ceefefcc 100644 --- a/t/Makefile +++ b/t/Makefile @@ -17,9 +17,9 @@ DEFAULT_TEST_TARGET ?= test # Shell quote; SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) -T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) -TSVN = $(wildcard t91[0-9][0-9]-*.sh) -TGITWEB = $(wildcard t95[0-9][0-9]-*.sh) +T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) +TSVN = $(sort $(wildcard t91[0-9][0-9]-*.sh)) +TGITWEB = $(sort $(wildcard t95[0-9][0-9]-*.sh)) all: $(DEFAULT_TEST_TARGET) -- cgit v1.2.1 From ba5d445b653f874d9e4f2fd97fdd87438f45a70d Mon Sep 17 00:00:00 2001 From: Clemens Buchacher Date: Sat, 21 Jan 2012 18:57:28 +0100 Subject: git-gui: fix git-gui crash due to uninitialized variable Recently, a clone initiated via git gui on Windows crashed on me due to an "unknown variable cdone". It turns out that there is a code path where this variable is used uninitialized. Signed-off-by: Clemens Buchacher Signed-off-by: Pat Thoyts --- lib/status_bar.tcl | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/status_bar.tcl b/lib/status_bar.tcl index 95cb44991f..02111a1742 100644 --- a/lib/status_bar.tcl +++ b/lib/status_bar.tcl @@ -77,6 +77,7 @@ method start {msg uds} { method update {have total} { set pdone 0 + set cdone 0 if {$total > 0} { set pdone [expr {100 * $have / $total}] set cdone [expr {[winfo width $w_c] * $have / $total}] -- cgit v1.2.1 From 69204d0ab18d28d07ee2c8c9b50bbf5bd80343ab Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Mon, 23 Jan 2012 13:09:58 +0100 Subject: Fix typo in 1.7.9 release notes Signed-off-by: Michael Haggerty Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.9.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/RelNotes/1.7.9.txt b/Documentation/RelNotes/1.7.9.txt index 9b0a6ebf15..f1294b458b 100644 --- a/Documentation/RelNotes/1.7.9.txt +++ b/Documentation/RelNotes/1.7.9.txt @@ -63,7 +63,7 @@ Updates since v1.7.8 knows MATLAB. * "git log --format=''" learned new %g[nNeE] specifiers to - show information from the reflog entries when warlking the reflog + show information from the reflog entries when walking the reflog (i.e. with "-g"). * "git pull" can be used to fetch and merge an annotated/signed tag, -- cgit v1.2.1 From 50dd0f2fd910d9973760db052897ee8e73ed2f1f Mon Sep 17 00:00:00 2001 From: Albert Yale Date: Mon, 23 Jan 2012 18:52:44 +0100 Subject: grep: fix -l/-L interaction with decoration lines In threaded mode, git-grep emits file breaks (enabled with context, -W and --break) into the accumulation buffers even if they are not required. The output collection thread then uses skip_first_line to skip the first such line in the output, which would otherwise be at the very top. This is wrong when the user also specified -l/-L/-c, in which case every line is relevant. While arguably giving these options together doesn't make any sense, git-grep has always quietly accepted it. So do not skip anything in these cases. Signed-off-by: Albert Yale Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- builtin/grep.c | 5 +++-- t/t7810-grep.sh | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index 9ce064ac11..5c2ae94e55 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1034,8 +1034,9 @@ int cmd_grep(int argc, const char **argv, const char *prefix) #ifndef NO_PTHREADS if (use_threads) { - if (opt.pre_context || opt.post_context || opt.file_break || - opt.funcbody) + if (!(opt.name_only || opt.unmatch_name_only || opt.count) + && (opt.pre_context || opt.post_context || + opt.file_break || opt.funcbody)) skip_first_line = 1; start_threads(&opt); } diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 7ba5b16f99..75f4716d8c 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -245,6 +245,28 @@ do ' done +cat >expected <actual && + test_cmp expected actual +' + +cat >expected <actual && + test_cmp expected actual +' + +test_expect_success 'grep -L -C' ' + git ls-files >expected && + git grep -L -C1 nonexistent_string >actual && + test_cmp expected actual +' + cat >expected < Date: Thu, 19 Jan 2012 23:47:35 -0800 Subject: mergetool: Provide an empty file when needed Some merge tools cannot cope when $LOCAL, $BASE, or $REMOTE are missing. $BASE can be missing when two branches independently add the same filename. Provide an empty file to make these tools happy. When a delete/modify conflict occurs, $LOCAL and $REMOTE can also be missing. We have special case code to handle such case so this change may not affect that codepath, but try to be consistent and create an empty file for them anyway. Reported-by: Jason Wenger Signed-off-by: David Aguilar Signed-off-by: Junio C Hamano --- git-mergetool.sh | 12 ++++++++---- t/t7610-mergetool.sh | 28 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/git-mergetool.sh b/git-mergetool.sh index 085e213a12..a9f23f7fcd 100755 --- a/git-mergetool.sh +++ b/git-mergetool.sh @@ -181,10 +181,14 @@ stage_submodule () { } checkout_staged_file () { - tmpfile=$(expr "$(git checkout-index --temp --stage="$1" "$2")" : '\([^ ]*\) ') + tmpfile=$(expr \ + "$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" \ + : '\([^ ]*\) ') if test $? -eq 0 -a -n "$tmpfile" ; then mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3" + else + >"$3" fi } @@ -224,9 +228,9 @@ merge_file () { mv -- "$MERGED" "$BACKUP" cp -- "$BACKUP" "$MERGED" - base_present && checkout_staged_file 1 "$MERGED" "$BASE" - local_present && checkout_staged_file 2 "$MERGED" "$LOCAL" - remote_present && checkout_staged_file 3 "$MERGED" "$REMOTE" + checkout_staged_file 1 "$MERGED" "$BASE" + checkout_staged_file 2 "$MERGED" "$LOCAL" + checkout_staged_file 3 "$MERGED" "$REMOTE" if test -z "$local_mode" -o -z "$remote_mode"; then echo "Deleted merge conflict for '$MERGED':" diff --git a/t/t7610-mergetool.sh b/t/t7610-mergetool.sh index 4aab2a75b8..f5e16fc7db 100755 --- a/t/t7610-mergetool.sh +++ b/t/t7610-mergetool.sh @@ -39,6 +39,7 @@ test_expect_success 'setup' ' echo branch1 change >file1 && echo branch1 newfile >file2 && echo branch1 spaced >"spaced name" && + echo branch1 both added >both && echo branch1 change file11 >file11 && echo branch1 change file13 >file13 && echo branch1 sub >subdir/file3 && @@ -50,6 +51,7 @@ test_expect_success 'setup' ' git checkout -b submod-branch1 ) && git add file1 "spaced name" file11 file13 file2 subdir/file3 submod && + git add both && git rm file12 && git commit -m "branch1 changes" && @@ -58,6 +60,7 @@ test_expect_success 'setup' ' echo master updated >file1 && echo master new >file2 && echo master updated spaced >"spaced name" && + echo master both added >both && echo master updated file12 >file12 && echo master updated file14 >file14 && echo master new sub >subdir/file3 && @@ -69,18 +72,22 @@ test_expect_success 'setup' ' git checkout -b submod-master ) && git add file1 "spaced name" file12 file14 file2 subdir/file3 submod && + git add both && git rm file11 && git commit -m "master updates" && git config merge.tool mytool && git config mergetool.mytool.cmd "cat \"\$REMOTE\" >\"\$MERGED\"" && - git config mergetool.mytool.trustExitCode true + git config mergetool.mytool.trustExitCode true && + git config mergetool.mybase.cmd "cat \"\$BASE\" >\"\$MERGED\"" && + git config mergetool.mybase.trustExitCode true ' test_expect_success 'custom mergetool' ' git checkout -b test1 branch1 && git submodule update -N && test_must_fail git merge master >/dev/null 2>&1 && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "" | git mergetool file1 file1 ) && ( yes "" | git mergetool file2 "spaced name" >/dev/null 2>&1 ) && ( yes "" | git mergetool subdir/file3 >/dev/null 2>&1 ) && @@ -101,6 +108,7 @@ test_expect_success 'mergetool crlf' ' ( yes "" | git mergetool file1 >/dev/null 2>&1 ) && ( yes "" | git mergetool file2 >/dev/null 2>&1 ) && ( yes "" | git mergetool "spaced name" >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "" | git mergetool subdir/file3 >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 >/dev/null 2>&1 ) && ( yes "d" | git mergetool file12 >/dev/null 2>&1 ) && @@ -131,6 +139,7 @@ test_expect_success 'mergetool on file in parent dir' ' cd subdir && ( yes "" | git mergetool ../file1 >/dev/null 2>&1 ) && ( yes "" | git mergetool ../file2 ../spaced\ name >/dev/null 2>&1 ) && + ( yes "" | git mergetool ../both >/dev/null 2>&1 ) && ( yes "d" | git mergetool ../file11 >/dev/null 2>&1 ) && ( yes "d" | git mergetool ../file12 >/dev/null 2>&1 ) && ( yes "l" | git mergetool ../submod >/dev/null 2>&1 ) && @@ -212,6 +221,7 @@ test_expect_success 'deleted vs modified submodule' ' test_must_fail git merge master && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "r" | git mergetool submod ) && rmdir submod && mv submod-movedaside submod && @@ -228,6 +238,7 @@ test_expect_success 'deleted vs modified submodule' ' test_must_fail git merge master && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "l" | git mergetool submod ) && test ! -e submod && @@ -241,6 +252,7 @@ test_expect_success 'deleted vs modified submodule' ' test_must_fail git merge test6 && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "r" | git mergetool submod ) && test ! -e submod && @@ -256,6 +268,7 @@ test_expect_success 'deleted vs modified submodule' ' test_must_fail git merge test6 && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "l" | git mergetool submod ) && test "$(cat submod/bar)" = "master submodule" && @@ -279,6 +292,7 @@ test_expect_success 'file vs modified submodule' ' test_must_fail git merge master && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "r" | git mergetool submod ) && rmdir submod && mv submod-movedaside submod && @@ -294,6 +308,7 @@ test_expect_success 'file vs modified submodule' ' test_must_fail git merge master && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "l" | git mergetool submod ) && git submodule update -N && @@ -309,6 +324,7 @@ test_expect_success 'file vs modified submodule' ' test_must_fail git merge test7 && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both >/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "r" | git mergetool submod ) && test -d submod.orig && @@ -324,6 +340,7 @@ test_expect_success 'file vs modified submodule' ' test_must_fail git merge test7 && test -n "$(git ls-files -u)" && ( yes "" | git mergetool file1 file2 spaced\ name subdir/file3 >/dev/null 2>&1 ) && + ( yes "" | git mergetool both>/dev/null 2>&1 ) && ( yes "d" | git mergetool file11 file12 >/dev/null 2>&1 ) && ( yes "l" | git mergetool submod ) && test "$(cat submod/bar)" = "master submodule" && @@ -445,4 +462,13 @@ test_expect_success 'directory vs modified submodule' ' git submodule update -N ' +test_expect_success 'file with no base' ' + git checkout -b test13 branch1 && + test_must_fail git merge master && + git mergetool --no-prompt --tool mybase -- both && + >expected && + test_cmp both expected && + git reset --hard master >/dev/null 2>&1 +' + test_done -- cgit v1.2.1 From 42f16113ee87ff667fef3a821e530ce2393c6a35 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 23 Jan 2012 14:02:55 -0800 Subject: git-sh-i18n: restructure the logic to compute gettext.sh scheme Instead of having a single long and complex chain of commands to decide what to do and carry out the decision, split the code so that we first decide which scheme to use, and in the second section define what exactly is done by the chosen scheme. It makes the code much easier to follow and update. Signed-off-by: Junio C Hamano --- git-sh-i18n.sh | 103 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/git-sh-i18n.sh b/git-sh-i18n.sh index b4575fb3a1..6648bd3726 100644 --- a/git-sh-i18n.sh +++ b/git-sh-i18n.sh @@ -16,61 +16,45 @@ else fi export TEXTDOMAINDIR -if test -z "$GIT_GETTEXT_POISON" +# First decide what scheme to use... +GIT_INTERNAL_GETTEXT_SH_SCHEME=fallthrough +if test -n "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" +then + : no probing necessary +elif test -n "$GIT_GETTEXT_POISON" then - if test -z "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" && type gettext.sh >/dev/null 2>&1 - then - # This is GNU libintl's gettext.sh, we don't need to do anything - # else than setting up the environment and loading gettext.sh - GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu - export GIT_INTERNAL_GETTEXT_SH_SCHEME - - # Try to use libintl's gettext.sh, or fall back to English if we - # can't. - . gettext.sh - - elif test -z "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" && test "$(gettext -h 2>&1)" = "-h" - then - # We don't have gettext.sh, but there's a gettext binary in our - # path. This is probably Solaris or something like it which has a - # gettext implementation that isn't GNU libintl. - GIT_INTERNAL_GETTEXT_SH_SCHEME=solaris - export GIT_INTERNAL_GETTEXT_SH_SCHEME - - # Solaris has a gettext(1) but no eval_gettext(1) - eval_gettext () { - gettext "$1" | ( - export PATH $(git sh-i18n--envsubst --variables "$1"); - git sh-i18n--envsubst "$1" - ) - } - - else - # Since gettext.sh isn't available we'll have to define our own - # dummy pass-through functions. - - # Tell our tests that we don't have the real gettext.sh - GIT_INTERNAL_GETTEXT_SH_SCHEME=fallthrough - export GIT_INTERNAL_GETTEXT_SH_SCHEME - - gettext () { - printf "%s" "$1" - } - - eval_gettext () { - printf "%s" "$1" | ( - export PATH $(git sh-i18n--envsubst --variables "$1"); - git sh-i18n--envsubst "$1" - ) - } - fi -else - # Emit garbage under GETTEXT_POISON=YesPlease. Unlike the C tests - # this relies on an environment variable - GIT_INTERNAL_GETTEXT_SH_SCHEME=poison - export GIT_INTERNAL_GETTEXT_SH_SCHEME +elif type gettext.sh >/dev/null 2>&1 +then + # GNU libintl's gettext.sh + GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu +elif test "$(gettext -h 2>&1)" = "-h" +then + # gettext binary exists but no gettext.sh. likely to be a gettext + # binary on a Solaris or something that is not GNU libintl and + # lack eval_gettext. + GIT_INTERNAL_GETTEXT_SH_SCHEME=gettext_without_eval_gettext +fi +export GIT_INTERNAL_GETTEXT_SH_SCHEME +# ... and then follow that decision. +case "$GIT_INTERNAL_GETTEXT_SH_SCHEME" in +gnu) + # Use libintl's gettext.sh, or fall back to English if we can't. + . gettext.sh + ;; +gettext_without_eval_gettext) + # Solaris has a gettext(1) but no eval_gettext(1) + eval_gettext () { + gettext "$1" | ( + export PATH $(git sh-i18n--envsubst --variables "$1"); + git sh-i18n--envsubst "$1" + ) + } + ;; +poison) + # Emit garbage so that tests that incorrectly rely on translatable + # strings will fail. gettext () { printf "%s" "# GETTEXT POISON #" } @@ -78,7 +62,20 @@ else eval_gettext () { printf "%s" "# GETTEXT POISON #" } -fi + ;; +*) + gettext () { + printf "%s" "$1" + } + + eval_gettext () { + printf "%s" "$1" | ( + export PATH $(git sh-i18n--envsubst --variables "$1"); + git sh-i18n--envsubst "$1" + ) + } + ;; +esac # Git-specific wrapper functions gettextln () { -- cgit v1.2.1 From ad17ea734770126bfe1027a8a999e9e2d9f62145 Mon Sep 17 00:00:00 2001 From: Alex Riesen Date: Mon, 23 Jan 2012 14:04:29 -0800 Subject: add a Makefile switch to avoid gettext translation in shell scripts Some systems have gettext.sh (GNU gettext) installed, but it is either broken or misconfigured in such a way so its output is not usable. In case the users of these systems are unable or not interested in fixing them, setting the new Makefile switch should help: make USE_GETTEXT_SCHEME=fallthrough This will replace the translation routines with fallthrough versions, that does not use gettext from the platform. Signed-off-by: Alex Riesen Signed-off-by: Junio C Hamano --- Makefile | 6 +++++- git-sh-i18n.sh | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9470a10343..a0f2464a9b 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,9 @@ all:: # A translated Git requires GNU libintl or another gettext implementation, # plus libintl-perl at runtime. # +# Define USE_GETTEXT_SCHEME and set it to 'fallthrough', if you don't trust +# the installed gettext translation of the shell scripts output. +# # Define HAVE_LIBCHARSET_H if you haven't set NO_GETTEXT and you can't # trust the langinfo.h's nl_langinfo(CODESET) function to return the # current character set. GNU and Solaris have a nl_langinfo(CODESET), @@ -1874,6 +1877,7 @@ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ -e 's|@@LOCALEDIR@@|$(localedir_SQ)|g' \ -e 's/@@NO_CURL@@/$(NO_CURL)/g' \ + -e 's/@@USE_GETTEXT_SCHEME@@/$(USE_GETTEXT_SCHEME)/g' \ -e $(BROKEN_PATH_FIX) \ $@.sh >$@+ endef @@ -2251,7 +2255,7 @@ cscope: ### Detect prefix changes TRACK_CFLAGS = $(CC):$(subst ','\'',$(ALL_CFLAGS)):\ $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ):\ - $(localedir_SQ) + $(localedir_SQ):$(USE_GETTEXT_SCHEME) GIT-CFLAGS: FORCE @FLAGS='$(TRACK_CFLAGS)'; \ diff --git a/git-sh-i18n.sh b/git-sh-i18n.sh index 6648bd3726..d5fae993b0 100644 --- a/git-sh-i18n.sh +++ b/git-sh-i18n.sh @@ -18,7 +18,10 @@ export TEXTDOMAINDIR # First decide what scheme to use... GIT_INTERNAL_GETTEXT_SH_SCHEME=fallthrough -if test -n "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" +if test -n "@@USE_GETTEXT_SCHEME@@" +then + GIT_INTERNAL_GETTEXT_SH_SCHEME="@@USE_GETTEXT_SCHEME@@" +elif test -n "@@USE_FALLTHROUGH_GETTEXT_SCHEME@@$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" then : no probing necessary elif test -n "$GIT_GETTEXT_POISON" -- cgit v1.2.1 From ba8c6ef62787c5c8a8c1e911ce72fb07294553e3 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 23 Jan 2012 14:25:19 -0800 Subject: i18n: Make NO_GETTEXT imply fallthrough scheme in shell l10n Signed-off-by: Junio C Hamano --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index a0f2464a9b..a4a33d4355 100644 --- a/Makefile +++ b/Makefile @@ -1515,6 +1515,7 @@ ifdef GETTEXT_POISON endif ifdef NO_GETTEXT BASIC_CFLAGS += -DNO_GETTEXT + USE_GETTEXT_SCHEME = fallthrough endif ifdef NO_STRCASESTR COMPAT_CFLAGS += -DNO_STRCASESTR -- cgit v1.2.1 From f8246281af9adb0fdddbcc90d2e19cb5cd5217e5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 10 Jan 2012 22:44:45 -0800 Subject: merge: use editor by default in interactive sessions Traditionally, a cleanly resolved merge was committed by "git merge" using the auto-generated merge commit log message without invoking the editor. After 5 years of use in the field, it turns out that people perform too many unjustified merges of the upstream history into their topic branches. These merges are not just useless, but they are often not explained well, and making the end result unreadable when it gets time for merging their history back to their upstream. Earlier we added the "--edit" option to the command, so that people can edit the log message to explain and justify their merge commits. Let's take it one step further and spawn the editor by default when we are in an interactive session (i.e. the standard input and the standard output are pointing at the same tty device). There may be existing scripts that leave the standard input and the standard output of the "git merge" connected to whatever environment the scripts were started, and such invocation might trigger the above "interactive session" heuristics. GIT_MERGE_AUTOEDIT environment variable can be set to "no" at the beginning of such scripts to use the historical behaviour while the script runs. Note that this backward compatibility is meant only for scripts, and we deliberately do *not* support "merge.edit = yes/no/auto" configuration option to allow people to keep the historical behaviour. Suggested-by: Linus Torvalds Signed-off-by: Junio C Hamano --- Documentation/git-merge.txt | 2 +- Documentation/merge-options.txt | 16 +++++++++++++--- builtin/merge.c | 38 ++++++++++++++++++++++++++++++++++---- t/test-lib.sh | 3 ++- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt index e2e6aba17e..3ceefb8a1f 100644 --- a/Documentation/git-merge.txt +++ b/Documentation/git-merge.txt @@ -9,7 +9,7 @@ git-merge - Join two or more development histories together SYNOPSIS -------- [verse] -'git merge' [-n] [--stat] [--no-commit] [--squash] +'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit] [-s ] [-X ] [--[no-]rerere-autoupdate] [-m ] [...] 'git merge' HEAD ... diff --git a/Documentation/merge-options.txt b/Documentation/merge-options.txt index 6bd0b041c3..f2f1d0f51c 100644 --- a/Documentation/merge-options.txt +++ b/Documentation/merge-options.txt @@ -8,10 +8,20 @@ failed and do not autocommit, to give the user a chance to inspect and further tweak the merge result before committing. --edit:: --e:: +--no-edit:: + Invoke an editor before committing successful mechanical merge to + further edit the auto-generated merge message, so that the user + can explain and justify the merge. The `--no-edit` option can be + used to accept the auto-generated message (this is generally + discouraged). The `--edit` option is still useful if you are + giving a draft message with the `-m` option from the command line + and want to edit it in the editor. + - Invoke editor before committing successful merge to further - edit the default merge message. +Older scripts may depend on the historical behaviour of not allowing the +user to edit the merge log message. They will see an editor opened when +they run `git merge`. To make it easier to adjust such scripts to the +updated behaviour, the environment variable `GIT_MERGE_AUTOEDIT` can be +set to `no` at the beginning of them. --ff:: --no-ff:: diff --git a/builtin/merge.c b/builtin/merge.c index 99f1429b35..0006175d15 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -46,7 +46,7 @@ static const char * const builtin_merge_usage[] = { static int show_diffstat = 1, shortlog_len, squash; static int option_commit = 1, allow_fast_forward = 1; -static int fast_forward_only, option_edit; +static int fast_forward_only, option_edit = -1; static int allow_trivial = 1, have_message; static struct strbuf merge_msg; static struct commit_list *remoteheads; @@ -189,7 +189,7 @@ static struct option builtin_merge_options[] = { "create a single commit instead of doing a merge"), OPT_BOOLEAN(0, "commit", &option_commit, "perform a commit if the merge succeeds (default)"), - OPT_BOOLEAN('e', "edit", &option_edit, + OPT_BOOL('e', "edit", &option_edit, "edit message before committing"), OPT_BOOLEAN(0, "ff", &allow_fast_forward, "allow fast-forward (default)"), @@ -877,12 +877,12 @@ static void prepare_to_commit(void) write_merge_msg(&msg); run_hook(get_index_file(), "prepare-commit-msg", git_path("MERGE_MSG"), "merge", NULL, NULL); - if (option_edit) { + if (0 < option_edit) { if (launch_editor(git_path("MERGE_MSG"), NULL, NULL)) abort_commit(NULL); } read_merge_msg(&msg); - stripspace(&msg, option_edit); + stripspace(&msg, 0 < option_edit); if (!msg.len) abort_commit(_("Empty commit message.")); strbuf_release(&merge_msg); @@ -1076,6 +1076,33 @@ static void write_merge_state(void) close(fd); } +static int default_edit_option(void) +{ + static const char name[] = "GIT_MERGE_AUTOEDIT"; + const char *e = getenv(name); + struct stat st_stdin, st_stdout; + + if (have_message) + /* an explicit -m msg without --[no-]edit */ + return 0; + + if (e) { + int v = git_config_maybe_bool(name, e); + if (v < 0) + die("Bad value '%s' in environment '%s'", e, name); + return v; + } + + /* Use editor if stdin and stdout are the same and is a tty */ + return (!fstat(0, &st_stdin) && + !fstat(1, &st_stdout) && + isatty(0) && + st_stdin.st_dev == st_stdout.st_dev && + st_stdin.st_ino == st_stdout.st_ino && + st_stdin.st_mode == st_stdout.st_mode); +} + + int cmd_merge(int argc, const char **argv, const char *prefix) { unsigned char result_tree[20]; @@ -1261,6 +1288,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix) } } + if (option_edit < 0) + option_edit = default_edit_option(); + if (!use_strategies) { if (!remoteheads->next) add_strategies(pull_twohead, DEFAULT_TWOHEAD); diff --git a/t/test-lib.sh b/t/test-lib.sh index bdd9513b84..ed32c2ab7b 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -63,7 +63,8 @@ GIT_AUTHOR_NAME='A U Thor' GIT_COMMITTER_EMAIL=committer@example.com GIT_COMMITTER_NAME='C O Mitter' GIT_MERGE_VERBOSITY=5 -export GIT_MERGE_VERBOSITY +GIT_MERGE_AUTOEDIT=no +export GIT_MERGE_VERBOSITY GIT_MERGE_AUTOEDIT export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME export EDITOR -- cgit v1.2.1 From dad0b3d8e5586ff8c901a6bd643ea9bfe55d75a7 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 23 Jan 2012 16:34:22 -0800 Subject: push: do not let configured foreign-vcs permanently clobbered Recently, 6f48d39 (clone: delay cloning until after remote HEAD checking, 2012-01-16) tried to record if a remote helper needs to be called after parsing the remote when transport_get() is called, by overwriting the field meant to store the configured remote helper name in the remote structure. This is OK when a remote represents a single remote repository, but fails miserably when pushing to locations with multiple URLs, like this: $ cat .git/config [remote "origin"] url = https://code.google.com/p/git-htmldocs/ url = github.com:gitster/git-htmldocs.git push = refs/heads/master:refs/heads/master $ git push The second url that is supposed to use the git-over-ssh transport mistakenly use https:// and fails with: error: Couldn't resolve host 'github.com:gitster' while accessing github.com:gitster/git-htmldocs.git/info/refs fatal: HTTP request failed The right solution would probably be to dedicate a separate field to store the detected external helper to be used, which is valid only during a single use of transport until it is disconnected, instead of overwriting foreign_vcs field, but in the meantime, this band-aid should suffice. Signed-off-by: Junio C Hamano Signed-off-by: Junio C Hamano --- builtin/push.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/builtin/push.c b/builtin/push.c index 35cce532f2..5fb98a0094 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -204,11 +204,13 @@ static int do_push(const char *repo, int flags) url_nr = remote->url_nr; } if (url_nr) { + const char *configured_foreign_vcs = remote->foreign_vcs; for (i = 0; i < url_nr; i++) { struct transport *transport = transport_get(remote, url[i]); if (push_with_options(transport, flags)) errs++; + remote->foreign_vcs = configured_foreign_vcs; } } else { struct transport *transport = -- cgit v1.2.1 From 9049816140e75d3f7b15264e97e042f5ab0bf392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Tue, 24 Jan 2012 18:10:38 +0700 Subject: clone: fix up delay cloning conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6f48d39 (clone: delay cloning until after remote HEAD checking - 2012-01-16) allows us to perform some checks on remote refs before the actual cloning happens. But not all transport types support this. Remote helper with "import" capability will not return complete ref information until fetch is performed and therefore the clone cannot be delayed. foreign_vcs field in struct remote was used to detect this kind of transport and save the result. This is a mistake because foreign_vcs is designed to override url-based transport detection. As a result, if the same "struct transport *" object is used on many different urls and one of them attached remote transport, the following urls will be mistakenly attached to the same transport. This fault is worked around by dad0b3d (push: do not let configured foreign-vcs permanently clobbered - 2012-01-23) To fix this, detect incomplete refs from transport_get_remote_refs() by SHA-1. Incomplete ones must have null SHA-1 (*). Then revert changes related to foreign_cvs field in 6f48d39 and dad0b3d. A good thing from this change is that cloning smart http transport can also be delayed. Earlier it falls into the same category "remote transport, no delay". (*) Theoretically if one of the remote refs happens to have null SHA-1, it will trigger false alarm and the clone will not be delayed. But that chance may be too small for us to pay attention to. Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/clone.c | 20 ++++++++++++++------ builtin/push.c | 2 -- transport.c | 5 +---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index 72eebca535..379cafa63f 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -631,12 +631,13 @@ int cmd_clone(int argc, const char **argv, const char *prefix) const struct ref *remote_head_points_at; const struct ref *our_head_points_at; struct ref *mapped_refs; + const struct ref *ref; struct strbuf key = STRBUF_INIT, value = STRBUF_INIT; struct strbuf branch_top = STRBUF_INIT, reflog_msg = STRBUF_INIT; struct transport *transport = NULL; const char *src_ref_prefix = "refs/heads/"; struct remote *remote; - int err = 0; + int err = 0, complete_refs_before_fetch = 1; struct refspec *refspec; const char *fetch_pattern; @@ -816,15 +817,22 @@ int cmd_clone(int argc, const char **argv, const char *prefix) mapped_refs = refs ? wanted_peer_refs(refs, refspec) : NULL; /* - * mapped_refs may be updated if transport-helper is used so - * we need fetch it early because remote_head code below - * relies on it. + * transport_get_remote_refs() may return refs with null sha-1 + * in mapped_refs (see struct transport->get_refs_list + * comment). In that case we need fetch it early because + * remote_head code below relies on it. * * for normal clones, transport_get_remote_refs() should * return reliable ref set, we can delay cloning until after * remote HEAD check. */ - if (!is_local && remote->foreign_vcs && refs) + for (ref = refs; ref; ref = ref->next) + if (is_null_sha1(ref->old_sha1)) { + complete_refs_before_fetch = 0; + break; + } + + if (!is_local && !complete_refs_before_fetch && refs) transport_fetch_refs(transport, mapped_refs); if (refs) { @@ -856,7 +864,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) if (is_local) clone_local(path, git_dir); - else if (refs && !remote->foreign_vcs) + else if (refs && complete_refs_before_fetch) transport_fetch_refs(transport, mapped_refs); update_remote_refs(refs, mapped_refs, remote_head_points_at, diff --git a/builtin/push.c b/builtin/push.c index 5fb98a0094..35cce532f2 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -204,13 +204,11 @@ static int do_push(const char *repo, int flags) url_nr = remote->url_nr; } if (url_nr) { - const char *configured_foreign_vcs = remote->foreign_vcs; for (i = 0; i < url_nr; i++) { struct transport *transport = transport_get(remote, url[i]); if (push_with_options(transport, flags)) errs++; - remote->foreign_vcs = configured_foreign_vcs; } } else { struct transport *transport = diff --git a/transport.c b/transport.c index 43666394df..a99b7c9c45 100644 --- a/transport.c +++ b/transport.c @@ -895,10 +895,8 @@ struct transport *transport_get(struct remote *remote, const char *url) while (is_urlschemechar(p == url, *p)) p++; - if (!prefixcmp(p, "::")) { + if (!prefixcmp(p, "::")) helper = xstrndup(url, p - url); - remote->foreign_vcs = helper; - } } if (helper) { @@ -940,7 +938,6 @@ struct transport *transport_get(struct remote *remote, const char *url) char *handler = xmalloc(len + 1); handler[len] = 0; strncpy(handler, url, len); - remote->foreign_vcs = handler; transport_helper_init(ret, handler); } -- cgit v1.2.1 From 1017c1abcb6d733e1de83eb5a1cf7e1bf4ad6aca Mon Sep 17 00:00:00 2001 From: Jens Lehmann Date: Tue, 24 Jan 2012 22:49:56 +0100 Subject: submodule add: fix breakage when re-adding a deep submodule Since recently a submodule with name has its git directory in the .git/modules/ directory of the superproject while the work tree contains a gitfile pointing there. When the same submodule is added on a branch where it wasn't present so far (it is not found in the .gitmodules file), the name is not initialized from the path as it should. This leads to a wrong path entered in the gitfile when the .git/modules/ directory is found, as this happily uses the - now empty - name. It then always points only a single directory up, even if we have a path deeper in the directory hierarchy. Fix that by initializing the name of the submodule early in module_clone() if module_name() returned an empty name and add a test to catch that bug. Reported-by: Jehan Bing Signed-off-by: Jens Lehmann Signed-off-by: Junio C Hamano --- git-submodule.sh | 1 + t/t7406-submodule-update.sh | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/git-submodule.sh b/git-submodule.sh index 3adab93635..9bb2e13e92 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -131,6 +131,7 @@ module_clone() gitdir= gitdir_base= name=$(module_name "$path" 2>/dev/null) + test -n "$name" || name="$path" base_path=$(dirname "$path") gitdir=$(git rev-parse --git-dir) diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index 33b292b8a8..5b97222c48 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -611,4 +611,12 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir re ) ' +test_expect_success 'submodule add properly re-creates deeper level submodules' ' + (cd super && + git reset --hard master && + rm -rf deeper/ && + git submodule add ../submodule deeper/submodule + ) +' + test_done -- cgit v1.2.1 From 733137496aee6b74e49bd74d342efce8a3d2e95e Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 25 Jan 2012 17:20:03 -0500 Subject: docs: minor grammar fixes for v1.7.9 release notes Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.9.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Documentation/RelNotes/1.7.9.txt b/Documentation/RelNotes/1.7.9.txt index f1294b458b..95320aad5d 100644 --- a/Documentation/RelNotes/1.7.9.txt +++ b/Documentation/RelNotes/1.7.9.txt @@ -12,19 +12,20 @@ Updates since v1.7.8 * Git uses gettext to translate its most common interface messages into the user's language if translations are available and the - locale is appropriately set. Distributors can drop in new PO files + locale is appropriately set. Distributors can drop new PO files in po/ to add new translations. - * The code to handle username/password for HTTP transaction used in + * The code to handle username/password for HTTP transactions used in "git push" & "git fetch" learned to talk "credential API" to external programs to cache or store them, to allow integration with platform native keychain mechanisms. - * The prompted input in the terminal use our own getpass() replacement - when possible. HTTP transactions used to ask username without echoing - back what was typed, but with this change you will see it as you type. + * The input prompts in the terminal use our own getpass() replacement + when possible. HTTP transactions used to ask for the username without + echoing back what was typed, but with this change you will see it as + you type. - * The internal of "revert/cherry-pick" has been tweaked to prepare + * The internals of "revert/cherry-pick" have been tweaked to prepare building more generic "sequencer" on top of the implementation that drives them. -- cgit v1.2.1 From fed23693ba1a1ea7beb851cd385d458136e91664 Mon Sep 17 00:00:00 2001 From: Vitor Antunes Date: Wed, 25 Jan 2012 23:48:22 +0000 Subject: git-p4: Search for parent commit on branch creation To find out which is its parent the commit of the new branch is compared sequentially to each blob of the parent branch from the newest to the oldest. The first blob which results in a zero diff is considered the parent commit. If none is found, then the commit is applied to the top of the parent branch. A fast-import "checkpoint" call is required because diff-tree is only able to work with blobs on disk. But most of these commits will not be part of the final imported tree, making fast-import fail. To avoid this, the temporary branches are tracked and then removed at the end of the import process. Signed-off-by: Vitor Antunes Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index bcb0e6cd23..0bf2625077 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -1429,6 +1429,8 @@ class P4Sync(Command, P4UserMap): self.cloneExclude = [] self.useClientSpec = False self.clientSpecDirs = None + self.tempBranches = [] + self.tempBranchLocation = "git-p4-tmp" if gitConfig("git-p4.syncFromOrigin") == "false": self.syncWithOrigin = False @@ -1450,6 +1452,14 @@ class P4Sync(Command, P4UserMap): .replace("%25", "%") return path + # Force a checkpoint in fast-import and wait for it to finish + def checkpoint(self): + self.gitStream.write("checkpoint\n\n") + self.gitStream.write("progress checkpoint\n\n") + out = self.gitOutput.readline() + if self.verbose: + print "checkpoint finished: " + out + def extractFilesFromCommit(self, commit): self.cloneExclude = [re.sub(r"\.\.\.$", "", path) for path in self.cloneExclude] @@ -1957,6 +1967,20 @@ class P4Sync(Command, P4UserMap): self.importChanges(changes) return True + def searchParent(self, parent, branch, target): + parentFound = False + for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]): + blob = blob.strip() + if len(read_pipe(["git", "diff-tree", blob, target])) == 0: + parentFound = True + if self.verbose: + print "Found parent of %s in commit %s" % (branch, blob) + break + if parentFound: + return blob + else: + return None + def importChanges(self, changes): cnt = 1 for change in changes: @@ -2013,7 +2037,21 @@ class P4Sync(Command, P4UserMap): parent = self.initialParents[branch] del self.initialParents[branch] - self.commit(description, filesForCommit, branch, [branchPrefix], parent) + blob = None + if len(parent) > 0: + tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change)) + if self.verbose: + print "Creating temporary branch: " + tempBranch + self.commit(description, filesForCommit, tempBranch, [branchPrefix]) + self.tempBranches.append(tempBranch) + self.checkpoint() + blob = self.searchParent(parent, branch, tempBranch) + if blob: + self.commit(description, filesForCommit, branch, [branchPrefix], blob) + else: + if self.verbose: + print "Parent of %s not found. Committing into head of %s" % (branch, parent) + self.commit(description, filesForCommit, branch, [branchPrefix], parent) else: files = self.extractFilesFromCommit(description) self.commit(description, files, self.branch, self.depotPaths, @@ -2348,6 +2386,12 @@ class P4Sync(Command, P4UserMap): self.gitOutput.close() self.gitError.close() + # Cleanup temporary branches created during import + if self.tempBranches != []: + for branch in self.tempBranches: + read_pipe("git update-ref -d %s" % branch) + os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation)) + return True class P4Rebase(Command): -- cgit v1.2.1 From c5665efed2619fae9ec5a57c555c89a91804e886 Mon Sep 17 00:00:00 2001 From: Vitor Antunes Date: Wed, 25 Jan 2012 23:48:23 +0000 Subject: git-p4: Add test case for complex branch import Check if branches created from old changelists are correctly imported. Also included some updates to simple branch test so that both are coherent in respect to each other. Signed-off-by: Vitor Antunes Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- t/t9801-git-p4-branch.sh | 94 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/t/t9801-git-p4-branch.sh b/t/t9801-git-p4-branch.sh index a25f18d36a..6ff713b1f7 100755 --- a/t/t9801-git-p4-branch.sh +++ b/t/t9801-git-p4-branch.sh @@ -172,9 +172,9 @@ test_expect_success 'add simple p4 branches' ' echo file1 >file1 && echo file2 >file2 && p4 add file1 file2 && - p4 submit -d "branch1" && + p4 submit -d "Create branch1" && p4 integrate //depot/branch1/... //depot/branch2/... && - p4 submit -d "branch2" && + p4 submit -d "Integrate branch2 from branch1" && echo file3 >file3 && p4 add file3 && p4 submit -d "add file3 in branch1" && @@ -182,7 +182,7 @@ test_expect_success 'add simple p4 branches' ' echo update >>file2 && p4 submit -d "update file2 in branch1" && p4 integrate //depot/branch1/... //depot/branch3/... && - p4 submit -d "branch3" + p4 submit -d "Integrate branch3 from branch1" ) ' @@ -203,17 +203,17 @@ test_expect_success 'git-p4 clone simple branches' ' test -f file1 && test -f file2 && test -f file3 && - grep -q update file2 && + grep update file2 && git reset --hard p4/depot/branch2 && test -f file1 && test -f file2 && test ! -f file3 && - test_must_fail grep -q update file2 && + test_must_fail grep update file2 && git reset --hard p4/depot/branch3 && test -f file1 && test -f file2 && test -f file3 && - grep -q update file2 && + grep update file2 && cd "$cli" && cd branch1 && p4 edit file2 && @@ -222,7 +222,87 @@ test_expect_success 'git-p4 clone simple branches' ' cd "$git" && git reset --hard p4/depot/branch1 && "$GITP4" rebase && - grep -q file2_ file2 + grep file2_ file2 + ) +' + +# Create a complex branch structure in P4 depot to check if they are correctly +# cloned. The branches are created from older changelists to check if git-p4 is +# able to correctly detect them. +# The final expected structure is: +# `branch1 +# | `- file1 +# | `- file2 (updated) +# | `- file3 +# `branch2 +# | `- file1 +# | `- file2 +# `branch3 +# | `- file1 +# | `- file2 (updated) +# | `- file3 +# `branch4 +# | `- file1 +# | `- file2 +# `branch5 +# `- file1 +# `- file2 +# `- file3 +test_expect_success 'git-p4 add complex branches' ' + test_when_finished cleanup_git && + test_create_repo "$git" && + ( + cd "$cli" && + changelist=$(p4 changes -m1 //depot/... | cut -d" " -f2) && + changelist=$(($changelist - 5)) && + p4 integrate //depot/branch1/...@$changelist //depot/branch4/... && + p4 submit -d "Integrate branch4 from branch1@${changelist}" && + changelist=$(($changelist + 2)) && + p4 integrate //depot/branch1/...@$changelist //depot/branch5/... && + p4 submit -d "Integrate branch5 from branch1@${changelist}" + ) +' + +# Configure branches through git-config and clone them. git-p4 will only be able +# to clone the original structure if it is able to detect the origin changelist +# of each branch. +test_expect_success 'git-p4 clone complex branches' ' + test_when_finished cleanup_git && + test_create_repo "$git" && + ( + cd "$git" && + git config git-p4.branchList branch1:branch2 && + git config --add git-p4.branchList branch1:branch3 && + git config --add git-p4.branchList branch1:branch4 && + git config --add git-p4.branchList branch1:branch5 && + "$GITP4" clone --dest=. --detect-branches //depot@all && + git log --all --graph --decorate --stat && + git reset --hard p4/depot/branch1 && + test_path_is_file file1 && + test_path_is_file file2 && + test_path_is_file file3 && + grep update file2 && + git reset --hard p4/depot/branch2 && + test_path_is_file file1 && + test_path_is_file file2 && + test_path_is_missing file3 && + test_must_fail grep update file2 && + git reset --hard p4/depot/branch3 && + test_path_is_file file1 && + test_path_is_file file2 && + test_path_is_file file3 && + grep update file2 && + git reset --hard p4/depot/branch4 && + test_path_is_file file1 && + test_path_is_file file2 && + test_path_is_missing file3 && + test_must_fail grep update file2 && + git reset --hard p4/depot/branch5 && + test_path_is_file file1 && + test_path_is_file file2 && + test_path_is_file file3 && + test_must_fail grep update file2 && + test_path_is_missing .git/git-p4-tmp ) ' -- cgit v1.2.1 From 3558f32f1f5fdb3d566e39f6e07cd3f97d124da6 Mon Sep 17 00:00:00 2001 From: Pete Wyckoff Date: Wed, 25 Jan 2012 23:48:24 +0000 Subject: git-p4: Change p4 command invocation Change p4 command invocation to avoid going through the shell. This allows names with spaces and wildcards to work. Signed-off-by: Pete Wyckoff Signed-off-by: Vitor Antunes Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 0bf2625077..b951ce58bb 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -1984,7 +1984,7 @@ class P4Sync(Command, P4UserMap): def importChanges(self, changes): cnt = 1 for change in changes: - description = p4Cmd("describe %s" % change) + description = p4Cmd(["describe", str(change)]) self.updateOptionDict(description) if not self.silent: -- cgit v1.2.1 From e7d7a56796c457b0a96e58e7638950db824b52af Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 26 Jan 2012 11:40:09 -0800 Subject: t9801: do not overuse test_must_fail test_must_fail is to make sure a program we can potentially break during the course of updating git itself exits with a non-zero status in a clean and controlled way. When we expect a non-zero exit status from the commands we use from the underlying platform in tests, e.g. making sure a string "error: " does not appear in the output by running "grep 'error: '", just use "! grep" for readability. It is not like we will try to update Git and suddenly 'grep' we use from the system starts segfaulting. Signed-off-by: Junio C Hamano --- t/t9801-git-p4-branch.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/t/t9801-git-p4-branch.sh b/t/t9801-git-p4-branch.sh index 6ff713b1f7..d414705416 100755 --- a/t/t9801-git-p4-branch.sh +++ b/t/t9801-git-p4-branch.sh @@ -208,7 +208,7 @@ test_expect_success 'git-p4 clone simple branches' ' test -f file1 && test -f file2 && test ! -f file3 && - test_must_fail grep update file2 && + ! grep update file2 && git reset --hard p4/depot/branch3 && test -f file1 && test -f file2 && @@ -286,7 +286,7 @@ test_expect_success 'git-p4 clone complex branches' ' test_path_is_file file1 && test_path_is_file file2 && test_path_is_missing file3 && - test_must_fail grep update file2 && + ! grep update file2 && git reset --hard p4/depot/branch3 && test_path_is_file file1 && test_path_is_file file2 && @@ -296,12 +296,12 @@ test_expect_success 'git-p4 clone complex branches' ' test_path_is_file file1 && test_path_is_file file2 && test_path_is_missing file3 && - test_must_fail grep update file2 && + ! grep update file2 && git reset --hard p4/depot/branch5 && test_path_is_file file1 && test_path_is_file file2 && test_path_is_file file3 && - test_must_fail grep update file2 && + ! grep update file2 && test_path_is_missing .git/git-p4-tmp ) ' -- cgit v1.2.1 From f15026b514a9fd7eac31313466345c9fae649afc Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Wed, 25 Jan 2012 03:37:02 +0200 Subject: git-completion: workaround zsh COMPREPLY bug zsh adds a backslash (foo\ ) for each item in the COMPREPLY array if IFS doesn't contain spaces. This issue has been reported[1], but there is no solution yet. This wasn't a problem due to another bug[2], which was fixed in zsh version 4.3.12. After this change, 'git checkout ma' would resolve to 'git checkout master\ '. Aditionally, the introduction of __gitcomp_nl in commit a31e626 (completion: optimize refs completion) in git also made the problem apparent, as Matthieu Moy reported. The simplest and most generic solution is to hide all the changes we do to IFS, so that "foo \nbar " is recognized by zsh as "foo bar". This works on versions of git before and after the introduction of __gitcomp_nl (a31e626), and versions of zsh before and after 4.3.12. Once zsh is fixed, we should conditionally disable this workaround to have the same benefits as bash users. [1] http://www.zsh.org/mla/workers/2012/msg00053.html [2] http://zsh.git.sourceforge.net/git/gitweb.cgi?p=zsh/zsh;a=commitdiff;h=2e25dfb8fd38dbef0a306282ffab1d343ce3ad8d Signed-off-by: Felipe Contreras Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index b0062bac22..1496c6dc05 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -2631,6 +2631,10 @@ _git () # workaround zsh's bug that leaves 'words' as a special # variable in versions < 4.3.12 typeset -h words + + # workaround zsh's bug that quotes spaces in the COMPREPLY + # array if IFS doesn't contain spaces. + typeset -h IFS fi local cur words cword prev @@ -2687,6 +2691,10 @@ _gitk () # workaround zsh's bug that leaves 'words' as a special # variable in versions < 4.3.12 typeset -h words + + # workaround zsh's bug that quotes spaces in the COMPREPLY + # array if IFS doesn't contain spaces. + typeset -h IFS fi local cur words cword prev -- cgit v1.2.1 From 634a5f265ad729b91266de65272e2b5a35d05b1c Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 26 Jan 2012 21:48:33 -0800 Subject: INSTALL: warn about recent Fedora breakage Recent releases of Redhat/Fedora are reported to ship Perl binary package with some core modules stripped away (see http://lwn.net/Articles/477234/) against the upstream Perl5 people's wishes. The Time::HiRes module used by gitweb one of them. Signed-off-by: Junio C Hamano --- INSTALL | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/INSTALL b/INSTALL index 8120641a51..6fa83fe2b8 100644 --- a/INSTALL +++ b/INSTALL @@ -83,7 +83,11 @@ Issues of note: - "Perl" version 5.8 or later is needed to use some of the features (e.g. preparing a partial commit using "git add -i/-p", interacting with svn repositories with "git svn"). If you can - live without these, use NO_PERL. + live without these, use NO_PERL. Note that recent releases of + Redhat/Fedora are reported to ship Perl binary package with some + core modules stripped away (see http://lwn.net/Articles/477234/), + so you might need to install additional packages other than Perl + itself, e.g. Time::HiRes. - "openssl" library is used by git-imap-send to use IMAP over SSL. If you don't need it, use NO_OPENSSL. -- cgit v1.2.1 From 60f40791f9f7b671b300b752a9beebc7737a5242 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 23 Jan 2012 18:31:09 -0600 Subject: i18n: Do not force USE_GETTEXT_SCHEME=fallthrough on NO_GETTEXT It should merely be the default used when the builder does not say anything about USE_GETTEXT_SCHEME. Even with NO_GETTEXT, USE_GETTEXT_SCHEME=gnu may be a way to avoid possibly slower emulation in our shell scripts. Signed-off-by: Jonathan Nieder Signed-off-by: Junio C Hamano --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a4a33d4355..634b2ece05 100644 --- a/Makefile +++ b/Makefile @@ -1515,7 +1515,7 @@ ifdef GETTEXT_POISON endif ifdef NO_GETTEXT BASIC_CFLAGS += -DNO_GETTEXT - USE_GETTEXT_SCHEME = fallthrough + USE_GETTEXT_SCHEME ?= fallthrough endif ifdef NO_STRCASESTR COMPAT_CFLAGS += -DNO_STRCASESTR -- cgit v1.2.1 From 828ea97de486c1693d6e4f2c7347acb50235a85d Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 27 Jan 2012 11:31:02 -0800 Subject: Git 1.7.9 Signed-off-by: Junio C Hamano --- Documentation/git.txt | 5 +++++ GIT-VERSION-GEN | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Documentation/git.txt b/Documentation/git.txt index db5d5570cf..c991430642 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -44,6 +44,11 @@ unreleased) version of git, that is available from 'master' branch of the `git.git` repository. Documentation for older releases are available here: +* link:v1.7.9/git.html[documentation for release 1.7.9] + +* release notes for + link:RelNotes/1.7.9.txt[1.7.9]. + * link:v1.7.8.4/git.html[documentation for release 1.7.8.4] * release notes for diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index ba8a07a2ea..70204f87f9 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.7.9-rc2 +DEF_VER=v1.7.9 LF=' ' -- cgit v1.2.1 From 58ebd9865d2bb9d42842fbac5a1c4eae49e92859 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 27 Jan 2012 11:58:56 -0800 Subject: vcs-svn/svndiff.c: squelch false "unused" warning from gcc Curiously, pre_len given to read_length() does not trigger the same warning even though the code structure is the same. Most likely this is because read_offset() is used only once and inlining it will make gcc realize that it has a chance to do more flow analysis. Alas, the analysis is flawed, so it does not help X-<. Signed-off-by: Junio C Hamano --- vcs-svn/svndiff.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcs-svn/svndiff.c b/vcs-svn/svndiff.c index 9ee41bbc90..1647c1a780 100644 --- a/vcs-svn/svndiff.c +++ b/vcs-svn/svndiff.c @@ -295,7 +295,7 @@ int svndiff0_apply(struct line_buffer *delta, off_t delta_len, if (read_magic(delta, &delta_len)) return -1; while (delta_len) { /* For each window: */ - off_t pre_off; + off_t pre_off = pre_off; /* stupid GCC... */ size_t pre_len; if (read_offset(delta, &pre_off, &delta_len) || -- cgit v1.2.1 From 48c07d868419f1597e87dcb4743a0a9f5828daa9 Mon Sep 17 00:00:00 2001 From: Ralf Thielow Date: Sun, 29 Jan 2012 13:55:33 +0100 Subject: completion: --edit-description option for git-branch Signed-off-by: Ralf Thielow Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 1496c6dc05..e44eefdf1c 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1152,7 +1152,7 @@ _git_branch () __gitcomp " --color --no-color --verbose --abbrev= --no-abbrev --track --no-track --contains --merged --no-merged - --set-upstream + --set-upstream --edit-description " ;; *) -- cgit v1.2.1 From 85da4d459bd6fe11189a2ccf560c952b31ed67b2 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 29 Jan 2012 13:46:18 -0800 Subject: Kick off the post 1.7.9 cycle Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.10.txt | 69 +++++++++++++++++++++++++++++++++++++++ GIT-VERSION-GEN | 2 +- RelNotes | 2 +- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 Documentation/RelNotes/1.7.10.txt diff --git a/Documentation/RelNotes/1.7.10.txt b/Documentation/RelNotes/1.7.10.txt new file mode 100644 index 0000000000..cc222819a0 --- /dev/null +++ b/Documentation/RelNotes/1.7.10.txt @@ -0,0 +1,69 @@ +Git v1.7.10 Release Notes +========================= + +Updates since v1.7.9 +-------------------- + +UI, Workflows & Features + + * Improved handling of views, labels and branches in git-p4 (in contrib). + + * "git am" learned to pass "-b" option to underlying "git mailinfo", so + that bracketed string other than "PATCH" at the beginning can be kept. + + * "git clone" learned "--single-branch" option to limit cloning to a + single branch (surprise!). + + * When showing a patch while ignoring whitespace changes, the context + lines are taken from the postimage, in order to make it easier to + view the output. + +Performance + + * During "git upload-pack" in respose to "git fetch", unnecessary calls + to parse_object() have been eliminated, to help performance in + repositories with excessive number of refs. + +Internal Implementation + + * Recursive call chains in "git index-pack" to deal with long delta + chains have been flattened, to reduce the stack footprint. + + * Use of add_extra_ref() API is slowly getting removed, to make it + possible to cleanly restructure the overall refs API. + + * The test suite supports the new "test_pause" helper function. + +Also contains minor documentation updates and code clean-ups. + + +Fixes since v1.7.9 +------------------ + +Unless otherwise noted, all the fixes since v1.7.9 in the maintenance +releases are contained in this release (see release notes to them for +details). + + * When "git push" fails to update any refs, the client side did not + report an error correctly to the end user. + (merge 5238cbf sp/smart-http-failure-to-push later to maint). + + * "git push -q" was not sufficiently quiet. + (merge d336572 cb/push-quiet later to maint). + + * "git log --first-parent $pathspec" did not stay on the first parent + chain and veered into side branch from which the whole change to the + specified paths came. + (merge 36ed191 jc/maint-log-first-parent-pathspec later to maint). + + * Subprocesses spawned from various git programs were often left running + to completion even when the top-level process was killed. + (merge 10c6cdd cb/maint-kill-subprocess-upon-signal later to maint). + +--- +exec >/var/tmp/1 +O=v1.7.9 +echo O=$(git describe) +git log --first-parent --oneline ^maint $O.. +echo +git shortlog --no-merges ^maint $O.. diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 70204f87f9..c25fd2a374 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.7.9 +DEF_VER=v1.7.9.GIT LF=' ' diff --git a/RelNotes b/RelNotes index 766bbaf8f5..2c2a169555 120000 --- a/RelNotes +++ b/RelNotes @@ -1 +1 @@ -Documentation/RelNotes/1.7.9.txt \ No newline at end of file +Documentation/RelNotes/1.7.10.txt \ No newline at end of file -- cgit v1.2.1 From aad070922157d3249af7095ecff6ebc074c13bb9 Mon Sep 17 00:00:00 2001 From: Adrian Weimann Date: Mon, 30 Jan 2012 20:29:33 +0100 Subject: completion: --edit and --no-edit for git-merge Signed-off-by: Adrian Weimann Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 1496c6dc05..78be195838 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1622,7 +1622,7 @@ _git_log () __git_merge_options=" --no-commit --no-stat --log --no-log --squash --strategy - --commit --stat --no-squash --ff --no-ff --ff-only + --commit --stat --no-squash --ff --no-ff --ff-only --edit --no-edit " _git_merge () -- cgit v1.2.1 From f26af3fcbcbd27392b8c94b1e4eb6e671b2466ab Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Mon, 30 Jan 2012 21:25:30 +0100 Subject: merge: add instructions to the commit message when editing Before f824628 (merge: use editor by default in interactive sessions, 2012-01-10), git-merge only started an editor if the user explicitly asked for it with --edit. Thus it seemed unlikely that the user would need extra guidance. After f824628 the _normal_ thing is to start an editor. Give at least an indication of why we are doing it. The sentence about justification is one of the few things about standard git that are not agnostic to the workflow that the user chose. However, f824628 was proposed by Linus specifically to discourage users from merging unrelated upstream progress into topic branches. So we may as well take another step in the same direction. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- builtin/merge.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/builtin/merge.c b/builtin/merge.c index 3a451727d0..41d835e3f7 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -885,11 +885,21 @@ static void abort_commit(const char *err_msg) exit(1); } +static const char merge_editor_comment[] = +N_("Please enter a commit message to explain why this merge is necessary,\n" + "especially if it merges an updated upstream into a topic branch.\n" + "\n" + "Lines starting with '#' will be ignored, and an empty message aborts\n" + "the commit.\n"); + static void prepare_to_commit(void) { struct strbuf msg = STRBUF_INIT; + const char *comment = _(merge_editor_comment); strbuf_addbuf(&msg, &merge_msg); strbuf_addch(&msg, '\n'); + if (0 < option_edit) + strbuf_add_lines(&msg, "# ", comment, strlen(comment)); write_merge_msg(&msg); run_hook(get_index_file(), "prepare-commit-msg", git_path("MERGE_MSG"), "merge", NULL, NULL); -- cgit v1.2.1 From 2ad9ba0382ca0d9a36746db804d1bf01f5281b8e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 31 Jan 2012 21:06:06 -0800 Subject: request-pull: explicitly ask tags/$name to be pulled When asking for a tag to be pulled, disambiguate by leaving tags/ prefix in front of the name of the tag. E.g. ... in the git repository at: git://example.com/git/git.git/ tags/v1.2.3 for you to fetch changes up to 123456... This way, older versions of "git pull" can be used to respond to such a request more easily, as "git pull $URL v1.2.3" did not DWIM to fetch v1.2.3 tag in older versions. Also this makes it clearer for humans that the pull request is made for a tag and he should anticipate a signed one. Signed-off-by: Junio C Hamano --- Documentation/howto/using-signed-tag-in-pull-request.txt | 4 ++-- git-request-pull.sh | 2 +- t/t5150-request-pull.sh | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Documentation/howto/using-signed-tag-in-pull-request.txt b/Documentation/howto/using-signed-tag-in-pull-request.txt index a1351c5bb8..98c0033a55 100644 --- a/Documentation/howto/using-signed-tag-in-pull-request.txt +++ b/Documentation/howto/using-signed-tag-in-pull-request.txt @@ -109,7 +109,7 @@ The resulting msg.txt file begins like so: are available in the git repository at: - example.com:/git/froboz.git frotz-for-xyzzy + example.com:/git/froboz.git tags/frotz-for-xyzzy for you to fetch changes up to 703f05ad5835c...: @@ -141,7 +141,7 @@ After receiving such a pull request message, the integrator fetches and integrates the tag named in the request, with: ------------ - $ git pull example.com:/git/froboz.git/ frotz-for-xyzzy + $ git pull example.com:/git/froboz.git/ tags/frotz-for-xyzzy ------------ This operation will always open an editor to allow the integrator to fine diff --git a/git-request-pull.sh b/git-request-pull.sh index 64960d65a1..e6438e24c7 100755 --- a/git-request-pull.sh +++ b/git-request-pull.sh @@ -63,7 +63,7 @@ die "fatal: No commits in common between $base and $head" find_matching_ref=' sub abbr { my $ref = shift; - if ($ref =~ s|refs/heads/|| || $ref =~ s|refs/tags/||) { + if ($ref =~ s|^refs/heads/|| || $ref =~ s|^refs/tags/|tags/|) { return $ref; } else { return $ref; diff --git a/t/t5150-request-pull.sh b/t/t5150-request-pull.sh index da25bc2d1f..7c1dc641de 100755 --- a/t/t5150-request-pull.sh +++ b/t/t5150-request-pull.sh @@ -179,11 +179,7 @@ test_expect_success 'request names an appropriate branch' ' read repository && read branch } Date: Mon, 30 Jan 2012 21:05:47 +0100 Subject: gitweb: move hard coded .git suffix out of git_get_projects_list Use of the filter option of git_get_projects_list is currently limited to forks. It hard codes removal of ".git" suffixes from the filter. To make it more generic move the .git suffix removal to the callers. Signed-off-by: Bernhard R. Link Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index abb5a79afc..e074cd7c63 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -2829,8 +2829,6 @@ sub git_get_projects_list { my $filter = shift || ''; my @list; - $filter =~ s/\.git$//; - if (-d $projects_list) { # search in directory my $dir = $projects_list; @@ -6005,7 +6003,9 @@ sub git_forks { die_error(400, "Unknown order parameter"); } - my @list = git_get_projects_list($project); + my $filter = $project; + $filter =~ s/\.git$//; + my @list = git_get_projects_list($filter); if (!@list) { die_error(404, "No forks found"); } @@ -6064,7 +6064,9 @@ sub git_summary { if ($check_forks) { # find forks of a project - @forklist = git_get_projects_list($project); + my $filter = $project; + $filter =~ s/\.git$//; + @forklist = git_get_projects_list($filter); # filter out forks of forks @forklist = filter_forks_from_projects_list(\@forklist) if (@forklist); -- cgit v1.2.1 From 348a6589e016669a3cf2d71c612e32dde9ef6f27 Mon Sep 17 00:00:00 2001 From: "Bernhard R. Link" Date: Mon, 30 Jan 2012 21:06:38 +0100 Subject: gitweb: prepare git_get_projects_list for use outside 'forks'. Use of the filter option of git_get_projects_list is currently limited to forks. It currently assumes the project belonging to the filter directory was already validated to be visible in the project list. To make it more generic add an optional argument to denote visibility verification is still needed. If there is a projects list file (GITWEB_LIST) only projects from this list are returned anyway, so no more checks needed. If there is no projects list file and the caller requests strict checking (GITWEB_STRICT_EXPORT), do not jump directly to the given directory but instead do a normal search and filter the results instead. The only effect of GITWEB_STRICT_EXPORT without GITWEB_LIST is to make sure no project can be viewed without also be found starting from project root. git_get_projects_list without this patch does not enforce this but all callers only call it with a filter already checked this way. With this parameter a caller can request this check if the filter cannot be checked this way. Signed-off-by: Bernhard R. Link Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index e074cd7c63..48a2a375be 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -2827,6 +2827,7 @@ sub git_get_project_url_list { sub git_get_projects_list { my $filter = shift || ''; + my $paranoid = shift; my @list; if (-d $projects_list) { @@ -2837,7 +2838,7 @@ sub git_get_projects_list { my $pfxlen = length("$dir"); my $pfxdepth = ($dir =~ tr!/!!); # when filtering, search only given subdirectory - if ($filter) { + if ($filter && !$paranoid) { $dir .= "/$filter"; $dir =~ s!/+$!!; } @@ -2862,6 +2863,10 @@ sub git_get_projects_list { } my $path = substr($File::Find::name, $pfxlen + 1); + # paranoidly only filter here + if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) { + next; + } # we check related file in $projectroot if (check_export_ok("$projectroot/$path")) { push @list, { path => $path }; -- cgit v1.2.1 From 19d2d23998bba7c7f0ee87451ddbbf8906381939 Mon Sep 17 00:00:00 2001 From: "Bernhard R. Link" Date: Mon, 30 Jan 2012 21:07:37 +0100 Subject: gitweb: add project_filter to limit project list to a subdirectory This commit changes the project listing views (project_list, project_index and opml) to limit the output to only projects in a subdirectory if the new optional parameter ?pf=directory name is used. The implementation of the filter reuses the implementation used for the 'forks' action (i.e. listing all projects within that directory from the projects list file (GITWEB_LIST) or only projects in the given subdirectory of the project root directory without a projects list file). Reusing $project instead of adding a new parameter would have been nicer from a UI point-of-view (including PATH_INFO support) but would complicate the $project validating code that is currently being used to ensure nothing is exported that should not be viewable. Signed-off-by: Bernhard R. Link Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 48a2a375be..daacf87e6a 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -760,6 +760,7 @@ our @cgi_param_mapping = ( search_use_regexp => "sr", ctag => "by_tag", diff_style => "ds", + project_filter => "pf", # this must be last entry (for manipulation from JavaScript) javascript => "js" ); @@ -976,7 +977,7 @@ sub evaluate_path_info { our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base, $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp, - $searchtext, $search_regexp); + $searchtext, $search_regexp, $project_filter); sub evaluate_and_validate_params { our $action = $input_params{'action'}; if (defined $action) { @@ -994,6 +995,13 @@ sub evaluate_and_validate_params { } } + our $project_filter = $input_params{'project_filter'}; + if (defined $project_filter) { + if (!validate_pathname($project_filter)) { + die_error(404, "Invalid project_filter parameter"); + } + } + our $file_name = $input_params{'file_name'}; if (defined $file_name) { if (!validate_pathname($file_name)) { @@ -3732,7 +3740,12 @@ sub run_highlighter { sub get_page_title { my $title = to_utf8($site_name); - return $title unless (defined $project); + unless (defined $project) { + if (defined $project_filter) { + $title .= " - " . to_utf8($project_filter); + } + return $title; + } $title .= " - " . to_utf8($project); return $title unless (defined $action); @@ -5982,7 +5995,7 @@ sub git_project_list { die_error(400, "Unknown order parameter"); } - my @list = git_get_projects_list(); + my @list = git_get_projects_list($project_filter, $strict_export); if (!@list) { die_error(404, "No projects found"); } @@ -6023,7 +6036,7 @@ sub git_forks { } sub git_project_index { - my @projects = git_get_projects_list(); + my @projects = git_get_projects_list($project_filter, $strict_export); if (!@projects) { die_error(404, "No projects found"); } @@ -7862,7 +7875,7 @@ sub git_atom { } sub git_opml { - my @list = git_get_projects_list(); + my @list = git_get_projects_list($project_filter, $strict_export); if (!@list) { die_error(404, "No projects found"); } @@ -7873,11 +7886,17 @@ sub git_opml { -content_disposition => 'inline; filename="opml.xml"'); my $title = esc_html($site_name); + my $filter = " within subdirectory "; + if (defined $project_filter) { + $filter .= esc_html($project_filter); + } else { + $filter = ""; + } print < - $title OPML Export + $title OPML Export$filter -- cgit v1.2.1 From 56efd9d2524694717adf5405fccad90e2791ebfa Mon Sep 17 00:00:00 2001 From: "Bernhard R. Link" Date: Mon, 30 Jan 2012 21:09:00 +0100 Subject: gitweb: limit links to alternate forms of project_list to active project_filter If project_list action is given a project_filter argument, pass that to TXT and OPML formats. This way [OPML] and [TXT] links provide the same list of projects as the projects_list page they are linked from. Signed-off-by: Bernhard R. Link Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index daacf87e6a..ecd4a39d22 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -3979,9 +3979,11 @@ sub git_footer_html { } } else { - print $cgi->a({-href => href(project=>undef, action=>"opml"), + print $cgi->a({-href => href(project=>undef, action=>"opml", + project_filter => $project_filter), -class => $feed_class}, "OPML") . " "; - print $cgi->a({-href => href(project=>undef, action=>"project_index"), + print $cgi->a({-href => href(project=>undef, action=>"project_index", + project_filter => $project_filter), -class => $feed_class}, "TXT") . "\n"; } print "\n"; # class="page_footer" -- cgit v1.2.1 From 40efa22309458546a3ea861689034acf9fbf9d1a Mon Sep 17 00:00:00 2001 From: "Bernhard R. Link" Date: Mon, 30 Jan 2012 21:09:43 +0100 Subject: gitweb: show active project_filter in project_list page header In the page header of a project_list view with a project_filter given show breadcrumbs in the page headers showing which directory it is currently limited to and also containing links to the parent directories. Signed-off-by: Bernhard R. Link Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index ecd4a39d22..7d36f563e4 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -3839,6 +3839,18 @@ sub print_header_links { } } +sub print_nav_breadcrumbs_path { + my $dirprefix = undef; + while (my $part = shift) { + $dirprefix .= "/" if defined $dirprefix; + $dirprefix .= $part; + print $cgi->a({-href => href(project => undef, + project_filter => $dirprefix, + action => "project_list")}, + esc_html($part)) . " / "; + } +} + sub print_nav_breadcrumbs { my %opts = @_; @@ -3857,6 +3869,8 @@ sub print_nav_breadcrumbs { print " / $opts{-action_extra}"; } print "\n"; + } elsif (defined $project_filter) { + print_nav_breadcrumbs_path(split '/', $project_filter); } } -- cgit v1.2.1 From 4426ba2919697d57ab8b6335ca63b2c14e4e6339 Mon Sep 17 00:00:00 2001 From: "Bernhard R. Link" Date: Mon, 30 Jan 2012 21:10:23 +0100 Subject: gitweb: place links to parent directories in page header Change html page headers to not only link the project root and the currently selected project but also the directories in between using project_filter. (Allowing to jump to a list of all projects within that intermediate directory directly and making the project_filter feature visible to users). Signed-off-by: Bernhard R. Link Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 7d36f563e4..3ab608c7f9 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -3856,7 +3856,10 @@ sub print_nav_breadcrumbs { print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / "; if (defined $project) { - print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); + my @dirname = split '/', $project; + my $projectbasename = pop @dirname; + print_nav_breadcrumbs_path(@dirname); + print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename)); if (defined $action) { my $action_print = $action ; if (defined $opts{-action_extra}) { -- cgit v1.2.1 From a1e1b2d77b9fc5866d12bf984214b15f1dccf4a8 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Tue, 31 Jan 2012 01:20:54 +0100 Subject: gitweb: improve usability of projects search form Refactor generating project search form into git_project_search_form(). Make text field wider and add on mouse over explanation (via "title" attribute), add an option to use regular expressions, and replace 'Search:' label with [Search] button. Also add "List all projects" link to make it easier to go back from search result to list of all projects (note that an empty search term is disallowed). Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 27 ++++++++++++++++++++++----- gitweb/static/gitweb.css | 7 ++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 3ab608c7f9..c81743b0a1 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5158,6 +5158,26 @@ sub git_patchset_body { # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +sub git_project_search_form { + my ($searchtext, $search_use_regexp); + + print "
\n"; + print $cgi->startform(-method => 'get', -action => $my_uri) . + $cgi->hidden(-name => 'a', -value => 'project_list') . "\n" . + $cgi->textfield(-name => 's', -value => $searchtext, + -title => 'Search project by name and description', + -size => 60) . "\n" . + "" . + $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', + -checked => $search_use_regexp) . + "\n" . + $cgi->submit(-name => 'btnS', -value => 'Search') . + $cgi->end_form() . "\n" . + $cgi->a({-href => href(project => undef, searchtext => undef)}, + 'List all projects') . "
\n"; + print "
\n"; +} + # fills project list info (age, description, owner, category, forks) # for each project in the list, removing invalid projects from # returned list @@ -6025,11 +6045,8 @@ sub git_project_list { insert_file($home_text); print "\n"; } - print $cgi->startform(-method => "get") . - "

Search:\n" . - $cgi->textfield(-name => "s", -value => $searchtext) . "\n" . - "

" . - $cgi->end_form() . "\n"; + + git_project_search_form($searchtext, $search_use_regexp); git_project_list_body(\@list, $order); git_footer_html(); } diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index c7827e8f1d..c530355a7c 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -520,8 +520,13 @@ div.search { right: 12px } -p.projsearch { +div.projsearch { text-align: center; + margin: 20px 0px; +} + +div.projsearch form { + margin-bottom: 2px; } td.linenr { -- cgit v1.2.1 From abc0c9d2d7312c4b153ba1b2b9cd8ba0696f1b04 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Tue, 31 Jan 2012 01:20:55 +0100 Subject: gitweb: Make project search respect project_filter Make gitweb search within filtered projects (i.e. projects shown), and change "List all projects" to "List all projects in '$project_filter/'" if project_filter is used. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index c81743b0a1..f211594c45 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5161,11 +5161,18 @@ sub git_patchset_body { sub git_project_search_form { my ($searchtext, $search_use_regexp); + my $limit = ''; + if ($project_filter) { + $limit = " in '$project_filter/'"; + } + print "
\n"; print $cgi->startform(-method => 'get', -action => $my_uri) . - $cgi->hidden(-name => 'a', -value => 'project_list') . "\n" . - $cgi->textfield(-name => 's', -value => $searchtext, - -title => 'Search project by name and description', + $cgi->hidden(-name => 'a', -value => 'project_list') . "\n"; + print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n" + if (defined $project_filter); + print $cgi->textfield(-name => 's', -value => $searchtext, + -title => "Search project by name and description$limit", -size => 60) . "\n" . "" . $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', @@ -5173,8 +5180,9 @@ sub git_project_search_form { "\n" . $cgi->submit(-name => 'btnS', -value => 'Search') . $cgi->end_form() . "\n" . - $cgi->a({-href => href(project => undef, searchtext => undef)}, - 'List all projects') . "
\n"; + $cgi->a({-href => href(project => undef, searchtext => undef, + project_filter => $project_filter)}, + esc_html("List all projects$limit")) . "
\n"; print "
\n"; } -- cgit v1.2.1 From 6a301345a545ce5faf1a054d6c9bf1558dd46b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Wed, 1 Feb 2012 22:17:18 +0700 Subject: pack-objects: do not accept "--index-version=version," MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 2 +- t/t5302-pack-index.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 0f2e7b8f5c..297f792851 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -2471,7 +2471,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) pack_idx_opts.version = strtoul(arg + 16, &c, 10); if (pack_idx_opts.version > 2) die("bad %s", arg); - if (*c == ',') + if (*c == ',' && c[1]) pack_idx_opts.off32_limit = strtoul(c+1, &c, 0); if (*c || pack_idx_opts.off32_limit & 0x80000000) die("bad %s", arg); diff --git a/t/t5302-pack-index.sh b/t/t5302-pack-index.sh index f8fa92446c..fe82025d4a 100755 --- a/t/t5302-pack-index.sh +++ b/t/t5302-pack-index.sh @@ -73,6 +73,10 @@ test_expect_success 'index-pack --verify on index version 2' ' git index-pack --verify "test-2-${pack2}.pack" ' +test_expect_success \ + 'pack-objects --index-version=2, is not accepted' \ + 'test_must_fail git pack-objects --index-version=2, test-3 Date: Wed, 1 Feb 2012 22:17:19 +0700 Subject: pack-objects: remove bogus comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment was introduced in b5d97e6 (pack-objects: run rev-list equivalent internally. - 2006-09-04), stating that git pack-objects [options] base-name is acceptable and refs should be passed into rev-list. But that's not true. All arguments after base-name are ignored. Remove the comment and reject this syntax (i.e. no more arguments after base name) Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 15 +-------------- t/t5300-pack-object.sh | 4 ++++ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 297f792851..80e3114573 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -2484,23 +2484,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) usage(pack_usage); } - /* Traditionally "pack-objects [options] base extra" failed; - * we would however want to take refs parameter that would - * have been given to upstream rev-list ourselves, which means - * we somehow want to say what the base name is. So the - * syntax would be: - * - * pack-objects [options] base - * - * in other words, we would treat the first non-option as the - * base_name and send everything else to the internal revision - * walker. - */ - if (!pack_to_stdout) base_name = argv[i++]; - if (pack_to_stdout != !base_name) + if (pack_to_stdout != !base_name || argv[i]) usage(pack_usage); if (!pack_to_stdout && !pack_size_limit) diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh index 602806d09c..d9d856b87b 100755 --- a/t/t5300-pack-object.sh +++ b/t/t5300-pack-object.sh @@ -38,6 +38,10 @@ test_expect_success \ 'pack without delta' \ 'packname_1=$(git pack-objects --window=0 test-1 Date: Wed, 1 Feb 2012 22:17:20 +0700 Subject: pack-objects: convert to use parse_options() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 315 ++++++++++++++++++++++--------------------------- 1 file changed, 139 insertions(+), 176 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 80e3114573..e21e5af8f9 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -18,16 +18,11 @@ #include "refs.h" #include "thread-utils.h" -static const char pack_usage[] = - "git pack-objects [ -q | --progress | --all-progress ]\n" - " [--all-progress-implied]\n" - " [--max-pack-size=] [--local] [--incremental]\n" - " [--window=] [--window-memory=] [--depth=]\n" - " [--no-reuse-delta] [--no-reuse-object] [--delta-base-offset]\n" - " [--threads=] [--non-empty] [--revs [--unpacked | --all]]\n" - " [--reflog] [--stdout | base-name] [--include-tag]\n" - " [--keep-unreachable | --unpack-unreachable]\n" - " [< ref-list | < object-list]"; +static const char *pack_usage[] = { + "git pack-objects --stdout [options...] [< ref-list | < object-list]", + "git pack-objects [options...] base-name [< ref-list | < object-list]", + NULL +}; struct object_entry { struct pack_idx_entry idx; @@ -2305,191 +2300,159 @@ static void get_object_list(int ac, const char **av) loosen_unused_packed_objects(&revs); } +static int option_parse_index_version(const struct option *opt, + const char *arg, int unset) +{ + char *c; + const char *val = arg; + pack_idx_opts.version = strtoul(val, &c, 10); + if (pack_idx_opts.version > 2) + die(_("unsupported index version %s"), val); + if (*c == ',' && c[1]) + pack_idx_opts.off32_limit = strtoul(c+1, &c, 0); + if (*c || pack_idx_opts.off32_limit & 0x80000000) + die(_("bad index version '%s'"), val); + return 0; +} + +static int option_parse_ulong(const struct option *opt, + const char *arg, int unset) +{ + if (unset) + die(_("option %s does not accept negative form"), + opt->long_name); + + if (!git_parse_ulong(arg, opt->value)) + die(_("unable to parse value '%s' for option %s"), + arg, opt->long_name); + return 0; +} + +#define OPT_ULONG(s, l, v, h) \ + { OPTION_CALLBACK, (s), (l), (v), "n", (h), \ + PARSE_OPT_NONEG, option_parse_ulong } + int cmd_pack_objects(int argc, const char **argv, const char *prefix) { int use_internal_rev_list = 0; int thin = 0; int all_progress_implied = 0; - uint32_t i; - const char **rp_av; - int rp_ac_alloc = 64; - int rp_ac; + const char *rp_av[6]; + int rp_ac = 0; + int rev_list_unpacked = 0, rev_list_all = 0, rev_list_reflog = 0; + struct option pack_objects_options[] = { + OPT_SET_INT('q', "quiet", &progress, + "do not show progress meter", 0), + OPT_SET_INT(0, "progress", &progress, + "show progress meter", 1), + OPT_SET_INT(0, "all-progress", &progress, + "show progress meter during object writing phase", 2), + OPT_BOOL(0, "all-progress-implied", + &all_progress_implied, + "similar to --all-progress when progress meter is shown"), + { OPTION_CALLBACK, 0, "index-version", NULL, "version[,offset]", + "write the pack index file in the specified idx format version", + 0, option_parse_index_version }, + OPT_ULONG(0, "max-pack-size", &pack_size_limit, + "maximum size of each output pack file"), + OPT_BOOL(0, "local", &local, + "ignore borrowed objects from alternate object store"), + OPT_BOOL(0, "incremental", &incremental, + "ignore packed objects"), + OPT_INTEGER(0, "window", &window, + "limit pack window by objects"), + OPT_ULONG(0, "window-memory", &window_memory_limit, + "limit pack window by memory in addition to object limit"), + OPT_INTEGER(0, "depth", &depth, + "maximum length of delta chain allowed in the resulting pack"), + OPT_BOOL(0, "reuse-delta", &reuse_delta, + "reuse existing deltas"), + OPT_BOOL(0, "reuse-object", &reuse_object, + "reuse existing objects"), + OPT_BOOL(0, "delta-base-offset", &allow_ofs_delta, + "use OFS_DELTA objects"), + OPT_INTEGER(0, "threads", &delta_search_threads, + "use threads when searching for best delta matches"), + OPT_BOOL(0, "non-empty", &non_empty, + "do not create an empty pack output"), + OPT_BOOL(0, "revs", &use_internal_rev_list, + "read revision arguments from standard input"), + { OPTION_SET_INT, 0, "unpacked", &rev_list_unpacked, NULL, + "limit the objects to those that are not yet packed", + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { OPTION_SET_INT, 0, "all", &rev_list_all, NULL, + "include objects reachable from any reference", + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { OPTION_SET_INT, 0, "reflog", &rev_list_reflog, NULL, + "include objects referred by reflog entries", + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + OPT_BOOL(0, "stdout", &pack_to_stdout, + "output pack to stdout"), + OPT_BOOL(0, "include-tag", &include_tag, + "include tag objects that refer to objects to be packed"), + OPT_BOOL(0, "keep-unreachable", &keep_unreachable, + "keep unreachable objects"), + OPT_BOOL(0, "unpack-unreachable", &unpack_unreachable, + "unpack unreachable objects"), + OPT_BOOL(0, "thin", &thin, + "create thin packs"), + OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep, + "ignore packs that have companion .keep file"), + OPT_INTEGER(0, "compression", &pack_compression_level, + "pack compression level"), + OPT_SET_INT(0, "keep-true-parents", &grafts_replace_parents, + "do not hide commits by grafts", 0), + OPT_END(), + }; read_replace_refs = 0; - rp_av = xcalloc(rp_ac_alloc, sizeof(*rp_av)); - - rp_av[0] = "pack-objects"; - rp_av[1] = "--objects"; /* --thin will make it --objects-edge */ - rp_ac = 2; - reset_pack_idx_option(&pack_idx_opts); git_config(git_pack_config, NULL); if (!pack_compression_seen && core_compression_seen) pack_compression_level = core_compression_level; progress = isatty(2); - for (i = 1; i < argc; i++) { - const char *arg = argv[i]; + argc = parse_options(argc, argv, prefix, pack_objects_options, + pack_usage, 0); - if (*arg != '-') - break; - - if (!strcmp("--non-empty", arg)) { - non_empty = 1; - continue; - } - if (!strcmp("--local", arg)) { - local = 1; - continue; - } - if (!strcmp("--incremental", arg)) { - incremental = 1; - continue; - } - if (!strcmp("--honor-pack-keep", arg)) { - ignore_packed_keep = 1; - continue; - } - if (!prefixcmp(arg, "--compression=")) { - char *end; - int level = strtoul(arg+14, &end, 0); - if (!arg[14] || *end) - usage(pack_usage); - if (level == -1) - level = Z_DEFAULT_COMPRESSION; - else if (level < 0 || level > Z_BEST_COMPRESSION) - die("bad pack compression level %d", level); - pack_compression_level = level; - continue; - } - if (!prefixcmp(arg, "--max-pack-size=")) { - pack_size_limit_cfg = 0; - if (!git_parse_ulong(arg+16, &pack_size_limit)) - usage(pack_usage); - continue; - } - if (!prefixcmp(arg, "--window=")) { - char *end; - window = strtoul(arg+9, &end, 0); - if (!arg[9] || *end) - usage(pack_usage); - continue; - } - if (!prefixcmp(arg, "--window-memory=")) { - if (!git_parse_ulong(arg+16, &window_memory_limit)) - usage(pack_usage); - continue; - } - if (!prefixcmp(arg, "--threads=")) { - char *end; - delta_search_threads = strtoul(arg+10, &end, 0); - if (!arg[10] || *end || delta_search_threads < 0) - usage(pack_usage); -#ifdef NO_PTHREADS - if (delta_search_threads != 1) - warning("no threads support, " - "ignoring %s", arg); -#endif - continue; - } - if (!prefixcmp(arg, "--depth=")) { - char *end; - depth = strtoul(arg+8, &end, 0); - if (!arg[8] || *end) - usage(pack_usage); - continue; - } - if (!strcmp("--progress", arg)) { - progress = 1; - continue; - } - if (!strcmp("--all-progress", arg)) { - progress = 2; - continue; - } - if (!strcmp("--all-progress-implied", arg)) { - all_progress_implied = 1; - continue; - } - if (!strcmp("-q", arg)) { - progress = 0; - continue; - } - if (!strcmp("--no-reuse-delta", arg)) { - reuse_delta = 0; - continue; - } - if (!strcmp("--no-reuse-object", arg)) { - reuse_object = reuse_delta = 0; - continue; - } - if (!strcmp("--delta-base-offset", arg)) { - allow_ofs_delta = 1; - continue; - } - if (!strcmp("--stdout", arg)) { - pack_to_stdout = 1; - continue; - } - if (!strcmp("--revs", arg)) { - use_internal_rev_list = 1; - continue; - } - if (!strcmp("--keep-unreachable", arg)) { - keep_unreachable = 1; - continue; - } - if (!strcmp("--unpack-unreachable", arg)) { - unpack_unreachable = 1; - continue; - } - if (!strcmp("--include-tag", arg)) { - include_tag = 1; - continue; - } - if (!strcmp("--unpacked", arg) || - !strcmp("--reflog", arg) || - !strcmp("--all", arg)) { - use_internal_rev_list = 1; - if (rp_ac >= rp_ac_alloc - 1) { - rp_ac_alloc = alloc_nr(rp_ac_alloc); - rp_av = xrealloc(rp_av, - rp_ac_alloc * sizeof(*rp_av)); - } - rp_av[rp_ac++] = arg; - continue; - } - if (!strcmp("--thin", arg)) { - use_internal_rev_list = 1; - thin = 1; - rp_av[1] = "--objects-edge"; - continue; - } - if (!prefixcmp(arg, "--index-version=")) { - char *c; - pack_idx_opts.version = strtoul(arg + 16, &c, 10); - if (pack_idx_opts.version > 2) - die("bad %s", arg); - if (*c == ',' && c[1]) - pack_idx_opts.off32_limit = strtoul(c+1, &c, 0); - if (*c || pack_idx_opts.off32_limit & 0x80000000) - die("bad %s", arg); - continue; - } - if (!strcmp(arg, "--keep-true-parents")) { - grafts_replace_parents = 0; - continue; - } - usage(pack_usage); + if (argc) { + base_name = argv[0]; + argc--; } + if (pack_to_stdout != !base_name || argc) + usage_with_options(pack_usage, pack_objects_options); - if (!pack_to_stdout) - base_name = argv[i++]; + rp_av[rp_ac++] = "pack-objects"; + if (thin) { + use_internal_rev_list = 1; + rp_av[rp_ac++] = "--objects-edge"; + } else + rp_av[rp_ac++] = "--objects"; - if (pack_to_stdout != !base_name || argv[i]) - usage(pack_usage); + if (rev_list_all) { + use_internal_rev_list = 1; + rp_av[rp_ac++] = "--all"; + } + if (rev_list_reflog) { + use_internal_rev_list = 1; + rp_av[rp_ac++] = "--reflog"; + } + if (rev_list_unpacked) { + use_internal_rev_list = 1; + rp_av[rp_ac++] = "--unpacked"; + } + if (!reuse_object) + reuse_delta = 0; + if (pack_compression_level == -1) + pack_compression_level = Z_DEFAULT_COMPRESSION; + else if (pack_compression_level < 0 || pack_compression_level > Z_BEST_COMPRESSION) + die("bad pack compression level %d", pack_compression_level); +#ifdef NO_PTHREADS + if (delta_search_threads != 1) + warning("no threads support, ignoring %s", arg); +#endif if (!pack_to_stdout && !pack_size_limit) pack_size_limit = pack_size_limit_cfg; if (pack_to_stdout && pack_size_limit) -- cgit v1.2.1 From 95099731bf2c79ccf5870655e36caa4215f0ced0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Wed, 1 Feb 2012 20:48:54 +0700 Subject: sha1_file.c: move the core logic of find_pack_entry() into fill_pack_entry() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new helper function implements the logic to find the offset for the object in one pack and fill a pack_entry structure. The next patch will restructure the loop and will call the helper from two places. Signed-off-by: Nguyễn Thái Ngọc Duy Acked-by: Nicolas Pitre Signed-off-by: Junio C Hamano --- sha1_file.c | 61 +++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/sha1_file.c b/sha1_file.c index 88f2151ff3..61e51edc42 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -2010,11 +2010,44 @@ int is_pack_valid(struct packed_git *p) return !open_packed_git(p); } +static int fill_pack_entry(const unsigned char *sha1, + struct pack_entry *e, + struct packed_git *p) +{ + off_t offset; + + if (p->num_bad_objects) { + unsigned i; + for (i = 0; i < p->num_bad_objects; i++) + if (!hashcmp(sha1, p->bad_object_sha1 + 20 * i)) + return 0; + } + + offset = find_pack_entry_one(sha1, p); + if (!offset) + return 0; + + /* + * We are about to tell the caller where they can locate the + * requested object. We better make sure the packfile is + * still here and can be accessed before supplying that + * answer, as it may have been deleted since the index was + * loaded! + */ + if (!is_pack_valid(p)) { + warning("packfile %s cannot be accessed", p->pack_name); + return 0; + } + e->offset = offset; + e->p = p; + hashcpy(e->sha1, sha1); + return 1; +} + static int find_pack_entry(const unsigned char *sha1, struct pack_entry *e) { static struct packed_git *last_found = (void *)1; struct packed_git *p; - off_t offset; prepare_packed_git(); if (!packed_git) @@ -2022,35 +2055,11 @@ static int find_pack_entry(const unsigned char *sha1, struct pack_entry *e) p = (last_found == (void *)1) ? packed_git : last_found; do { - if (p->num_bad_objects) { - unsigned i; - for (i = 0; i < p->num_bad_objects; i++) - if (!hashcmp(sha1, p->bad_object_sha1 + 20 * i)) - goto next; - } - - offset = find_pack_entry_one(sha1, p); - if (offset) { - /* - * We are about to tell the caller where they can - * locate the requested object. We better make - * sure the packfile is still here and can be - * accessed before supplying that answer, as - * it may have been deleted since the index - * was loaded! - */ - if (!is_pack_valid(p)) { - warning("packfile %s cannot be accessed", p->pack_name); - goto next; - } - e->offset = offset; - e->p = p; - hashcpy(e->sha1, sha1); + if (fill_pack_entry(sha1, e, p)) { last_found = p; return 1; } - next: if (p == last_found) p = packed_git; else -- cgit v1.2.1 From c01f51cc750dbd76e50919bf4e3b94e1b47d2e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=C3=A1i=20Ng=E1=BB=8Dc=20Duy?= Date: Wed, 1 Feb 2012 20:48:55 +0700 Subject: find_pack_entry(): do not keep packed_git pointer locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit f7c22cc (always start looking up objects in the last used pack first - 2007-05-30) introduce a static packed_git* pointer as an optimization. The kept pointer however may become invalid if free_pack_by_name() happens to free that particular pack. Current code base does not access packs after calling free_pack_by_name() so it should not be a problem. Anyway, move the pointer out so that free_pack_by_name() can reset it to avoid running into troubles in future. Signed-off-by: Nguyễn Thái Ngọc Duy Acked-by: Nicolas Pitre Signed-off-by: Junio C Hamano --- sha1_file.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/sha1_file.c b/sha1_file.c index 61e51edc42..6b1b5125c8 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -54,6 +54,8 @@ static struct cached_object empty_tree = { 0 }; +static struct packed_git *last_found_pack; + static struct cached_object *find_cached_object(const unsigned char *sha1) { int i; @@ -720,6 +722,8 @@ void free_pack_by_name(const char *pack_name) close_pack_index(p); free(p->bad_object_sha1); *pp = p->next; + if (last_found_pack == p) + last_found_pack = NULL; free(p); return; } @@ -2046,27 +2050,22 @@ static int fill_pack_entry(const unsigned char *sha1, static int find_pack_entry(const unsigned char *sha1, struct pack_entry *e) { - static struct packed_git *last_found = (void *)1; struct packed_git *p; prepare_packed_git(); if (!packed_git) return 0; - p = (last_found == (void *)1) ? packed_git : last_found; - do { - if (fill_pack_entry(sha1, e, p)) { - last_found = p; - return 1; - } + if (last_found_pack && fill_pack_entry(sha1, e, last_found_pack)) + return 1; - if (p == last_found) - p = packed_git; - else - p = p->next; - if (p == last_found) - p = p->next; - } while (p); + for (p = packed_git; p; p = p->next) { + if (p == last_found_pack || !fill_pack_entry(sha1, e, p)) + continue; + + last_found_pack = p; + return 1; + } return 0; } -- cgit v1.2.1 From f3fb07509c2e0b21b12a598fcd0a19a92fc38a9d Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 31 Jan 2012 22:31:35 -0800 Subject: Update draft release notes to 1.7.10 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/1.7.10.txt | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Documentation/RelNotes/1.7.10.txt b/Documentation/RelNotes/1.7.10.txt index cc222819a0..e1d70bd27f 100644 --- a/Documentation/RelNotes/1.7.10.txt +++ b/Documentation/RelNotes/1.7.10.txt @@ -14,10 +14,18 @@ UI, Workflows & Features * "git clone" learned "--single-branch" option to limit cloning to a single branch (surprise!). + * "git clone" learned to detach the HEAD in the resulting repository + when the source repository's HEAD does not point to a branch. + * When showing a patch while ignoring whitespace changes, the context lines are taken from the postimage, in order to make it easier to view the output. + * "git merge" in an interactive session learned to spawn the editor + by default to let the user edit the auto-generated merge message, + to encourage people to explain their merges better. Legacy scripts + can export MERGE_AUTOEDIT=no to retain the historical behaviour. + Performance * During "git upload-pack" in respose to "git fetch", unnecessary calls @@ -34,6 +42,9 @@ Internal Implementation * The test suite supports the new "test_pause" helper function. + * t/Makefile is adjusted to prevent newer versions of GNU make from + running tests in seemingly random order. + Also contains minor documentation updates and code clean-ups. @@ -60,9 +71,25 @@ details). to completion even when the top-level process was killed. (merge 10c6cdd cb/maint-kill-subprocess-upon-signal later to maint). + * "git mergetool" now gives an empty file as the common base version + to the backend when dealing with the "both sides added, differently" + case. + (merge ec245ba da/maint-mergetool-twoway later to maint). + + * "git submodule add $path" forgot to recompute the name to be stored + in .gitmodules when the submodule at $path was once added to the + superproject and already initialized. + (merge 1017c1a jl/submodule-re-add later to maint). + + * Using "git grep -l/-L" together with options -W or --break may not + make much sense as the output is to only count the number of hits + and there is no place for file breaks, but the latter options made + "-l/-L" to miscount the hits. + (merge 50dd0f2 tr/grep-l-with-decoration later to maint). + --- exec >/var/tmp/1 -O=v1.7.9 +O=v1.7.9-110-g873ce7c echo O=$(git describe) git log --first-parent --oneline ^maint $O.. echo -- cgit v1.2.1 From be39de2b262bfcd440ed81b4de33711eb8a37cf4 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Thu, 2 Feb 2012 01:20:30 +0800 Subject: i18n: git-commit whence_s "merge/cherry-pick" message Mark the "merge/cherry-pick" messages in whence_s for translation. These messages returned from whence_s function are used as argument to build other messages. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/commit.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index eba1377eb3..470b4a4e8c 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -196,16 +196,16 @@ static void determine_whence(struct wt_status *s) static const char *whence_s(void) { - char *s = ""; + const char *s = ""; switch (whence) { case FROM_COMMIT: break; case FROM_MERGE: - s = "merge"; + s = _("merge"); break; case FROM_CHERRY_PICK: - s = "cherry-pick"; + s = _("cherry-pick"); break; } -- cgit v1.2.1 From 8a5b749428d39a0e85fb0f76306b88945cb5c6e0 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Thu, 2 Feb 2012 10:02:23 +0800 Subject: i18n: format_tracking_info "Your branch is behind" message Function format_tracking_info in remote.c is called by wt_status_print_tracking in wt-status.c, which will print branch tracking message in git-status. git-checkout also show these messages through it's report_tracking function. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- remote.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/remote.c b/remote.c index 73a3809300..af597b3a62 100644 --- a/remote.c +++ b/remote.c @@ -1572,19 +1572,29 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb) base = branch->merge[0]->dst; base = shorten_unambiguous_ref(base, 0); if (!num_theirs) - strbuf_addf(sb, "Your branch is ahead of '%s' " - "by %d commit%s.\n", - base, num_ours, (num_ours == 1) ? "" : "s"); + strbuf_addf(sb, + Q_("Your branch is ahead of '%s' by %d commit.\n", + "Your branch is ahead of '%s' by %d commits.\n", + num_ours), + base, num_ours); else if (!num_ours) - strbuf_addf(sb, "Your branch is behind '%s' " - "by %d commit%s, " - "and can be fast-forwarded.\n", - base, num_theirs, (num_theirs == 1) ? "" : "s"); + strbuf_addf(sb, + Q_("Your branch is behind '%s' by %d commit, " + "and can be fast-forwarded.\n", + "Your branch is behind '%s' by %d commits, " + "and can be fast-forwarded.\n", + num_theirs), + base, num_theirs); else - strbuf_addf(sb, "Your branch and '%s' have diverged,\n" - "and have %d and %d different commit(s) each, " - "respectively.\n", - base, num_ours, num_theirs); + strbuf_addf(sb, + Q_("Your branch and '%s' have diverged,\n" + "and have %d and %d different commit each, " + "respectively.\n", + "Your branch and '%s' have diverged,\n" + "and have %d and %d different commits each, " + "respectively.\n", + num_theirs), + base, num_ours, num_theirs); return 1; } -- cgit v1.2.1 From ce8ebcdaf31dd56955635808bba1a6df50601563 Mon Sep 17 00:00:00 2001 From: Ramsay Jones Date: Thu, 2 Feb 2012 03:57:23 -0600 Subject: vcs-svn: rename check_overflow and its arguments for clarity The canonical interpretation of a range a,b is as an interval [a,b), not [a,a+b), so this function taking argument names a and b feels unnatural. Use more explicit names "offset" and "len" to make the arguments' type and function clearer. While at it, rename the function to convey that we are making sure the sum of this offset and length do not overflow an off_t, not a size_t. [jn: split out from a patch from Ramsay Jones, then improved with advice from Thomas Rast, Dmitry Ivankov, and David Barr] Signed-off-by: Jonathan Nieder Improved-by: Dmitry Ivankov --- vcs-svn/sliding_window.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vcs-svn/sliding_window.c b/vcs-svn/sliding_window.c index 1bac7a4c7f..c6c2effd30 100644 --- a/vcs-svn/sliding_window.c +++ b/vcs-svn/sliding_window.c @@ -31,15 +31,15 @@ static int read_to_fill_or_whine(struct line_buffer *file, return 0; } -static int check_overflow(off_t a, size_t b) +static int check_offset_overflow(off_t offset, size_t len) { - if (b > maximum_signed_value_of_type(off_t)) + if (len > maximum_signed_value_of_type(off_t)) return error("unrepresentable length in delta: " - "%"PRIuMAX" > OFF_MAX", (uintmax_t) b); - if (signed_add_overflows(a, (off_t) b)) + "%"PRIuMAX" > OFF_MAX", (uintmax_t) len); + if (signed_add_overflows(offset, (off_t) len)) return error("unrepresentable offset in delta: " "%"PRIuMAX" + %"PRIuMAX" > OFF_MAX", - (uintmax_t) a, (uintmax_t) b); + (uintmax_t) offset, (uintmax_t) len); return 0; } @@ -48,9 +48,9 @@ int move_window(struct sliding_view *view, off_t off, size_t width) off_t file_offset; assert(view); assert(view->width <= view->buf.len); - assert(!check_overflow(view->off, view->buf.len)); + assert(!check_offset_overflow(view->off, view->buf.len)); - if (check_overflow(off, width)) + if (check_offset_overflow(off, width)) return -1; if (off < view->off || off + width < view->off + view->width) return error("invalid delta: window slides left"); -- cgit v1.2.1 From 2d54b9ea8b99df7b448f64a7847f7f3879964e12 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Thu, 2 Feb 2012 04:19:35 -0600 Subject: vcs-svn: allow import of > 4GiB files There is no reason in principle that an svn-format dump would not be able to represent a file whose length does not fit in a 32-bit integer. Use off_t consistently (instead of uint32_t) to represent file lengths so we can handle that. Most of our code is already ready to do that without this patch and already passes values of type off_t around. The type mismatch due to stragglers was noticed with gcc -Wtype-limits. Inspired-by: Ramsay Jones Signed-off-by: Jonathan Nieder --- vcs-svn/fast_export.c | 15 +++++++++------ vcs-svn/fast_export.h | 4 ++-- vcs-svn/svndump.c | 21 +++++++++++++++------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 19d7c34c25..b823b8519c 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -227,15 +227,18 @@ static long apply_delta(off_t len, struct line_buffer *input, return ret; } -void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input) +void fast_export_data(uint32_t mode, off_t len, struct line_buffer *input) { + assert(len >= 0); if (mode == REPO_MODE_LNK) { /* svn symlink blobs start with "link " */ + if (len < 5) + die("invalid dump: symlink too short for \"link\" prefix"); len -= 5; if (buffer_skip_bytes(input, 5) != 5) die_short_read(input); } - printf("data %"PRIu32"\n", len); + printf("data %"PRIuMAX"\n", (uintmax_t) len); if (buffer_copy_bytes(input, len) != len) die_short_read(input); fputc('\n', stdout); @@ -297,12 +300,12 @@ int fast_export_ls(const char *path, uint32_t *mode, struct strbuf *dataref) void fast_export_blob_delta(uint32_t mode, uint32_t old_mode, const char *old_data, - uint32_t len, struct line_buffer *input) + off_t len, struct line_buffer *input) { long postimage_len; - if (len > maximum_signed_value_of_type(off_t)) - die("enormous delta"); - postimage_len = apply_delta((off_t) len, input, old_data, old_mode); + + assert(len >= 0); + postimage_len = apply_delta(len, input, old_data, old_mode); if (mode == REPO_MODE_LNK) { buffer_skip_bytes(&postimage, strlen("link ")); postimage_len -= strlen("link "); diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index 43d05b65ef..aa629f54ff 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -14,10 +14,10 @@ void fast_export_begin_commit(uint32_t revision, const char *author, const struct strbuf *log, const char *uuid, const char *url, unsigned long timestamp); void fast_export_end_commit(uint32_t revision); -void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input); +void fast_export_data(uint32_t mode, off_t len, struct line_buffer *input); void fast_export_blob_delta(uint32_t mode, uint32_t old_mode, const char *old_data, - uint32_t len, struct line_buffer *input); + off_t len, struct line_buffer *input); /* If there is no such file at that rev, returns -1, errno == ENOENT. */ int fast_export_ls_rev(uint32_t rev, const char *path, diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index ca63760fe2..644fdc71ba 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -40,7 +40,8 @@ static struct line_buffer input = LINE_BUFFER_INIT; static struct { - uint32_t action, propLength, textLength, srcRev, type; + uint32_t action, propLength, srcRev, type; + off_t text_length; struct strbuf src, dst; uint32_t text_delta, prop_delta; } node_ctx; @@ -61,7 +62,7 @@ static void reset_node_ctx(char *fname) node_ctx.type = 0; node_ctx.action = NODEACT_UNKNOWN; node_ctx.propLength = LENGTH_UNKNOWN; - node_ctx.textLength = LENGTH_UNKNOWN; + node_ctx.text_length = -1; strbuf_reset(&node_ctx.src); node_ctx.srcRev = 0; strbuf_reset(&node_ctx.dst); @@ -209,7 +210,7 @@ static void handle_node(void) { const uint32_t type = node_ctx.type; const int have_props = node_ctx.propLength != LENGTH_UNKNOWN; - const int have_text = node_ctx.textLength != LENGTH_UNKNOWN; + const int have_text = node_ctx.text_length != -1; /* * Old text for this node: * NULL - directory or bug @@ -291,12 +292,12 @@ static void handle_node(void) } if (!node_ctx.text_delta) { fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); - fast_export_data(node_ctx.type, node_ctx.textLength, &input); + fast_export_data(node_ctx.type, node_ctx.text_length, &input); return; } fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); fast_export_blob_delta(node_ctx.type, old_mode, old_data, - node_ctx.textLength, &input); + node_ctx.text_length, &input); } static void begin_revision(void) @@ -409,7 +410,15 @@ void svndump_read(const char *url) break; case sizeof("Text-content-length"): if (!constcmp(t, "Text-content-length")) { - node_ctx.textLength = atoi(val); + char *end; + uintmax_t textlen; + + textlen = strtoumax(val, &end, 10); + if (!isdigit(*val) || *end) + die("invalid dump: non-numeric length %s", val); + if (textlen > maximum_signed_value_of_type(off_t)) + die("unrepresentable length in dump: %s", val); + node_ctx.text_length = (off_t) textlen; break; } if (constcmp(t, "Prop-content-length")) -- cgit v1.2.1 From 5b8bf02930674a5c2cba42d919ba3c0c96bf58e4 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Thu, 2 Feb 2012 04:28:02 -0600 Subject: vcs-svn: suppress -Wtype-limits warning On 32-bit architectures with 64-bit file offsets, gcc 4.3 and earlier produce the following warning: CC vcs-svn/sliding_window.o vcs-svn/sliding_window.c: In function `check_overflow': vcs-svn/sliding_window.c:36: warning: comparison is always false \ due to limited range of data type The warning appears even when gcc is run without any warning flags (PR12963). In later versions it can be reproduced with -Wtype-limits, which is implied by -Wextra. On 64-bit architectures it really is possible for a size_t not to be representable as an off_t so the check being warned about is not actually redundant. But even false positives are distracting. Avoid the warning by making the "len" argument to check_overflow a uintmax_t; no functional change intended. Reported-by: Ramsay Jones Signed-off-by: Jonathan Nieder --- vcs-svn/sliding_window.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vcs-svn/sliding_window.c b/vcs-svn/sliding_window.c index c6c2effd30..ec2707c9c4 100644 --- a/vcs-svn/sliding_window.c +++ b/vcs-svn/sliding_window.c @@ -31,15 +31,15 @@ static int read_to_fill_or_whine(struct line_buffer *file, return 0; } -static int check_offset_overflow(off_t offset, size_t len) +static int check_offset_overflow(off_t offset, uintmax_t len) { if (len > maximum_signed_value_of_type(off_t)) return error("unrepresentable length in delta: " - "%"PRIuMAX" > OFF_MAX", (uintmax_t) len); + "%"PRIuMAX" > OFF_MAX", len); if (signed_add_overflows(offset, (off_t) len)) return error("unrepresentable offset in delta: " "%"PRIuMAX" + %"PRIuMAX" > OFF_MAX", - (uintmax_t) offset, (uintmax_t) len); + (uintmax_t) offset, len); return 0; } -- cgit v1.2.1 From 78db6ea9dc1a872f9d07a36fe7aec700a5c963b9 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:18:29 -0500 Subject: grep: make locking flag global The low-level grep code traditionally didn't care about threading, as it doesn't do any threading itself and didn't call out to other non-thread-safe code. That changed with 0579f91 (grep: enable threading with -p and -W using lazy attribute lookup, 2011-12-12), which pushed the lookup of funcname attributes (which is not thread-safe) into the low-level grep code. As a result, the low-level code learned about a new global "grep_attr_mutex" to serialize access to the attribute code. A multi-threaded caller (e.g., builtin/grep.c) is expected to initialize the mutex and set "use_threads" in the grep_opt structure. The low-level code only uses the lock if use_threads is set. However, putting the use_threads flag into the grep_opt struct is not the most logical place. Whether threading is in use is not something that matters for each call to grep_buffer, but is instead global to the whole program (i.e., if any thread is doing multi-threaded grep, every other thread, even if it thinks it is doing its own single-threaded grep, would need to use the locking). In practice, this distinction isn't a problem for us, because the only user of multi-threaded grep is "git-grep", which does nothing except call grep. This patch turns the opt->use_threads flag into a global flag. More important than the nit-picking semantic argument above is that this means that the locking functions don't need to actually have access to a grep_opt to know whether to lock. Which in turn can make adding new locks simpler, as we don't need to pass around a grep_opt. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/grep.c | 4 ++-- grep.c | 18 ++++++++++-------- grep.h | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index 9ce064ac11..5b881870f6 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -259,6 +259,7 @@ static void start_threads(struct grep_opt *opt) pthread_cond_init(&cond_add, NULL); pthread_cond_init(&cond_write, NULL); pthread_cond_init(&cond_result, NULL); + grep_use_locks = 1; for (i = 0; i < ARRAY_SIZE(todo); i++) { strbuf_init(&todo[i].out, 0); @@ -307,6 +308,7 @@ static int wait_all(void) pthread_cond_destroy(&cond_add); pthread_cond_destroy(&cond_write); pthread_cond_destroy(&cond_result); + grep_use_locks = 0; return hit; } @@ -1030,8 +1032,6 @@ int cmd_grep(int argc, const char **argv, const char *prefix) use_threads = 0; #endif - opt.use_threads = use_threads; - #ifndef NO_PTHREADS if (use_threads) { if (opt.pre_context || opt.post_context || opt.file_break || diff --git a/grep.c b/grep.c index 486230b511..7a67c2ff6f 100644 --- a/grep.c +++ b/grep.c @@ -807,26 +807,28 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol, } #ifndef NO_PTHREADS +int grep_use_locks; + /* * This lock protects access to the gitattributes machinery, which is * not thread-safe. */ pthread_mutex_t grep_attr_mutex; -static inline void grep_attr_lock(struct grep_opt *opt) +static inline void grep_attr_lock(void) { - if (opt->use_threads) + if (grep_use_locks) pthread_mutex_lock(&grep_attr_mutex); } -static inline void grep_attr_unlock(struct grep_opt *opt) +static inline void grep_attr_unlock(void) { - if (opt->use_threads) + if (grep_use_locks) pthread_mutex_unlock(&grep_attr_mutex); } #else -#define grep_attr_lock(opt) -#define grep_attr_unlock(opt) +#define grep_attr_lock() +#define grep_attr_unlock() #endif static int match_funcname(struct grep_opt *opt, const char *name, char *bol, char *eol) @@ -834,9 +836,9 @@ static int match_funcname(struct grep_opt *opt, const char *name, char *bol, cha xdemitconf_t *xecfg = opt->priv; if (xecfg && !xecfg->find_func) { struct userdiff_driver *drv; - grep_attr_lock(opt); + grep_attr_lock(); drv = userdiff_find_by_path(name); - grep_attr_unlock(opt); + grep_attr_unlock(); if (drv && drv->funcname.pattern) { const struct userdiff_funcname *pe = &drv->funcname; xdiff_set_find_func(xecfg, pe->pattern, pe->cflags); diff --git a/grep.h b/grep.h index fb205f3542..3653bb333c 100644 --- a/grep.h +++ b/grep.h @@ -116,7 +116,6 @@ struct grep_opt { int show_hunk_mark; int file_break; int heading; - int use_threads; void *priv; void (*output)(struct grep_opt *opt, const void *data, size_t size); @@ -138,6 +137,7 @@ extern int grep_threads_ok(const struct grep_opt *opt); * Mutex used around access to the attributes machinery if * opt->use_threads. Must be initialized/destroyed by callers! */ +extern int grep_use_locks; extern pthread_mutex_t grep_attr_mutex; #endif -- cgit v1.2.1 From b3aeb285d0ac1dcb4d578a61a68e08646f96501f Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:18:41 -0500 Subject: grep: move sha1-reading mutex into low-level code The multi-threaded git-grep code needs to serialize access to the thread-unsafe read_sha1_file call. It does this with a mutex that is local to builtin/grep.c. Let's instead push this down into grep.c, where it can be used by both builtin/grep.c and grep.c. This will let us safely teach the low-level grep.c code tricks that involve reading from the object db. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/grep.c | 29 ++++++----------------------- grep.c | 6 ++++++ grep.h | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index 5b881870f6..aeb361663a 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -85,21 +85,6 @@ static inline void grep_unlock(void) pthread_mutex_unlock(&grep_mutex); } -/* Used to serialize calls to read_sha1_file. */ -static pthread_mutex_t read_sha1_mutex; - -static inline void read_sha1_lock(void) -{ - if (use_threads) - pthread_mutex_lock(&read_sha1_mutex); -} - -static inline void read_sha1_unlock(void) -{ - if (use_threads) - pthread_mutex_unlock(&read_sha1_mutex); -} - /* Signalled when a new work_item is added to todo. */ static pthread_cond_t cond_add; @@ -254,7 +239,7 @@ static void start_threads(struct grep_opt *opt) int i; pthread_mutex_init(&grep_mutex, NULL); - pthread_mutex_init(&read_sha1_mutex, NULL); + pthread_mutex_init(&grep_read_mutex, NULL); pthread_mutex_init(&grep_attr_mutex, NULL); pthread_cond_init(&cond_add, NULL); pthread_cond_init(&cond_write, NULL); @@ -303,7 +288,7 @@ static int wait_all(void) } pthread_mutex_destroy(&grep_mutex); - pthread_mutex_destroy(&read_sha1_mutex); + pthread_mutex_destroy(&grep_read_mutex); pthread_mutex_destroy(&grep_attr_mutex); pthread_cond_destroy(&cond_add); pthread_cond_destroy(&cond_write); @@ -313,8 +298,6 @@ static int wait_all(void) return hit; } #else /* !NO_PTHREADS */ -#define read_sha1_lock() -#define read_sha1_unlock() static int wait_all(void) { @@ -376,9 +359,9 @@ static void *lock_and_read_sha1_file(const unsigned char *sha1, enum object_type { void *data; - read_sha1_lock(); + grep_read_lock(); data = read_sha1_file(sha1, type, size); - read_sha1_unlock(); + grep_read_unlock(); return data; } @@ -617,10 +600,10 @@ static int grep_object(struct grep_opt *opt, const struct pathspec *pathspec, struct strbuf base; int hit, len; - read_sha1_lock(); + grep_read_lock(); data = read_object_with_reference(obj->sha1, tree_type, &size, NULL); - read_sha1_unlock(); + grep_read_unlock(); if (!data) die(_("unable to read tree (%s)"), sha1_to_hex(obj->sha1)); diff --git a/grep.c b/grep.c index 7a67c2ff6f..db58a29c2b 100644 --- a/grep.c +++ b/grep.c @@ -826,6 +826,12 @@ static inline void grep_attr_unlock(void) if (grep_use_locks) pthread_mutex_unlock(&grep_attr_mutex); } + +/* + * Same as git_attr_mutex, but protecting the thread-unsafe object db access. + */ +pthread_mutex_t grep_read_mutex; + #else #define grep_attr_lock() #define grep_attr_unlock() diff --git a/grep.h b/grep.h index 3653bb333c..4f1b0251b0 100644 --- a/grep.h +++ b/grep.h @@ -139,6 +139,23 @@ extern int grep_threads_ok(const struct grep_opt *opt); */ extern int grep_use_locks; extern pthread_mutex_t grep_attr_mutex; +extern pthread_mutex_t grep_read_mutex; + +static inline void grep_read_lock(void) +{ + if (grep_use_locks) + pthread_mutex_lock(&grep_read_mutex); +} + +static inline void grep_read_unlock(void) +{ + if (grep_use_locks) + pthread_mutex_unlock(&grep_read_mutex); +} + +#else +#define grep_read_lock() +#define grep_read_unlock() #endif #endif -- cgit v1.2.1 From e1327023ea22c3bf57e7d28596da356043f073fc Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:19:28 -0500 Subject: grep: refactor the concept of "grep source" into an object The main interface to the low-level grep code is grep_buffer, which takes a pointer to a buffer and a size. This is convenient and flexible (we use it to grep commit bodies, files on disk, and blobs by sha1), but it makes it hard to pass extra information about what we are grepping (either for correctness, like overriding binary auto-detection, or for optimizations, like lazily loading blob contents). Instead, let's encapsulate the idea of a "grep source", including the buffer, its size, and where the data is coming from. This is similar to the diff_filespec structure used by the diff code (unsurprising, since future patches will implement some of the same optimizations found there). The diffstat is slightly scarier than the actual patch content. Most of the modified lines are simply replacing access to raw variables with their counterparts that are now in a "struct grep_source". Most of the added lines were taken from builtin/grep.c, which partially abstracted the idea of grep sources (for file vs sha1 sources). Instead of dropping the now-redundant code, this patch leaves builtin/grep.c using the traditional grep_buffer interface (which now wraps the grep_source interface). That makes it easy to test that there is no change of behavior (yet). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- grep.c | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- grep.h | 22 ++++++++ 2 files changed, 186 insertions(+), 34 deletions(-) diff --git a/grep.c b/grep.c index db58a29c2b..8204ca25e5 100644 --- a/grep.c +++ b/grep.c @@ -837,13 +837,13 @@ pthread_mutex_t grep_read_mutex; #define grep_attr_unlock() #endif -static int match_funcname(struct grep_opt *opt, const char *name, char *bol, char *eol) +static int match_funcname(struct grep_opt *opt, struct grep_source *gs, char *bol, char *eol) { xdemitconf_t *xecfg = opt->priv; if (xecfg && !xecfg->find_func) { struct userdiff_driver *drv; grep_attr_lock(); - drv = userdiff_find_by_path(name); + drv = userdiff_find_by_path(gs->name); grep_attr_unlock(); if (drv && drv->funcname.pattern) { const struct userdiff_funcname *pe = &drv->funcname; @@ -866,33 +866,33 @@ static int match_funcname(struct grep_opt *opt, const char *name, char *bol, cha return 0; } -static void show_funcname_line(struct grep_opt *opt, const char *name, - char *buf, char *bol, unsigned lno) +static void show_funcname_line(struct grep_opt *opt, struct grep_source *gs, + char *bol, unsigned lno) { - while (bol > buf) { + while (bol > gs->buf) { char *eol = --bol; - while (bol > buf && bol[-1] != '\n') + while (bol > gs->buf && bol[-1] != '\n') bol--; lno--; if (lno <= opt->last_shown) break; - if (match_funcname(opt, name, bol, eol)) { - show_line(opt, bol, eol, name, lno, '='); + if (match_funcname(opt, gs, bol, eol)) { + show_line(opt, bol, eol, gs->name, lno, '='); break; } } } -static void show_pre_context(struct grep_opt *opt, const char *name, char *buf, +static void show_pre_context(struct grep_opt *opt, struct grep_source *gs, char *bol, char *end, unsigned lno) { unsigned cur = lno, from = 1, funcname_lno = 0; int funcname_needed = !!opt->funcname; - if (opt->funcbody && !match_funcname(opt, name, bol, end)) + if (opt->funcbody && !match_funcname(opt, gs, bol, end)) funcname_needed = 2; if (opt->pre_context < lno) @@ -901,14 +901,14 @@ static void show_pre_context(struct grep_opt *opt, const char *name, char *buf, from = opt->last_shown + 1; /* Rewind. */ - while (bol > buf && + while (bol > gs->buf && cur > (funcname_needed == 2 ? opt->last_shown + 1 : from)) { char *eol = --bol; - while (bol > buf && bol[-1] != '\n') + while (bol > gs->buf && bol[-1] != '\n') bol--; cur--; - if (funcname_needed && match_funcname(opt, name, bol, eol)) { + if (funcname_needed && match_funcname(opt, gs, bol, eol)) { funcname_lno = cur; funcname_needed = 0; } @@ -916,7 +916,7 @@ static void show_pre_context(struct grep_opt *opt, const char *name, char *buf, /* We need to look even further back to find a function signature. */ if (opt->funcname && funcname_needed) - show_funcname_line(opt, name, buf, bol, cur); + show_funcname_line(opt, gs, bol, cur); /* Back forward. */ while (cur < lno) { @@ -924,7 +924,7 @@ static void show_pre_context(struct grep_opt *opt, const char *name, char *buf, while (*eol != '\n') eol++; - show_line(opt, bol, eol, name, cur, sign); + show_line(opt, bol, eol, gs->name, cur, sign); bol = eol + 1; cur++; } @@ -991,11 +991,10 @@ static void std_output(struct grep_opt *opt, const void *buf, size_t size) fwrite(buf, size, 1, stdout); } -static int grep_buffer_1(struct grep_opt *opt, const char *name, - char *buf, unsigned long size, int collect_hits) +static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int collect_hits) { - char *bol = buf; - unsigned long left = size; + char *bol; + unsigned long left; unsigned lno = 1; unsigned last_hit = 0; int binary_match_only = 0; @@ -1023,13 +1022,16 @@ static int grep_buffer_1(struct grep_opt *opt, const char *name, } opt->last_shown = 0; + if (grep_source_load(gs) < 0) + return 0; + switch (opt->binary) { case GREP_BINARY_DEFAULT: - if (buffer_is_binary(buf, size)) + if (buffer_is_binary(gs->buf, gs->size)) binary_match_only = 1; break; case GREP_BINARY_NOMATCH: - if (buffer_is_binary(buf, size)) + if (buffer_is_binary(gs->buf, gs->size)) return 0; /* Assume unmatch */ break; case GREP_BINARY_TEXT: @@ -1043,6 +1045,8 @@ static int grep_buffer_1(struct grep_opt *opt, const char *name, try_lookahead = should_lookahead(opt); + bol = gs->buf; + left = gs->size; while (left) { char *eol, ch; int hit; @@ -1091,14 +1095,14 @@ static int grep_buffer_1(struct grep_opt *opt, const char *name, if (opt->status_only) return 1; if (opt->name_only) { - show_name(opt, name); + show_name(opt, gs->name); return 1; } if (opt->count) goto next_line; if (binary_match_only) { opt->output(opt, "Binary file ", 12); - output_color(opt, name, strlen(name), + output_color(opt, gs->name, strlen(gs->name), opt->color_filename); opt->output(opt, " matches\n", 9); return 1; @@ -1107,23 +1111,23 @@ static int grep_buffer_1(struct grep_opt *opt, const char *name, * pre-context lines, we would need to show them. */ if (opt->pre_context || opt->funcbody) - show_pre_context(opt, name, buf, bol, eol, lno); + show_pre_context(opt, gs, bol, eol, lno); else if (opt->funcname) - show_funcname_line(opt, name, buf, bol, lno); - show_line(opt, bol, eol, name, lno, ':'); + show_funcname_line(opt, gs, bol, lno); + show_line(opt, bol, eol, gs->name, lno, ':'); last_hit = lno; if (opt->funcbody) show_function = 1; goto next_line; } - if (show_function && match_funcname(opt, name, bol, eol)) + if (show_function && match_funcname(opt, gs, bol, eol)) show_function = 0; if (show_function || (last_hit && lno <= last_hit + opt->post_context)) { /* If the last hit is within the post context, * we need to show this line. */ - show_line(opt, bol, eol, name, lno, '-'); + show_line(opt, bol, eol, gs->name, lno, '-'); } next_line: @@ -1141,7 +1145,7 @@ static int grep_buffer_1(struct grep_opt *opt, const char *name, return 0; if (opt->unmatch_name_only) { /* We did not see any hit, so we want to show this */ - show_name(opt, name); + show_name(opt, gs->name); return 1; } @@ -1155,7 +1159,7 @@ static int grep_buffer_1(struct grep_opt *opt, const char *name, */ if (opt->count && count) { char buf[32]; - output_color(opt, name, strlen(name), opt->color_filename); + output_color(opt, gs->name, strlen(gs->name), opt->color_filename); output_sep(opt, ':'); snprintf(buf, sizeof(buf), "%u\n", count); opt->output(opt, buf, strlen(buf)); @@ -1190,23 +1194,149 @@ static int chk_hit_marker(struct grep_expr *x) } } -int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size) +int grep_source(struct grep_opt *opt, struct grep_source *gs) { /* * we do not have to do the two-pass grep when we do not check * buffer-wide "all-match". */ if (!opt->all_match) - return grep_buffer_1(opt, name, buf, size, 0); + return grep_source_1(opt, gs, 0); /* Otherwise the toplevel "or" terms hit a bit differently. * We first clear hit markers from them. */ clr_hit_marker(opt->pattern_expression); - grep_buffer_1(opt, name, buf, size, 1); + grep_source_1(opt, gs, 1); if (!chk_hit_marker(opt->pattern_expression)) return 0; - return grep_buffer_1(opt, name, buf, size, 0); + return grep_source_1(opt, gs, 0); +} + +int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size) +{ + struct grep_source gs; + int r; + + grep_source_init(&gs, GREP_SOURCE_BUF, name, NULL); + gs.buf = buf; + gs.size = size; + + r = grep_source(opt, &gs); + + grep_source_clear(&gs); + return r; +} + +void grep_source_init(struct grep_source *gs, enum grep_source_type type, + const char *name, const void *identifier) +{ + gs->type = type; + gs->name = name ? xstrdup(name) : NULL; + gs->buf = NULL; + gs->size = 0; + + switch (type) { + case GREP_SOURCE_FILE: + gs->identifier = xstrdup(identifier); + break; + case GREP_SOURCE_SHA1: + gs->identifier = xmalloc(20); + memcpy(gs->identifier, identifier, 20); + break; + case GREP_SOURCE_BUF: + gs->identifier = NULL; + } +} + +void grep_source_clear(struct grep_source *gs) +{ + free(gs->name); + gs->name = NULL; + free(gs->identifier); + gs->identifier = NULL; + grep_source_clear_data(gs); +} + +void grep_source_clear_data(struct grep_source *gs) +{ + switch (gs->type) { + case GREP_SOURCE_FILE: + case GREP_SOURCE_SHA1: + free(gs->buf); + gs->buf = NULL; + gs->size = 0; + break; + case GREP_SOURCE_BUF: + /* leave user-provided buf intact */ + break; + } +} + +static int grep_source_load_sha1(struct grep_source *gs) +{ + enum object_type type; + + grep_read_lock(); + gs->buf = read_sha1_file(gs->identifier, &type, &gs->size); + grep_read_unlock(); + + if (!gs->buf) + return error(_("'%s': unable to read %s"), + gs->name, + sha1_to_hex(gs->identifier)); + return 0; +} + +static int grep_source_load_file(struct grep_source *gs) +{ + const char *filename = gs->identifier; + struct stat st; + char *data; + size_t size; + int i; + + if (lstat(filename, &st) < 0) { + err_ret: + if (errno != ENOENT) + error(_("'%s': %s"), filename, strerror(errno)); + return -1; + } + if (!S_ISREG(st.st_mode)) + return -1; + size = xsize_t(st.st_size); + i = open(filename, O_RDONLY); + if (i < 0) + goto err_ret; + data = xmalloc(size + 1); + if (st.st_size != read_in_full(i, data, size)) { + error(_("'%s': short read %s"), filename, strerror(errno)); + close(i); + free(data); + return -1; + } + close(i); + data[size] = 0; + + gs->buf = data; + gs->size = size; + return 0; +} + +int grep_source_load(struct grep_source *gs) +{ + if (gs->buf) + return 0; + + switch (gs->type) { + case GREP_SOURCE_FILE: + return grep_source_load_file(gs); + case GREP_SOURCE_SHA1: + return grep_source_load_sha1(gs); + case GREP_SOURCE_BUF: + return gs->buf ? 0 : -1; + } + die("BUG: invalid grep_source type"); } diff --git a/grep.h b/grep.h index 4f1b0251b0..e386ca4c73 100644 --- a/grep.h +++ b/grep.h @@ -129,6 +129,28 @@ extern void compile_grep_patterns(struct grep_opt *opt); extern void free_grep_patterns(struct grep_opt *opt); extern int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size); +struct grep_source { + char *name; + + enum grep_source_type { + GREP_SOURCE_SHA1, + GREP_SOURCE_FILE, + GREP_SOURCE_BUF, + } type; + void *identifier; + + char *buf; + unsigned long size; +}; + +void grep_source_init(struct grep_source *gs, enum grep_source_type type, + const char *name, const void *identifier); +int grep_source_load(struct grep_source *gs); +void grep_source_clear_data(struct grep_source *gs); +void grep_source_clear(struct grep_source *gs); + +int grep_source(struct grep_opt *opt, struct grep_source *gs); + extern struct grep_opt *grep_opt_dup(const struct grep_opt *opt); extern int grep_threads_ok(const struct grep_opt *opt); -- cgit v1.2.1 From 8f24a6323ece9be1bf1a04b4b5856112438337f2 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:19:37 -0500 Subject: convert git-grep to use grep_source interface The grep_source interface (as opposed to grep_buffer) will eventually gives us a richer interface for telling the low-level grep code about our buffers. Eventually this will lead to things like better binary-file handling. For now, it lets us drop a lot of now-redundant code. The conversion is mostly straight-forward. One thing to note is that the memory ownership rules for "struct grep_source" are different than the "struct work_item" found here (the former will copy things like the filename, rather than taking ownership). Therefore you will also see some slight tweaking of when filename buffers are released. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/grep.c | 142 ++++++++++----------------------------------------------- 1 file changed, 23 insertions(+), 119 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index aeb361663a..c9b6a138fe 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -29,25 +29,12 @@ static int use_threads = 1; #define THREADS 8 static pthread_t threads[THREADS]; -static void *load_sha1(const unsigned char *sha1, unsigned long *size, - const char *name); -static void *load_file(const char *filename, size_t *sz); - -enum work_type {WORK_SHA1, WORK_FILE}; - /* We use one producer thread and THREADS consumer * threads. The producer adds struct work_items to 'todo' and the * consumers pick work items from the same array. */ struct work_item { - enum work_type type; - char *name; - - /* if type == WORK_SHA1, then 'identifier' is a SHA1, - * otherwise type == WORK_FILE, and 'identifier' is a NUL - * terminated filename. - */ - void *identifier; + struct grep_source source; char done; struct strbuf out; }; @@ -98,7 +85,8 @@ static pthread_cond_t cond_result; static int skip_first_line; -static void add_work(enum work_type type, char *name, void *id) +static void add_work(enum grep_source_type type, const char *name, + const void *id) { grep_lock(); @@ -106,9 +94,7 @@ static void add_work(enum work_type type, char *name, void *id) pthread_cond_wait(&cond_write, &grep_mutex); } - todo[todo_end].type = type; - todo[todo_end].name = name; - todo[todo_end].identifier = id; + grep_source_init(&todo[todo_end].source, type, name, id); todo[todo_end].done = 0; strbuf_reset(&todo[todo_end].out); todo_end = (todo_end + 1) % ARRAY_SIZE(todo); @@ -136,21 +122,6 @@ static struct work_item *get_work(void) return ret; } -static void grep_sha1_async(struct grep_opt *opt, char *name, - const unsigned char *sha1) -{ - unsigned char *s; - s = xmalloc(20); - memcpy(s, sha1, 20); - add_work(WORK_SHA1, name, s); -} - -static void grep_file_async(struct grep_opt *opt, char *name, - const char *filename) -{ - add_work(WORK_FILE, name, xstrdup(filename)); -} - static void work_done(struct work_item *w) { int old_done; @@ -177,8 +148,7 @@ static void work_done(struct work_item *w) write_or_die(1, p, len); } - free(w->name); - free(w->identifier); + grep_source_clear(&w->source); } if (old_done != todo_done) @@ -201,25 +171,8 @@ static void *run(void *arg) break; opt->output_priv = w; - if (w->type == WORK_SHA1) { - unsigned long sz; - void* data = load_sha1(w->identifier, &sz, w->name); - - if (data) { - hit |= grep_buffer(opt, w->name, data, sz); - free(data); - } - } else if (w->type == WORK_FILE) { - size_t sz; - void* data = load_file(w->identifier, &sz); - if (data) { - hit |= grep_buffer(opt, w->name, data, sz); - free(data); - } - } else { - assert(0); - } - + hit |= grep_source(opt, &w->source); + grep_source_clear_data(&w->source); work_done(w); } free_grep_patterns(arg); @@ -365,23 +318,10 @@ static void *lock_and_read_sha1_file(const unsigned char *sha1, enum object_type return data; } -static void *load_sha1(const unsigned char *sha1, unsigned long *size, - const char *name) -{ - enum object_type type; - void *data = lock_and_read_sha1_file(sha1, &type, size); - - if (!data) - error(_("'%s': unable to read %s"), name, sha1_to_hex(sha1)); - - return data; -} - static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, const char *filename, int tree_name_len) { struct strbuf pathbuf = STRBUF_INIT; - char *name; if (opt->relative && opt->prefix_length) { quote_path_relative(filename + tree_name_len, -1, &pathbuf, @@ -391,87 +331,51 @@ static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, strbuf_addstr(&pathbuf, filename); } - name = strbuf_detach(&pathbuf, NULL); - #ifndef NO_PTHREADS if (use_threads) { - grep_sha1_async(opt, name, sha1); + add_work(GREP_SOURCE_SHA1, pathbuf.buf, sha1); + strbuf_release(&pathbuf); return 0; } else #endif { + struct grep_source gs; int hit; - unsigned long sz; - void *data = load_sha1(sha1, &sz, name); - if (!data) - hit = 0; - else - hit = grep_buffer(opt, name, data, sz); - free(data); - free(name); - return hit; - } -} + grep_source_init(&gs, GREP_SOURCE_SHA1, pathbuf.buf, sha1); + strbuf_release(&pathbuf); + hit = grep_source(opt, &gs); -static void *load_file(const char *filename, size_t *sz) -{ - struct stat st; - char *data; - int i; - - if (lstat(filename, &st) < 0) { - err_ret: - if (errno != ENOENT) - error(_("'%s': %s"), filename, strerror(errno)); - return NULL; - } - if (!S_ISREG(st.st_mode)) - return NULL; - *sz = xsize_t(st.st_size); - i = open(filename, O_RDONLY); - if (i < 0) - goto err_ret; - data = xmalloc(*sz + 1); - if (st.st_size != read_in_full(i, data, *sz)) { - error(_("'%s': short read %s"), filename, strerror(errno)); - close(i); - free(data); - return NULL; + grep_source_clear(&gs); + return hit; } - close(i); - data[*sz] = 0; - return data; } static int grep_file(struct grep_opt *opt, const char *filename) { struct strbuf buf = STRBUF_INIT; - char *name; if (opt->relative && opt->prefix_length) quote_path_relative(filename, -1, &buf, opt->prefix); else strbuf_addstr(&buf, filename); - name = strbuf_detach(&buf, NULL); #ifndef NO_PTHREADS if (use_threads) { - grep_file_async(opt, name, filename); + add_work(GREP_SOURCE_FILE, buf.buf, filename); + strbuf_release(&buf); return 0; } else #endif { + struct grep_source gs; int hit; - size_t sz; - void *data = load_file(filename, &sz); - if (!data) - hit = 0; - else - hit = grep_buffer(opt, name, data, sz); - free(data); - free(name); + grep_source_init(&gs, GREP_SOURCE_FILE, buf.buf, filename); + strbuf_release(&buf); + hit = grep_source(opt, &gs); + + grep_source_clear(&gs); return hit; } } -- cgit v1.2.1 From c876d6da88d9bf1f3377d4eed66fc7705e31c30e Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:20:10 -0500 Subject: grep: drop grep_buffer's "name" parameter Before the grep_source interface existed, grep_buffer was used by two types of callers: 1. Ones which pulled a file into a buffer, and then wanted to supply the file's name for the output (i.e., git grep). 2. Ones which really just wanted to grep a buffer (i.e., git log --grep). Callers in set (1) should now be using grep_source. Callers in set (2) always pass NULL for the "name" parameter of grep_buffer. We can therefore get rid of this now-useless parameter. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- grep.c | 4 ++-- grep.h | 2 +- revision.c | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/grep.c b/grep.c index 8204ca25e5..2a3fe7ce6f 100644 --- a/grep.c +++ b/grep.c @@ -1215,12 +1215,12 @@ int grep_source(struct grep_opt *opt, struct grep_source *gs) return grep_source_1(opt, gs, 0); } -int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size) +int grep_buffer(struct grep_opt *opt, char *buf, unsigned long size) { struct grep_source gs; int r; - grep_source_init(&gs, GREP_SOURCE_BUF, name, NULL); + grep_source_init(&gs, GREP_SOURCE_BUF, NULL, NULL); gs.buf = buf; gs.size = size; diff --git a/grep.h b/grep.h index e386ca4c73..8bf3001417 100644 --- a/grep.h +++ b/grep.h @@ -127,7 +127,7 @@ extern void append_grep_pattern(struct grep_opt *opt, const char *pat, const cha extern void append_header_grep_pattern(struct grep_opt *, enum grep_header_field, const char *); extern void compile_grep_patterns(struct grep_opt *opt); extern void free_grep_patterns(struct grep_opt *opt); -extern int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size); +extern int grep_buffer(struct grep_opt *opt, char *buf, unsigned long size); struct grep_source { char *name; diff --git a/revision.c b/revision.c index 064e351084..89d3a8fafb 100644 --- a/revision.c +++ b/revision.c @@ -2128,7 +2128,6 @@ static int commit_match(struct commit *commit, struct rev_info *opt) if (!opt->grep_filter.pattern_list && !opt->grep_filter.header_list) return 1; return grep_buffer(&opt->grep_filter, - NULL, /* we say nothing, not even filename */ commit->buffer, strlen(commit->buffer)); } -- cgit v1.2.1 From 94ad9d9e075d3f7802cf56641ebb342d43fb46e3 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:20:43 -0500 Subject: grep: cache userdiff_driver in grep_source Right now, grep only uses the userdiff_driver for one thing: looking up funcname patterns for "-p" and "-W". As new uses for userdiff drivers are added to the grep code, we want to minimize attribute lookups, which can be expensive. It might seem at first that this would also optimize multiple lookups when the funcname pattern for a file is needed multiple times. However, the compiled funcname pattern is already cached in struct grep_opt's "priv" member, so multiple lookups are already suppressed. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- grep.c | 22 ++++++++++++++++------ grep.h | 4 ++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/grep.c b/grep.c index 2a3fe7ce6f..bb1856985b 100644 --- a/grep.c +++ b/grep.c @@ -841,12 +841,9 @@ static int match_funcname(struct grep_opt *opt, struct grep_source *gs, char *bo { xdemitconf_t *xecfg = opt->priv; if (xecfg && !xecfg->find_func) { - struct userdiff_driver *drv; - grep_attr_lock(); - drv = userdiff_find_by_path(gs->name); - grep_attr_unlock(); - if (drv && drv->funcname.pattern) { - const struct userdiff_funcname *pe = &drv->funcname; + grep_source_load_driver(gs); + if (gs->driver->funcname.pattern) { + const struct userdiff_funcname *pe = &gs->driver->funcname; xdiff_set_find_func(xecfg, pe->pattern, pe->cflags); } else { xecfg = opt->priv = NULL; @@ -1237,6 +1234,7 @@ void grep_source_init(struct grep_source *gs, enum grep_source_type type, gs->name = name ? xstrdup(name) : NULL; gs->buf = NULL; gs->size = 0; + gs->driver = NULL; switch (type) { case GREP_SOURCE_FILE: @@ -1340,3 +1338,15 @@ int grep_source_load(struct grep_source *gs) } die("BUG: invalid grep_source type"); } + +void grep_source_load_driver(struct grep_source *gs) +{ + if (gs->driver) + return; + + grep_attr_lock(); + gs->driver = userdiff_find_by_path(gs->name); + if (!gs->driver) + gs->driver = userdiff_find_by_name("default"); + grep_attr_unlock(); +} diff --git a/grep.h b/grep.h index 8bf3001417..73b28c2df7 100644 --- a/grep.h +++ b/grep.h @@ -9,6 +9,7 @@ typedef int pcre_extra; #endif #include "kwset.h" #include "thread-utils.h" +#include "userdiff.h" enum grep_pat_token { GREP_PATTERN, @@ -141,6 +142,8 @@ struct grep_source { char *buf; unsigned long size; + + struct userdiff_driver *driver; }; void grep_source_init(struct grep_source *gs, enum grep_source_type type, @@ -148,6 +151,7 @@ void grep_source_init(struct grep_source *gs, enum grep_source_type type, int grep_source_load(struct grep_source *gs); void grep_source_clear_data(struct grep_source *gs); void grep_source_clear(struct grep_source *gs); +void grep_source_load_driver(struct grep_source *gs); int grep_source(struct grep_opt *opt, struct grep_source *gs); -- cgit v1.2.1 From 41b59bfcb16abb738e5c95c95fb462e717d47d4d Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:21:02 -0500 Subject: grep: respect diff attributes for binary-ness There is currently no way for users to tell git-grep that a particular path is or is not a binary file; instead, grep always relies on its auto-detection (or the user specifying "-a" to treat all binary-looking files like text). This patch teaches git-grep to use the same attribute lookup that is used by git-diff. We could add a new "grep" flag, but that is unnecessarily complex and unlikely to be useful. Despite the name, the "-diff" attribute (or "diff=foo" and the associated diff.foo.binary config option) are really about describing the contents of the path. It's simply historical that diff was the only thing that cared about these attributes in the past. And if this simple approach turns out to be insufficient, we still have a backwards-compatible path forward: we can add a separate "grep" attribute, and fall back to respecting "diff" if it is unset. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- grep.c | 16 ++++++++++++++-- grep.h | 1 + t/t7008-grep-binary.sh | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/grep.c b/grep.c index bb1856985b..a50d161721 100644 --- a/grep.c +++ b/grep.c @@ -1024,11 +1024,11 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle switch (opt->binary) { case GREP_BINARY_DEFAULT: - if (buffer_is_binary(gs->buf, gs->size)) + if (grep_source_is_binary(gs)) binary_match_only = 1; break; case GREP_BINARY_NOMATCH: - if (buffer_is_binary(gs->buf, gs->size)) + if (grep_source_is_binary(gs)) return 0; /* Assume unmatch */ break; case GREP_BINARY_TEXT: @@ -1350,3 +1350,15 @@ void grep_source_load_driver(struct grep_source *gs) gs->driver = userdiff_find_by_name("default"); grep_attr_unlock(); } + +int grep_source_is_binary(struct grep_source *gs) +{ + grep_source_load_driver(gs); + if (gs->driver->binary != -1) + return gs->driver->binary; + + if (!grep_source_load(gs)) + return buffer_is_binary(gs->buf, gs->size); + + return 0; +} diff --git a/grep.h b/grep.h index 73b28c2df7..36e49d8255 100644 --- a/grep.h +++ b/grep.h @@ -152,6 +152,7 @@ int grep_source_load(struct grep_source *gs); void grep_source_clear_data(struct grep_source *gs); void grep_source_clear(struct grep_source *gs); void grep_source_load_driver(struct grep_source *gs); +int grep_source_is_binary(struct grep_source *gs); int grep_source(struct grep_opt *opt, struct grep_source *gs); diff --git a/t/t7008-grep-binary.sh b/t/t7008-grep-binary.sh index 917a264eea..fd6410fc71 100755 --- a/t/t7008-grep-binary.sh +++ b/t/t7008-grep-binary.sh @@ -99,4 +99,28 @@ test_expect_success 'git grep yx a' " test_must_fail git grep -f f a " +test_expect_success 'grep respects binary diff attribute' ' + echo text >t && + git add t && + echo t:text >expect && + git grep text t >actual && + test_cmp expect actual && + echo "t -diff" >.gitattributes && + echo "Binary file t matches" >expect && + git grep text t >actual && + test_cmp expect actual +' + +test_expect_success 'grep respects not-binary diff attribute' ' + echo binQary | q_to_nul >b && + git add b && + echo "Binary file b matches" >expect && + git grep bin b >actual && + test_cmp expect actual && + echo "b diff" >.gitattributes && + echo "b:binQary" >expect && + git grep bin b | nul_to_q >actual && + test_cmp expect actual +' + test_done -- cgit v1.2.1 From 08265798e1ff6abc1b0aaff31c1471f83bd51425 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:21:11 -0500 Subject: grep: load file data after checking binary-ness Usually we load each file to grep into memory, check whether it's binary, and then either grep it (the default) or not (if "-I" was given). In the "-I" case, we can skip loading the file entirely if it is marked as binary via gitattributes. On my giant 3-gigabyte media repository, doing "git grep -I foo" went from: real 0m0.712s user 0m0.044s sys 0m4.780s to: real 0m0.026s user 0m0.016s sys 0m0.020s Obviously this is an extreme example. The repo is almost entirely binary files, and you can see that we spent all of our time asking the kernel to read() the data. However, with a cold disk cache, even avoiding a few binary files can have an impact. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- grep.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grep.c b/grep.c index a50d161721..3821400966 100644 --- a/grep.c +++ b/grep.c @@ -1019,9 +1019,6 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle } opt->last_shown = 0; - if (grep_source_load(gs) < 0) - return 0; - switch (opt->binary) { case GREP_BINARY_DEFAULT: if (grep_source_is_binary(gs)) @@ -1042,6 +1039,9 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle try_lookahead = should_lookahead(opt); + if (grep_source_load(gs) < 0) + return 0; + bol = gs->buf; left = gs->size; while (left) { -- cgit v1.2.1 From 9dd5245c1043dd18fd7b3f44b9e51eef7e4b58d8 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 03:24:28 -0500 Subject: grep: pre-load userdiff drivers when threaded The low-level grep_source code will automatically load the userdiff driver to see whether a file is binary. However, when we are threaded, it will load the drivers in a non-deterministic order, handling each one as its assigned thread happens to be scheduled. Meanwhile, the attribute lookup code (which underlies the userdiff driver lookup) is optimized to handle paths in sequential order (because they tend to share the same gitattributes files). Multi-threading the lookups destroys the locality and makes this optimization less effective. We can fix this by pre-loading the userdiff driver in the main thread, before we hand off the file to a worker thread. My best-of-five for "git grep foo" on the linux-2.6 repository went from: real 0m0.391s user 0m1.708s sys 0m0.584s to: real 0m0.360s user 0m1.576s sys 0m0.572s Not a huge speedup, but it's quite easy to do. The only trick is that we shouldn't perform this optimization if "-a" was used, in which case we won't bother checking whether the files are binary at all. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/grep.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index c9b6a138fe..e741aca18c 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -85,8 +85,8 @@ static pthread_cond_t cond_result; static int skip_first_line; -static void add_work(enum grep_source_type type, const char *name, - const void *id) +static void add_work(struct grep_opt *opt, enum grep_source_type type, + const char *name, const void *id) { grep_lock(); @@ -95,6 +95,8 @@ static void add_work(enum grep_source_type type, const char *name, } grep_source_init(&todo[todo_end].source, type, name, id); + if (opt->binary != GREP_BINARY_TEXT) + grep_source_load_driver(&todo[todo_end].source); todo[todo_end].done = 0; strbuf_reset(&todo[todo_end].out); todo_end = (todo_end + 1) % ARRAY_SIZE(todo); @@ -333,7 +335,7 @@ static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, #ifndef NO_PTHREADS if (use_threads) { - add_work(GREP_SOURCE_SHA1, pathbuf.buf, sha1); + add_work(opt, GREP_SOURCE_SHA1, pathbuf.buf, sha1); strbuf_release(&pathbuf); return 0; } else @@ -362,7 +364,7 @@ static int grep_file(struct grep_opt *opt, const char *filename) #ifndef NO_PTHREADS if (use_threads) { - add_work(GREP_SOURCE_FILE, buf.buf, filename); + add_work(opt, GREP_SOURCE_FILE, buf.buf, filename); strbuf_release(&buf); return 0; } else -- cgit v1.2.1 From 173223aa62519033e547937c692f5540f59d9025 Mon Sep 17 00:00:00 2001 From: Ramsay Jones Date: Thu, 2 Feb 2012 04:59:23 -0600 Subject: vcs-svn: rename check_overflow arguments for clarity Code using the argument names a and b just doesn't look right (not sure why!). Use more explicit names "offset" and "len" to make their type and meaning clearer. Also rename check_overflow() to check_offset_overflow() to clarify that we are making sure that "len" bytes beyond "offset" still fits the type to represent an offset. Signed-off-by: Jonathan Nieder Signed-off-by: Junio C Hamano --- vcs-svn/sliding_window.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vcs-svn/sliding_window.c b/vcs-svn/sliding_window.c index 1bac7a4c7f..c6c2effd30 100644 --- a/vcs-svn/sliding_window.c +++ b/vcs-svn/sliding_window.c @@ -31,15 +31,15 @@ static int read_to_fill_or_whine(struct line_buffer *file, return 0; } -static int check_overflow(off_t a, size_t b) +static int check_offset_overflow(off_t offset, size_t len) { - if (b > maximum_signed_value_of_type(off_t)) + if (len > maximum_signed_value_of_type(off_t)) return error("unrepresentable length in delta: " - "%"PRIuMAX" > OFF_MAX", (uintmax_t) b); - if (signed_add_overflows(a, (off_t) b)) + "%"PRIuMAX" > OFF_MAX", (uintmax_t) len); + if (signed_add_overflows(offset, (off_t) len)) return error("unrepresentable offset in delta: " "%"PRIuMAX" + %"PRIuMAX" > OFF_MAX", - (uintmax_t) a, (uintmax_t) b); + (uintmax_t) offset, (uintmax_t) len); return 0; } @@ -48,9 +48,9 @@ int move_window(struct sliding_view *view, off_t off, size_t width) off_t file_offset; assert(view); assert(view->width <= view->buf.len); - assert(!check_overflow(view->off, view->buf.len)); + assert(!check_offset_overflow(view->off, view->buf.len)); - if (check_overflow(off, width)) + if (check_offset_overflow(off, width)) return -1; if (off < view->off || off + width < view->off + view->width) return error("invalid delta: window slides left"); -- cgit v1.2.1 From 150f75467cdd6eaf581d22175bb377399c62893a Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Thu, 2 Feb 2012 05:03:16 -0600 Subject: vcs-svn: allow import of > 4GiB files There is no reason in principle that an svn-format dump would not be able to represent a file whose length does not fit in a 32-bit integer. Use off_t consistently to represent file lengths (in place of using uint32_t in some contexts) so we can handle that. Most svn-fe code is already ready to do that without this patch and passes values of type off_t around. The type mismatch from stragglers was noticed with gcc -Wtype-limits. While at it, tighten the parsing of the Text-content-length field to make sure it is a number and does not overflow, and tighten other overflow checks as that value is passed around and manipulated. Inspired-by: Ramsay Jones Signed-off-by: Jonathan Nieder Signed-off-by: Junio C Hamano --- vcs-svn/fast_export.c | 15 +++++++++------ vcs-svn/fast_export.h | 4 ++-- vcs-svn/svndump.c | 21 +++++++++++++++------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/vcs-svn/fast_export.c b/vcs-svn/fast_export.c index 19d7c34c25..b823b8519c 100644 --- a/vcs-svn/fast_export.c +++ b/vcs-svn/fast_export.c @@ -227,15 +227,18 @@ static long apply_delta(off_t len, struct line_buffer *input, return ret; } -void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input) +void fast_export_data(uint32_t mode, off_t len, struct line_buffer *input) { + assert(len >= 0); if (mode == REPO_MODE_LNK) { /* svn symlink blobs start with "link " */ + if (len < 5) + die("invalid dump: symlink too short for \"link\" prefix"); len -= 5; if (buffer_skip_bytes(input, 5) != 5) die_short_read(input); } - printf("data %"PRIu32"\n", len); + printf("data %"PRIuMAX"\n", (uintmax_t) len); if (buffer_copy_bytes(input, len) != len) die_short_read(input); fputc('\n', stdout); @@ -297,12 +300,12 @@ int fast_export_ls(const char *path, uint32_t *mode, struct strbuf *dataref) void fast_export_blob_delta(uint32_t mode, uint32_t old_mode, const char *old_data, - uint32_t len, struct line_buffer *input) + off_t len, struct line_buffer *input) { long postimage_len; - if (len > maximum_signed_value_of_type(off_t)) - die("enormous delta"); - postimage_len = apply_delta((off_t) len, input, old_data, old_mode); + + assert(len >= 0); + postimage_len = apply_delta(len, input, old_data, old_mode); if (mode == REPO_MODE_LNK) { buffer_skip_bytes(&postimage, strlen("link ")); postimage_len -= strlen("link "); diff --git a/vcs-svn/fast_export.h b/vcs-svn/fast_export.h index 43d05b65ef..aa629f54ff 100644 --- a/vcs-svn/fast_export.h +++ b/vcs-svn/fast_export.h @@ -14,10 +14,10 @@ void fast_export_begin_commit(uint32_t revision, const char *author, const struct strbuf *log, const char *uuid, const char *url, unsigned long timestamp); void fast_export_end_commit(uint32_t revision); -void fast_export_data(uint32_t mode, uint32_t len, struct line_buffer *input); +void fast_export_data(uint32_t mode, off_t len, struct line_buffer *input); void fast_export_blob_delta(uint32_t mode, uint32_t old_mode, const char *old_data, - uint32_t len, struct line_buffer *input); + off_t len, struct line_buffer *input); /* If there is no such file at that rev, returns -1, errno == ENOENT. */ int fast_export_ls_rev(uint32_t rev, const char *path, diff --git a/vcs-svn/svndump.c b/vcs-svn/svndump.c index ca63760fe2..644fdc71ba 100644 --- a/vcs-svn/svndump.c +++ b/vcs-svn/svndump.c @@ -40,7 +40,8 @@ static struct line_buffer input = LINE_BUFFER_INIT; static struct { - uint32_t action, propLength, textLength, srcRev, type; + uint32_t action, propLength, srcRev, type; + off_t text_length; struct strbuf src, dst; uint32_t text_delta, prop_delta; } node_ctx; @@ -61,7 +62,7 @@ static void reset_node_ctx(char *fname) node_ctx.type = 0; node_ctx.action = NODEACT_UNKNOWN; node_ctx.propLength = LENGTH_UNKNOWN; - node_ctx.textLength = LENGTH_UNKNOWN; + node_ctx.text_length = -1; strbuf_reset(&node_ctx.src); node_ctx.srcRev = 0; strbuf_reset(&node_ctx.dst); @@ -209,7 +210,7 @@ static void handle_node(void) { const uint32_t type = node_ctx.type; const int have_props = node_ctx.propLength != LENGTH_UNKNOWN; - const int have_text = node_ctx.textLength != LENGTH_UNKNOWN; + const int have_text = node_ctx.text_length != -1; /* * Old text for this node: * NULL - directory or bug @@ -291,12 +292,12 @@ static void handle_node(void) } if (!node_ctx.text_delta) { fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); - fast_export_data(node_ctx.type, node_ctx.textLength, &input); + fast_export_data(node_ctx.type, node_ctx.text_length, &input); return; } fast_export_modify(node_ctx.dst.buf, node_ctx.type, "inline"); fast_export_blob_delta(node_ctx.type, old_mode, old_data, - node_ctx.textLength, &input); + node_ctx.text_length, &input); } static void begin_revision(void) @@ -409,7 +410,15 @@ void svndump_read(const char *url) break; case sizeof("Text-content-length"): if (!constcmp(t, "Text-content-length")) { - node_ctx.textLength = atoi(val); + char *end; + uintmax_t textlen; + + textlen = strtoumax(val, &end, 10); + if (!isdigit(*val) || *end) + die("invalid dump: non-numeric length %s", val); + if (textlen > maximum_signed_value_of_type(off_t)) + die("unrepresentable length in dump: %s", val); + node_ctx.text_length = (off_t) textlen; break; } if (constcmp(t, "Prop-content-length")) -- cgit v1.2.1 From 3f790003a356284d92beff6965985917e58b6707 Mon Sep 17 00:00:00 2001 From: Jonathan Nieder Date: Thu, 2 Feb 2012 05:06:01 -0600 Subject: vcs-svn: suppress a -Wtype-limits warning On 32-bit architectures with 64-bit file offsets, gcc 4.3 and earlier produce the following warning: CC vcs-svn/sliding_window.o vcs-svn/sliding_window.c: In function `check_overflow': vcs-svn/sliding_window.c:36: warning: comparison is always false \ due to limited range of data type The warning appears even when gcc is run without any warning flags (this is gcc bug 12963). In later versions the same warning can be reproduced with -Wtype-limits, which is implied by -Wextra. On 64-bit architectures it really is possible for a size_t not to be representable as an off_t so the check this is warning about is not actually redundant. But even false positives are distracting. Avoid the warning by making the "len" argument to check_overflow a uintmax_t; no functional change intended. Reported-by: Ramsay Jones Signed-off-by: Jonathan Nieder Signed-off-by: Junio C Hamano --- vcs-svn/sliding_window.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vcs-svn/sliding_window.c b/vcs-svn/sliding_window.c index c6c2effd30..ec2707c9c4 100644 --- a/vcs-svn/sliding_window.c +++ b/vcs-svn/sliding_window.c @@ -31,15 +31,15 @@ static int read_to_fill_or_whine(struct line_buffer *file, return 0; } -static int check_offset_overflow(off_t offset, size_t len) +static int check_offset_overflow(off_t offset, uintmax_t len) { if (len > maximum_signed_value_of_type(off_t)) return error("unrepresentable length in delta: " - "%"PRIuMAX" > OFF_MAX", (uintmax_t) len); + "%"PRIuMAX" > OFF_MAX", len); if (signed_add_overflows(offset, (off_t) len)) return error("unrepresentable offset in delta: " "%"PRIuMAX" + %"PRIuMAX" > OFF_MAX", - (uintmax_t) offset, (uintmax_t) len); + (uintmax_t) offset, len); return 0; } -- cgit v1.2.1 From b3256eb8b35937192e85725d0c2bcb422295790c Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 2 Feb 2012 16:59:13 -0500 Subject: standardize and improve lookup rules for external local repos When you specify a local repository on the command line of clone, ls-remote, upload-pack, receive-pack, or upload-archive, or in a request to git-daemon, we perform a little bit of lookup magic, doing things like looking in working trees for .git directories and appending ".git" for bare repos. For clone, this magic happens in get_repo_path. For everything else, it happens in enter_repo. In both cases, there are some ambiguous or confusing cases that aren't handled well, and there is one case that is not handled the same by both methods. This patch tries to provide (and test!) standard, sensible lookup rules for both code paths. The intended changes are: 1. When looking up "foo", we have always preferred a working tree "foo" (containing "foo/.git" over the bare "foo.git". But we did not prefer a bare "foo" over "foo.git". With this patch, we do so. 2. We would select directories that existed but didn't actually look like git repositories. With this patch, we make sure a selected directory looks like a git repo. Not only is this more sensible in general, but it will help anybody who is negatively affected by change (1) negatively (e.g., if they had "foo.git" next to its separate work tree "foo", and expect to keep finding "foo.git" when they reference "foo"). 3. The enter_repo code path would, given "foo", look for "foo.git/.git" (i.e., do the ".git" append magic even for a repo with working tree). The clone code path did not; with this patch, they now behave the same. In the unlikely case of a working tree overlaying a bare repo (i.e., a ".git" directory _inside_ a bare repo), we continue to treat it as a working tree (prefering the "inner" .git over the bare repo). This is mainly because the combination seems nonsensical, and I'd rather stick with existing behavior on the off chance that somebody is relying on it. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/clone.c | 4 +- cache.h | 1 + path.c | 7 +++- setup.c | 2 +- t/t5900-repo-selection.sh | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 5 deletions(-) create mode 100755 t/t5900-repo-selection.sh diff --git a/builtin/clone.c b/builtin/clone.c index efe8b6cce5..b11dd6dd61 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -105,7 +105,7 @@ static const char *argv_submodule[] = { static char *get_repo_path(const char *repo, int *is_bundle) { - static char *suffix[] = { "/.git", ".git", "" }; + static char *suffix[] = { "/.git", "", ".git/.git", ".git" }; static char *bundle_suffix[] = { ".bundle", "" }; struct stat st; int i; @@ -115,7 +115,7 @@ static char *get_repo_path(const char *repo, int *is_bundle) path = mkpath("%s%s", repo, suffix[i]); if (stat(path, &st)) continue; - if (S_ISDIR(st.st_mode)) { + if (S_ISDIR(st.st_mode) && is_git_directory(path)) { *is_bundle = 0; return xstrdup(absolute_path(path)); } else if (S_ISREG(st.st_mode) && st.st_size > 8) { diff --git a/cache.h b/cache.h index 2e6ad3604e..3dfb95d8aa 100644 --- a/cache.h +++ b/cache.h @@ -432,6 +432,7 @@ extern char *git_work_tree_cfg; extern int is_inside_work_tree(void); extern int have_git_dir(void); extern const char *get_git_dir(void); +extern int is_git_directory(const char *path); extern char *get_object_directory(void); extern char *get_index_file(void); extern char *get_graft_file(void); diff --git a/path.c b/path.c index b6f71d1086..6f2aa699ad 100644 --- a/path.c +++ b/path.c @@ -293,7 +293,7 @@ const char *enter_repo(const char *path, int strict) if (!strict) { static const char *suffix[] = { - ".git/.git", "/.git", ".git", "", NULL, + "/.git", "", ".git/.git", ".git", NULL, }; const char *gitfile; int len = strlen(path); @@ -324,8 +324,11 @@ const char *enter_repo(const char *path, int strict) return NULL; len = strlen(used_path); for (i = 0; suffix[i]; i++) { + struct stat st; strcpy(used_path + len, suffix[i]); - if (!access(used_path, F_OK)) { + if (!stat(used_path, &st) && + (S_ISREG(st.st_mode) || + (S_ISDIR(st.st_mode) && is_git_directory(used_path)))) { strcat(validated_path, suffix[i]); break; } diff --git a/setup.c b/setup.c index 61c22e6bec..7a3618fab7 100644 --- a/setup.c +++ b/setup.c @@ -247,7 +247,7 @@ const char **get_pathspec(const char *prefix, const char **pathspec) * a proper "ref:", or a regular file HEAD that has a properly * formatted sha1 object name. */ -static int is_git_directory(const char *suspect) +int is_git_directory(const char *suspect) { char path[PATH_MAX]; size_t len = strlen(suspect); diff --git a/t/t5900-repo-selection.sh b/t/t5900-repo-selection.sh new file mode 100755 index 0000000000..3d5b418bb4 --- /dev/null +++ b/t/t5900-repo-selection.sh @@ -0,0 +1,100 @@ +#!/bin/sh + +test_description='selecting remote repo in ambiguous cases' +. ./test-lib.sh + +reset() { + rm -rf foo foo.git fetch clone +} + +make_tree() { + git init "$1" && + (cd "$1" && test_commit "$1") +} + +make_bare() { + git init --bare "$1" && + (cd "$1" && + tree=`git hash-object -w -t tree /dev/null` && + commit=$(echo "$1" | git commit-tree $tree) && + git update-ref HEAD $commit + ) +} + +get() { + git init --bare fetch && + (cd fetch && git fetch "../$1") && + git clone "$1" clone +} + +check() { + echo "$1" >expect && + (cd fetch && git log -1 --format=%s FETCH_HEAD) >actual.fetch && + (cd clone && git log -1 --format=%s HEAD) >actual.clone && + test_cmp expect actual.fetch && + test_cmp expect actual.clone +} + +test_expect_success 'find .git dir in worktree' ' + reset && + make_tree foo && + get foo && + check foo +' + +test_expect_success 'automagically add .git suffix' ' + reset && + make_bare foo.git && + get foo && + check foo.git +' + +test_expect_success 'automagically add .git suffix to worktree' ' + reset && + make_tree foo.git && + get foo && + check foo.git +' + +test_expect_success 'prefer worktree foo over bare foo.git' ' + reset && + make_tree foo && + make_bare foo.git && + get foo && + check foo +' + +test_expect_success 'prefer bare foo over bare foo.git' ' + reset && + make_bare foo && + make_bare foo.git && + get foo && + check foo +' + +test_expect_success 'disambiguate with full foo.git' ' + reset && + make_bare foo && + make_bare foo.git && + get foo.git && + check foo.git +' + +test_expect_success 'we are not fooled by non-git foo directory' ' + reset && + make_bare foo.git && + mkdir foo && + get foo && + check foo.git +' + +test_expect_success 'prefer inner .git over outer bare' ' + reset && + make_tree foo && + make_bare foo.git && + mv foo/.git foo.git && + get foo.git && + check foo +' + +test_done -- cgit v1.2.1 From 84d9e2d50ca9fbcf34e31cb74797fc182187c7b5 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Fri, 3 Feb 2012 13:44:54 +0100 Subject: gitweb: Allow UTF-8 encoded CGI query parameters and path_info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitweb forgot to turn query parameters into UTF-8. This results in a bug that one cannot search for a string with characters outside US-ASCII. For example searching for "Michał Kiedrowicz" (containing letter 'ł' - LATIN SMALL LETTER L WITH STROKE, with Unicode codepoint U+0142, represented with 0xc5 0x82 bytes in UTF-8 and percent-encoded as %C5%82) result in the following incorrect data in search field MichaÅ\202 Kiedrowicz This is caused by CGI by default treating '0xc5 0x82' bytes as two characters in Perl legacy encoding latin-1 (iso-8859-1), because 's' query parameter is not processed explicitly as UTF-8 encoded string. The solution used here follows "Using Unicode in a Perl CGI script" article on http://www.lemoda.net/cgi/perl-unicode/index.html: use CGI; use Encode 'decode_utf8; my $value = params('input'); $value = decode_utf8($value); Decoding UTF-8 is done when filling %input_params hash and $path_info variable; the former requires to move from explicit $cgi->param(