diff --git a/.gitignore b/.gitignore index b99a37e9ba..d190c0ad07 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ git-ssh-push git-ssh-upload git-status git-stripspace +git-symbolic-ref git-tag git-tar-tree git-unpack-file diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index bd53ef430d..7d713c7385 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -9,7 +9,7 @@ git-clone - Clones a repository. SYNOPSIS -------- -'git clone' [-l] [-u ] [-q] +'git clone' [-l [-s]] [-q] [-n] [-u ] DESCRIPTION ----------- @@ -17,6 +17,7 @@ Clones a repository into a newly created directory. OPTIONS ------- +--local:: -l:: When the repository to clone from is on a local machine, this flag bypasses normal "git aware" transport @@ -25,10 +26,22 @@ OPTIONS The files under .git/objects/ directory are hardlinked to save space when possible. +--shared:: +-s:: + When the repository to clone is on the local machine, + instead of using hard links automatically setup + .git/objects/info/alternatives to share the objects + with the source repository + +--quiet:: -q:: Operate quietly. This flag is passed to "rsync" and "git-clone-pack" commands when given. +-n:: + No checkout of HEAD is performed after the clone is complete. + +--upload-pack :: -u :: When given, and the repository to clone from is handled by 'git-clone-pack', '--exec=' is passed to diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt index 591f4ed462..87cc362ad8 100644 --- a/Documentation/git-ls-files.txt +++ b/Documentation/git-ls-files.txt @@ -13,7 +13,7 @@ SYNOPSIS (-[c|d|o|i|s|u|k|m])\* [-x |--exclude=] [-X |--exclude-from=] - [--exclude-per-directory=] + [--exclude-per-directory=] [--] []\* DESCRIPTION ----------- @@ -77,6 +77,13 @@ OPTIONS K to be killed ? other +--:: + Do not interpret any more arguments as options. + +:: + Files to show. If no files are given all files which match the other + specified criteria are shown. + Output ------ show files just outputs the filename unless '--stage' is specified in diff --git a/Makefile b/Makefile index 38330c2e90..4ea6d9a71b 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ # DEFINES += -DUSE_STDEV -GIT_VERSION = 0.99.7.GIT +GIT_VERSION = 0.99.8.GIT CFLAGS = -g -O2 -Wall ALL_CFLAGS = $(CFLAGS) $(PLATFORM_DEFINES) $(DEFINES) @@ -103,25 +103,29 @@ SIMPLE_PROGRAMS = \ # ... and all the rest PROGRAMS = \ - git-apply$X git-cat-file$X git-checkout-index$X \ - git-clone-pack$X git-commit-tree$X git-convert-objects$X \ - git-diff-files$X git-diff-index$X git-diff-stages$X \ - git-diff-tree$X git-fetch-pack$X git-fsck-objects$X \ - git-hash-object$X git-init-db$X git-local-fetch$X \ - git-ls-files$X git-ls-tree$X git-merge-base$X \ - git-merge-index$X git-mktag$X git-pack-objects$X \ - git-patch-id$X git-peek-remote$X git-prune-packed$X \ - git-read-tree$X git-receive-pack$X git-rev-list$X \ - git-rev-parse$X git-send-pack$X git-show-branch$X \ - git-show-index$X git-ssh-fetch$X git-ssh-upload$X \ - git-tar-tree$X git-unpack-file$X git-unpack-objects$X \ - git-update-index$X git-update-server-info$X \ - git-upload-pack$X git-verify-pack$X git-write-tree$X \ - git-update-ref$X $(SIMPLE_PROGRAMS) + git-apply$X git-cat-file$X \ + git-checkout-index$X git-clone-pack$X git-commit-tree$X \ + git-convert-objects$X git-diff-files$X \ + git-diff-index$X git-diff-stages$X \ + git-diff-tree$X git-fetch-pack$X git-fsck-objects$X \ + git-hash-object$X git-init-db$X \ + git-local-fetch$X git-ls-files$X git-ls-tree$X git-merge-base$X \ + git-merge-index$X git-mktag$X git-pack-objects$X git-patch-id$X \ + git-peek-remote$X git-prune-packed$X git-read-tree$X \ + git-receive-pack$X git-rev-list$X git-rev-parse$X \ + git-send-pack$X git-show-branch$X \ + git-show-index$X git-ssh-fetch$X \ + git-ssh-upload$X git-tar-tree$X git-unpack-file$X \ + git-unpack-objects$X git-update-index$X git-update-server-info$X \ + git-upload-pack$X git-verify-pack$X git-write-tree$X \ + git-update-ref$X git-symbolic-ref$X \ + $(SIMPLE_PROGRAMS) # Backward compatibility -- to be removed after 1.0 PROGRAMS += git-ssh-pull$X git-ssh-push$X +GIT_LIST_TWEAK = + PYMODULES = \ gitMergeCommon.py @@ -131,6 +135,8 @@ endif ifdef WITH_SEND_EMAIL SCRIPT_PERL += git-send-email.perl +else + GIT_LIST_TWEAK += -e '/^send-email$$/d' endif LIB_FILE=libgit.a @@ -181,6 +187,10 @@ endif ifneq (,$(findstring arm,$(shell uname -m))) ARM_SHA1 = YesPlease endif +ifeq ($(shell uname -s),OpenBSD) + NEEDS_LIBICONV = YesPlease + PLATFORM_DEFINES += -I/usr/local/include -L/usr/local/lib +endif ifndef NO_CURL ifdef CURLDIR @@ -206,18 +216,32 @@ endif ifndef NO_OPENSSL LIB_OBJS += epoch.o OPENSSL_LIBSSL = -lssl + ifdef OPENSSLDIR + # Again this may be problematic -- gcc does not always want -R. + CFLAGS += -I$(OPENSSLDIR)/include + OPENSSL_LINK = -L$(OPENSSLDIR)/lib -R$(OPENSSLDIR)/lib + else + OPENSSL_LINK = + endif else DEFINES += '-DNO_OPENSSL' MOZILLA_SHA1 = 1 OPENSSL_LIBSSL = endif ifdef NEEDS_SSL_WITH_CRYPTO - LIB_4_CRYPTO = -lcrypto -lssl + LIB_4_CRYPTO = $(OPENSSL_LINK) -lcrypto -lssl else - LIB_4_CRYPTO = -lcrypto + LIB_4_CRYPTO = $(OPENSSL_LINK) -lcrypto endif ifdef NEEDS_LIBICONV - LIB_4_ICONV = -liconv + ifdef ICONVDIR + # Again this may be problematic -- gcc does not always want -R. + CFLAGS += -I$(ICONVDIR)/include + ICONV_LINK = -L$(ICONVDIR)/lib -R$(ICONVDIR)/lib + else + ICONV_LINK = + endif + LIB_4_ICONV = $(ICONV_LINK) -liconv else LIB_4_ICONV = endif @@ -273,8 +297,13 @@ all: git: git.sh Makefile rm -f $@+ $@ sed -e '1s|#!.*/sh|#!$(SHELL_PATH)|' \ +<<<<<<< Makefile -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ -e 's/@@X@@/$(X)/g' <$@.sh >$@+ +======= + -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ + $(GIT_LIST_TWEAK) <$@.sh >$@+ +>>>>>>> .merge_file_3QHyD4 chmod +x $@+ mv $@+ $@ diff --git a/apply.c b/apply.c index 964df2db10..f8862722fd 100644 --- a/apply.c +++ b/apply.c @@ -723,6 +723,16 @@ static int parse_single_patch(char *line, unsigned long size, struct patch *patc return offset; } +static inline int metadata_changes(struct patch *patch) +{ + return patch->is_rename > 0 || + patch->is_copy > 0 || + patch->is_new > 0 || + patch->is_delete || + (patch->old_mode && patch->new_mode && + patch->old_mode != patch->new_mode); +} + static int parse_chunk(char *buffer, unsigned long size, struct patch *patch) { int hdrsize, patchsize; @@ -733,6 +743,9 @@ static int parse_chunk(char *buffer, unsigned long size, struct patch *patch) patchsize = parse_single_patch(buffer + offset + hdrsize, size - offset - hdrsize, patch); + if (!patchsize && !metadata_changes(patch)) + die("patch with only garbage at line %d", linenr); + return offset + hdrsize + patchsize; } diff --git a/cache.h b/cache.h index 52a45f9c9a..ec2a1610b2 100644 --- a/cache.h +++ b/cache.h @@ -189,6 +189,7 @@ extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2) extern char *sha1_file_name(const unsigned char *sha1); extern char *sha1_pack_name(const unsigned char *sha1); extern char *sha1_pack_index_name(const unsigned char *sha1); +extern const unsigned char null_sha1[20]; int git_mkstemp(char *path, size_t n, const char *template); @@ -228,6 +229,10 @@ extern int has_pack_index(const unsigned char *sha1); extern int get_sha1(const char *str, unsigned char *sha1); extern int get_sha1_hex(const char *hex, unsigned char *sha1); extern char *sha1_to_hex(const unsigned char *sha1); /* static buffer result! */ +extern int read_ref(const char *filename, unsigned char *sha1); +extern const char *resolve_ref(const char *path, unsigned char *sha1, int); +extern int create_symref(const char *git_HEAD, const char *refs_heads_master); +extern int validate_symref(const char *git_HEAD); /* General helper functions */ extern void usage(const char *err) NORETURN; diff --git a/debian/changelog b/debian/changelog index 128513a455..bebc1919ba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +git-core (0.99.8-0) unstable; urgency=low + + * GIT 0.99.8 + + -- Junio C Hamano Sun, 2 Oct 2005 12:54:26 -0700 + git-core (0.99.7-0) unstable; urgency=low * GIT 0.99.7 diff --git a/diff-files.c b/diff-files.c index e8db3d2d69..5e598322ff 100644 --- a/diff-files.c +++ b/diff-files.c @@ -34,7 +34,6 @@ static void show_modified(int oldmode, int mode, int main(int argc, const char **argv) { - static const unsigned char null_sha1[20] = { 0, }; const char **pathspec; const char *prefix = setup_git_directory(); int entries, i; diff --git a/diff.c b/diff.c index 9bded28729..7d06b035ae 100644 --- a/diff.c +++ b/diff.c @@ -10,7 +10,6 @@ #include "diffcore.h" static const char *diff_opts = "-pu"; -static unsigned char null_sha1[20] = { 0, }; static int use_size_cache; @@ -414,7 +413,7 @@ void diff_free_filespec_data(struct diff_filespec *s) static void prep_temp_blob(struct diff_tempfile *temp, void *blob, unsigned long size, - unsigned char *sha1, + const unsigned char *sha1, int mode) { int fd; diff --git a/fsck-objects.c b/fsck-objects.c index 247edf0529..65cec7d12b 100644 --- a/fsck-objects.c +++ b/fsck-objects.c @@ -402,25 +402,17 @@ static void fsck_object_dir(const char *path) static int fsck_head_link(void) { - int fd, count; - char hex[40]; unsigned char sha1[20]; - static char path[PATH_MAX], link[PATH_MAX]; - const char *git_dir = get_git_dir(); - - snprintf(path, sizeof(path), "%s/HEAD", git_dir); - if (readlink(path, link, sizeof(link)) < 0) - return error("HEAD is not a symlink"); - if (strncmp("refs/heads/", link, 11)) - return error("HEAD points to something strange (%s)", link); - fd = open(path, O_RDONLY); - if (fd < 0) - return error("HEAD: %s", strerror(errno)); - count = read(fd, hex, sizeof(hex)); - close(fd); - if (count < 0) - return error("HEAD: %s", strerror(errno)); - if (count < 40 || get_sha1_hex(hex, sha1)) + const char *git_HEAD = strdup(git_path("HEAD")); + const char *git_refs_heads_master = resolve_ref(git_HEAD, sha1, 1); + int pfxlen = strlen(git_HEAD) - 4; /* strip .../.git/ part */ + + if (!git_refs_heads_master) + return error("HEAD is not a symbolic ref"); + if (strncmp(git_refs_heads_master + pfxlen, "refs/heads/", 11)) + return error("HEAD points to something strange (%s)", + git_refs_heads_master + pfxlen); + if (!memcmp(null_sha1, sha1, 20)) return error("HEAD: not a valid git pointer"); return 0; } diff --git a/git-archimport.perl b/git-archimport.perl index 3749b8b572..980e827b27 100755 --- a/git-archimport.perl +++ b/git-archimport.perl @@ -228,10 +228,12 @@ foreach my $ps (@psets) { # skip commits already in repo # if (ptag($ps->{id})) { - $opt_v && print "Skipping already imported: $ps->{id}\n"; + $opt_v && print " * Skipping already imported: $ps->{id}\n"; next; } + print " * Starting to work on $ps->{id}\n"; + # # create the branch if needed # @@ -675,6 +677,10 @@ sub find_parents { # that branch. # foreach my $branch (keys %branches) { + + # check that we actually know about the branch + next unless -e "$git_dir/refs/heads/$branch"; + my $mergebase = `git-merge-base $branch $ps->{branch}`; die "Cannot find merge base for $branch and $ps->{branch}" if $?; chomp $mergebase; diff --git a/git-bisect.sh b/git-bisect.sh index 8dc77c991c..1ab2f187dc 100755 --- a/git-bisect.sh +++ b/git-bisect.sh @@ -38,7 +38,8 @@ bisect_start() { # Verify HEAD. If we were bisecting before this, reset to the # top-of-line master first! # - head=$(readlink $GIT_DIR/HEAD) || die "Bad HEAD - I need a symlink" + head=$(GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD) || + die "Bad HEAD - I need a symbolic ref" case "$head" in refs/heads/bisect*) git checkout master || exit @@ -46,7 +47,7 @@ bisect_start() { refs/heads/*) ;; *) - die "Bad HEAD - strange symlink" + die "Bad HEAD - strange symbolic ref" ;; esac @@ -135,7 +136,7 @@ bisect_next() { echo "$rev" > "$GIT_DIR/refs/heads/new-bisect" git checkout new-bisect || exit mv "$GIT_DIR/refs/heads/new-bisect" "$GIT_DIR/refs/heads/bisect" && - ln -sf refs/heads/bisect "$GIT_DIR/HEAD" + GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD refs/heads/bisect git-show-branch "$rev" } diff --git a/git-branch.sh b/git-branch.sh index dcec2a9f2f..074229c206 100755 --- a/git-branch.sh +++ b/git-branch.sh @@ -14,7 +14,8 @@ If two arguments, create a new branch based off of . delete_branch () { option="$1" branch_name="$2" - headref=$(readlink "$GIT_DIR/HEAD" | sed -e 's|^refs/heads/||') + headref=$(GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD | + sed -e 's|^refs/heads/||') case ",$headref," in ",$branch_name,") die "Cannot delete the branch you are on." ;; @@ -67,7 +68,8 @@ done case "$#" in 0) - headref=$(readlink "$GIT_DIR/HEAD" | sed -e 's|^refs/heads/||') + headref=$(GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD | + sed -e 's|^refs/heads/||') git-rev-parse --symbolic --all | sed -ne 's|^refs/heads/||p' | sort | diff --git a/git-checkout.sh b/git-checkout.sh index 37afcdda30..c3825904b6 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -71,7 +71,8 @@ if [ "$?" -eq 0 ]; then echo $new > "$GIT_DIR/refs/heads/$newbranch" branch="$newbranch" fi - [ "$branch" ] && ln -sf "refs/heads/$branch" "$GIT_DIR/HEAD" + [ "$branch" ] && + GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD "refs/heads/$branch" rm -f "$GIT_DIR/MERGE_HEAD" else exit 1 diff --git a/git-commit.sh b/git-commit.sh index 18b259c708..1206c20c2e 100755 --- a/git-commit.sh +++ b/git-commit.sh @@ -153,15 +153,8 @@ if [ -f "$GIT_DIR/MERGE_HEAD" ]; then fi >>.editmsg PARENTS="-p HEAD" -if [ ! -r "$GIT_DIR/HEAD" ]; then - if [ -z "$(git-ls-files)" ]; then - echo Nothing to commit 1>&2 - exit 1 - fi - PARENTS="" - current= -else - current=$(git-rev-parse --verify HEAD) +if GIT_DIR="$GIT_DIR" git-rev-parse --verify HEAD >/dev/null 2>&1 +then if [ -f "$GIT_DIR/MERGE_HEAD" ]; then PARENTS="-p HEAD "`sed -e 's/^/-p /' "$GIT_DIR/MERGE_HEAD"` fi @@ -194,6 +187,12 @@ else export GIT_AUTHOR_EMAIL export GIT_AUTHOR_DATE fi +else + if [ -z "$(git-ls-files)" ]; then + echo Nothing to commit 1>&2 + exit 1 + fi + PARENTS="" fi git-status >>.editmsg if [ "$?" != "0" -a ! -f $GIT_DIR/MERGE_HEAD ] diff --git a/git-cvsimport.perl b/git-cvsimport.perl index 565f4f1b32..f35c0d045b 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -510,7 +510,7 @@ unless($pid) { my $state = 0; -my($patchset,$date,$author,$branch,$ancestor,$tag,$logmsg); +my($patchset,$date,$author_name,$author_email,$branch,$ancestor,$tag,$logmsg); my(@old,@new); my $commit = sub { my $pid; @@ -591,11 +591,11 @@ my $commit = sub { } exec("env", - "GIT_AUTHOR_NAME=$author", - "GIT_AUTHOR_EMAIL=$author", + "GIT_AUTHOR_NAME=$author_name", + "GIT_AUTHOR_EMAIL=$author_email", "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), - "GIT_COMMITTER_NAME=$author", - "GIT_COMMITTER_EMAIL=$author", + "GIT_COMMITTER_NAME=$author_name", + "GIT_COMMITTER_EMAIL=$author_email", "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), "git-commit-tree", $tree,@par); die "Cannot exec git-commit-tree: $!\n"; @@ -638,7 +638,7 @@ my $commit = sub { print $out "object $cid\n". "type commit\n". "tag $xtag\n". - "tagger $author <$author>\n" + "tagger $author_name <$author_email>\n" or die "Cannot create tag object $xtag: $!\n"; close($out) or die "Cannot create tag object $xtag: $!\n"; @@ -683,7 +683,11 @@ while() { $state=3; } elsif($state == 3 and s/^Author:\s+//) { s/\s+$//; - $author = $_; + if (/^(.*?)\s+<(.*)>/) { + ($author_name, $author_email) = ($1, $2); + } else { + $author_name = $author_email = $_; + } $state = 4; } elsif($state == 4 and s/^Branch:\s+//) { s/\s+$//; diff --git a/git-fetch.sh b/git-fetch.sh index 27407c1d35..61da6a9e31 100755 --- a/git-fetch.sh +++ b/git-fetch.sh @@ -5,6 +5,7 @@ _x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' _x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +tags= append= force= update_head_ok= @@ -17,6 +18,9 @@ do -f|--f|--fo|--for|--forc|--force) force=t ;; + -t|--t|--ta|--tag|--tags) + tags=t + ;; -u|--u|--up|--upd|--upda|--updat|--update|--update-|--update-h|\ --update-he|--update-hea|--update-head|--update-head-|\ --update-head-o|--update-head-ok) @@ -158,7 +162,26 @@ case "$update_head_ok" in ;; esac -for ref in $(get_remote_refs_for_fetch "$@") +# If --tags (and later --heads or --all) is specified, then we are +# not talking about defaults stored in Pull: line of remotes or +# branches file, and just fetch those and refspecs explicitly given. +# Otherwise we do what we always did. + +reflist=$(get_remote_refs_for_fetch "$@") +if test "$tags" +then + taglist=$(git-ls-remote --tags "$remote" | awk '{ print "."$2":"$2 }') + if test "$#" -gt 1 + then + # remote URL plus explicit refspecs; we need to merge them. + reflist="$reflist $taglist" + else + # No explicit refspecs; fetch tags only. + reflist=$taglist + fi +fi + +for ref in $reflist do refs="$refs $ref" diff --git a/git-merge-recursive.py b/git-merge-recursive.py index 689f91430b..b80a860357 100755 --- a/git-merge-recursive.py +++ b/git-merge-recursive.py @@ -7,9 +7,6 @@ from sets import Set sys.path.append('@@GIT_PYTHON_PATH@@') from gitMergeCommon import * -# The actual merge code -# --------------------- - originalIndexFile = os.environ.get('GIT_INDEX_FILE', os.environ.get('GIT_DIR', '.git') + '/index') temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \ @@ -21,11 +18,23 @@ def setupIndex(temporary): pass if temporary: newIndex = temporaryIndexFile - os.environ else: newIndex = originalIndexFile os.environ['GIT_INDEX_FILE'] = newIndex +# This is a global variable which is used in a number of places but +# only written to in the 'merge' function. + +# cacheOnly == True => Don't leave any non-stage 0 entries in the cache and +# don't update the working directory. +# False => Leave unmerged entries in the cache and update +# the working directory. + +cacheOnly = False + +# The entry point to the merge code +# --------------------------------- + def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0): '''Merge the commits h1 and h2, return the resulting virtual commit object and a flag indicating the cleaness of the merge.''' @@ -35,6 +44,7 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0): def infoMsg(*args): sys.stdout.write(' '*callDepth) printList(args) + infoMsg('Merging:') infoMsg(h1) infoMsg(h2) @@ -46,27 +56,27 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0): infoMsg(x) sys.stdout.flush() - Ms = ca[0] + mergedCA = ca[0] for h in ca[1:]: - [Ms, ignore] = merge(Ms, h, - 'Temporary shared merge branch 1', - 'Temporary shared merge branch 2', - graph, callDepth+1) - assert(isinstance(Ms, Commit)) + [mergedCA, dummy] = merge(mergedCA, h, + 'Temporary shared merge branch 1', + 'Temporary shared merge branch 2', + graph, callDepth+1) + assert(isinstance(mergedCA, Commit)) + global cacheOnly if callDepth == 0: setupIndex(False) - cleanCache = False + cacheOnly = False else: setupIndex(True) runProgram(['git-read-tree', h1.tree()]) - cleanCache = True + cacheOnly = True - [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(), - branch1Name, branch2Name, - cleanCache) + [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(), + branch1Name, branch2Name) - if clean or cleanCache: + if clean or cacheOnly: res = Commit(None, [h1, h2], tree=shaRes) graph.addNode(res) else: @@ -89,16 +99,255 @@ def getFilesAndDirs(tree): return [files, dirs] +# Those two global variables are used in a number of places but only +# written to in 'mergeTrees' and 'uniquePath'. They keep track of +# every file and directory in the two branches that are about to be +# merged. +currentFileSet = None +currentDirectorySet = None + +def mergeTrees(head, merge, common, branch1Name, branch2Name): + '''Merge the trees 'head' and 'merge' with the common ancestor + 'common'. The name of the head branch is 'branch1Name' and the name of + the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge) + where tree is the resulting tree and cleanMerge is True iff the + merge was clean.''' + + assert(isSha(head) and isSha(merge) and isSha(common)) + + if common == merge: + print 'Already uptodate!' + return [head, True] + + if cacheOnly: + updateArg = '-i' + else: + updateArg = '-u' + + [out, code] = runProgram(['git-read-tree', updateArg, '-m', + common, head, merge], returnCode = True) + if code != 0: + die('git-read-tree:', out) + + [tree, code] = runProgram('git-write-tree', returnCode=True) + tree = tree.rstrip() + if code != 0: + global currentFileSet, currentDirectorySet + [currentFileSet, currentDirectorySet] = getFilesAndDirs(head) + [filesM, dirsM] = getFilesAndDirs(merge) + currentFileSet.union_update(filesM) + currentDirectorySet.union_update(dirsM) + + entries = unmergedCacheEntries() + renamesHead = getRenames(head, common, head, merge, entries) + renamesMerge = getRenames(merge, common, head, merge, entries) + + cleanMerge = processRenames(renamesHead, renamesMerge, + branch1Name, branch2Name) + for entry in entries: + if entry.processed: + continue + if not processEntry(entry, branch1Name, branch2Name): + cleanMerge = False + + if cleanMerge or cacheOnly: + tree = runProgram('git-write-tree').rstrip() + else: + tree = None + else: + cleanMerge = True + + return [tree, cleanMerge] + +# Low level file merging, update and removal +# ------------------------------------------ + +def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode, + branch1Name, branch2Name): + + merge = False + clean = True + + if stat.S_IFMT(aMode) != stat.S_IFMT(bMode): + clean = False + if stat.S_ISREG(aMode): + mode = aMode + sha = aSha + else: + mode = bMode + sha = bSha + else: + if aSha != oSha and bSha != oSha: + merge = True + + if aMode == oMode: + mode = bMode + else: + mode = aMode + + if aSha == oSha: + sha = bSha + elif bSha == oSha: + sha = aSha + elif stat.S_ISREG(aMode): + assert(stat.S_ISREG(bMode)) + + orig = runProgram(['git-unpack-file', oSha]).rstrip() + src1 = runProgram(['git-unpack-file', aSha]).rstrip() + src2 = runProgram(['git-unpack-file', bSha]).rstrip() + [out, code] = runProgram(['merge', + '-L', branch1Name + '/' + aPath, + '-L', 'orig/' + oPath, + '-L', branch2Name + '/' + bPath, + src1, orig, src2], returnCode=True) + + sha = runProgram(['git-hash-object', '-t', 'blob', '-w', + src1]).rstrip() + + os.unlink(orig) + os.unlink(src1) + os.unlink(src2) + + clean = (code == 0) + else: + assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode)) + sha = aSha + + if aSha != bSha: + clean = False + + return [sha, mode, clean, merge] + +def updateFile(clean, sha, mode, path): + updateCache = cacheOnly or clean + updateWd = not cacheOnly + + return updateFileExt(sha, mode, path, updateCache, updateWd) + +def updateFileExt(sha, mode, path, updateCache, updateWd): + if cacheOnly: + updateWd = False + + if updateWd: + pathComponents = path.split('/') + for x in xrange(1, len(pathComponents)): + p = '/'.join(pathComponents[0:x]) + + try: + createDir = not stat.S_ISDIR(os.lstat(p).st_mode) + except: + createDir = True + + if createDir: + try: + os.mkdir(p) + except OSError, e: + die("Couldn't create directory", p, e.strerror) + + prog = ['git-cat-file', 'blob', sha] + if stat.S_ISREG(mode): + try: + os.unlink(path) + except OSError: + pass + if mode & 0100: + mode = 0777 + else: + mode = 0666 + fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode) + proc = subprocess.Popen(prog, stdout=fd) + proc.wait() + os.close(fd) + elif stat.S_ISLNK(mode): + linkTarget = runProgram(prog) + os.symlink(linkTarget, path) + else: + assert(False) + + if updateWd and updateCache: + runProgram(['git-update-index', '--add', '--', path]) + elif updateCache: + runProgram(['git-update-index', '--add', '--cacheinfo', + '0%o' % mode, sha, path]) + +def removeFile(clean, path): + updateCache = cacheOnly or clean + updateWd = not cacheOnly + + if updateCache: + runProgram(['git-update-index', '--force-remove', '--', path]) + + if updateWd: + try: + os.unlink(path) + except OSError, e: + if e.errno != errno.ENOENT and e.errno != errno.EISDIR: + raise + +def uniquePath(path, branch): + def fileExists(path): + try: + os.lstat(path) + return True + except OSError, e: + if e.errno == errno.ENOENT: + return False + else: + raise + + newPath = path + '_' + branch + suffix = 0 + while newPath in currentFileSet or \ + newPath in currentDirectorySet or \ + fileExists(newPath): + suffix += 1 + newPath = path + '_' + branch + '_' + str(suffix) + currentFileSet.add(newPath) + return newPath + +# Cache entry management +# ---------------------- + class CacheEntry: def __init__(self, path): class Stage: def __init__(self): self.sha1 = None self.mode = None + + # Used for debugging only + def __str__(self): + if self.mode != None: + m = '0%o' % self.mode + else: + m = 'None' + + if self.sha1: + sha1 = self.sha1 + else: + sha1 = 'None' + return 'sha1: ' + sha1 + ' mode: ' + m - self.stages = [Stage(), Stage(), Stage()] + self.stages = [Stage(), Stage(), Stage(), Stage()] self.path = path + self.processed = False + + def __str__(self): + return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages]) +class CacheEntryContainer: + def __init__(self): + self.entries = {} + + def add(self, entry): + self.entries[entry.path] = entry + + def get(self, path): + return self.entries.get(path) + + def __iter__(self): + return self.entries.itervalues() + unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S) def unmergedCacheEntries(): '''Create a dictionary mapping file names to CacheEntry @@ -108,155 +357,340 @@ def unmergedCacheEntries(): lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0') lines.pop() - res = {} + res = CacheEntryContainer() for l in lines: m = unmergedRE.match(l) if m: mode = int(m.group(1), 8) sha1 = m.group(2) - stage = int(m.group(3)) - 1 + stage = int(m.group(3)) path = m.group(4) - if res.has_key(path): - e = res[path] - else: + e = res.get(path) + if not e: e = CacheEntry(path) - res[path] = e - + res.add(e) + e.stages[stage].mode = mode e.stages[stage].sha1 = sha1 else: - die('Error: Merge program failed: Unexpected output from', \ + die('Error: Merge program failed: Unexpected output from', 'git-ls-files:', l) return res -def mergeTrees(head, merge, common, branch1Name, branch2Name, - cleanCache): - '''Merge the trees 'head' and 'merge' with the common ancestor - 'common'. The name of the head branch is 'branch1Name' and the name of - the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge) - where tree is the resulting tree and cleanMerge is True iff the - merge was clean.''' +lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S) +def getCacheEntry(path, origTree, aTree, bTree): + '''Returns a CacheEntry object which doesn't have to correspond to + a real cache entry in Git's index.''' - assert(isSha(head) and isSha(merge) and isSha(common)) + def parse(out): + if out == '': + return [None, None] + else: + m = lsTreeRE.match(out) + if not m: + die('Unexpected output from git-ls-tree:', out) + elif m.group(2) == 'blob': + return [m.group(3), int(m.group(1), 8)] + else: + return [None, None] - if common == merge: - print 'Already uptodate!' - return [head, True] + res = CacheEntry(path) - if cleanCache: - updateArg = '-i' - else: - updateArg = '-u' + [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path])) + [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path])) + [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path])) - [out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True) - if code != 0: - die('git-read-tree:', out) + res.stages[1].sha1 = oSha + res.stages[1].mode = oMode + res.stages[2].sha1 = aSha + res.stages[2].mode = aMode + res.stages[3].sha1 = bSha + res.stages[3].mode = bMode - cleanMerge = True + return res - [tree, code] = runProgram('git-write-tree', returnCode=True) - tree = tree.rstrip() - if code != 0: - [files, dirs] = getFilesAndDirs(head) - [filesM, dirsM] = getFilesAndDirs(merge) - files.union_update(filesM) - dirs.union_update(dirsM) - - cleanMerge = True - entries = unmergedCacheEntries() - for name in entries: - if not processEntry(entries[name], branch1Name, branch2Name, - files, dirs, cleanCache): - cleanMerge = False - - if cleanMerge or cleanCache: - tree = runProgram('git-write-tree').rstrip() +# Rename detection and handling +# ----------------------------- + +class RenameEntry: + def __init__(self, + src, srcSha, srcMode, srcCacheEntry, + dst, dstSha, dstMode, dstCacheEntry, + score): + self.srcName = src + self.srcSha = srcSha + self.srcMode = srcMode + self.srcCacheEntry = srcCacheEntry + self.dstName = dst + self.dstSha = dstSha + self.dstMode = dstMode + self.dstCacheEntry = dstCacheEntry + self.score = score + + self.processed = False + +class RenameEntryContainer: + def __init__(self): + self.entriesSrc = {} + self.entriesDst = {} + + def add(self, entry): + self.entriesSrc[entry.srcName] = entry + self.entriesDst[entry.dstName] = entry + + def getSrc(self, path): + return self.entriesSrc.get(path) + + def getDst(self, path): + return self.entriesDst.get(path) + + def __iter__(self): + return self.entriesSrc.itervalues() + +parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$') +def getRenames(tree, oTree, aTree, bTree, cacheEntries): + '''Get information of all renames which occured between 'oTree' and + 'tree'. We need the three trees in the merge ('oTree', 'aTree' and + 'bTree') to be able to associate the correct cache entries with + the rename information. 'tree' is always equal to either aTree or bTree.''' + + assert(tree == aTree or tree == bTree) + inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r', + '-z', oTree, tree]) + + ret = RenameEntryContainer() + try: + recs = inp.split("\0") + recs.pop() # remove last entry (which is '') + it = recs.__iter__() + while True: + rec = it.next() + m = parseDiffRenamesRE.match(rec) + + if not m: + die('Unexpected output from git-diff-tree:', rec) + + srcMode = int(m.group(1), 8) + dstMode = int(m.group(2), 8) + srcSha = m.group(3) + dstSha = m.group(4) + score = m.group(5) + src = it.next() + dst = it.next() + + srcCacheEntry = cacheEntries.get(src) + if not srcCacheEntry: + srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree) + cacheEntries.add(srcCacheEntry) + + dstCacheEntry = cacheEntries.get(dst) + if not dstCacheEntry: + dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree) + cacheEntries.add(dstCacheEntry) + + ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry, + dst, dstSha, dstMode, dstCacheEntry, + score)) + except StopIteration: + pass + return ret + +def fmtRename(src, dst): + srcPath = src.split('/') + dstPath = dst.split('/') + path = [] + endIndex = min(len(srcPath), len(dstPath)) - 1 + for x in range(0, endIndex): + if srcPath[x] == dstPath[x]: + path.append(srcPath[x]) else: - tree = None + endIndex = x + break + + if len(path) > 0: + return '/'.join(path) + \ + '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \ + '/'.join(dstPath[endIndex:]) + '}' else: - cleanMerge = True + return src + ' => ' + dst - return [tree, cleanMerge] +def processRenames(renamesA, renamesB, branchNameA, branchNameB): + srcNames = Set() + for x in renamesA: + srcNames.add(x.srcName) + for x in renamesB: + srcNames.add(x.srcName) -def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): - '''Merge one cache entry. 'files' is a Set with the files in both of - the heads that we are going to merge. 'dirs' contains the - corresponding data for directories. If 'cleanCache' is True no - non-zero stages will be left in the cache for the path - corresponding to the entry 'entry'.''' + cleanMerge = True + for path in srcNames: + if renamesA.getSrc(path): + renames1 = renamesA + renames2 = renamesB + branchName1 = branchNameA + branchName2 = branchNameB + else: + renames1 = renamesB + renames2 = renamesA + branchName1 = branchNameB + branchName2 = branchNameA + + ren1 = renames1.getSrc(path) + ren2 = renames2.getSrc(path) + + ren1.dstCacheEntry.processed = True + ren1.srcCacheEntry.processed = True + + if ren1.processed: + continue + + ren1.processed = True + removeFile(True, ren1.srcName) + if ren2: + # Renamed in 1 and renamed in 2 + assert(ren1.srcName == ren2.srcName) + ren2.dstCacheEntry.processed = True + ren2.processed = True + + if ren1.dstName != ren2.dstName: + print 'CONFLICT (rename/rename): Rename', \ + fmtRename(path, ren1.dstName), 'in branch', branchName1, \ + 'rename', fmtRename(path, ren2.dstName), 'in', branchName2 + cleanMerge = False -# cleanCache == True => Don't leave any non-stage 0 entries in the cache and -# don't update the working directory -# False => Leave unmerged entries and update the working directory + if ren1.dstName in currentDirectorySet: + dstName1 = uniquePath(ren1.dstName, branchName1) + print ren1.dstName, 'is a directory in', branchName2, \ + 'adding as', dstName1, 'instead.' + removeFile(False, ren1.dstName) + else: + dstName1 = ren1.dstName -# clean == True => non-conflict case -# False => conflict case + if ren2.dstName in currentDirectorySet: + dstName2 = uniquePath(ren2.dstName, branchName2) + print ren2.dstName, 'is a directory in', branchName1, \ + 'adding as', dstName2, 'instead.' + removeFile(False, ren2.dstName) + else: + dstName2 = ren1.dstName -# If cleanCache == False then the cache shouldn't be updated if clean == False + updateFile(False, ren1.dstSha, ren1.dstMode, dstName1) + updateFile(False, ren2.dstSha, ren2.dstMode, dstName2) + else: + print 'Renaming', fmtRename(path, ren1.dstName) + [resSha, resMode, clean, merge] = \ + mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode, + ren1.dstName, ren1.dstSha, ren1.dstMode, + ren2.dstName, ren2.dstSha, ren2.dstMode, + branchName1, branchName2) + + if merge: + print 'Auto-merging', ren1.dstName + + if not clean: + print 'CONFLICT (content): merge conflict in', ren1.dstName + cleanMerge = False + + if not cacheOnly: + updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName, + updateCache=True, updateWd=False) + updateFile(clean, resSha, resMode, ren1.dstName) + else: + # Renamed in 1, maybe changed in 2 + if renamesA == renames1: + stage = 3 + else: + stage = 2 + + srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1 + srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode + + dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1 + dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode + + tryMerge = False + + if ren1.dstName in currentDirectorySet: + newPath = uniquePath(ren1.dstName, branchName1) + print 'CONFLICT (rename/directory): Rename', \ + fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,\ + 'directory', ren1.dstName, 'added in', branchName2 + print 'Renaming', ren1.srcName, 'to', newPath, 'instead' + cleanMerge = False + removeFile(False, ren1.dstName) + updateFile(False, ren1.dstSha, ren1.dstMode, newPath) + elif srcShaOtherBranch == None: + print 'CONFLICT (rename/delete): Rename', \ + fmtRename(ren1.srcName, ren1.dstName), 'in', \ + branchName1, 'and deleted in', branchName2 + cleanMerge = False + updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName) + elif dstShaOtherBranch: + newPath = uniquePath(ren1.dstName, branchName2) + print 'CONFLICT (rename/add): Rename', \ + fmtRename(ren1.srcName, ren1.dstName), 'in', \ + branchName1 + '.', ren1.dstName, 'added in', branchName2 + print 'Adding as', newPath, 'instead' + updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath) + cleanMerge = False + tryMerge = True + elif renames2.getDst(ren1.dstName): + dst2 = renames2.getDst(ren1.dstName) + newPath1 = uniquePath(ren1.dstName, branchName1) + newPath2 = uniquePath(dst2.dstName, branchName2) + print 'CONFLICT (rename/rename): Rename', \ + fmtRename(ren1.srcName, ren1.dstName), 'in', \ + branchName1+'. Rename', \ + fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2 + print 'Renaming', ren1.srcName, 'to', newPath1, 'and', \ + dst2.srcName, 'to', newPath2, 'instead' + removeFile(False, ren1.dstName) + updateFile(False, ren1.dstSha, ren1.dstMode, newPath1) + updateFile(False, dst2.dstSha, dst2.dstMode, newPath2) + dst2.processed = True + cleanMerge = False + else: + tryMerge = True - def updateFile(clean, sha, mode, path, onlyWd=False): - updateCache = not onlyWd and (cleanCache or (not cleanCache and clean)) - updateWd = onlyWd or (not cleanCache and clean) + if tryMerge: + print 'Renaming', fmtRename(ren1.srcName, ren1.dstName) + [resSha, resMode, clean, merge] = \ + mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode, + ren1.dstName, ren1.dstSha, ren1.dstMode, + ren1.srcName, srcShaOtherBranch, srcModeOtherBranch, + branchName1, branchName2) - if updateWd: - prog = ['git-cat-file', 'blob', sha] - if stat.S_ISREG(mode): - try: - os.unlink(path) - except OSError: - pass - if mode & 0100: - mode = 0777 - else: - mode = 0666 - fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode) - proc = subprocess.Popen(prog, stdout=fd) - proc.wait() - os.close(fd) - elif stat.S_ISLNK(mode): - linkTarget = runProgram(prog) - os.symlink(linkTarget, path) - else: - assert(False) + if merge: + print 'Auto-merging', ren1.dstName - if updateWd and updateCache: - runProgram(['git-update-index', '--add', '--', path]) - elif updateCache: - runProgram(['git-update-index', '--add', '--cacheinfo', - '0%o' % mode, sha, path]) + if not clean: + print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName + cleanMerge = False - def removeFile(clean, path): - if cleanCache or (not cleanCache and clean): - runProgram(['git-update-index', '--force-remove', '--', path]) + if not cacheOnly: + updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName, + updateCache=True, updateWd=False) + updateFile(clean, resSha, resMode, ren1.dstName) - if not cleanCache and clean: - try: - os.unlink(path) - except OSError, e: - if e.errno != errno.ENOENT and e.errno != errno.EISDIR: - raise + return cleanMerge - def uniquePath(path, branch): - newPath = path + '_' + branch - suffix = 0 - while newPath in files or newPath in dirs: - suffix += 1 - newPath = path + '_' + branch + '_' + str(suffix) - files.add(newPath) - return newPath +# Per entry merge function +# ------------------------ - debug('processing', entry.path, 'clean cache:', cleanCache) +def processEntry(entry, branch1Name, branch2Name): + '''Merge one cache entry.''' + + debug('processing', entry.path, 'clean cache:', cacheOnly) cleanMerge = True path = entry.path - oSha = entry.stages[0].sha1 - oMode = entry.stages[0].mode - aSha = entry.stages[1].sha1 - aMode = entry.stages[1].mode - bSha = entry.stages[2].sha1 - bMode = entry.stages[2].mode + oSha = entry.stages[1].sha1 + oMode = entry.stages[1].mode + aSha = entry.stages[2].sha1 + aMode = entry.stages[2].mode + bSha = entry.stages[3].sha1 + bMode = entry.stages[3].mode assert(oSha == None or isSha(oSha)) assert(aSha == None or isSha(aSha)) @@ -275,28 +709,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): (not aSha and bSha == oSha): # Deleted in both or deleted in one and unchanged in the other if aSha: - print 'Removing ' + path + print 'Removing', path removeFile(True, path) else: # Deleted in one and changed in the other cleanMerge = False if not aSha: - print 'CONFLICT (del/mod): "' + path + '" deleted in', \ - branch1Name, 'and modified in', branch2Name, \ - '. Version', branch2Name, ' of "' + path + \ - '" left in tree' + print 'CONFLICT (delete/modify):', path, 'deleted in', \ + branch1Name, 'and modified in', branch2Name + '.', \ + 'Version', branch2Name, 'of', path, 'left in tree.' mode = bMode sha = bSha else: - print 'CONFLICT (mod/del): "' + path + '" deleted in', \ - branch2Name, 'and modified in', branch1Name + \ - '. Version', branch1Name, 'of "' + path + \ - '" left in tree' + print 'CONFLICT (modify/delete):', path, 'deleted in', \ + branch2Name, 'and modified in', branch1Name + '.', \ + 'Version', branch1Name, 'of', path, 'left in tree.' mode = aMode sha = aSha updateFile(False, sha, mode, path) - + elif (not oSha and aSha and not bSha) or \ (not oSha and not aSha and bSha): # @@ -307,27 +739,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): otherBranch = branch2Name mode = aMode sha = aSha - conf = 'file/dir' + conf = 'file/directory' else: addBranch = branch2Name otherBranch = branch1Name mode = bMode sha = bSha - conf = 'dir/file' + conf = 'directory/file' - if path in dirs: + if path in currentDirectorySet: cleanMerge = False newPath = uniquePath(path, addBranch) - print 'CONFLICT (' + conf + \ - '): There is a directory with name "' + path + '" in', \ - otherBranch + '. Adding "' + path + '" as "' + newPath + '"' + print 'CONFLICT (' + conf + '):', \ + 'There is a directory with name', path, 'in', \ + otherBranch + '. Adding', path, 'as', newPath removeFile(False, path) - path = newPath + updateFile(False, sha, mode, newPath) else: - print 'Adding "' + path + '"' - - updateFile(True, sha, mode, path) + print 'Adding', path + updateFile(True, sha, mode, path) elif not oSha and aSha and bSha: # @@ -336,10 +767,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): if aSha == bSha: if aMode != bMode: cleanMerge = False - print 'CONFLICT: File "' + path + \ - '" added identically in both branches,', \ - 'but permissions conflict', '0%o' % aMode, '->', \ - '0%o' % bMode + print 'CONFLICT: File', path, \ + 'added identically in both branches, but permissions', \ + 'conflict', '0%o' % aMode, '->', '0%o' % bMode print 'CONFLICT: adding with permission:', '0%o' % aMode updateFile(False, aSha, aMode, path) @@ -350,8 +780,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): cleanMerge = False newPath1 = uniquePath(path, branch1Name) newPath2 = uniquePath(path, branch2Name) - print 'CONFLICT (add/add): File "' + path + \ - '" added non-identically in both branches.' + print 'CONFLICT (add/add): File', path, \ + 'added non-identically in both branches. Adding as', \ + newPath1, 'and', newPath2, 'instead.' removeFile(False, path) updateFile(False, aSha, aMode, newPath1) updateFile(False, bSha, bMode, newPath2) @@ -360,39 +791,24 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): # # case D: Modified in both, but differently. # - print 'Auto-merging', path - orig = runProgram(['git-unpack-file', oSha]).rstrip() - src1 = runProgram(['git-unpack-file', aSha]).rstrip() - src2 = runProgram(['git-unpack-file', bSha]).rstrip() - [out, ret] = runProgram(['merge', - '-L', branch1Name + '/' + path, - '-L', 'orig/' + path, - '-L', branch2Name + '/' + path, - src1, orig, src2], returnCode=True) - - if aMode == oMode: - mode = bMode + print 'Auto-merging', path + [sha, mode, clean, dummy] = \ + mergeFile(path, oSha, oMode, + path, aSha, aMode, + path, bSha, bMode, + branch1Name, branch2Name) + if clean: + updateFile(True, sha, mode, path) else: - mode = aMode - - sha = runProgram(['git-hash-object', '-t', 'blob', '-w', - src1]).rstrip() - - if ret != 0: cleanMerge = False - print 'CONFLICT (content): Merge conflict in "' + path + '".' + print 'CONFLICT (content): Merge conflict in', path - if cleanCache: + if cacheOnly: updateFile(False, sha, mode, path) else: - updateFile(True, aSha, aMode, path) - updateFile(False, sha, mode, path, True) - else: - updateFile(True, sha, mode, path) - - os.unlink(orig) - os.unlink(src1) - os.unlink(src2) + updateFileExt(aSha, aMode, path, + updateCache=True, updateWd=False) + updateFileExt(sha, mode, path, updateCache=False, updateWd=True) else: die("ERROR: Fatal merge failure, shouldn't happen.") @@ -416,7 +832,7 @@ for nextArg in xrange(1, len(sys.argv)): try: h1 = firstBranch = sys.argv[nextArg + 1] h2 = secondBranch = sys.argv[nextArg + 2] - except IndexError: + except IndexError: usage() break @@ -428,8 +844,8 @@ try: graph = buildGraph([h1, h2]) - [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], - firstBranch, secondBranch, graph) + [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], + firstBranch, secondBranch, graph) print '' except: diff --git a/git-merge.sh b/git-merge.sh index 29e86c6953..d12a2a93b1 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -123,10 +123,30 @@ case "$#,$common" in dropsave exit 0 ;; -1,*) +1,?*"$LF"?*) # We are not doing octopus and not fast forward. Need a # real merge. ;; +1,*) + # We are not doing octopus, not fast forward, and have only + # one common. See if it is really trivial. + echo "Trying really trivial in-index merge..." + git-update-index --refresh 2>/dev/null + if git-read-tree --trivial -m -u $common $head "$1" && + result_tree=$(git-write-tree) + then + echo "Wonderful." + result_commit=$( + echo "$merge_msg" | + git-commit-tree $result_tree -p HEAD -p "$1" + ) || exit + git-update-ref HEAD $result_commit $head + summary $result_commit + dropsave + exit 0 + fi + echo "Nope." + ;; *) # An octopus. If we can reach all the remote we are up to date. up_to_date=t diff --git a/git-sh-setup.sh b/git-sh-setup.sh index 55db795843..a0172686a9 100755 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -13,10 +13,13 @@ unset CDPATH die() { - echo "$@" >&2 + echo >&2 "$@" exit 1 } -[ -h "$GIT_DIR/HEAD" ] && +case "$(GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD 2>/dev/null)" in +refs/*) : ;; +*) false ;; +esac && [ -d "$GIT_DIR/refs" ] && [ -d "$GIT_OBJECT_DIRECTORY/00" ] diff --git a/git-status.sh b/git-status.sh index 621fa49d2b..44398d760c 100755 --- a/git-status.sh +++ b/git-status.sh @@ -31,15 +31,15 @@ report () { [ "$header" ] } -branch=`readlink "$GIT_DIR/HEAD"` +branch=$(GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD) case "$branch" in refs/heads/master) ;; *) echo "# On branch $branch" ;; esac -git-update-index --refresh >/dev/null 2>&1 +git-update-index -q --unmerged --refresh || exit -if test -f "$GIT_DIR/HEAD" +if GIT_DIR="$GIT_DIR" git-rev-parse --verify HEAD >/dev/null 2>&1 then git-diff-index -M --cached HEAD | sed 's/^://' | diff --git a/git.sh b/git.sh index 2986f08ce2..7400c16257 100755 --- a/git.sh +++ b/git.sh @@ -26,17 +26,50 @@ esac echo "Usage: git COMMAND [OPTIONS] [TARGET]" if [ -n "$cmd" ]; then - echo " git command '$cmd' not found: commands are:" -else - echo " git commands are:" + echo "git command '$cmd' not found." fi +echo "git commands are:" -cat <<\EOF - add apply archimport bisect branch checkout cherry clone - commit count-objects cvsimport diff fetch format-patch - fsck-cache get-tar-commit-id init-db log ls-remote octopus - pack-objects parse-remote patch-id prune pull push rebase - relink rename repack request-pull reset resolve revert - send-email shortlog show-branch status tag verify-tag - whatchanged +fmt <<\EOF | sed -e 's/^/ /' +add +apply +archimport +bisect +branch +checkout +cherry +clone +commit +count-objects +cvsimport +diff +fetch +format-patch +fsck-objects +get-tar-commit-id +init-db +log +ls-remote +octopus +pack-objects +parse-remote +patch-id +prune +pull +push +rebase +relink +rename +repack +request-pull +reset +resolve +revert +send-email +shortlog +show-branch +status +tag +verify-tag +whatchanged EOF diff --git a/http-fetch.c b/http-fetch.c index 0566a9125c..71a8c60b56 100644 --- a/http-fetch.c +++ b/http-fetch.c @@ -1,6 +1,6 @@ #include "cache.h" #include "commit.h" - +#include "pack.h" #include "fetch.h" #include @@ -13,8 +13,12 @@ #define curl_global_init(a) do { /* nothing */ } while(0) #endif +#define PREV_BUF_SIZE 4096 +#define RANGE_HEADER_SIZE 30 + static CURL *curl; static struct curl_slist *no_pragma_header; +static struct curl_slist *no_range_header; static char curl_errorstr[CURL_ERROR_SIZE]; static char *initial_base; @@ -87,12 +91,37 @@ void prefetch(unsigned char *sha1) { } +int relink_or_rename(char *old, char *new) { + int ret; + + ret = link(old, new); + if (ret < 0) { + /* Same Coda hack as in write_sha1_file(sha1_file.c) */ + ret = errno; + if (ret == EXDEV && !rename(old, new)) + return 0; + } + unlink(old); + if (ret) { + if (ret != EEXIST) + return ret; + } + + return 0; +} + static int got_alternates = 0; static int fetch_index(struct alt_base *repo, unsigned char *sha1) { char *filename; char *url; + char tmpfile[PATH_MAX]; + int ret; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + CURLcode curl_result; FILE *indexfile; @@ -108,7 +137,8 @@ static int fetch_index(struct alt_base *repo, unsigned char *sha1) repo->base, sha1_to_hex(sha1)); filename = sha1_pack_index_name(sha1); - indexfile = fopen(filename, "w"); + snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename); + indexfile = fopen(tmpfile, "a"); if (!indexfile) return error("Unable to open local file %s for pack index", filename); @@ -119,13 +149,36 @@ static int fetch_index(struct alt_base *repo, unsigned char *sha1) curl_easy_setopt(curl, CURLOPT_HTTPHEADER, no_pragma_header); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errorstr); - if (curl_easy_perform(curl)) { + /* If there is data present from a previous transfer attempt, + resume where it left off */ + prev_posn = ftell(indexfile); + if (prev_posn>0) { + if (get_verbosely) + fprintf(stderr, + "Resuming fetch of index for pack %s at byte %ld\n", + sha1_to_hex(sha1), prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, range_header); + } + + /* Clear out the Range: header after performing the request, so + other curl requests don't inherit inappropriate header data */ + curl_result = curl_easy_perform(curl); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, no_range_header); + if (curl_result != 0) { fclose(indexfile); return error("Unable to get pack index %s\n%s", url, curl_errorstr); } fclose(indexfile); + + ret = relink_or_rename(tmpfile, filename); + if (ret) + return error("unable to write index filename %s: %s", + filename, strerror(ret)); + return 0; } @@ -306,6 +359,12 @@ static int fetch_pack(struct alt_base *repo, unsigned char *sha1) struct packed_git **lst; FILE *packfile; char *filename; + char tmpfile[PATH_MAX]; + int ret; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + CURLcode curl_result; if (fetch_indices(repo)) return -1; @@ -325,7 +384,8 @@ static int fetch_pack(struct alt_base *repo, unsigned char *sha1) repo->base, sha1_to_hex(target->sha1)); filename = sha1_pack_name(target->sha1); - packfile = fopen(filename, "w"); + snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename); + packfile = fopen(tmpfile, "a"); if (!packfile) return error("Unable to open local file %s for pack", filename); @@ -336,7 +396,24 @@ static int fetch_pack(struct alt_base *repo, unsigned char *sha1) curl_easy_setopt(curl, CURLOPT_HTTPHEADER, no_pragma_header); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errorstr); - if (curl_easy_perform(curl)) { + /* If there is data present from a previous transfer attempt, + resume where it left off */ + prev_posn = ftell(packfile); + if (prev_posn>0) { + if (get_verbosely) + fprintf(stderr, + "Resuming fetch of pack %s at byte %ld\n", + sha1_to_hex(target->sha1), prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, range_header); + } + + /* Clear out the Range: header after performing the request, so + other curl requests don't inherit inappropriate header data */ + curl_result = curl_easy_perform(curl); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, no_range_header); + if (curl_result != 0) { fclose(packfile); return error("Unable to get pack file %s\n%s", url, curl_errorstr); @@ -344,11 +421,18 @@ static int fetch_pack(struct alt_base *repo, unsigned char *sha1) fclose(packfile); + ret = relink_or_rename(tmpfile, filename); + if (ret) + return error("unable to write pack filename %s: %s", + filename, strerror(ret)); + lst = &repo->packs; while (*lst != target) lst = &((*lst)->next); *lst = (*lst)->next; + if (verify_pack(target, 0)) + return -1; install_packed_git(target); return 0; @@ -360,14 +444,33 @@ static int fetch_object(struct alt_base *repo, unsigned char *sha1) char *filename = sha1_file_name(sha1); unsigned char real_sha1[20]; char tmpfile[PATH_MAX]; + char prevfile[PATH_MAX]; int ret; char *url; char *posn; + int prevlocal; + unsigned char prev_buf[PREV_BUF_SIZE]; + ssize_t prev_read = 0; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + CURLcode curl_result; + + snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename); + snprintf(prevfile, sizeof(prevfile), "%s.prev", filename); + + if (unlink(prevfile) && (errno != ENOENT)) + return error("Failed to unlink %s (%s)", + prevfile, strerror(errno)); + if (rename(tmpfile, prevfile) && (errno != ENOENT)) + return error("Failed to rename %s to %s (%s)", + tmpfile, prevfile, strerror(errno)); - snprintf(tmpfile, sizeof(tmpfile), "%s/obj_XXXXXX", - get_object_directory()); + local = open(tmpfile, O_WRONLY | O_CREAT | O_EXCL, 0666); + + /* Note: if another instance starts now, it will turn our new + tmpfile into its prevfile. */ - local = mkstemp(tmpfile); if (local < 0) return error("Couldn't create temporary file %s for %s: %s\n", tmpfile, filename, strerror(errno)); @@ -396,8 +499,57 @@ static int fetch_object(struct alt_base *repo, unsigned char *sha1) curl_easy_setopt(curl, CURLOPT_URL, url); - if (curl_easy_perform(curl)) { - unlink(filename); + /* If a previous temp file is present, process what was already + fetched. */ + prevlocal = open(prevfile, O_RDONLY); + if (prevlocal != -1) { + do { + prev_read = read(prevlocal, prev_buf, PREV_BUF_SIZE); + if (prev_read>0) { + if (fwrite_sha1_file(prev_buf, + 1, + prev_read, + NULL) == prev_read) { + prev_posn += prev_read; + } else { + prev_read = -1; + } + } + } while (prev_read > 0); + close(prevlocal); + } + unlink(prevfile); + + /* Reset inflate/SHA1 if there was an error reading the previous temp + file; also rewind to the beginning of the local file. */ + if (prev_read == -1) { + memset(&stream, 0, sizeof(stream)); + inflateInit(&stream); + SHA1_Init(&c); + if (prev_posn>0) { + prev_posn = 0; + lseek(local, SEEK_SET, 0); + ftruncate(local, 0); + } + } + + /* If we have successfully processed data from a previous fetch + attempt, only fetch the data we don't already have. */ + if (prev_posn>0) { + if (get_verbosely) + fprintf(stderr, + "Resuming fetch of object %s at byte %ld\n", + hex, prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, range_header); + } + + /* Clear out the Range: header after performing the request, so + other curl requests don't inherit inappropriate header data */ + curl_result = curl_easy_perform(curl); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, no_range_header); + if (curl_result != 0) { return error("%s", curl_errorstr); } @@ -413,20 +565,11 @@ static int fetch_object(struct alt_base *repo, unsigned char *sha1) unlink(tmpfile); return error("File %s has bad hash\n", hex); } - ret = link(tmpfile, filename); - if (ret < 0) { - /* Same Coda hack as in write_sha1_file(sha1_file.c) */ - ret = errno; - if (ret == EXDEV && !rename(tmpfile, filename)) - goto out; - } - unlink(tmpfile); - if (ret) { - if (ret != EEXIST) - return error("unable to write sha1 filename %s: %s", - filename, strerror(ret)); - } - out: + ret = relink_or_rename(tmpfile, filename); + if (ret) + return error("unable to write sha1 filename %s: %s", + filename, strerror(ret)); + pull_say("got %s\n", hex); return 0; } @@ -519,6 +662,7 @@ int main(int argc, char **argv) curl = curl_easy_init(); no_pragma_header = curl_slist_append(no_pragma_header, "Pragma:"); + no_range_header = curl_slist_append(no_range_header, "Range:"); curl_ssl_verify = getenv("GIT_SSL_NO_VERIFY") ? 0 : 1; curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, curl_ssl_verify); diff --git a/index.c b/index.c index 87fc7b0387..bdde65f75c 100644 --- a/index.c +++ b/index.c @@ -29,7 +29,7 @@ int hold_index_file_for_update(struct cache_file *cf, const char *path) signal(SIGINT, remove_lock_file_on_signal); atexit(remove_lock_file); } - return open(cf->lockfile, O_RDWR | O_CREAT | O_EXCL, 0600); + return open(cf->lockfile, O_RDWR | O_CREAT | O_EXCL, 0666); } int commit_index_file(struct cache_file *cf) diff --git a/init-db.c b/init-db.c index da2bc8f42b..aabc09f4e1 100644 --- a/init-db.c +++ b/init-db.c @@ -166,6 +166,7 @@ static void create_default_files(const char *git_dir, { unsigned len = strlen(git_dir); static char path[PATH_MAX]; + unsigned char sha1[20]; if (len > sizeof(path)-50) die("insane git directory %s", git_dir); @@ -186,15 +187,14 @@ static void create_default_files(const char *git_dir, /* * Create the default symlink from ".git/HEAD" to the "master" - * branch + * branch, if it does not exist yet. */ strcpy(path + len, "HEAD"); - if (symlink("refs/heads/master", path) < 0) { - if (errno != EEXIST) { - perror(path); + if (read_ref(path, sha1) < 0) { + if (create_symref(path, "refs/heads/master") < 0) exit(1); - } } + path[len] = 0; copy_templates(path, len, template_path); } diff --git a/ls-files.c b/ls-files.c index 956be09350..f47114a168 100644 --- a/ls-files.c +++ b/ls-files.c @@ -530,7 +530,7 @@ static void verify_pathspec(void) static const char ls_files_usage[] = "git-ls-files [-z] [-t] (--[cached|deleted|others|stage|unmerged|killed|modified])* " "[ --ignored ] [--exclude=] [--exclude-from=] " - "[ --exclude-per-directory= ]"; + "[ --exclude-per-directory= ] [--] []*"; int main(int argc, const char **argv) { @@ -544,6 +544,10 @@ int main(int argc, const char **argv) for (i = 1; i < argc; i++) { const char *arg = argv[i]; + if (!strcmp(arg, "--")) { + i++; + break; + } if (!strcmp(arg, "-z")) { line_terminator = 0; continue; diff --git a/mailsplit.c b/mailsplit.c index a3238c20da..7afea1aaca 100644 --- a/mailsplit.c +++ b/mailsplit.c @@ -128,7 +128,7 @@ int main(int argc, char **argv) unsigned long len = parse_email(map, size); assert(len <= size); sprintf(name, "%04d", ++nr); - fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0600); + fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0666); if (fd < 0) { perror(name); exit(1); diff --git a/read-cache.c b/read-cache.c index 0e345bdb2f..d2aebdd6bc 100644 --- a/read-cache.c +++ b/read-cache.c @@ -464,11 +464,15 @@ int read_cache(void) errno = EBUSY; if (active_cache) - return error("more than one cachefile"); + return active_nr; + errno = ENOENT; fd = open(get_index_file(), O_RDONLY); - if (fd < 0) - return (errno == ENOENT) ? 0 : error("open failed"); + if (fd < 0) { + if (errno == ENOENT) + return 0; + die("index file open failed (%s)", strerror(errno)); + } size = 0; // avoid gcc warning map = MAP_FAILED; @@ -480,7 +484,7 @@ int read_cache(void) } close(fd); if (map == MAP_FAILED) - return error("mmap failed"); + die("index file mmap failed (%s)", strerror(errno)); hdr = map; if (verify_hdr(hdr, size) < 0) @@ -501,7 +505,7 @@ int read_cache(void) unmap: munmap(map, size); errno = EINVAL; - return error("verify header failed"); + die("index file corrupt"); } #define WRITE_BUFFER_SIZE 8192 diff --git a/read-tree.c b/read-tree.c index ca808739db..5fdf58d240 100644 --- a/read-tree.c +++ b/read-tree.c @@ -13,6 +13,8 @@ static int merge = 0; static int update = 0; static int index_only = 0; +static int nontrivial_merge = 0; +static int trivial_merges_only = 0; static int head_idx = -1; static int merge_size = 0; @@ -275,6 +277,9 @@ static int unpack_trees(merge_fn_t fn) if (unpack_trees_rec(posns, len, "", fn, &indpos)) return -1; + if (trivial_merges_only && nontrivial_merge) + die("Merge requires file-level merging"); + check_updates(active_cache, active_nr); return 0; } @@ -460,6 +465,8 @@ static int threeway_merge(struct cache_entry **stages) verify_uptodate(index); } + nontrivial_merge = 1; + /* #2, #3, #4, #6, #7, #9, #11. */ count = 0; if (!head_match || !remote_match) { @@ -629,9 +636,9 @@ int main(int argc, char **argv) continue; } - if (!strcmp(arg, "--head")) { - head_idx = stage - 1; - fn = threeway_merge; + if (!strcmp(arg, "--trivial")) { + trivial_merges_only = 1; + continue; } /* "-m" stands for "merge", meaning we start in stage 1 */ @@ -657,7 +664,8 @@ int main(int argc, char **argv) } if ((update||index_only) && !merge) usage(read_tree_usage); - if (merge && !fn) { + + if (merge) { if (stage < 2) die("just how do you expect me to merge %d trees?", stage-1); switch (stage - 1) { @@ -674,9 +682,7 @@ int main(int argc, char **argv) fn = threeway_merge; break; } - } - if (head_idx < 0) { if (stage - 1 >= 3) head_idx = stage - 2; else diff --git a/refs.c b/refs.c index 161018097d..2aac90ca54 100644 --- a/refs.c +++ b/refs.c @@ -2,19 +2,157 @@ #include "cache.h" #include +#include -static int read_ref(const char *refname, unsigned char *sha1) +/* We allow "recursive" symbolic refs. Only within reason, though */ +#define MAXDEPTH 5 + +#ifndef USE_SYMLINK_HEAD +#define USE_SYMLINK_HEAD 1 +#endif + +int validate_symref(const char *path) { - int ret = -1; - int fd = open(git_path("%s", refname), O_RDONLY); + struct stat st; + char *buf, buffer[256]; + int len, fd; + + if (lstat(path, &st) < 0) + return -1; + + /* Make sure it is a "refs/.." symlink */ + if (S_ISLNK(st.st_mode)) { + len = readlink(path, buffer, sizeof(buffer)-1); + if (len >= 5 && !memcmp("refs/", buffer, 5)) + return 0; + return -1; + } + + /* + * Anything else, just open it and try to see if it is a symbolic ref. + */ + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + len = read(fd, buffer, sizeof(buffer)-1); + close(fd); + + /* + * Is it a symbolic ref? + */ + if (len < 4 || memcmp("ref:", buffer, 4)) + return -1; + buf = buffer + 4; + len -= 4; + while (len && isspace(*buf)) + buf++, len--; + if (len >= 5 && !memcmp("refs/", buffer, 5)) + return 0; + return -1; +} - if (fd >= 0) { - char buffer[60]; - if (read(fd, buffer, sizeof(buffer)) >= 40) - ret = get_sha1_hex(buffer, sha1); +const char *resolve_ref(const char *path, unsigned char *sha1, int reading) +{ + int depth = MAXDEPTH, len; + char buffer[256]; + + for (;;) { + struct stat st; + char *buf; + int fd; + + if (--depth < 0) + return NULL; + + /* Special case: non-existing file. + * Not having the refs/heads/new-branch is OK + * if we are writing into it, so is .git/HEAD + * that points at refs/heads/master still to be + * born. It is NOT OK if we are resolving for + * reading. + */ + if (lstat(path, &st) < 0) { + if (reading || errno != ENOENT) + return NULL; + memset(sha1, 0, 20); + return path; + } + + /* Follow "normalized" - ie "refs/.." symlinks by hand */ + if (S_ISLNK(st.st_mode)) { + len = readlink(path, buffer, sizeof(buffer)-1); + if (len >= 5 && !memcmp("refs/", buffer, 5)) { + path = git_path("%.*s", len, buffer); + continue; + } + } + + /* + * Anything else, just open it and try to use it as + * a ref + */ + fd = open(path, O_RDONLY); + if (fd < 0) + return NULL; + len = read(fd, buffer, sizeof(buffer)-1); close(fd); + + /* + * Is it a symbolic ref? + */ + if (len < 4 || memcmp("ref:", buffer, 4)) + break; + buf = buffer + 4; + len -= 4; + while (len && isspace(*buf)) + buf++, len--; + while (len && isspace(buf[len-1])) + buf[--len] = 0; + path = git_path("%.*s", len, buf); } - return ret; + if (len < 40 || get_sha1_hex(buffer, sha1)) + return NULL; + return path; +} + +int create_symref(const char *git_HEAD, const char *refs_heads_master) +{ +#if USE_SYMLINK_HEAD + unlink(git_HEAD); + return symlink(refs_heads_master, git_HEAD); +#else + const char *lockpath; + char ref[1000]; + int fd, len, written; + + len = snprintf(ref, sizeof(ref), "ref: %s\n", refs_heads_master); + if (sizeof(ref) <= len) { + error("refname too long: %s", refs_heads_master); + return -1; + } + lockpath = mkpath("%s.lock", git_HEAD); + fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666); + written = write(fd, ref, len); + close(fd); + if (written != len) { + unlink(lockpath); + error("Unable to write to %s", lockpath); + return -2; + } + if (rename(lockpath, git_HEAD) < 0) { + unlink(lockpath); + error("Unable to create %s", git_HEAD); + return -3; + } + return 0; +#endif +} + +int read_ref(const char *filename, unsigned char *sha1) +{ + if (resolve_ref(filename, sha1, 1)) + return 0; + return -1; } static int do_for_each_ref(const char *base, int (*fn)(const char *path, const unsigned char *sha1)) @@ -54,7 +192,7 @@ static int do_for_each_ref(const char *base, int (*fn)(const char *path, const u break; continue; } - if (read_ref(path, sha1) < 0) + if (read_ref(git_path("%s", path), sha1) < 0) continue; if (!has_sha1_file(sha1)) continue; @@ -71,7 +209,7 @@ static int do_for_each_ref(const char *base, int (*fn)(const char *path, const u int head_ref(int (*fn)(const char *path, const unsigned char *sha1)) { unsigned char sha1[20]; - if (!read_ref("HEAD", sha1)) + if (!read_ref(git_path("HEAD"), sha1)) return fn("HEAD", sha1); return 0; } @@ -101,33 +239,14 @@ static char *ref_lock_file_name(const char *ref) return ret; } -static int read_ref_file(const char *filename, unsigned char *sha1) { - int fd = open(filename, O_RDONLY); - char hex[41]; - if (fd < 0) { - return error("Couldn't open %s\n", filename); - } - if ((read(fd, hex, 41) < 41) || - (hex[40] != '\n') || - get_sha1_hex(hex, sha1)) { - error("Couldn't read a hash from %s\n", filename); - close(fd); - return -1; - } - close(fd); - return 0; -} - int get_ref_sha1(const char *ref, unsigned char *sha1) { - char *filename; - int retval; + const char *filename; + if (check_ref_format(ref)) return -1; - filename = ref_file_name(ref); - retval = read_ref_file(filename, sha1); - free(filename); - return retval; + filename = git_path("refs/%s", ref); + return read_ref(filename, sha1); } static int lock_ref_file(const char *filename, const char *lock_filename, @@ -140,7 +259,7 @@ static int lock_ref_file(const char *filename, const char *lock_filename, return error("Couldn't open lock file for %s: %s", filename, strerror(errno)); } - retval = read_ref_file(filename, current_sha1); + retval = read_ref(filename, current_sha1); if (old_sha1) { if (retval) { close(fd); diff --git a/setup.c b/setup.c index 9e20160d94..c487d7eb9d 100644 --- a/setup.c +++ b/setup.c @@ -76,18 +76,20 @@ const char **get_pathspec(const char *prefix, const char **pathspec) * Test it it looks like we're at the top * level git directory. We want to see a * - * - a HEAD symlink and a refs/ directory under ".git" * - either a .git/objects/ directory _or_ the proper * GIT_OBJECT_DIRECTORY environment variable + * - a refs/ directory under ".git" + * - either a HEAD symlink or a HEAD file that is formatted as + * a proper "ref:". */ static int is_toplevel_directory(void) { - struct stat st; - - return !lstat(".git/HEAD", &st) && - S_ISLNK(st.st_mode) && - !access(".git/refs/", X_OK) && - (getenv(DB_ENVIRONMENT) || !access(".git/objects/", X_OK)); + if (access(".git/refs/", X_OK) || + access(getenv(DB_ENVIRONMENT) ? + getenv(DB_ENVIRONMENT) : ".git/objects/", X_OK) || + validate_symref(".git/HEAD")) + return 0; + return 1; } const char *setup_git_directory(void) diff --git a/sha1_file.c b/sha1_file.c index 1e847a891a..895c1fab6f 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -20,6 +20,8 @@ #endif #endif +const unsigned char null_sha1[20] = { 0, }; + static unsigned int sha1_file_open_flag = O_NOATIME; static unsigned hexval(char c) diff --git a/sha1_name.c b/sha1_name.c index b4fed924f7..57e6cd3bdf 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -119,21 +119,6 @@ static int get_short_sha1(const char *name, int len, unsigned char *sha1) return -1; } -static int get_sha1_file(const char *path, unsigned char *result) -{ - char buffer[60]; - int fd = open(path, O_RDONLY); - int len; - - if (fd < 0) - return -1; - len = read(fd, buffer, sizeof(buffer)); - close(fd); - if (len < 40) - return -1; - return get_sha1_hex(buffer, result); -} - static int get_sha1_basic(const char *str, int len, unsigned char *sha1) { static const char *prefix[] = { @@ -150,7 +135,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1) for (p = prefix; *p; p++) { char *pathname = git_path("%s/%.*s", *p, len, str); - if (!get_sha1_file(pathname, sha1)) + if (!read_ref(pathname, sha1)) return 0; } diff --git a/show-branch.c b/show-branch.c index 5778a594f4..8429c171cf 100644 --- a/show-branch.c +++ b/show-branch.c @@ -349,6 +349,7 @@ int main(int ac, char **av) int all_heads = 0, all_tags = 0; int all_mask, all_revs, shown_merge_point; char head_path[128]; + const char *head_path_p; int head_path_len; unsigned char head_sha1[20]; int merge_base = 0; @@ -430,11 +431,15 @@ int main(int ac, char **av) if (0 <= extra) join_revs(&list, &seen, num_rev, extra); - head_path_len = readlink(".git/HEAD", head_path, sizeof(head_path)-1); - if ((head_path_len < 0) || get_sha1("HEAD", head_sha1)) + head_path_p = resolve_ref(git_path("HEAD"), head_sha1, 1); + if (head_path_p) { + head_path_len = strlen(head_path_p); + memcpy(head_path, head_path_p, head_path_len + 1); + } + else { + head_path_len = 0; head_path[0] = 0; - else - head_path[head_path_len] = 0; + } if (merge_base) return show_merge_base(seen, num_rev); diff --git a/symbolic-ref.c b/symbolic-ref.c new file mode 100644 index 0000000000..af087d211d --- /dev/null +++ b/symbolic-ref.c @@ -0,0 +1,34 @@ +#include "cache.h" + +static const char git_symbolic_ref_usage[] = +"git-symbolic-ref name [ref]"; + +static int check_symref(const char *HEAD) +{ + unsigned char sha1[20]; + const char *git_HEAD = strdup(git_path("%s", HEAD)); + const char *git_refs_heads_master = resolve_ref(git_HEAD, sha1, 0); + if (git_refs_heads_master) { + /* we want to strip the .git/ part */ + int pfxlen = strlen(git_HEAD) - strlen(HEAD); + puts(git_refs_heads_master + pfxlen); + } + else + die("No such ref: %s", HEAD); +} + +int main(int argc, const char **argv) +{ + setup_git_directory(); + switch (argc) { + case 2: + check_symref(argv[1]); + break; + case 3: + create_symref(strdup(git_path("%s", argv[1])), argv[2]); + break; + default: + usage(git_symbolic_ref_usage); + } + return 0; +} diff --git a/t/Makefile b/t/Makefile index a6b80882ce..e71da7782e 100644 --- a/t/Makefile +++ b/t/Makefile @@ -5,6 +5,7 @@ #GIT_TEST_OPTS=--verbose --debug SHELL_PATH ?= $(SHELL) +TAR ?= $(TAR) T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) diff --git a/t/t3002-ls-files-dashpath.sh b/t/t3002-ls-files-dashpath.sh new file mode 100755 index 0000000000..b42f1382bc --- /dev/null +++ b/t/t3002-ls-files-dashpath.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-files test (-- to terminate the path list). + +This test runs git-ls-files --others with the following on the +filesystem. + + path0 - a file + -foo - a file with a funny name. + -- - another file with a funny name. +' +. ./test-lib.sh + +test_expect_success \ + setup \ + 'echo frotz >path0 && + echo frotz >./-foo && + echo frotz >./--' + +test_expect_success \ + 'git-ls-files without path restriction.' \ + 'git-ls-files --others >output && + diff -u output - <output && + diff -u output - <output && + diff -u output - <output && + diff -u output - <output && + diff -u output - <b.mtime && echo "2005-05-27 22:00:00" >expected.mtime && @@ -63,7 +63,7 @@ test_expect_success \ test_expect_success \ 'extract tar archive' \ - '(cd b && tar xf -) .git/HEAD && + git-update-ref HEAD "$commit" && git-clone -l ./. victim && cd victim && git-log && cd .. && - echo $zero >.git/HEAD && + git-update-ref HEAD "$zero" && parent=$zero && for i in $cnt do @@ -33,7 +33,7 @@ test_expect_success setup ' commit=$(echo "Rebase #$i" | git-commit-tree $tree -p $parent) && parent=$commit || return 1 done && - echo "$commit" >.git/HEAD && + git-update-ref HEAD "$commit" && echo Rebase && git-log' diff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh index d0a4ff29c9..42fcbc60ca 100755 --- a/t/t6002-rev-list-bisect.sh +++ b/t/t6002-rev-list-bisect.sh @@ -7,20 +7,6 @@ test_description='Tests git-rev-list --bisect functionality' . ./test-lib.sh . ../t6000lib.sh # t6xxx specific functions -bc_expr() -{ -bc <=0) { return (x); } else { return (-x); } -} -define floor(x) { - save=scale; scale=0; result=x/1; scale=save; return (result); -} -$* -EOF -} - # usage: test_bisection max-diff bisect-option head ^prune... # # e.g. test_bisection 1 --bisect l1 ^l0 @@ -35,8 +21,19 @@ test_bisection_diff() _head=$1 shift 1 _bisection_size=$(git-rev-list $_bisection "$@" | wc -l) - [ -n "$_list_size" -a -n "$_bisection_size" ] || error "test_bisection_diff failed" - test_expect_success "bisection diff $_bisect_option $_head $* <= $_max_diff" "[ $(bc_expr "floor(abs($_list_size/2)-$_bisection_size)") -le $_max_diff ]" + [ -n "$_list_size" -a -n "$_bisection_size" ] || + error "test_bisection_diff failed" + + # Test if bisection size is close to half of list size within + # tolerance. + # + _bisect_err=`expr $_list_size - $_bisection_size \* 2` + test "$_bisect_err" -lt 0 && _bisect_err=`expr 0 - $_bisect_err` + _bisect_err=`expr $_bisect_err / 2` ; # floor + + test_expect_success \ + "bisection diff $_bisect_option $_head $* <= $_max_diff" \ + 'test $_bisect_err -le $_max_diff' } date >path0 diff --git a/tar-tree.c b/tar-tree.c index 2716ae3eb1..970c4bb54e 100644 --- a/tar-tree.c +++ b/tar-tree.c @@ -353,6 +353,8 @@ static void traverse_tree(void *buffer, unsigned long size, if (size < namelen + 20 || sscanf(buffer, "%o", &mode) != 1) die("corrupt 'tree' file"); + if (S_ISDIR(mode) || S_ISREG(mode)) + mode |= (mode & 0100) ? 0777 : 0666; buffer = sha1 + 20; size -= namelen + 20; diff --git a/update-index.c b/update-index.c index 01eaa1a984..b825a11d2f 100644 --- a/update-index.c +++ b/update-index.c @@ -13,7 +13,7 @@ * like "git-update-index *" and suddenly having all the object * files be revision controlled. */ -static int allow_add = 0, allow_remove = 0, allow_replace = 0, not_new = 0, quiet = 0, info_only = 0; +static int allow_add = 0, allow_remove = 0, allow_replace = 0, allow_unmerged = 0, not_new = 0, quiet = 0, info_only = 0; static int force_remove; /* Three functions to allow overloaded pointer return; see linux/err.h */ @@ -135,7 +135,7 @@ static struct cache_entry *refresh_entry(struct cache_entry *ce) changed = ce_match_stat(ce, &st); if (!changed) - return ce; + return NULL; if (ce_modified(ce, &st)) return ERR_PTR(-EINVAL); @@ -156,16 +156,20 @@ static int refresh_cache(void) struct cache_entry *ce, *new; ce = active_cache[i]; if (ce_stage(ce)) { - printf("%s: needs merge\n", ce->name); - has_errors = 1; while ((i < active_nr) && ! strcmp(active_cache[i]->name, ce->name)) i++; i--; + if (allow_unmerged) + continue; + printf("%s: needs merge\n", ce->name); + has_errors = 1; continue; } new = refresh_entry(ce); + if (!new) + continue; if (IS_ERR(new)) { if (not_new && PTR_ERR(new) == -ENOENT) continue; @@ -335,6 +339,10 @@ int main(int argc, const char **argv) allow_remove = 1; continue; } + if (!strcmp(path, "--unmerged")) { + allow_unmerged = 1; + continue; + } if (!strcmp(path, "--refresh")) { has_errors |= refresh_cache(); continue; @@ -383,9 +391,11 @@ int main(int argc, const char **argv) update_one(buf.buf, prefix, prefix_length); } } - if (write_cache(newfd, active_cache, active_nr) || - commit_index_file(&cache_file)) - die("Unable to write new cachefile"); + if (active_cache_changed) { + if (write_cache(newfd, active_cache, active_nr) || + commit_index_file(&cache_file)) + die("Unable to write new cachefile"); + } return has_errors ? 1 : 0; } diff --git a/update-ref.c b/update-ref.c index 1863b82324..4a1704c1a5 100644 --- a/update-ref.c +++ b/update-ref.c @@ -4,53 +4,6 @@ static const char git_update_ref_usage[] = "git-update-ref []"; -#define MAXDEPTH 5 - -static const char *resolve_ref(const char *path, unsigned char *sha1) -{ - int depth = MAXDEPTH, len; - char buffer[256]; - - for (;;) { - struct stat st; - int fd; - - if (--depth < 0) - return NULL; - - /* Special case: non-existing file */ - if (lstat(path, &st) < 0) { - if (errno != ENOENT) - return NULL; - memset(sha1, 0, 20); - return path; - } - - /* Follow "normalized" - ie "refs/.." symlinks by hand */ - if (S_ISLNK(st.st_mode)) { - len = readlink(path, buffer, sizeof(buffer)-1); - if (len >= 5 && !memcmp("refs/", buffer, 5)) { - path = git_path("%.*s", len, buffer); - continue; - } - } - - /* - * Anything else, just open it and try to use it as - * a ref - */ - fd = open(path, O_RDONLY); - if (fd < 0) - return NULL; - len = read(fd, buffer, sizeof(buffer)-1); - close(fd); - break; - } - if (len < 40 || get_sha1_hex(buffer, sha1)) - return NULL; - return path; -} - static int re_verify(const char *path, unsigned char *oldsha1, unsigned char *currsha1) { char buf[40]; @@ -84,7 +37,7 @@ int main(int argc, char **argv) if (oldval && get_sha1(oldval, oldsha1) < 0) die("%s: not a valid old SHA1", oldval); - path = resolve_ref(git_path("%s", refname), currsha1); + path = resolve_ref(git_path("%s", refname), currsha1, !!oldval); if (!path) die("No such ref: %s", refname); diff --git a/usage.c b/usage.c index 86211c9141..dfa87fe119 100644 --- a/usage.c +++ b/usage.c @@ -15,7 +15,7 @@ static void report(const char *prefix, const char *err, va_list params) void usage(const char *err) { fprintf(stderr, "usage: %s\n", err); - exit(1); + exit(129); } void die(const char *err, ...) @@ -25,7 +25,7 @@ void die(const char *err, ...) va_start(params, err); report("fatal: ", err, params); va_end(params); - exit(1); + exit(128); } int error(const char *err, ...)