summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--Documentation/git-cvsserver.txt89
-rw-r--r--Documentation/git-rm.txt89
-rw-r--r--Documentation/git-svnimport.txt30
-rw-r--r--Makefile13
-rw-r--r--apply.c158
-rw-r--r--blame.c443
-rw-r--r--cache.h4
-rw-r--r--combine-diff.c14
-rw-r--r--commit.c3
-rwxr-xr-xcontrib/git-svn/git-svn.perl57
-rw-r--r--contrib/git-svn/git-svn.txt18
-rwxr-xr-xcontrib/gitview/gitview134
-rw-r--r--diff.c138
-rw-r--r--diffcore-rename.c4
-rw-r--r--environment.c1
-rw-r--r--epoch.c1
-rw-r--r--epoch.h1
-rw-r--r--exec_cmd.c2
-rw-r--r--fetch-pack.c17
-rw-r--r--fsck-objects.c2
-rwxr-xr-xgit-am.sh10
-rwxr-xr-xgit-annotate.perl488
-rwxr-xr-xgit-clone.sh4
-rwxr-xr-xgit-cvsserver.perl2449
-rwxr-xr-xgit-fetch.sh36
-rwxr-xr-xgit-fmt-merge-msg.perl31
-rwxr-xr-xgit-format-patch.sh7
-rwxr-xr-xgit-merge.sh4
-rwxr-xr-xgit-push.sh4
-rwxr-xr-xgit-rm.sh70
-rwxr-xr-xgit-svnimport.perl112
-rw-r--r--git.c153
-rw-r--r--http-fetch.c8
-rw-r--r--ls-files.c7
-rw-r--r--pack-objects.c496
-rw-r--r--pack-redundant.c4
-rw-r--r--read-tree.c64
-rw-r--r--receive-pack.c4
-rw-r--r--refs.c2
-rw-r--r--rev-list.c427
-rw-r--r--rev-parse.c1
-rw-r--r--revision.c383
-rw-r--r--revision.h50
-rw-r--r--send-pack.c48
-rw-r--r--sha1_file.c24
-rwxr-xr-xt/t3600-rm.sh60
-rw-r--r--templates/hooks--applypatch-msg1
-rw-r--r--templates/hooks--pre-applypatch1
-rw-r--r--upload-pack.c11
50 files changed, 5431 insertions, 751 deletions
diff --git a/.gitignore b/.gitignore
index d7e8d2ac0d..5be239a4aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ GIT-VERSION-FILE
git
git-add
git-am
+git-annotate
git-apply
git-applymbox
git-applypatch
@@ -22,6 +23,7 @@ git-convert-objects
git-count-objects
git-cvsexportcommit
git-cvsimport
+git-cvsserver
git-daemon
git-diff
git-diff-files
@@ -53,6 +55,7 @@ git-mailsplit
git-merge
git-merge-base
git-merge-index
+git-merge-tree
git-merge-octopus
git-merge-one-file
git-merge-ours
@@ -60,6 +63,7 @@ git-merge-recursive
git-merge-resolve
git-merge-stupid
git-mktag
+git-mktree
git-name-rev
git-mv
git-pack-redundant
@@ -84,6 +88,7 @@ git-resolve
git-rev-list
git-rev-parse
git-revert
+git-rm
git-send-email
git-send-pack
git-sh-setup
diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt
new file mode 100644
index 0000000000..88f07ff15d
--- /dev/null
+++ b/Documentation/git-cvsserver.txt
@@ -0,0 +1,89 @@
+git-cvsserver(1)
+================
+
+NAME
+----
+git-cvsserver - A CVS server emulator for git
+
+
+SYNOPSIS
+--------
+[verse]
+export CVS_SERVER=git-cvsserver
+'cvs' -d :ext:user@server/path/repo.git co <HEAD_name>
+
+
+DESCRIPTION
+-----------
+
+This application is a CVS emulation layer for git.
+
+It is highly functional. However, not all methods are implemented,
+and for those methods that are implemented,
+not all switches are implemented.
+
+Testing has been done using both the CLI CVS client, and the Eclipse CVS
+plugin. Most functionality works fine with both of these clients.
+
+LIMITATIONS
+-----------
+Currently gitcvs only works over ssh connections.
+
+
+INSTALLATION
+------------
+1. Put server.pl somewhere useful on the same machine that is hosting your git repos
+
+2. For each repo that you want accessible from CVS you need to edit config in
+ the repo and add the following section.
+
+ [gitcvs]
+ enabled=1
+ logfile=/path/to/logfile
+
+ n.b. you need to ensure each user that is going to invoke server.pl has
+ write access to the log file.
+
+5. On each client machine you need to set the following variables.
+ CVSROOT should be set as per normal, but the directory should point at the
+ appropriate git repo.
+ CVS_SERVER should be set to the server.pl script that has been put on the
+ remote machine.
+
+6. Clients should now be able to check out modules (where modules are the names
+ of branches in git).
+ $ cvs co -d mylocaldir master
+
+Operations supported
+--------------------
+
+All the operations required for normal use are supported, including
+checkout, diff, status, update, log, add, remove, commit.
+Legacy monitoring operations are not supported (edit, watch and related).
+Exports and tagging (tags and branches) are not supported at this stage.
+
+The server will set the -k mode to binary when relevant. In proper GIT
+tradition, the contents of the files are always respected.
+No keyword expansion or newline munging is supported.
+
+Dependencies
+------------
+
+git-cvsserver depends on DBD::SQLite.
+
+Copyright and Authors
+---------------------
+
+This program is copyright The Open University UK - 2006.
+
+Authors: Martyn Smith <martyn@catalyst.net.nz>
+ Martin Langhoff <martin@catalyst.net.nz>
+ with ideas and patches from participants of the git-list <git@vger.kernel.org>.
+
+Documentation
+--------------
+Documentation by Martyn Smith <martyn@catalyst.net.nz> and Martin Langhoff <martin@catalyst.net.nz>Matthias Urlichs <smurf@smurf.noris.de>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
new file mode 100644
index 0000000000..401bfb2d9c
--- /dev/null
+++ b/Documentation/git-rm.txt
@@ -0,0 +1,89 @@
+git-rm(1)
+=========
+
+NAME
+----
+git-rm - Remove files from the working tree and from the index.
+
+SYNOPSIS
+--------
+'git-rm' [-f] [-n] [-v] [--] <file>...
+
+DESCRIPTION
+-----------
+A convenience wrapper for git-update-index --remove. For those coming
+from cvs, git-rm provides an operation similar to "cvs rm" or "cvs
+remove".
+
+
+OPTIONS
+-------
+<file>...::
+ Files to remove from the index and optionally, from the
+ working tree as well.
+
+-f::
+ Remove files from the working tree as well as from the index.
+
+-n::
+ Don't actually remove the file(s), just show if they exist in
+ the index.
+
+-v::
+ Be verbose.
+
+--::
+ This option can be used to separate command-line options from
+ the list of files, (useful when filenames might be mistaken
+ for command-line options).
+
+
+DISCUSSION
+----------
+
+The list of <file> given to the command is fed to `git-ls-files`
+command to list files that are registered in the index and
+are not ignored/excluded by `$GIT_DIR/info/exclude` file or
+`.gitignore` file in each directory. This means two things:
+
+. You can put the name of a directory on the command line, and the
+ command will remove all files in it and its subdirectories (the
+ directories themselves are never removed from the working tree);
+
+. Giving the name of a file that is not in the index does not
+ remove that file.
+
+
+EXAMPLES
+--------
+git-rm Documentation/\\*.txt::
+
+ Removes all `\*.txt` files from the index that are under the
+ `Documentation` directory and any of its subdirectories. The
+ files are not removed from the working tree.
++
+Note that the asterisk `\*` is quoted from the shell in this
+example; this lets the command include the files from
+subdirectories of `Documentation/` directory.
+
+git-rm -f git-*.sh::
+
+ Remove all git-*.sh scripts that are in the index. The files
+ are removed from the index, and (because of the -f option),
+ from the working tree as well. Because this example lets the
+ shell expand the asterisk (i.e. you are listing the files
+ explicitly), it does not remove `subdir/git-foo.sh`.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
+
diff --git a/Documentation/git-svnimport.txt b/Documentation/git-svnimport.txt
index 5c543d5d1b..a1588138ea 100644
--- a/Documentation/git-svnimport.txt
+++ b/Documentation/git-svnimport.txt
@@ -10,10 +10,11 @@ git-svnimport - Import a SVN repository into git
SYNOPSIS
--------
'git-svnimport' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] [ -d | -D ]
- [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev]
- [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ]
- [ -s start_chg ] [ -m ] [ -M regex ]
- <SVN_repository_URL> [ <path> ]
+ [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev]
+ [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ]
+ [ -s start_chg ] [ -m ] [ -r ] [ -M regex ]
+ [ -I <ignorefile_name> ] [ -A <author_file> ]
+ <SVN_repository_URL> [ <path> ]
DESCRIPTION
@@ -65,6 +66,27 @@ When importing incrementally, you might need to edit the .git/svn2git file.
Prepend 'rX: ' to commit messages, where X is the imported
subversion revision.
+-I <ignorefile_name>::
+ Import the svn:ignore directory property to files with this
+ name in each directory. (The Subversion and GIT ignore
+ syntaxes are similar enough that using the Subversion patterns
+ directly with "-I .gitignore" will almost always just work.)
+
+-A <author_file>::
+ Read a file with lines on the form
+
+ username = User's Full Name <email@addr.es>
+
+ and use "User's Full Name <email@addr.es>" as the GIT
+ author and committer for Subversion commits made by
+ "username". If encountering a commit made by a user not in the
+ list, abort.
+
+ For convenience, this data is saved to $GIT_DIR/svn-authors
+ each time the -A option is provided, and read from that same
+ file each time git-svnimport is run with an existing GIT
+ repository without -A.
+
-m::
Attempt to detect merges based on the commit message. This option
will enable default regexes that try to capture the name source
diff --git a/Makefile b/Makefile
index 0c04882646..5e93f278fc 100644
--- a/Makefile
+++ b/Makefile
@@ -120,7 +120,7 @@ SCRIPT_SH = \
git-merge-one-file.sh git-parse-remote.sh \
git-prune.sh git-pull.sh git-push.sh git-rebase.sh \
git-repack.sh git-request-pull.sh git-reset.sh \
- git-resolve.sh git-revert.sh git-sh-setup.sh \
+ git-resolve.sh git-revert.sh git-rm.sh git-sh-setup.sh \
git-tag.sh git-verify-tag.sh git-whatchanged.sh \
git-applymbox.sh git-applypatch.sh git-am.sh \
git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \
@@ -130,6 +130,7 @@ SCRIPT_SH = \
SCRIPT_PERL = \
git-archimport.perl git-cvsimport.perl git-relink.perl \
git-shortlog.perl git-fmt-merge-msg.perl git-rerere.perl \
+ git-annotate.perl git-cvsserver.perl \
git-svnimport.perl git-mv.perl git-cvsexportcommit.perl
SCRIPT_PYTHON = \
@@ -152,8 +153,8 @@ PROGRAMS = \
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-index-pack$X git-init-db$X \
- git-local-fetch$X git-ls-files$X git-ls-tree$X git-merge-base$X \
+ git-hash-object$X git-index-pack$X git-init-db$X git-local-fetch$X \
+ git-ls-files$X git-ls-tree$X git-mailinfo$X git-merge-base$X \
git-merge-index$X git-mktag$X git-mktree$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 \
@@ -164,7 +165,7 @@ PROGRAMS = \
git-upload-pack$X git-verify-pack$X git-write-tree$X \
git-update-ref$X git-symbolic-ref$X git-check-ref-format$X \
git-name-rev$X git-pack-redundant$X git-repo-config$X git-var$X \
- git-describe$X git-merge-tree$X
+ git-describe$X git-merge-tree$X git-blame$X
# what 'all' will build and 'install' will install, in gitexecdir
ALL_PROGRAMS = $(PROGRAMS) $(SIMPLE_PROGRAMS) $(SCRIPTS)
@@ -191,7 +192,7 @@ LIB_FILE=libgit.a
LIB_H = \
blob.h cache.h commit.h count-delta.h csum-file.h delta.h \
diff.h epoch.h object.h pack.h pkt-line.h quote.h refs.h \
- run-command.h strbuf.h tag.h tree.h git-compat-util.h
+ run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h
DIFF_OBJS = \
diff.o diffcore-break.o diffcore-order.o diffcore-pathspec.o \
@@ -204,7 +205,7 @@ LIB_OBJS = \
quote.o read-cache.o refs.o run-command.o \
server-info.o setup.o sha1_file.o sha1_name.o strbuf.o \
tag.o tree.o usage.o config.o environment.o ctype.o copy.o \
- fetch-clone.o \
+ fetch-clone.o revision.o \
$(DIFF_OBJS)
LIBS = $(LIB_FILE)
diff --git a/apply.c b/apply.c
index 244718ca13..9deb206faa 100644
--- a/apply.c
+++ b/apply.c
@@ -34,6 +34,56 @@ static int line_termination = '\n';
static const char apply_usage[] =
"git-apply [--stat] [--numstat] [--summary] [--check] [--index] [--apply] [--no-add] [--index-info] [--allow-binary-replacement] [-z] [-pNUM] <patch>...";
+static enum whitespace_eol {
+ nowarn_whitespace,
+ warn_on_whitespace,
+ error_on_whitespace,
+ strip_whitespace,
+} new_whitespace = warn_on_whitespace;
+static int whitespace_error = 0;
+static int squelch_whitespace_errors = 5;
+static int applied_after_stripping = 0;
+static const char *patch_input_file = NULL;
+
+static void parse_whitespace_option(const char *option)
+{
+ if (!option) {
+ new_whitespace = warn_on_whitespace;
+ return;
+ }
+ if (!strcmp(option, "warn")) {
+ new_whitespace = warn_on_whitespace;
+ return;
+ }
+ if (!strcmp(option, "nowarn")) {
+ new_whitespace = nowarn_whitespace;
+ return;
+ }
+ if (!strcmp(option, "error")) {
+ new_whitespace = error_on_whitespace;
+ return;
+ }
+ if (!strcmp(option, "error-all")) {
+ new_whitespace = error_on_whitespace;
+ squelch_whitespace_errors = 0;
+ return;
+ }
+ if (!strcmp(option, "strip")) {
+ new_whitespace = strip_whitespace;
+ return;
+ }
+ die("unrecognized whitespace option '%s'", option);
+}
+
+static void set_default_whitespace_mode(const char *whitespace_option)
+{
+ if (!whitespace_option && !apply_default_whitespace) {
+ new_whitespace = (apply
+ ? warn_on_whitespace
+ : nowarn_whitespace);
+ }
+}
+
/*
* For "diff-stat" like behaviour, we keep track of the biggest change
* we've seen, and the longest filename. That allows us to do simple
@@ -815,6 +865,25 @@ static int parse_fragment(char *line, unsigned long size, struct patch *patch, s
oldlines--;
break;
case '+':
+ /*
+ * We know len is at least two, since we have a '+' and
+ * we checked that the last character was a '\n' above.
+ * That is, an addition of an empty line would check
+ * the '+' here. Sneaky...
+ */
+ if ((new_whitespace != nowarn_whitespace) &&
+ isspace(line[len-2])) {
+ whitespace_error++;
+ if (squelch_whitespace_errors &&
+ squelch_whitespace_errors <
+ whitespace_error)
+ ;
+ else {
+ fprintf(stderr, "Adds trailing whitespace.\n%s:%d:%.*s\n",
+ patch_input_file,
+ linenr, len-2, line+1);
+ }
+ }
added++;
newlines--;
break;
@@ -1092,6 +1161,28 @@ struct buffer_desc {
unsigned long alloc;
};
+static int apply_line(char *output, const char *patch, int plen)
+{
+ /* plen is number of bytes to be copied from patch,
+ * starting at patch+1 (patch[0] is '+'). Typically
+ * patch[plen] is '\n'.
+ */
+ int add_nl_to_tail = 0;
+ if ((new_whitespace == strip_whitespace) &&
+ 1 < plen && isspace(patch[plen-1])) {
+ if (patch[plen] == '\n')
+ add_nl_to_tail = 1;
+ plen--;
+ while (0 < plen && isspace(patch[plen]))
+ plen--;
+ applied_after_stripping++;
+ }
+ memcpy(output, patch + 1, plen);
+ if (add_nl_to_tail)
+ output[plen++] = '\n';
+ return plen;
+}
+
static int apply_one_fragment(struct buffer_desc *desc, struct fragment *frag)
{
char *buf = desc->buffer;
@@ -1127,10 +1218,9 @@ static int apply_one_fragment(struct buffer_desc *desc, struct fragment *frag)
break;
/* Fall-through for ' ' */
case '+':
- if (*patch != '+' || !no_add) {
- memcpy(new + newsize, patch + 1, plen);
- newsize += plen;
- }
+ if (*patch != '+' || !no_add)
+ newsize += apply_line(new + newsize, patch,
+ plen);
break;
case '@': case '\\':
/* Ignore it, we already handled it */
@@ -1699,7 +1789,7 @@ static int use_patch(struct patch *p)
return 1;
}
-static int apply_patch(int fd)
+static int apply_patch(int fd, const char *filename)
{
int newfd;
unsigned long offset, size;
@@ -1707,6 +1797,7 @@ static int apply_patch(int fd)
struct patch *list = NULL, **listp = &list;
int skipped_patch = 0;
+ patch_input_file = filename;
if (!buffer)
return -1;
offset = 0;
@@ -1733,6 +1824,9 @@ static int apply_patch(int fd)
}
newfd = -1;
+ if (whitespace_error && (new_whitespace == error_on_whitespace))
+ apply = 0;
+
write_index = check_index && apply;
if (write_index)
newfd = hold_index_file_for_update(&cache_file, get_index_file());
@@ -1769,17 +1863,28 @@ static int apply_patch(int fd)
return 0;
}
+static int git_apply_config(const char *var, const char *value)
+{
+ if (!strcmp(var, "apply.whitespace")) {
+ apply_default_whitespace = strdup(value);
+ return 0;
+ }
+ return git_default_config(var, value);
+}
+
+
int main(int argc, char **argv)
{
int i;
int read_stdin = 1;
+ const char *whitespace_option = NULL;
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
int fd;
if (!strcmp(arg, "-")) {
- apply_patch(0);
+ apply_patch(0, "<stdin>");
read_stdin = 0;
continue;
}
@@ -1839,11 +1944,18 @@ int main(int argc, char **argv)
line_termination = 0;
continue;
}
+ if (!strncmp(arg, "--whitespace=", 13)) {
+ whitespace_option = arg + 13;
+ parse_whitespace_option(arg + 13);
+ continue;
+ }
if (check_index && prefix_length < 0) {
prefix = setup_git_directory();
prefix_length = prefix ? strlen(prefix) : 0;
- git_config(git_default_config);
+ git_config(git_apply_config);
+ if (!whitespace_option && apply_default_whitespace)
+ parse_whitespace_option(apply_default_whitespace);
}
if (0 < prefix_length)
arg = prefix_filename(prefix, prefix_length, arg);
@@ -1852,10 +1964,38 @@ int main(int argc, char **argv)
if (fd < 0)
usage(apply_usage);
read_stdin = 0;
- apply_patch(fd);
+ set_default_whitespace_mode(whitespace_option);
+ apply_patch(fd, arg);
close(fd);
}
+ set_default_whitespace_mode(whitespace_option);
if (read_stdin)
- apply_patch(0);
+ apply_patch(0, "<stdin>");
+ if (whitespace_error) {
+ if (squelch_whitespace_errors &&
+ squelch_whitespace_errors < whitespace_error) {
+ int squelched =
+ whitespace_error - squelch_whitespace_errors;
+ fprintf(stderr, "warning: squelched %d whitespace error%s\n",
+ squelched,
+ squelched == 1 ? "" : "s");
+ }
+ if (new_whitespace == error_on_whitespace)
+ die("%d line%s add%s trailing whitespaces.",
+ whitespace_error,
+ whitespace_error == 1 ? "" : "s",
+ whitespace_error == 1 ? "s" : "");
+ if (applied_after_stripping)
+ fprintf(stderr, "warning: %d line%s applied after"
+ " stripping trailing whitespaces.\n",
+ applied_after_stripping,
+ applied_after_stripping == 1 ? "" : "s");
+ else if (whitespace_error)
+ fprintf(stderr, "warning: %d line%s add%s trailing"
+ " whitespaces.\n",
+ whitespace_error,
+ whitespace_error == 1 ? "" : "s",
+ whitespace_error == 1 ? "s" : "");
+ }
return 0;
}
diff --git a/blame.c b/blame.c
new file mode 100644
index 0000000000..1e655466a8
--- /dev/null
+++ b/blame.c
@@ -0,0 +1,443 @@
+#include <assert.h>
+
+#include "cache.h"
+#include "refs.h"
+#include "tag.h"
+#include "commit.h"
+#include "tree.h"
+#include "blob.h"
+#include "epoch.h"
+#include "diff.h"
+
+#define DEBUG 0
+
+struct commit** blame_lines;
+int num_blame_lines;
+
+struct util_info
+{
+ int* line_map;
+ int num_lines;
+ unsigned char sha1[20]; /* blob sha, not commit! */
+ char* buf;
+ unsigned long size;
+// const char* path;
+};
+
+struct chunk
+{
+ int off1, len1; // ---
+ int off2, len2; // +++
+};
+
+struct patch
+{
+ struct chunk* chunks;
+ int num;
+};
+
+static void get_blob(struct commit* commit);
+
+int num_get_patch = 0;
+int num_commits = 0;
+
+struct patch* get_patch(struct commit* commit, struct commit* other)
+{
+ struct patch* ret = xmalloc(sizeof(struct patch));
+ ret->chunks = NULL;
+ ret->num = 0;
+
+ struct util_info* info_c = (struct util_info*) commit->object.util;
+ struct util_info* info_o = (struct util_info*) other->object.util;
+
+ if(!memcmp(info_c->sha1, info_o->sha1, 20))
+ return ret;
+
+ get_blob(commit);
+ get_blob(other);
+
+ FILE* fout = fopen("/tmp/git-blame-tmp1", "w");
+ if(!fout)
+ die("fopen tmp1 failed: %s", strerror(errno));
+
+ if(fwrite(info_c->buf, info_c->size, 1, fout) != 1)
+ die("fwrite 1 failed: %s", strerror(errno));
+ fclose(fout);
+
+ fout = fopen("/tmp/git-blame-tmp2", "w");
+ if(!fout)
+ die("fopen tmp2 failed: %s", strerror(errno));
+
+ if(fwrite(info_o->buf, info_o->size, 1, fout) != 1)
+ die("fwrite 2 failed: %s", strerror(errno));
+ fclose(fout);
+
+ FILE* fin = popen("diff -u0 /tmp/git-blame-tmp1 /tmp/git-blame-tmp2", "r");
+ if(!fin)
+ die("popen failed: %s", strerror(errno));
+
+ char buf[1024];
+ while(fgets(buf, sizeof(buf), fin)) {
+ if(buf[0] != '@' || buf[1] != '@')
+ continue;
+
+ if(DEBUG)
+ printf("chunk line: %s", buf);
+ ret->num++;
+ ret->chunks = xrealloc(ret->chunks, sizeof(struct chunk)*ret->num);
+ struct chunk* chunk = &ret->chunks[ret->num-1];
+
+ assert(!strncmp(buf, "@@ -", 4));
+
+ char* start = buf+4;
+ char* sp = index(start, ' ');
+ *sp = '\0';
+ if(index(start, ',')) {
+ int ret = sscanf(start, "%d,%d", &chunk->off1, &chunk->len1);
+ assert(ret == 2);
+ } else {
+ int ret = sscanf(start, "%d", &chunk->off1);
+ assert(ret == 1);
+ chunk->len1 = 1;
+ }
+ *sp = ' ';
+
+ start = sp+1;
+ sp = index(start, ' ');
+ *sp = '\0';
+ if(index(start, ',')) {
+ int ret = sscanf(start, "%d,%d", &chunk->off2, &chunk->len2);
+ assert(ret == 2);
+ } else {
+ int ret = sscanf(start, "%d", &chunk->off2);
+ assert(ret == 1);
+ chunk->len2 = 1;
+ }
+ *sp = ' ';
+
+ if(chunk->off1 > 0)
+ chunk->off1 -= 1;
+ if(chunk->off2 > 0)
+ chunk->off2 -= 1;
+
+ assert(chunk->off1 >= 0);
+ assert(chunk->off2 >= 0);
+ }
+ fclose(fin);
+
+ num_get_patch++;
+ return ret;
+}
+
+void free_patch(struct patch* p)
+{
+ free(p->chunks);
+ free(p);
+}
+
+static int get_blob_sha1_internal(unsigned char *sha1, const char *base, int baselen,
+ const char *pathname, unsigned mode, int stage);
+
+
+static unsigned char blob_sha1[20];
+static int get_blob_sha1(struct tree* t, const char* pathname, unsigned char* sha1)
+{
+ const char *pathspec[2];
+ pathspec[0] = pathname;
+ pathspec[1] = NULL;
+ memset(blob_sha1, 0, sizeof(blob_sha1));
+ read_tree_recursive(t, "", 0, 0, pathspec, get_blob_sha1_internal);
+
+ int i;
+ for(i = 0; i < 20; i++) {
+ if(blob_sha1[i] != 0)
+ break;
+ }
+
+ if(i == 20)
+ return -1;
+
+ memcpy(sha1, blob_sha1, 20);
+ return 0;
+}
+
+static int get_blob_sha1_internal(unsigned char *sha1, const char *base, int baselen,
+ const char *pathname, unsigned mode, int stage)
+{
+// printf("Got blob: %s base: '%s' baselen: %d pathname: '%s' mode: %o stage: %d\n",
+// sha1_to_hex(sha1), base, baselen, pathname, mode, stage);
+
+ if(S_ISDIR(mode))
+ return READ_TREE_RECURSIVE;
+
+ memcpy(blob_sha1, sha1, 20);
+ return -1;
+}
+
+static void get_blob(struct commit* commit)
+{
+ struct util_info* info = commit->object.util;
+ char type[20];
+
+ if(info->buf)
+ return;
+
+ info->buf = read_sha1_file(info->sha1, type, &info->size);
+ assert(!strcmp(type, "blob"));
+}
+
+void print_patch(struct patch* p)
+{
+ printf("Num chunks: %d\n", p->num);
+ int i;
+ for(i = 0; i < p->num; i++) {
+ printf("%d,%d %d,%d\n", p->chunks[i].off1, p->chunks[i].len1, p->chunks[i].off2, p->chunks[i].len2);
+ }
+}
+
+
+// p is a patch from commit to other.
+void fill_line_map(struct commit* commit, struct commit* other, struct patch* p)
+{
+ int num_lines = ((struct util_info*) commit->object.util)->num_lines;
+ int* line_map = ((struct util_info*) commit->object.util)->line_map;
+ int num_lines2 = ((struct util_info*) other->object.util)->num_lines;
+ int* line_map2 = ((struct util_info*) other->object.util)->line_map;
+ int cur_chunk = 0;
+ int i1, i2;
+
+ if(p->num && DEBUG)
+ print_patch(p);
+
+ for(i1 = 0; i1 < num_lines; i1++)
+ line_map[i1] = -1;
+
+ if(DEBUG)
+ printf("num lines 1: %d num lines 2: %d\n", num_lines, num_lines2);
+
+ for(i1 = 0, i2 = 0; i1 < num_lines; i1++, i2++) {
+ if(DEBUG > 1)
+ printf("%d %d\n", i1, i2);
+
+ if(i2 >= num_lines2)
+ break;
+
+ line_map[i1] = line_map2[i2];
+
+ struct chunk* chunk = NULL;
+ if(cur_chunk < p->num)
+ chunk = &p->chunks[cur_chunk];
+
+ if(chunk && chunk->off1 == i1) {
+ i2 = chunk->off2;
+
+ if(chunk->len1 > 0)
+ i1 += chunk->len1-1;
+ if(chunk->len2 > 0)
+ i2 += chunk->len2-1;
+ cur_chunk++;
+ }
+ }
+}
+
+int map_line(struct commit* commit, int line)
+{
+ struct util_info* info = commit->object.util;
+ assert(line >= 0 && line < info->num_lines);
+ return info->line_map[line];
+}
+
+int fill_util_info(struct commit* commit, const char* path)
+{
+ if(commit->object.util)
+ return 0;
+
+ struct util_info* util = xmalloc(sizeof(struct util_info));
+ util->buf = NULL;
+ util->size = 0;
+ util->num_lines = -1;
+ util->line_map = NULL;
+
+ commit->object.util = util;
+
+ if(get_blob_sha1(commit->tree, path, util->sha1))
+ return -1;
+
+ return 0;
+}
+
+void alloc_line_map(struct commit* commit)
+{
+ struct util_info* util = commit->object.util;
+
+ if(util->line_map)
+ return;
+
+ get_blob(commit);
+
+ int i;
+ util->num_lines = 0;
+ for(i = 0; i < util->size; i++) {
+ if(util->buf[i] == '\n')
+ util->num_lines++;
+ }
+ util->line_map = xmalloc(sizeof(int)*util->num_lines);
+}
+
+void copy_line_map(struct commit* dst, struct commit* src)
+{
+ struct util_info* u_dst = dst->object.util;
+ struct util_info* u_src = src->object.util;
+
+ u_dst->line_map = u_src->line_map;
+ u_dst->num_lines = u_src->num_lines;
+ u_dst->buf = u_src->buf;
+ u_dst->size = u_src->size;
+}
+
+void process_commits(struct commit_list* list, const char* path)
+{
+ int i;
+
+ while(list) {
+ struct commit* commit = pop_commit(&list);
+ struct commit_list* parents;
+ struct util_info* info;
+
+ info = commit->object.util;
+ num_commits++;
+ if(DEBUG)
+ printf("\nProcessing commit: %d %s\n", num_commits, sha1_to_hex(commit->object.sha1));
+ for(parents = commit->parents;
+ parents != NULL; parents = parents->next) {
+ struct commit* parent = parents->item;
+
+ if(parse_commit(parent) < 0)
+ die("parse_commit error");
+
+ if(DEBUG)
+ printf("parent: %s\n", sha1_to_hex(parent->object.sha1));
+
+ if(fill_util_info(parent, path))
+ continue;
+
+ // Temporarily assign everything to the parent.
+ int num_blame = 0;
+ for(i = 0; i < num_blame_lines; i++) {
+ if(blame_lines[i] == commit) {
+ num_blame++;
+ blame_lines[i] = parent;
+ }
+ }
+
+ if(num_blame == 0)
+ continue;
+
+ struct patch* patch = get_patch(parent, commit);
+ if(patch->num == 0) {
+ copy_line_map(parent, commit);
+ } else {
+ alloc_line_map(parent);
+ fill_line_map(parent, commit, patch);
+ }
+
+ for(i = 0; i < patch->num; i++) {
+ int l;
+ for(l = 0; l < patch->chunks[i].len2; l++) {
+ int mapped_line = map_line(commit, patch->chunks[i].off2 + l);
+ if(mapped_line != -1 && blame_lines[mapped_line] == parent)
+ blame_lines[mapped_line] = commit;
+ }
+ }
+ free_patch(patch);
+ }
+ }
+}
+
+#define SEEN 1
+struct commit_list* get_commit_list(struct commit* commit, const char* pathname)
+{
+ struct commit_list* ret = NULL;
+ struct commit_list* process = NULL;
+ unsigned char sha1[20];
+
+ commit_list_insert(commit, &process);
+
+ while(process) {
+ struct commit* com = pop_commit(&process);
+ if(com->object.flags & SEEN)
+ continue;
+
+ com->object.flags |= SEEN;
+ commit_list_insert(com, &ret);
+ struct commit_list* parents;
+
+ parse_commit(com);
+
+ for(parents = com->parents;
+ parents != NULL; parents = parents->next) {
+ struct commit* parent = parents->item;
+
+ parse_commit(parent);
+
+ if(!get_blob_sha1(parent->tree, pathname, sha1))
+ commit_list_insert(parent, &process);
+ }
+ }
+
+ return ret;
+}
+
+int main(int argc, const char **argv)
+{
+ unsigned char sha1[20];
+ struct commit *commit;
+ const char* filename;
+ int i;
+
+ setup_git_directory();
+
+ if (argc != 3)
+ die("Usage: blame commit-ish file");
+
+ if (get_sha1(argv[1], sha1))
+ die("get_sha1 failed");
+
+ commit = lookup_commit_reference(sha1);
+
+ filename = argv[2];
+
+ struct commit_list* list = get_commit_list(commit, filename);
+ sort_in_topological_order(&list, 1);
+
+ if(fill_util_info(commit, filename)) {
+ printf("%s not found in %s\n", filename, argv[1]);
+ return 0;
+ }
+ alloc_line_map(commit);
+
+ struct util_info* util = commit->object.util;
+ num_blame_lines = util->num_lines;
+ blame_lines = xmalloc(sizeof(struct commit*)*num_blame_lines);
+
+
+ for(i = 0; i < num_blame_lines; i++) {
+ blame_lines[i] = commit;
+
+ ((struct util_info*) commit->object.util)->line_map[i] = i;
+ }
+
+ process_commits(list, filename);
+
+ for(i = 0; i < num_blame_lines; i++) {
+ printf("%d %s\n", i+1-1, sha1_to_hex(blame_lines[i]->object.sha1));
+// printf("%d %s\n", i+1-1, find_unique_abbrev(blame_lines[i]->object.sha1, 6));
+ }
+
+ if(DEBUG) {
+ printf("num get patch: %d\n", num_get_patch);
+ printf("num commits: %d\n", num_commits);
+ }
+
+ return 0;
+}
diff --git a/cache.h b/cache.h
index 5020f0714a..0d3b244ddd 100644
--- a/cache.h
+++ b/cache.h
@@ -10,7 +10,7 @@
#define deflateBound(c,s) ((s) + (((s) + 7) >> 3) + (((s) + 63) >> 6) + 11)
#endif
-#if defined(DT_UNKNOWN) && !NO_D_TYPE_IN_DIRENT
+#if defined(DT_UNKNOWN) && !defined(NO_D_TYPE_IN_DIRENT)
#define DTYPE(de) ((de)->d_type)
#else
#undef DT_UNKNOWN
@@ -161,11 +161,13 @@ extern int hold_index_file_for_update(struct cache_file *, const char *path);
extern int commit_index_file(struct cache_file *);
extern void rollback_index_file(struct cache_file *);
+/* Environment bits from configuration mechanism */
extern int trust_executable_bit;
extern int assume_unchanged;
extern int only_use_symrefs;
extern int diff_rename_limit_default;
extern int shared_repository;
+extern const char *apply_default_whitespace;
#define GIT_REPO_VERSION 0
extern int repository_format_version;
diff --git a/combine-diff.c b/combine-diff.c
index d812600d11..a23894d869 100644
--- a/combine-diff.c
+++ b/combine-diff.c
@@ -621,7 +621,8 @@ static void reuse_combine_diff(struct sline *sline, unsigned long cnt,
}
static int show_patch_diff(struct combine_diff_path *elem, int num_parent,
- int dense, const char *header)
+ int dense, const char *header,
+ struct diff_options *opt)
{
unsigned long size, cnt, lno;
char *result, *cp, *ep;
@@ -631,6 +632,7 @@ static int show_patch_diff(struct combine_diff_path *elem, int num_parent,
char ourtmp_buf[TMPPATHLEN];
char *ourtmp = ourtmp_buf;
int working_tree_file = !memcmp(elem->sha1, null_sha1, 20);
+ int abbrev = opt->full_index ? 40 : DEFAULT_ABBREV;
/* Read the result of merge first */
if (!working_tree_file) {
@@ -724,7 +726,7 @@ static int show_patch_diff(struct combine_diff_path *elem, int num_parent,
if (header) {
shown_header++;
- puts(header);
+ printf("%s%c", header, opt->line_termination);
}
printf("diff --%s ", dense ? "cc" : "combined");
if (quote_c_style(elem->path, NULL, NULL, 0))
@@ -735,10 +737,10 @@ static int show_patch_diff(struct combine_diff_path *elem, int num_parent,
printf("index ");
for (i = 0; i < num_parent; i++) {
abb = find_unique_abbrev(elem->parent[i].sha1,
- DEFAULT_ABBREV);
+ abbrev);
printf("%s%s", i ? "," : "", abb);
}
- abb = find_unique_abbrev(elem->sha1, DEFAULT_ABBREV);
+ abb = find_unique_abbrev(elem->sha1, abbrev);
printf("..%s\n", abb);
if (mode_differs) {
@@ -797,7 +799,7 @@ static void show_raw_diff(struct combine_diff_path *p, int num_parent, const cha
inter_name_termination = 0;
if (header)
- puts(header);
+ printf("%s%c", header, line_termination);
for (i = 0; i < num_parent; i++) {
if (p->parent[i].mode)
@@ -862,7 +864,7 @@ int show_combined_diff(struct combine_diff_path *p,
default:
case DIFF_FORMAT_PATCH:
- return show_patch_diff(p, num_parent, dense, header);
+ return show_patch_diff(p, num_parent, dense, header, opt);
}
}
diff --git a/commit.c b/commit.c
index c550a00d82..06d5439152 100644
--- a/commit.c
+++ b/commit.c
@@ -212,7 +212,8 @@ int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size)
if (memcmp(bufptr, "tree ", 5))
return error("bogus commit object %s", sha1_to_hex(item->object.sha1));
if (get_sha1_hex(bufptr + 5, parent) < 0)
- return error("bad tree pointer in commit %s\n", sha1_to_hex(item->object.sha1));
+ return error("bad tree pointer in commit %s",
+ sha1_to_hex(item->object.sha1));
item->tree = lookup_tree(parent);
if (item->tree)
n_refs++;
diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl
index a32ce15701..0b7416526d 100755
--- a/contrib/git-svn/git-svn.perl
+++ b/contrib/git-svn/git-svn.perl
@@ -8,7 +8,7 @@ use vars qw/ $AUTHOR $VERSION
$GIT_SVN_INDEX $GIT_SVN
$GIT_DIR $REV_DIR/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
-$VERSION = '0.9.1';
+$VERSION = '0.10.0';
$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
$GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn';
$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
@@ -30,6 +30,7 @@ use File::Basename qw/dirname basename/;
use File::Path qw/mkpath/;
use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
use File::Spec qw//;
+use POSIX qw/strftime/;
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{6,40}/;
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
@@ -49,6 +50,7 @@ my %cmd = (
fetch => [ \&fetch, "Download new revisions from SVN" ],
init => [ \&init, "Initialize and fetch (import)"],
commit => [ \&commit, "Commit git revisions to SVN" ],
+ 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings" ],
rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)" ],
help => [ \&usage, "Show help" ],
);
@@ -258,6 +260,30 @@ sub commit {
}
+sub show_ignore {
+ require File::Find or die $!;
+ my $exclude_file = "$GIT_DIR/info/exclude";
+ open my $fh, '<', $exclude_file or croak $!;
+ chomp(my @excludes = (<$fh>));
+ close $fh or croak $!;
+
+ $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+ chdir $SVN_WC or croak $!;
+ my %ign;
+ File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
+ s#^\./##;
+ @{$ign{$_}} = safe_qx(qw(svn propget svn:ignore),$_);
+ }}, no_chdir=>1},'.');
+
+ print "\n# /\n";
+ foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ }
+ delete $ign{'.'};
+ foreach my $i (sort keys %ign) {
+ print "\n# ",$i,"\n";
+ foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ }
+ }
+}
+
########################### utility functions #########################
sub setup_git_svn {
@@ -566,6 +592,7 @@ sub handle_rmdir {
sub svn_commit_tree {
my ($svn_rev, $commit) = @_;
my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
+ my %log_msg = ( msg => '' );
open my $msg, '>', $commit_msg or croak $!;
chomp(my $type = `git-cat-file -t $commit`);
@@ -581,6 +608,7 @@ sub svn_commit_tree {
if (!$in_msg) {
$in_msg = 1 if (/^\s*$/);
} else {
+ $log_msg{msg} .= $_;
print $msg $_ or croak $!;
}
}
@@ -600,9 +628,30 @@ sub svn_commit_tree {
join("\n",@ci_output),"\n";
my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
- # resync immediately
- my @svn_up = (qw(svn up), "-r$svn_rev");
+ my @svn_up = qw(svn up);
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
+ if ($rev_committed == ($svn_rev + 1)) {
+ push @svn_up, "-r$rev_committed";
+ sys(@svn_up);
+ my $info = svn_info('.');
+ my $date = $info->{'Last Changed Date'} or die "Missing date\n";
+ if ($info->{'Last Changed Rev'} != $rev_committed) {
+ croak "$info->{'Last Changed Rev'} != $rev_committed\n"
+ }
+ my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
+ /(\d{4})\-(\d\d)\-(\d\d)\s
+ (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
+ or croak "Failed to parse date: $date\n";
+ $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
+ $log_msg{author} = $info->{'Last Changed Author'};
+ $log_msg{revision} = $rev_committed;
+ $log_msg{msg} .= "\n";
+ my $parent = file_to_s("$REV_DIR/$svn_rev");
+ git_commit(\%log_msg, $parent, $commit);
+ return $rev_committed;
+ }
+ # resync immediately
+ push @svn_up, "-r$svn_rev";
sys(@svn_up);
return fetch("$rev_committed=$commit")->{revision};
}
@@ -699,7 +748,7 @@ sub svn_info {
# only single-lines seem to exist in svn info output
while (<$info_fh>) {
chomp $_;
- if (m#^([^:]+)\s*:\s*(\S*)$#) {
+ if (m#^([^:]+)\s*:\s*(\S.*)$#) {
$ret->{$1} = $2;
push @{$ret->{-order}}, $1;
}
diff --git a/contrib/git-svn/git-svn.txt b/contrib/git-svn/git-svn.txt
index cf098d733a..b29073997c 100644
--- a/contrib/git-svn/git-svn.txt
+++ b/contrib/git-svn/git-svn.txt
@@ -43,6 +43,11 @@ fetch::
Fetch unfetched revisions from the SVN_URL we are tracking.
refs/heads/git-svn-HEAD will be updated to the latest revision.
+ Note: You should never attempt to modify the git-svn-HEAD branch
+ outside of git-svn. Instead, create a branch from git-svn-HEAD
+ and work on that branch. Use the 'commit' command (see below)
+ to write git commits back to git-svn-HEAD.
+
commit::
Commit specified commit or tree objects to SVN. This relies on
your imported fetch data being up-to-date. This makes
@@ -61,6 +66,11 @@ rebuild::
the directory/repository you're tracking has moved or changed
protocols.
+show-ignore::
+ Recursively finds and lists the svn:ignore property on
+ directories. The output is suitable for appending to
+ the $GIT_DIR/info/exclude file.
+
OPTIONS
-------
-r <ARG>::
@@ -149,9 +159,11 @@ Tracking and contributing to an Subversion managed-project:
# Commit only the git commits you want to SVN::
git-svn commit <tree-ish> [<tree-ish_2> ...]
# Commit all the git commits from my-branch that don't exist in SVN::
- git commit git-svn-HEAD..my-branch
+ git-svn commit git-svn-HEAD..my-branch
# Something is committed to SVN, pull the latest into your branch::
git-svn fetch && git pull . git-svn-HEAD
+# Append svn:ignore settings to the default git exclude file:
+ git-svn show-ignore >> .git/info/exclude
DESIGN PHILOSOPHY
-----------------
@@ -172,7 +184,9 @@ SVN repositories via one git repository. Simply set the GIT_SVN_ID
environment variable to a name other other than "git-svn" (the default)
and git-svn will ignore the contents of the $GIT_DIR/git-svn directory
and instead do all of its work in $GIT_DIR/$GIT_SVN_ID for that
-invocation.
+invocation. The interface branch will be $GIT_SVN_ID-HEAD, instead of
+git-svn-HEAD. Any $GIT_SVN_ID-HEAD branch should never be modified
+by the user outside of git-svn commands.
ADDITIONAL FETCH ARGUMENTS
--------------------------
diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview
index 5c338c0220..048caf6f86 100755
--- a/contrib/gitview/gitview
+++ b/contrib/gitview/gitview
@@ -154,7 +154,7 @@ class CellRendererGraph(gtk.GenericCellRenderer):
cols = self.node[0]
for start, end, colour in self.in_lines + self.out_lines:
- cols = max(cols, start, end)
+ cols = int(max(cols, start, end))
(column, colour, names) = self.node
names_len = 0
@@ -162,7 +162,7 @@ class CellRendererGraph(gtk.GenericCellRenderer):
for item in names:
names_len += len(item)
- width = box_size * (cols + 1 ) + names_len
+ width = box_size * (cols + 1 ) + names_len
height = box_size
# FIXME I have no idea how to use cell_area properly
@@ -261,11 +261,11 @@ class Commit:
children_sha1 = {}
def __init__(self, commit_lines):
- self.message = ""
+ self.message = ""
self.author = ""
- self.date = ""
- self.committer = ""
- self.commit_date = ""
+ self.date = ""
+ self.committer = ""
+ self.commit_date = ""
self.commit_sha1 = ""
self.parent_sha1 = [ ]
self.parse_commit(commit_lines)
@@ -391,7 +391,7 @@ class DiffWindow:
sourceview.show()
- def set_diff(self, commit_sha1, parent_sha1):
+ def set_diff(self, commit_sha1, parent_sha1, encoding):
"""Set the differences showed by this window.
Compares the two trees and populates the window with the
differences.
@@ -401,7 +401,7 @@ class DiffWindow:
return
fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
- self.buffer.set_text(fp.read())
+ self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
fp.close()
self.window.show()
@@ -422,14 +422,15 @@ class DiffWindow:
class GitView:
""" This is the main class
"""
- version = "0.6"
+ version = "0.7"
def __init__(self, with_diff=0):
self.with_diff = with_diff
- self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_border_width(0)
self.window.set_title("Git repository browser")
+ self.get_encoding()
self.get_bt_sha1()
# Use three-quarters of the screen by default
@@ -454,11 +455,7 @@ class GitView:
self.bt_sha1 = { }
ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
- git_dir = os.getenv("GIT_DIR")
- if (git_dir == None):
- git_dir = ".git"
-
- fp = os.popen('git ls-remote ' + git_dir)
+ fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
while 1:
line = string.strip(fp.readline())
if line == '':
@@ -472,6 +469,13 @@ class GitView:
self.bt_sha1[sha1].append(name)
fp.close()
+ def get_encoding(self):
+ fp = os.popen("git repo-config --get i18n.commitencoding")
+ self.encoding=string.strip(fp.readline())
+ fp.close()
+ if (self.encoding == ""):
+ self.encoding = "utf-8"
+
def construct(self):
"""Construct the window contents."""
@@ -687,7 +691,7 @@ class GitView:
self.revid_label.set_text(revid_label)
self.committer_label.set_text(committer)
self.timestamp_label.set_text(timestamp)
- self.message_buffer.set_text(message)
+ self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
for widget in self.parents_widgets:
self.table.remove(widget)
@@ -732,7 +736,7 @@ class GitView:
button.set_relief(gtk.RELIEF_NONE)
button.set_sensitive(True)
button.connect("clicked", self._show_clicked_cb,
- commit.commit_sha1, parent_id)
+ commit.commit_sha1, parent_id, self.encoding)
hbox.pack_start(button, expand=False, fill=True)
button.show()
@@ -827,6 +831,7 @@ class GitView:
self.colours = {}
self.nodepos = {}
self.incomplete_line = {}
+ self.commits = []
index = 0
last_colour = 0
@@ -844,12 +849,7 @@ class GitView:
commit = Commit(commit_lines)
if (commit != None ):
- (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
- index, out_line,
- last_colour,
- last_nodepos)
- self.index[commit.commit_sha1] = index
- index += 1
+ self.commits.append(commit)
# Skip the '\0
commit_lines = []
@@ -858,6 +858,14 @@ class GitView:
fp.close()
+ for commit in self.commits:
+ (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
+ index, out_line,
+ last_colour,
+ last_nodepos)
+ self.index[commit.commit_sha1] = index
+ index += 1
+
self.treeview.set_model(self.model)
self.treeview.show()
@@ -870,28 +878,22 @@ class GitView:
# Reset nodepostion
if (last_nodepos > 5):
- last_nodepos = 0
+ last_nodepos = -1
# Add the incomplete lines of the last cell in this
- for sha1 in self.incomplete_line.keys():
- if ( sha1 != commit.commit_sha1):
- for pos in self.incomplete_line[sha1]:
- in_line.append((pos, pos, self.colours[sha1]))
- else:
- del self.incomplete_line[sha1]
-
try:
colour = self.colours[commit.commit_sha1]
except KeyError:
- last_colour +=1
- self.colours[commit.commit_sha1] = last_colour
- colour = last_colour
+ self.colours[commit.commit_sha1] = last_colour+1
+ last_colour = self.colours[commit.commit_sha1]
+ colour = self.colours[commit.commit_sha1]
+
try:
node_pos = self.nodepos[commit.commit_sha1]
except KeyError:
- last_nodepos +=1
- self.nodepos[commit.commit_sha1] = last_nodepos
- node_pos = last_nodepos
+ self.nodepos[commit.commit_sha1] = last_nodepos+1
+ last_nodepos = self.nodepos[commit.commit_sha1]
+ node_pos = self.nodepos[commit.commit_sha1]
#The first parent always continue on the same line
try:
@@ -901,25 +903,26 @@ class GitView:
self.colours[commit.parent_sha1[0]] = colour
self.nodepos[commit.parent_sha1[0]] = node_pos
- in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
- self.colours[commit.parent_sha1[0]]))
-
- self.add_incomplete_line(commit.parent_sha1[0], index+1)
+ for sha1 in self.incomplete_line.keys():
+ if (sha1 != commit.commit_sha1):
+ self.draw_incomplete_line(sha1, node_pos,
+ out_line, in_line, index)
+ else:
+ del self.incomplete_line[sha1]
- if (len(commit.parent_sha1) > 1):
- for parent_id in commit.parent_sha1[1:]:
- try:
- tmp_node_pos = self.nodepos[parent_id]
- except KeyError:
- last_colour += 1;
- self.colours[parent_id] = last_colour
- last_nodepos +=1
- self.nodepos[parent_id] = last_nodepos
- in_line.append((node_pos, self.nodepos[parent_id],
- self.colours[parent_id]))
- self.add_incomplete_line(parent_id, index+1)
+ for parent_id in commit.parent_sha1:
+ try:
+ tmp_node_pos = self.nodepos[parent_id]
+ except KeyError:
+ self.colours[parent_id] = last_colour+1
+ last_colour = self.colours[parent_id]
+ self.nodepos[parent_id] = last_nodepos+1
+ last_nodepos = self.nodepos[parent_id]
+ in_line.append((node_pos, self.nodepos[parent_id],
+ self.colours[parent_id]))
+ self.add_incomplete_line(parent_id)
try:
branch_tag = self.bt_sha1[commit.commit_sha1]
@@ -934,12 +937,31 @@ class GitView:
return (in_line, last_colour, last_nodepos)
- def add_incomplete_line(self, sha1, index):
+ def add_incomplete_line(self, sha1):
try:
self.incomplete_line[sha1].append(self.nodepos[sha1])
except KeyError:
self.incomplete_line[sha1] = [self.nodepos[sha1]]
+ def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
+ for idx, pos in enumerate(self.incomplete_line[sha1]):
+ if(pos == node_pos):
+ #remove the straight line and add a slash
+ if ((pos, pos, self.colours[sha1]) in out_line):
+ out_line.remove((pos, pos, self.colours[sha1]))
+ out_line.append((pos, pos+0.5, self.colours[sha1]))
+ self.incomplete_line[sha1][idx] = pos = pos+0.5
+ try:
+ next_commit = self.commits[index+1]
+ if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
+ # join the line back to the node point
+ # This need to be done only if we modified it
+ in_line.append((pos, pos-0.5, self.colours[sha1]))
+ continue;
+ except IndexError:
+ pass
+ in_line.append((pos, pos, self.colours[sha1]))
+
def _go_clicked_cb(self, widget, revid):
"""Callback for when the go button for a parent is clicked."""
@@ -953,10 +975,10 @@ class GitView:
self.treeview.grab_focus()
- def _show_clicked_cb(self, widget, commit_sha1, parent_sha1):
+ def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
"""Callback for when the show button for a parent is clicked."""
window = DiffWindow()
- window.set_diff(commit_sha1, parent_sha1)
+ window.set_diff(commit_sha1, parent_sha1, encoding)
self.treeview.grab_focus()
if __name__ == "__main__":
diff --git a/diff.c b/diff.c
index 804c08c2cf..c0548eed98 100644
--- a/diff.c
+++ b/diff.c
@@ -178,11 +178,12 @@ static void emit_rewrite_diff(const char *name_a,
copy_file('+', temp[1].name);
}
-static void builtin_diff(const char *name_a,
+static const char *builtin_diff(const char *name_a,
const char *name_b,
struct diff_tempfile *temp,
const char *xfrm_msg,
- int complete_rewrite)
+ int complete_rewrite,
+ const char **args)
{
int i, next_at, cmd_size;
const char *const diff_cmd = "diff -L%s -L%s";
@@ -242,19 +243,24 @@ static void builtin_diff(const char *name_a,
}
if (xfrm_msg && xfrm_msg[0])
puts(xfrm_msg);
+ /*
+ * we do not run diff between different kind
+ * of objects.
+ */
if (strncmp(temp[0].mode, temp[1].mode, 3))
- /* we do not run diff between different kind
- * of objects.
- */
- exit(0);
+ return NULL;
if (complete_rewrite) {
- fflush(NULL);
emit_rewrite_diff(name_a, name_b, temp);
- exit(0);
+ return NULL;
}
}
- fflush(NULL);
- execlp("/bin/sh","sh", "-c", cmd, NULL);
+
+ /* This is disgusting */
+ *args++ = "sh";
+ *args++ = "-c";
+ *args++ = cmd;
+ *args = NULL;
+ return "/bin/sh";
}
struct diff_filespec *alloc_filespec(const char *path)
@@ -559,6 +565,40 @@ static void remove_tempfile_on_signal(int signo)
raise(signo);
}
+static int spawn_prog(const char *pgm, const char **arg)
+{
+ pid_t pid;
+ int status;
+
+ fflush(NULL);
+ pid = fork();
+ if (pid < 0)
+ die("unable to fork");
+ if (!pid) {
+ execvp(pgm, (char *const*) arg);
+ exit(255);
+ }
+
+ while (waitpid(pid, &status, 0) < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+
+ /* Earlier we did not check the exit status because
+ * diff exits non-zero if files are different, and
+ * we are not interested in knowing that. It was a
+ * mistake which made it harder to quit a diff-*
+ * session that uses the git-apply-patch-script as
+ * the GIT_EXTERNAL_DIFF. A custom GIT_EXTERNAL_DIFF
+ * should also exit non-zero only when it wants to
+ * abort the entire diff-* session.
+ */
+ if (WIFEXITED(status) && !WEXITSTATUS(status))
+ return 0;
+ return -1;
+}
+
/* An external diff command takes:
*
* diff-cmd name infile1 infile1-sha1 infile1-mode \
@@ -573,9 +613,9 @@ static void run_external_diff(const char *pgm,
const char *xfrm_msg,
int complete_rewrite)
{
+ const char *spawn_arg[10];
struct diff_tempfile *temp = diff_temp;
- pid_t pid;
- int status;
+ int retval;
static int atexit_asked = 0;
const char *othername;
@@ -592,59 +632,41 @@ static void run_external_diff(const char *pgm,
signal(SIGINT, remove_tempfile_on_signal);
}
- fflush(NULL);
- pid = fork();
- if (pid < 0)
- die("unable to fork");
- if (!pid) {
- if (pgm) {
- if (one && two) {
- const char *exec_arg[10];
- const char **arg = &exec_arg[0];
- *arg++ = pgm;
- *arg++ = name;
- *arg++ = temp[0].name;
- *arg++ = temp[0].hex;
- *arg++ = temp[0].mode;
- *arg++ = temp[1].name;
- *arg++ = temp[1].hex;
- *arg++ = temp[1].mode;
- if (other) {
- *arg++ = other;
- *arg++ = xfrm_msg;
- }
- *arg = NULL;
- execvp(pgm, (char *const*) exec_arg);
+ if (pgm) {
+ const char **arg = &spawn_arg[0];
+ if (one && two) {
+ *arg++ = pgm;
+ *arg++ = name;
+ *arg++ = temp[0].name;
+ *arg++ = temp[0].hex;
+ *arg++ = temp[0].mode;
+ *arg++ = temp[1].name;
+ *arg++ = temp[1].hex;
+ *arg++ = temp[1].mode;
+ if (other) {
+ *arg++ = other;
+ *arg++ = xfrm_msg;
}
- else
- execlp(pgm, pgm, name, NULL);
+ } else {
+ *arg++ = pgm;
+ *arg++ = name;
}
- /*
- * otherwise we use the built-in one.
- */
- if (one && two)
- builtin_diff(name, othername, temp, xfrm_msg,
- complete_rewrite);
- else
+ *arg = NULL;
+ } else {
+ if (one && two) {
+ pgm = builtin_diff(name, othername, temp, xfrm_msg, complete_rewrite, spawn_arg);
+ } else
printf("* Unmerged path %s\n", name);
- exit(0);
}
- if (waitpid(pid, &status, 0) < 0 ||
- !WIFEXITED(status) || WEXITSTATUS(status)) {
- /* Earlier we did not check the exit status because
- * diff exits non-zero if files are different, and
- * we are not interested in knowing that. It was a
- * mistake which made it harder to quit a diff-*
- * session that uses the git-apply-patch-script as
- * the GIT_EXTERNAL_DIFF. A custom GIT_EXTERNAL_DIFF
- * should also exit non-zero only when it wants to
- * abort the entire diff-* session.
- */
- remove_tempfile();
+
+ retval = 0;
+ if (pgm)
+ retval = spawn_prog(pgm, spawn_arg);
+ remove_tempfile();
+ if (retval) {
fprintf(stderr, "external diff died, stopping at %s.\n", name);
exit(1);
}
- remove_tempfile();
}
static void diff_fill_sha1_info(struct diff_filespec *one)
diff --git a/diffcore-rename.c b/diffcore-rename.c
index 39d9126cb9..ffd126af0d 100644
--- a/diffcore-rename.c
+++ b/diffcore-rename.c
@@ -176,8 +176,10 @@ static int estimate_similarity(struct diff_filespec *src,
/* A delta that has a lot of literal additions would have
* big delta_size no matter what else it does.
*/
- if (base_size * (MAX_SCORE-minimum_score) < delta_size * MAX_SCORE)
+ if (base_size * (MAX_SCORE-minimum_score) < delta_size * MAX_SCORE) {
+ free(delta);
return 0;
+ }
/* Estimate the edit size by interpreting delta. */
if (count_delta(delta, delta_size, &src_copied, &literal_added)) {
diff --git a/environment.c b/environment.c
index 251e53ca09..16c08f0697 100644
--- a/environment.c
+++ b/environment.c
@@ -17,6 +17,7 @@ int only_use_symrefs = 0;
int repository_format_version = 0;
char git_commit_encoding[MAX_ENCODING_LENGTH] = "utf-8";
int shared_repository = 0;
+const char *apply_default_whitespace = NULL;
static char *git_dir, *git_object_dir, *git_index_file, *git_refs_dir,
*git_graft_file;
diff --git a/epoch.c b/epoch.c
index 3a767486da..0f374921da 100644
--- a/epoch.c
+++ b/epoch.c
@@ -15,6 +15,7 @@
#include "cache.h"
#include "commit.h"
+#include "revision.h"
#include "epoch.h"
struct fraction {
diff --git a/epoch.h b/epoch.h
index 7493d5a241..3756009060 100644
--- a/epoch.h
+++ b/epoch.h
@@ -11,7 +11,6 @@ typedef int (*emitter_func) (struct commit *);
int sort_list_in_merge_order(struct commit_list *list, emitter_func emitter);
/* Low bits are used by rev-list */
-#define UNINTERESTING (1u<<10)
#define BOUNDARY (1u<<11)
#define VISITED (1u<<12)
#define DISCONTINUITY (1u<<13)
diff --git a/exec_cmd.c b/exec_cmd.c
index 55af33bb7e..b5e59a9ae9 100644
--- a/exec_cmd.c
+++ b/exec_cmd.c
@@ -13,7 +13,7 @@ void git_set_exec_path(const char *exec_path)
/* Returns the highest-priority, location to look for git programs. */
-const char *git_exec_path()
+const char *git_exec_path(void)
{
const char *env;
diff --git a/fetch-pack.c b/fetch-pack.c
index aa6f42ae1b..535de10660 100644
--- a/fetch-pack.c
+++ b/fetch-pack.c
@@ -8,7 +8,7 @@ static int keep_pack;
static int quiet;
static int verbose;
static const char fetch_pack_usage[] =
-"git-fetch-pack [-q] [-v] [-k] [--exec=upload-pack] [host:]directory <refs>...";
+"git-fetch-pack [-q] [-v] [-k] [--thin] [--exec=upload-pack] [host:]directory <refs>...";
static const char *exec = "git-upload-pack";
#define COMPLETE (1U << 0)
@@ -18,7 +18,7 @@ static const char *exec = "git-upload-pack";
#define POPPED (1U << 4)
static struct commit_list *rev_list = NULL;
-static int non_common_revs = 0, multi_ack = 0;
+static int non_common_revs = 0, multi_ack = 0, use_thin_pack = 0;
static void rev_list_push(struct commit *commit, int mark)
{
@@ -82,7 +82,7 @@ static void mark_common(struct commit *commit,
Get the next rev to send, ignoring the common.
*/
-static const unsigned char* get_rev()
+static const unsigned char* get_rev(void)
{
struct commit *commit = NULL;
@@ -156,8 +156,9 @@ static int find_common(int fd[2], unsigned char *result_sha1,
continue;
}
- packet_write(fd[1], "want %s%s\n", sha1_to_hex(remote),
- multi_ack ? " multi_ack" : "");
+ packet_write(fd[1], "want %s%s%s\n", sha1_to_hex(remote),
+ (multi_ack ? " multi_ack" : ""),
+ (use_thin_pack ? " thin-pack" : ""));
fetching++;
}
packet_flush(fd[1]);
@@ -421,6 +422,10 @@ int main(int argc, char **argv)
keep_pack = 1;
continue;
}
+ if (!strcmp("--thin", arg)) {
+ use_thin_pack = 1;
+ continue;
+ }
if (!strcmp("-v", arg)) {
verbose = 1;
continue;
@@ -434,6 +439,8 @@ int main(int argc, char **argv)
}
if (!dest)
usage(fetch_pack_usage);
+ if (keep_pack)
+ use_thin_pack = 0;
pid = git_connect(fd, dest, exec);
if (pid < 0)
return 1;
diff --git a/fsck-objects.c b/fsck-objects.c
index 6439d55126..4ddd67699c 100644
--- a/fsck-objects.c
+++ b/fsck-objects.c
@@ -20,7 +20,7 @@ static int check_strict = 0;
static int keep_cache_objects = 0;
static unsigned char head_sha1[20];
-#if NO_D_INO_IN_DIRENT
+#ifdef NO_D_INO_IN_DIRENT
#define SORT_DIRENT 0
#define DIRENT_SORT_HINT(de) 0
#else
diff --git a/git-am.sh b/git-am.sh
index 85ecada657..7cc4ae5a30 100755
--- a/git-am.sh
+++ b/git-am.sh
@@ -300,7 +300,7 @@ do
} >"$dotest/final-commit"
;;
*)
- case "$resolved,$interactive" in
+ case "$resolved$interactive" in
tt)
# This is used only for interactive view option.
git-diff-index -p --cached HEAD >"$dotest/patch"
@@ -364,6 +364,12 @@ do
# trust what the user has in the index file and the
# working tree.
resolved=
+ changed="$(git-diff-index --cached --name-only HEAD)"
+ if test '' = "$changed"
+ then
+ echo "No changes - did you forget update-index?"
+ stop_here $this
+ fi
apply_status=0
;;
esac
@@ -374,7 +380,7 @@ do
then
# Applying the patch to an earlier tree and merging the
# result may have produced the same tree as ours.
- changed="$(git-diff-index --cached --name-only -z HEAD)"
+ changed="$(git-diff-index --cached --name-only HEAD)"
if test '' = "$changed"
then
echo No changes -- Patch already applied.
diff --git a/git-annotate.perl b/git-annotate.perl
new file mode 100755
index 0000000000..f9c2c6caf5
--- /dev/null
+++ b/git-annotate.perl
@@ -0,0 +1,488 @@
+#!/usr/bin/perl
+# Copyright 2006, Ryan Anderson <ryan@michonline.com>
+#
+# GPL v2 (See COPYING)
+#
+# This file is licensed under the GPL v2, or a later version
+# at the discretion of Linus Torvalds.
+
+use warnings;
+use strict;
+use Getopt::Long;
+use POSIX qw(strftime gmtime);
+
+sub usage() {
+ print STDERR 'Usage: ${\basename $0} [-s] [-S revs-file] file [ revision ]
+ -l, --long
+ Show long rev (Defaults off)
+ -r, --rename
+ Follow renames (Defaults on).
+ -S, --rev-file revs-file
+ use revs from revs-file instead of calling git-rev-list
+ -h, --help
+ This message.
+';
+
+ exit(1);
+}
+
+our ($help, $longrev, $rename, $starting_rev, $rev_file) = (0, 0, 1);
+
+my $rc = GetOptions( "long|l" => \$longrev,
+ "help|h" => \$help,
+ "rename|r" => \$rename,
+ "rev-file|S" => \$rev_file);
+if (!$rc or $help) {
+ usage();
+}
+
+my $filename = shift @ARGV;
+if (@ARGV) {
+ $starting_rev = shift @ARGV;
+}
+
+my @stack = (
+ {
+ 'rev' => defined $starting_rev ? $starting_rev : "HEAD",
+ 'filename' => $filename,
+ },
+);
+
+our @filelines = ();
+
+if (defined $starting_rev) {
+ @filelines = git_cat_file($starting_rev, $filename);
+} else {
+ open(F,"<",$filename)
+ or die "Failed to open filename: $!";
+
+ while(<F>) {
+ chomp;
+ push @filelines, $_;
+ }
+ close(F);
+
+}
+
+our %revs;
+our @revqueue;
+our $head;
+
+my $revsprocessed = 0;
+while (my $bound = pop @stack) {
+ my @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'});
+ foreach my $revinst (@revisions) {
+ my ($rev, @parents) = @$revinst;
+ $head ||= $rev;
+
+ if (!defined($rev)) {
+ $rev = "";
+ }
+ $revs{$rev}{'filename'} = $bound->{'filename'};
+ if (scalar @parents > 0) {
+ $revs{$rev}{'parents'} = \@parents;
+ next;
+ }
+
+ if (!$rename) {
+ next;
+ }
+
+ my $newbound = find_parent_renames($rev, $bound->{'filename'});
+ if ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) {
+ push @stack, $newbound;
+ $revs{$rev}{'parents'} = [$newbound->{'rev'}];
+ }
+ }
+}
+push @revqueue, $head;
+init_claim( defined $starting_rev ? $starting_rev : 'dirty');
+unless (defined $starting_rev) {
+ my $diff = open_pipe("git","diff","-R", "HEAD", "--",$filename)
+ or die "Failed to call git diff to check for dirty state: $!";
+
+ _git_diff_parse($diff, $head, "dirty", (
+ 'author' => gitvar_name("GIT_AUTHOR_IDENT"),
+ 'author_date' => sprintf("%s +0000",time()),
+ )
+ );
+ close($diff);
+}
+handle_rev();
+
+
+my $i = 0;
+foreach my $l (@filelines) {
+ my ($output, $rev, $committer, $date);
+ if (ref $l eq 'ARRAY') {
+ ($output, $rev, $committer, $date) = @$l;
+ if (!$longrev && length($rev) > 8) {
+ $rev = substr($rev,0,8);
+ }
+ } else {
+ $output = $l;
+ ($rev, $committer, $date) = ('unknown', 'unknown', 'unknown');
+ }
+
+ printf("%s\t(%10s\t%10s\t%d)%s\n", $rev, $committer,
+ format_date($date), $i++, $output);
+}
+
+sub init_claim {
+ my ($rev) = @_;
+ for (my $i = 0; $i < @filelines; $i++) {
+ $filelines[$i] = [ $filelines[$i], '', '', '', 1];
+ # line,
+ # rev,
+ # author,
+ # date,
+ # 1 <-- belongs to the original file.
+ }
+ $revs{$rev}{'lines'} = \@filelines;
+}
+
+
+sub handle_rev {
+ my $i = 0;
+ my %seen;
+ while (my $rev = shift @revqueue) {
+ next if $seen{$rev}++;
+
+ my %revinfo = git_commit_info($rev);
+
+ foreach my $p (@{$revs{$rev}{'parents'}}) {
+
+ git_diff_parse($p, $rev, %revinfo);
+ push @revqueue, $p;
+ }
+
+
+ if (scalar @{$revs{$rev}{parents}} == 0) {
+ # We must be at the initial rev here, so claim everything that is left.
+ for (my $i = 0; $i < @{$revs{$rev}{lines}}; $i++) {
+ if (ref ${$revs{$rev}{lines}}[$i] eq '' || ${$revs{$rev}{lines}}[$i][1] eq '') {
+ claim_line($i, $rev, $revs{$rev}{lines}, %revinfo);
+ }
+ }
+ }
+ }
+}
+
+
+sub git_rev_list {
+ my ($rev, $file) = @_;
+
+ my $revlist;
+ if ($rev_file) {
+ open($revlist, '<' . $rev_file);
+ } else {
+ $revlist = open_pipe("git-rev-list","--parents","--remove-empty",$rev,"--",$file)
+ or die "Failed to exec git-rev-list: $!";
+ }
+
+ my @revs;
+ while(my $line = <$revlist>) {
+ chomp $line;
+ my ($rev, @parents) = split /\s+/, $line;
+ push @revs, [ $rev, @parents ];
+ }
+ close($revlist);
+
+ printf("0 revs found for rev %s (%s)\n", $rev, $file) if (@revs == 0);
+ return @revs;
+}
+
+sub find_parent_renames {
+ my ($rev, $file) = @_;
+
+ my $patch = open_pipe("git-diff-tree", "-M50", "-r","--name-status", "-z","$rev")
+ or die "Failed to exec git-diff: $!";
+
+ local $/ = "\0";
+ my %bound;
+ my $junk = <$patch>;
+ while (my $change = <$patch>) {
+ chomp $change;
+ my $filename = <$patch>;
+ chomp $filename;
+
+ if ($change =~ m/^[AMD]$/ ) {
+ next;
+ } elsif ($change =~ m/^R/ ) {
+ my $oldfilename = $filename;
+ $filename = <$patch>;
+ chomp $filename;
+ if ( $file eq $filename ) {
+ my $parent = git_find_parent($rev, $oldfilename);
+ @bound{'rev','filename'} = ($parent, $oldfilename);
+ last;
+ }
+ }
+ }
+ close($patch);
+
+ return \%bound;
+}
+
+
+sub git_find_parent {
+ my ($rev, $filename) = @_;
+
+ my $revparent = open_pipe("git-rev-list","--remove-empty", "--parents","--max-count=1","$rev","--",$filename)
+ or die "Failed to open git-rev-list to find a single parent: $!";
+
+ my $parentline = <$revparent>;
+ chomp $parentline;
+ my ($revfound,$parent) = split m/\s+/, $parentline;
+
+ close($revparent);
+
+ return $parent;
+}
+
+
+# Get a diff between the current revision and a parent.
+# Record the commit information that results.
+sub git_diff_parse {
+ my ($parent, $rev, %revinfo) = @_;
+
+ my $diff = open_pipe("git-diff-tree","-M","-p",$rev,$parent,"--",
+ $revs{$rev}{'filename'}, $revs{$parent}{'filename'})
+ or die "Failed to call git-diff for annotation: $!";
+
+ _git_diff_parse($diff, $parent, $rev, %revinfo);
+
+ close($diff);
+}
+
+sub _git_diff_parse {
+ my ($diff, $parent, $rev, %revinfo) = @_;
+
+ my ($ri, $pi) = (0,0);
+ my $slines = $revs{$rev}{'lines'};
+ my @plines;
+
+ my $gotheader = 0;
+ my ($remstart);
+ my ($hunk_start, $hunk_index);
+ while(<$diff>) {
+ chomp;
+ if (m/^@@ -(\d+),(\d+) \+(\d+),(\d+)/) {
+ $remstart = $1;
+ # Adjust for 0-based arrays
+ $remstart--;
+ # Reinit hunk tracking.
+ $hunk_start = $remstart;
+ $hunk_index = 0;
+ $gotheader = 1;
+
+ for (my $i = $ri; $i < $remstart; $i++) {
+ $plines[$pi++] = $slines->[$i];
+ $ri++;
+ }
+ next;
+ } elsif (!$gotheader) {
+ next;
+ }
+
+ if (m/^\+(.*)$/) {
+ my $line = $1;
+ $plines[$pi++] = [ $line, '', '', '', 0 ];
+ next;
+
+ } elsif (m/^-(.*)$/) {
+ my $line = $1;
+ if (get_line($slines, $ri) eq $line) {
+ # Found a match, claim
+ claim_line($ri, $rev, $slines, %revinfo);
+ } else {
+ die sprintf("Sync error: %d/%d\n|%s\n|%s\n%s => %s\n",
+ $ri, $hunk_start + $hunk_index,
+ $line,
+ get_line($slines, $ri),
+ $rev, $parent);
+ }
+ $ri++;
+
+ } else {
+ if (substr($_,1) ne get_line($slines,$ri) ) {
+ die sprintf("Line %d (%d) does not match:\n|%s\n|%s\n%s => %s\n",
+ $hunk_start + $hunk_index, $ri,
+ substr($_,1),
+ get_line($slines,$ri),
+ $rev, $parent);
+ }
+ $plines[$pi++] = $slines->[$ri++];
+ }
+ $hunk_index++;
+ }
+ for (my $i = $ri; $i < @{$slines} ; $i++) {
+ push @plines, $slines->[$ri++];
+ }
+
+ $revs{$parent}{lines} = \@plines;
+ return;
+}
+
+sub get_line {
+ my ($lines, $index) = @_;
+
+ return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index];
+}
+
+sub git_cat_file {
+ my ($rev, $filename) = @_;
+ return () unless defined $rev && defined $filename;
+
+ my $blob = git_ls_tree($rev, $filename);
+
+ my $catfile = open_pipe("git","cat-file", "blob", $blob)
+ or die "Failed to git-cat-file blob $blob (rev $rev, file $filename): " . $!;
+
+ my @lines;
+ while(<$catfile>) {
+ chomp;
+ push @lines, $_;
+ }
+ close($catfile);
+
+ return @lines;
+}
+
+sub git_ls_tree {
+ my ($rev, $filename) = @_;
+
+ my $lstree = open_pipe("git","ls-tree",$rev,$filename)
+ or die "Failed to call git ls-tree: $!";
+
+ my ($mode, $type, $blob, $tfilename);
+ while(<$lstree>) {
+ ($mode, $type, $blob, $tfilename) = split(/\s+/, $_, 4);
+ last if ($tfilename eq $filename);
+ }
+ close($lstree);
+
+ return $blob if $filename eq $filename;
+ die "git-ls-tree failed to find blob for $filename";
+
+}
+
+
+
+sub claim_line {
+ my ($floffset, $rev, $lines, %revinfo) = @_;
+ my $oline = get_line($lines, $floffset);
+ @{$lines->[$floffset]} = ( $oline, $rev,
+ $revinfo{'author'}, $revinfo{'author_date'} );
+ #printf("Claiming line %d with rev %s: '%s'\n",
+ # $floffset, $rev, $oline) if 1;
+}
+
+sub git_commit_info {
+ my ($rev) = @_;
+ my $commit = open_pipe("git-cat-file", "commit", $rev)
+ or die "Failed to call git-cat-file: $!";
+
+ my %info;
+ while(<$commit>) {
+ chomp;
+ last if (length $_ == 0);
+
+ if (m/^author (.*) <(.*)> (.*)$/) {
+ $info{'author'} = $1;
+ $info{'author_email'} = $2;
+ $info{'author_date'} = $3;
+ } elsif (m/^committer (.*) <(.*)> (.*)$/) {
+ $info{'committer'} = $1;
+ $info{'committer_email'} = $2;
+ $info{'committer_date'} = $3;
+ }
+ }
+ close($commit);
+
+ return %info;
+}
+
+sub format_date {
+ my ($timestamp, $timezone) = split(' ', $_[0]);
+
+ return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($timestamp));
+}
+
+# Copied from git-send-email.perl - We need a Git.pm module..
+sub gitvar {
+ my ($var) = @_;
+ my $fh;
+ my $pid = open($fh, '-|');
+ die "$!" unless defined $pid;
+ if (!$pid) {
+ exec('git-var', $var) or die "$!";
+ }
+ my ($val) = <$fh>;
+ close $fh or die "$!";
+ chomp($val);
+ return $val;
+}
+
+sub gitvar_name {
+ my ($name) = @_;
+ my $val = gitvar($name);
+ my @field = split(/\s+/, $val);
+ return join(' ', @field[0...(@field-4)]);
+}
+
+sub open_pipe {
+ if ($^O eq '##INSERT_ACTIVESTATE_STRING_HERE##') {
+ return open_pipe_activestate(@_);
+ } else {
+ return open_pipe_normal(@_);
+ }
+}
+
+sub open_pipe_activestate {
+ tie *fh, "Git::ActiveStatePipe", @_;
+ return *fh;
+}
+
+sub open_pipe_normal {
+ my (@execlist) = @_;
+
+ my $pid = open my $kid, "-|";
+ defined $pid or die "Cannot fork: $!";
+
+ unless ($pid) {
+ exec @execlist;
+ die "Cannot exec @execlist: $!";
+ }
+
+ return $kid;
+}
+
+package Git::ActiveStatePipe;
+use strict;
+
+sub TIEHANDLE {
+ my ($class, @params) = @_;
+ my $cmdline = join " ", @params;
+ my @data = qx{$cmdline};
+ bless { i => 0, data => \@data }, $class;
+}
+
+sub READLINE {
+ my $self = shift;
+ if ($self->{i} >= scalar @{$self->{data}}) {
+ return undef;
+ }
+ return $self->{'data'}->[ $self->{i}++ ];
+}
+
+sub CLOSE {
+ my $self = shift;
+ delete $self->{data};
+ delete $self->{i};
+}
+
+sub EOF {
+ my $self = shift;
+ return ($self->{i} >= scalar @{$self->{data}});
+}
diff --git a/git-clone.sh b/git-clone.sh
index dc0ad552a3..4ed861d576 100755
--- a/git-clone.sh
+++ b/git-clone.sh
@@ -118,7 +118,7 @@ dir="$2"
[ -e "$dir" ] && echo "$dir already exists." && usage
mkdir -p "$dir" &&
D=$(cd "$dir" && pwd) &&
-trap 'err=$?; rm -r $D; exit $err' exit
+trap 'err=$?; cd ..; rm -r "$D"; exit $err' exit
case "$bare" in
yes) GIT_DIR="$D" ;;
*) GIT_DIR="$D/.git" ;;
@@ -253,7 +253,7 @@ Pull: $head_points_at:$origin" &&
case "$no_checkout" in
'')
- git checkout
+ git-read-tree -m -u -v HEAD HEAD
esac
fi
diff --git a/git-cvsserver.perl b/git-cvsserver.perl
new file mode 100755
index 0000000000..d20d1a8c4b
--- /dev/null
+++ b/git-cvsserver.perl
@@ -0,0 +1,2449 @@
+#!/usr/bin/perl
+
+####
+#### This application is a CVS emulation layer for git.
+#### It is intended for clients to connect over SSH.
+#### See the documentation for more details.
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith <martyn@catalyst.net.nz>
+#### Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+#### Released under the GNU Public License, version 2.
+####
+####
+
+use strict;
+use warnings;
+
+use Fcntl;
+use File::Temp qw/tempdir tempfile/;
+use File::Basename;
+
+my $log = GITCVS::log->new();
+my $cfg;
+
+my $DATE_LIST = {
+ Jan => "01",
+ Feb => "02",
+ Mar => "03",
+ Apr => "04",
+ May => "05",
+ Jun => "06",
+ Jul => "07",
+ Aug => "08",
+ Sep => "09",
+ Oct => "10",
+ Nov => "11",
+ Dec => "12",
+};
+
+# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
+$| = 1;
+
+#### Definition and mappings of functions ####
+
+my $methods = {
+ 'Root' => \&req_Root,
+ 'Valid-responses' => \&req_Validresponses,
+ 'valid-requests' => \&req_validrequests,
+ 'Directory' => \&req_Directory,
+ 'Entry' => \&req_Entry,
+ 'Modified' => \&req_Modified,
+ 'Unchanged' => \&req_Unchanged,
+ 'Argument' => \&req_Argument,
+ 'Argumentx' => \&req_Argument,
+ 'expand-modules' => \&req_expandmodules,
+ 'add' => \&req_add,
+ 'remove' => \&req_remove,
+ 'co' => \&req_co,
+ 'update' => \&req_update,
+ 'ci' => \&req_ci,
+ 'diff' => \&req_diff,
+ 'log' => \&req_log,
+ 'tag' => \&req_CATCHALL,
+ 'status' => \&req_status,
+ 'admin' => \&req_CATCHALL,
+ 'history' => \&req_CATCHALL,
+ 'watchers' => \&req_CATCHALL,
+ 'editors' => \&req_CATCHALL,
+ 'annotate' => \&req_annotate,
+ 'Global_option' => \&req_Globaloption,
+ #'annotate' => \&req_CATCHALL,
+};
+
+##############################################
+
+
+# $state holds all the bits of information the clients sends us that could
+# potentially be useful when it comes to actually _doing_ something.
+my $state = {};
+$log->info("--------------- STARTING -----------------");
+
+my $TEMP_DIR = tempdir( CLEANUP => 1 );
+$log->debug("Temporary directory is '$TEMP_DIR'");
+
+# Keep going until the client closes the connection
+while (<STDIN>)
+{
+ chomp;
+
+ # Check to see if we've seen this method, and call appropiate function.
+ if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
+ {
+ # use the $methods hash to call the appropriate sub for this command
+ #$log->info("Method : $1");
+ &{$methods->{$1}}($1,$2);
+ } else {
+ # log fatal because we don't understand this function. If this happens
+ # we're fairly screwed because we don't know if the client is expecting
+ # a response. If it is, the client will hang, we'll hang, and the whole
+ # thing will be custard.
+ $log->fatal("Don't understand command $_\n");
+ die("Unknown command $_");
+ }
+}
+
+$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
+$log->info("--------------- FINISH -----------------");
+
+# Magic catchall method.
+# This is the method that will handle all commands we haven't yet
+# implemented. It simply sends a warning to the log file indicating a
+# command that hasn't been implemented has been invoked.
+sub req_CATCHALL
+{
+ my ( $cmd, $data ) = @_;
+ $log->warn("Unhandled command : req_$cmd : $data");
+}
+
+
+# Root pathname \n
+# Response expected: no. Tell the server which CVSROOT to use. Note that
+# pathname is a local directory and not a fully qualified CVSROOT variable.
+# pathname must already exist; if creating a new root, use the init
+# request, not Root. pathname does not include the hostname of the server,
+# how to access the server, etc.; by the time the CVS protocol is in use,
+# connection, authentication, etc., are already taken care of. The Root
+# request must be sent only once, and it must be sent before any requests
+# other than Valid-responses, valid-requests, UseUnchanged, Set or init.
+sub req_Root
+{
+ my ( $cmd, $data ) = @_;
+ $log->debug("req_Root : $data");
+
+ $state->{CVSROOT} = $data;
+
+ $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+ foreach my $line ( `git-var -l` )
+ {
+ next unless ( $line =~ /^(.*?)\.(.*?)=(.*)$/ );
+ $cfg->{$1}{$2} = $3;
+ }
+
+ unless ( defined ( $cfg->{gitcvs}{enabled} ) and $cfg->{gitcvs}{enabled} =~ /^\s*(1|true|yes)\s*$/i )
+ {
+ print "E GITCVS emulation needs to be enabled on this repo\n";
+ print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
+ print "E \n";
+ print "error 1 GITCVS emulation disabled\n";
+ }
+
+ if ( defined ( $cfg->{gitcvs}{logfile} ) )
+ {
+ $log->setfile($cfg->{gitcvs}{logfile});
+ } else {
+ $log->nofile();
+ }
+}
+
+# Global_option option \n
+# Response expected: no. Transmit one of the global options `-q', `-Q',
+# `-l', `-t', `-r', or `-n'. option must be one of those strings, no
+# variations (such as combining of options) are allowed. For graceful
+# handling of valid-requests, it is probably better to make new global
+# options separate requests, rather than trying to add them to this
+# request.
+sub req_Globaloption
+{
+ my ( $cmd, $data ) = @_;
+ $log->debug("req_Globaloption : $data");
+
+ # TODO : is this data useful ???
+}
+
+# Valid-responses request-list \n
+# Response expected: no. Tell the server what responses the client will
+# accept. request-list is a space separated list of tokens.
+sub req_Validresponses
+{
+ my ( $cmd, $data ) = @_;
+ $log->debug("req_Validrepsonses : $data");
+
+ # TODO : re-enable this, currently it's not particularly useful
+ #$state->{validresponses} = [ split /\s+/, $data ];
+}
+
+# valid-requests \n
+# Response expected: yes. Ask the server to send back a Valid-requests
+# response.
+sub req_validrequests
+{
+ my ( $cmd, $data ) = @_;
+
+ $log->debug("req_validrequests");
+
+ $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
+ $log->debug("SEND : ok");
+
+ print "Valid-requests " . join(" ",keys %$methods) . "\n";
+ print "ok\n";
+}
+
+# Directory local-directory \n
+# Additional data: repository \n. Response expected: no. Tell the server
+# what directory to use. The repository should be a directory name from a
+# previous server response. Note that this both gives a default for Entry
+# and Modified and also for ci and the other commands; normal usage is to
+# send Directory for each directory in which there will be an Entry or
+# Modified, and then a final Directory for the original directory, then the
+# command. The local-directory is relative to the top level at which the
+# command is occurring (i.e. the last Directory which is sent before the
+# command); to indicate that top level, `.' should be sent for
+# local-directory.
+sub req_Directory
+{
+ my ( $cmd, $data ) = @_;
+
+ my $repository = <STDIN>;
+ chomp $repository;
+
+
+ $state->{localdir} = $data;
+ $state->{repository} = $repository;
+ $state->{directory} = $repository;
+ $state->{directory} =~ s/^$state->{CVSROOT}\///;
+ $state->{module} = $1 if ($state->{directory} =~ s/^(.*?)(\/|$)//);
+ $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
+
+ $log->debug("req_Directory : localdir=$data repository=$repository directory=$state->{directory} module=$state->{module}");
+}
+
+# Entry entry-line \n
+# Response expected: no. Tell the server what version of a file is on the
+# local machine. The name in entry-line is a name relative to the directory
+# most recently specified with Directory. If the user is operating on only
+# some files in a directory, Entry requests for only those files need be
+# included. If an Entry request is sent without Modified, Is-modified, or
+# Unchanged, it means the file is lost (does not exist in the working
+# directory). If both Entry and one of Modified, Is-modified, or Unchanged
+# are sent for the same file, Entry must be sent first. For a given file,
+# one can send Modified, Is-modified, or Unchanged, but not more than one
+# of these three.
+sub req_Entry
+{
+ my ( $cmd, $data ) = @_;
+
+ $log->debug("req_Entry : $data");
+
+ my @data = split(/\//, $data);
+
+ $state->{entries}{$state->{directory}.$data[1]} = {
+ revision => $data[2],
+ conflict => $data[3],
+ options => $data[4],
+ tag_or_date => $data[5],
+ };
+}
+
+# add \n
+# Response expected: yes. Add a file or directory. This uses any previous
+# Argument, Directory, Entry, or Modified requests, if they have been sent.
+# The last Directory sent specifies the working directory at the time of
+# the operation. To add a directory, send the directory to be added using
+# Directory and Argument requests.
+sub req_add
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("add");
+
+ my $addcount = 0;
+
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
+ {
+ print "E cvs add: nothing known about `$filename'\n";
+ next;
+ }
+ # TODO : check we're not squashing an already existing file
+ if ( defined ( $state->{entries}{$filename}{revision} ) )
+ {
+ print "E cvs add: `$filename' has already been entered\n";
+ next;
+ }
+
+
+ my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+ print "E cvs add: scheduling file `$filename' for addition\n";
+
+ print "Checked-in $dirpart\n";
+ print "$filename\n";
+ print "/$filepart/0///\n";
+
+ $addcount++;
+ }
+
+ if ( $addcount == 1 )
+ {
+ print "E cvs add: use `cvs commit' to add this file permanently\n";
+ }
+ elsif ( $addcount > 1 )
+ {
+ print "E cvs add: use `cvs commit' to add these files permanently\n";
+ }
+
+ print "ok\n";
+}
+
+# remove \n
+# Response expected: yes. Remove a file. This uses any previous Argument,
+# Directory, Entry, or Modified requests, if they have been sent. The last
+# Directory sent specifies the working directory at the time of the
+# operation. Note that this request does not actually do anything to the
+# repository; the only effect of a successful remove request is to supply
+# the client with a new entries line containing `-' to indicate a removed
+# file. In fact, the client probably could perform this operation without
+# contacting the server, although using remove may cause the server to
+# perform a few more checks. The client sends a subsequent ci request to
+# actually record the removal in the repository.
+sub req_remove
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("remove");
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ #$log->debug("add state : " . Dumper($state));
+
+ my $rmcount = 0;
+
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
+ {
+ print "E cvs remove: file `$filename' still in working directory\n";
+ next;
+ }
+
+ my $meta = $updater->getmeta($filename);
+ my $wrev = revparse($filename);
+
+ unless ( defined ( $wrev ) )
+ {
+ print "E cvs remove: nothing known about `$filename'\n";
+ next;
+ }
+
+ if ( defined($wrev) and $wrev < 0 )
+ {
+ print "E cvs remove: file `$filename' already scheduled for removal\n";
+ next;
+ }
+
+ unless ( $wrev == $meta->{revision} )
+ {
+ # TODO : not sure if the format of this message is quite correct.
+ print "E cvs remove: Up to date check failed for `$filename'\n";
+ next;
+ }
+
+
+ my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+ print "E cvs remove: scheduling `$filename' for removal\n";
+
+ print "Checked-in $dirpart\n";
+ print "$filename\n";
+ print "/$filepart/-1.$wrev///\n";
+
+ $rmcount++;
+ }
+
+ if ( $rmcount == 1 )
+ {
+ print "E cvs remove: use `cvs commit' to remove this file permanently\n";
+ }
+ elsif ( $rmcount > 1 )
+ {
+ print "E cvs remove: use `cvs commit' to remove these files permanently\n";
+ }
+
+ print "ok\n";
+}
+
+# Modified filename \n
+# Response expected: no. Additional data: mode, \n, file transmission. Send
+# the server a copy of one locally modified file. filename is a file within
+# the most recent directory sent with Directory; it must not contain `/'.
+# If the user is operating on only some files in a directory, only those
+# files need to be included. This can also be sent without Entry, if there
+# is no entry for the file.
+sub req_Modified
+{
+ my ( $cmd, $data ) = @_;
+
+ my $mode = <STDIN>;
+ chomp $mode;
+ my $size = <STDIN>;
+ chomp $size;
+
+ # Grab config information
+ my $blocksize = 8192;
+ my $bytesleft = $size;
+ my $tmp;
+
+ # Get a filehandle/name to write it to
+ my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
+
+ # Loop over file data writing out to temporary file.
+ while ( $bytesleft )
+ {
+ $blocksize = $bytesleft if ( $bytesleft < $blocksize );
+ read STDIN, $tmp, $blocksize;
+ print $fh $tmp;
+ $bytesleft -= $blocksize;
+ }
+
+ close $fh;
+
+ # Ensure we have something sensible for the file mode
+ if ( $mode =~ /u=(\w+)/ )
+ {
+ $mode = $1;
+ } else {
+ $mode = "rw";
+ }
+
+ # Save the file data in $state
+ $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
+ $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
+ $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
+ $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
+
+ #$log->debug("req_Modified : file=$data mode=$mode size=$size");
+}
+
+# Unchanged filename \n
+# Response expected: no. Tell the server that filename has not been
+# modified in the checked out directory. The filename is a file within the
+# most recent directory sent with Directory; it must not contain `/'.
+sub req_Unchanged
+{
+ my ( $cmd, $data ) = @_;
+
+ $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
+
+ #$log->debug("req_Unchanged : $data");
+}
+
+# Argument text \n
+# Response expected: no. Save argument for use in a subsequent command.
+# Arguments accumulate until an argument-using command is given, at which
+# point they are forgotten.
+# Argumentx text \n
+# Response expected: no. Append \n followed by text to the current argument
+# being saved.
+sub req_Argument
+{
+ my ( $cmd, $data ) = @_;
+
+ # TODO : Not quite sure how Argument and Argumentx differ, but I assume
+ # it's for multi-line arguments ... somehow ...
+
+ $log->debug("$cmd : $data");
+
+ push @{$state->{arguments}}, $data;
+}
+
+# expand-modules \n
+# Response expected: yes. Expand the modules which are specified in the
+# arguments. Returns the data in Module-expansion responses. Note that the
+# server can assume that this is checkout or export, not rtag or rdiff; the
+# latter do not access the working directory and thus have no need to
+# expand modules on the client side. Expand may not be the best word for
+# what this request does. It does not necessarily tell you all the files
+# contained in a module, for example. Basically it is a way of telling you
+# which working directories the server needs to know about in order to
+# handle a checkout of the specified modules. For example, suppose that the
+# server has a module defined by
+# aliasmodule -a 1dir
+# That is, one can check out aliasmodule and it will take 1dir in the
+# repository and check it out to 1dir in the working directory. Now suppose
+# the client already has this module checked out and is planning on using
+# the co request to update it. Without using expand-modules, the client
+# would have two bad choices: it could either send information about all
+# working directories under the current directory, which could be
+# unnecessarily slow, or it could be ignorant of the fact that aliasmodule
+# stands for 1dir, and neglect to send information for 1dir, which would
+# lead to incorrect operation. With expand-modules, the client would first
+# ask for the module to be expanded:
+sub req_expandmodules
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit();
+
+ $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
+
+ unless ( ref $state->{arguments} eq "ARRAY" )
+ {
+ print "ok\n";
+ return;
+ }
+
+ foreach my $module ( @{$state->{arguments}} )
+ {
+ $log->debug("SEND : Module-expansion $module");
+ print "Module-expansion $module\n";
+ }
+
+ print "ok\n";
+ statecleanup();
+}
+
+# co \n
+# Response expected: yes. Get files from the repository. This uses any
+# previous Argument, Directory, Entry, or Modified requests, if they have
+# been sent. Arguments to this command are module names; the client cannot
+# know what directories they correspond to except by (1) just sending the
+# co request, and then seeing what directory names the server sends back in
+# its responses, and (2) the expand-modules request.
+sub req_co
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("co");
+
+ my $module = $state->{args}[0];
+ my $checkout_path = $module;
+
+ # use the user specified directory if we're given it
+ $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
+
+ $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
+
+ $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
+
+ $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
+ $updater->update();
+
+ # instruct the client that we're checking out to $checkout_path
+ print "E cvs server: updating $checkout_path\n";
+
+ foreach my $git ( @{$updater->gethead} )
+ {
+ # Don't want to check out deleted files
+ next if ( $git->{filehash} eq "deleted" );
+
+ ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
+
+ # modification time of this file
+ print "Mod-time $git->{modified}\n";
+
+ # print some information to the client
+ print "MT +updated\n";
+ print "MT text U\n";
+ if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
+ {
+ print "MT fname $checkout_path/$git->{dir}$git->{name}\n";
+ } else {
+ print "MT fname $checkout_path/$git->{name}\n";
+ }
+ print "MT newline\n";
+ print "MT -updated\n";
+
+ # instruct client we're sending a file to put in this path
+ print "Created $checkout_path/" . ( defined ( $git->{dir} ) ? $git->{dir} . "/" : "" ) . "\n";
+
+ print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
+
+ # this is an "entries" line
+ print "/$git->{name}/1.$git->{revision}///\n";
+ # permissions
+ print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
+
+ # transmit file
+ transmitfile($git->{filehash});
+ }
+
+ print "ok\n";
+
+ statecleanup();
+}
+
+# update \n
+# Response expected: yes. Actually do a cvs update command. This uses any
+# previous Argument, Directory, Entry, or Modified requests, if they have
+# been sent. The last Directory sent specifies the working directory at the
+# time of the operation. The -I option is not used--files which the client
+# can decide whether to ignore are not mentioned and the client sends the
+# Questionable request for others.
+sub req_update
+{
+ my ( $cmd, $data ) = @_;
+
+ $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
+
+ argsplit("update");
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+
+ $updater->update();
+
+ # if no files were specified, we need to work out what files we should be providing status on ...
+ argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+ #$log->debug("update state : " . Dumper($state));
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ # if we have a -C we should pretend we never saw modified stuff
+ if ( exists ( $state->{opt}{C} ) )
+ {
+ delete $state->{entries}{$filename}{modified_hash};
+ delete $state->{entries}{$filename}{modified_filename};
+ $state->{entries}{$filename}{unchanged} = 1;
+ }
+
+ my $meta;
+ if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
+ {
+ $meta = $updater->getmeta($filename, $1);
+ } else {
+ $meta = $updater->getmeta($filename);
+ }
+
+ next unless ( $meta->{revision} );
+
+ my $oldmeta = $meta;
+
+ my $wrev = revparse($filename);
+
+ # If the working copy is an old revision, lets get that version too for comparison.
+ if ( defined($wrev) and $wrev != $meta->{revision} )
+ {
+ $oldmeta = $updater->getmeta($filename, $wrev);
+ }
+
+ #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
+
+ # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified _and_ the user hasn't specified -C
+ next if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{unchanged} and not exists ( $state->{opt}{C} ) );
+
+ if ( $meta->{filehash} eq "deleted" )
+ {
+ my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+ $log->info("Removing '$filename' from working copy (no longer in the repo)");
+
+ print "E cvs update: `$filename' is no longer in the repository\n";
+ print "Removed $dirpart\n";
+ print "$filepart\n";
+ }
+ elsif ( not defined ( $state->{entries}{$filename}{modified_hash} ) or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} )
+ {
+ $log->info("Updating '$filename'");
+ # normal update, just send the new revision (either U=Update, or A=Add, or R=Remove)
+ print "MT +updated\n";
+ print "MT text U\n";
+ print "MT fname $filename\n";
+ print "MT newline\n";
+ print "MT -updated\n";
+
+ my ( $filepart, $dirpart ) = filenamesplit($filename);
+ $dirpart =~ s/^$state->{directory}//;
+
+ if ( defined ( $wrev ) )
+ {
+ # instruct client we're sending a file to put in this path as a replacement
+ print "Update-existing $dirpart\n";
+ $log->debug("Updating existing file 'Update-existing $dirpart'");
+ } else {
+ # instruct client we're sending a file to put in this path as a new file
+ print "Created $dirpart\n";
+ $log->debug("Creating new file 'Created $dirpart'");
+ }
+ print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+
+ # this is an "entries" line
+ $log->debug("/$filepart/1.$meta->{revision}///");
+ print "/$filepart/1.$meta->{revision}///\n";
+
+ # permissions
+ $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+ print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+
+ # transmit file
+ transmitfile($meta->{filehash});
+ } else {
+ my ( $filepart, $dirpart ) = filenamesplit($meta->{name});
+
+ my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
+
+ chdir $dir;
+ my $file_local = $filepart . ".mine";
+ system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
+ my $file_old = $filepart . "." . $oldmeta->{revision};
+ transmitfile($oldmeta->{filehash}, $file_old);
+ my $file_new = $filepart . "." . $meta->{revision};
+ transmitfile($meta->{filehash}, $file_new);
+
+ # we need to merge with the local changes ( M=successful merge, C=conflict merge )
+ $log->info("Merging $file_local, $file_old, $file_new");
+
+ $log->debug("Temporary directory for merge is $dir");
+
+ my $return = system("merge", $file_local, $file_old, $file_new);
+ $return >>= 8;
+
+ if ( $return == 0 )
+ {
+ $log->info("Merged successfully");
+ print "M M $filename\n";
+ $log->debug("Update-existing $dirpart");
+ print "Update-existing $dirpart\n";
+ $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
+ print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+ $log->debug("/$filepart/1.$meta->{revision}///");
+ print "/$filepart/1.$meta->{revision}///\n";
+ }
+ elsif ( $return == 1 )
+ {
+ $log->info("Merged with conflicts");
+ print "M C $filename\n";
+ print "Update-existing $dirpart\n";
+ print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+ print "/$filepart/1.$meta->{revision}/+//\n";
+ }
+ else
+ {
+ $log->warn("Merge failed");
+ next;
+ }
+
+ # permissions
+ $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+ print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+
+ # transmit file, format is single integer on a line by itself (file
+ # size) followed by the file contents
+ # TODO : we should copy files in blocks
+ my $data = `cat $file_local`;
+ $log->debug("File size : " . length($data));
+ print length($data) . "\n";
+ print $data;
+
+ chdir "/";
+ }
+
+ }
+
+ print "ok\n";
+}
+
+sub req_ci
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("ci");
+
+ #$log->debug("State : " . Dumper($state));
+
+ $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
+
+ if ( -e $state->{CVSROOT} . "/index" )
+ {
+ print "error 1 Index already exists in git repo\n";
+ exit;
+ }
+
+ my $lockfile = "$state->{CVSROOT}/refs/heads/$state->{module}.lock";
+ unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) )
+ {
+ print "error 1 Lock file '$lockfile' already exists, please try again\n";
+ exit;
+ }
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ my $tmpdir = tempdir ( DIR => $TEMP_DIR );
+ my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
+ $log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'");
+
+ $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+ $ENV{GIT_INDEX_FILE} = $file_index;
+
+ chdir $tmpdir;
+
+ # populate the temporary index based
+ system("git-read-tree", $state->{module});
+ unless ($? == 0)
+ {
+ die "Error running git-read-tree $state->{module} $file_index $!";
+ }
+ $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
+
+
+ my @committedfiles = ();
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
+
+ my $meta = $updater->getmeta($filename);
+
+ my $wrev = revparse($filename);
+
+ my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+ # do a checkout of the file if it part of this tree
+ if ($wrev) {
+ system('git-checkout-index', '-f', '-u', $filename);
+ unless ($? == 0) {
+ die "Error running git-checkout-index -f -u $filename : $!";
+ }
+ }
+
+ my $addflag = 0;
+ my $rmflag = 0;
+ $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
+ $addflag = 1 unless ( -e $filename );
+
+ # Do up to date checking
+ unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
+ {
+ # fail everything if an up to date check fails
+ print "error 1 Up to date check failed for $filename\n";
+ close LOCKFILE;
+ unlink($lockfile);
+ chdir "/";
+ exit;
+ }
+
+ push @committedfiles, $filename;
+ $log->info("Committing $filename");
+
+ system("mkdir","-p",$dirpart) unless ( -d $dirpart );
+
+ unless ( $rmflag )
+ {
+ $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
+ rename $state->{entries}{$filename}{modified_filename},$filename;
+
+ # Calculate modes to remove
+ my $invmode = "";
+ foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
+
+ $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
+ system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
+ }
+
+ if ( $rmflag )
+ {
+ $log->info("Removing file '$filename'");
+ unlink($filename);
+ system("git-update-index", "--remove", $filename);
+ }
+ elsif ( $addflag )
+ {
+ $log->info("Adding file '$filename'");
+ system("git-update-index", "--add", $filename);
+ } else {
+ $log->info("Updating file '$filename'");
+ system("git-update-index", $filename);
+ }
+ }
+
+ unless ( scalar(@committedfiles) > 0 )
+ {
+ print "E No files to commit\n";
+ print "ok\n";
+ close LOCKFILE;
+ unlink($lockfile);
+ chdir "/";
+ return;
+ }
+
+ my $treehash = `git-write-tree`;
+ my $parenthash = `cat $ENV{GIT_DIR}refs/heads/$state->{module}`;
+ chomp $treehash;
+ chomp $parenthash;
+
+ $log->debug("Treehash : $treehash, Parenthash : $parenthash");
+
+ # write our commit message out if we have one ...
+ my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
+ print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
+ print $msg_fh "\n\nvia git-CVS emulator\n";
+ close $msg_fh;
+
+ my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
+ $log->info("Commit hash : $commithash");
+
+ unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
+ {
+ $log->warn("Commit failed (Invalid commit hash)");
+ print "error 1 Commit failed (unknown reason)\n";
+ close LOCKFILE;
+ unlink($lockfile);
+ chdir "/";
+ exit;
+ }
+
+ open FILE, ">", "$ENV{GIT_DIR}refs/heads/$state->{module}";
+ print FILE $commithash;
+ close FILE;
+
+ $updater->update();
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @committedfiles )
+ {
+ $filename = filecleanup($filename);
+
+ my $meta = $updater->getmeta($filename);
+
+ my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+ $log->debug("Checked-in $dirpart : $filename");
+
+ if ( $meta->{filehash} eq "deleted" )
+ {
+ print "Remove-entry $dirpart\n";
+ print "$filename\n";
+ } else {
+ print "Checked-in $dirpart\n";
+ print "$filename\n";
+ print "/$filepart/1.$meta->{revision}///\n";
+ }
+ }
+
+ close LOCKFILE;
+ unlink($lockfile);
+ chdir "/";
+
+ print "ok\n";
+}
+
+sub req_status
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("status");
+
+ $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
+ #$log->debug("status state : " . Dumper($state));
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ # if no files were specified, we need to work out what files we should be providing status on ...
+ argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ my $meta = $updater->getmeta($filename);
+ my $oldmeta = $meta;
+
+ my $wrev = revparse($filename);
+
+ # If the working copy is an old revision, lets get that version too for comparison.
+ if ( defined($wrev) and $wrev != $meta->{revision} )
+ {
+ $oldmeta = $updater->getmeta($filename, $wrev);
+ }
+
+ # TODO : All possible statuses aren't yet implemented
+ my $status;
+ # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
+ $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
+ and
+ ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
+ or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
+ );
+
+ # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
+ $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
+ and
+ ( $state->{entries}{$filename}{unchanged}
+ or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
+ );
+
+ # Need checkout if it exists in the repo but doesn't have a working copy
+ $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
+
+ # Locally modified if working copy and repo copy have the same revision but there are local changes
+ $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
+
+ # Needs Merge if working copy revision is less than repo copy and there are local changes
+ $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
+
+ $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
+ $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
+ $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
+ $status ||= "File had conflicts on merge" if ( 0 );
+
+ $status ||= "Unknown";
+
+ print "M ===================================================================\n";
+ print "M File: $filename\tStatus: $status\n";
+ if ( defined($state->{entries}{$filename}{revision}) )
+ {
+ print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
+ } else {
+ print "M Working revision:\tNo entry for $filename\n";
+ }
+ if ( defined($meta->{revision}) )
+ {
+ print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{repository}/$filename,v\n";
+ print "M Sticky Tag:\t\t(none)\n";
+ print "M Sticky Date:\t\t(none)\n";
+ print "M Sticky Options:\t\t(none)\n";
+ } else {
+ print "M Repository revision:\tNo revision control file\n";
+ }
+ print "M\n";
+ }
+
+ print "ok\n";
+}
+
+sub req_diff
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("diff");
+
+ $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
+ #$log->debug("status state : " . Dumper($state));
+
+ my ($revision1, $revision2);
+ if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
+ {
+ $revision1 = $state->{opt}{r}[0];
+ $revision2 = $state->{opt}{r}[1];
+ } else {
+ $revision1 = $state->{opt}{r};
+ }
+
+ $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
+ $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
+
+ $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ # if no files were specified, we need to work out what files we should be providing status on ...
+ argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
+
+ my $wrev = revparse($filename);
+
+ # We need _something_ to diff against
+ next unless ( defined ( $wrev ) );
+
+ # if we have a -r switch, use it
+ if ( defined ( $revision1 ) )
+ {
+ ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+ $meta1 = $updater->getmeta($filename, $revision1);
+ unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
+ {
+ print "E File $filename at revision 1.$revision1 doesn't exist\n";
+ next;
+ }
+ transmitfile($meta1->{filehash}, $file1);
+ }
+ # otherwise we just use the working copy revision
+ else
+ {
+ ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+ $meta1 = $updater->getmeta($filename, $wrev);
+ transmitfile($meta1->{filehash}, $file1);
+ }
+
+ # if we have a second -r switch, use it too
+ if ( defined ( $revision2 ) )
+ {
+ ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+ $meta2 = $updater->getmeta($filename, $revision2);
+
+ unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
+ {
+ print "E File $filename at revision 1.$revision2 doesn't exist\n";
+ next;
+ }
+
+ transmitfile($meta2->{filehash}, $file2);
+ }
+ # otherwise we just use the working copy
+ else
+ {
+ $file2 = $state->{entries}{$filename}{modified_filename};
+ }
+
+ # if we have been given -r, and we don't have a $file2 yet, lets get one
+ if ( defined ( $revision1 ) and not defined ( $file2 ) )
+ {
+ ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+ $meta2 = $updater->getmeta($filename, $wrev);
+ transmitfile($meta2->{filehash}, $file2);
+ }
+
+ # We need to have retrieved something useful
+ next unless ( defined ( $meta1 ) );
+
+ # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
+ next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
+ and
+ ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
+ or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
+ );
+
+ # Apparently we only show diffs for locally modified files
+ next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
+
+ print "M Index: $filename\n";
+ print "M ===================================================================\n";
+ print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
+ print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
+ print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
+ print "M diff ";
+ foreach my $opt ( keys %{$state->{opt}} )
+ {
+ if ( ref $state->{opt}{$opt} eq "ARRAY" )
+ {
+ foreach my $value ( @{$state->{opt}{$opt}} )
+ {
+ print "-$opt $value ";
+ }
+ } else {
+ print "-$opt ";
+ print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
+ }
+ }
+ print "$filename\n";
+
+ $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
+
+ ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
+
+ if ( exists $state->{opt}{u} )
+ {
+ system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
+ } else {
+ system("diff $file1 $file2 > $filediff");
+ }
+
+ while ( <$fh> )
+ {
+ print "M $_";
+ }
+ close $fh;
+ }
+
+ print "ok\n";
+}
+
+sub req_log
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("log");
+
+ $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
+ #$log->debug("log state : " . Dumper($state));
+
+ my ( $minrev, $maxrev );
+ if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
+ {
+ my $control = $2;
+ $minrev = $1;
+ $maxrev = $3;
+ $minrev =~ s/^1\.// if ( defined ( $minrev ) );
+ $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
+ $minrev++ if ( defined($minrev) and $control eq "::" );
+ }
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ # if no files were specified, we need to work out what files we should be providing status on ...
+ argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ my $headmeta = $updater->getmeta($filename);
+
+ my $revisions = $updater->getlog($filename);
+ my $totalrevisions = scalar(@$revisions);
+
+ if ( defined ( $minrev ) )
+ {
+ $log->debug("Removing revisions less than $minrev");
+ while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
+ {
+ pop @$revisions;
+ }
+ }
+ if ( defined ( $maxrev ) )
+ {
+ $log->debug("Removing revisions greater than $maxrev");
+ while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
+ {
+ shift @$revisions;
+ }
+ }
+
+ next unless ( scalar(@$revisions) );
+
+ print "M \n";
+ print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
+ print "M Working file: $filename\n";
+ print "M head: 1.$headmeta->{revision}\n";
+ print "M branch:\n";
+ print "M locks: strict\n";
+ print "M access list:\n";
+ print "M symbolic names:\n";
+ print "M keyword substitution: kv\n";
+ print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
+ print "M description:\n";
+
+ foreach my $revision ( @$revisions )
+ {
+ print "M ----------------------------\n";
+ print "M revision 1.$revision->{revision}\n";
+ # reformat the date for log output
+ $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
+ $revision->{author} =~ s/\s+.*//;
+ $revision->{author} =~ s/^(.{8}).*/$1/;
+ print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
+ my $commitmessage = $updater->commitmessage($revision->{commithash});
+ $commitmessage =~ s/^/M /mg;
+ print $commitmessage . "\n";
+ }
+ print "M =============================================================================\n";
+ }
+
+ print "ok\n";
+}
+
+sub req_annotate
+{
+ my ( $cmd, $data ) = @_;
+
+ argsplit("annotate");
+
+ $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
+ #$log->debug("status state : " . Dumper($state));
+
+ # Grab a handle to the SQLite db and do any necessary updates
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ # if no files were specified, we need to work out what files we should be providing annotate on ...
+ argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+ # we'll need a temporary checkout dir
+ my $tmpdir = tempdir ( DIR => $TEMP_DIR );
+ my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
+ $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
+
+ $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+ $ENV{GIT_INDEX_FILE} = $file_index;
+
+ chdir $tmpdir;
+
+ # foreach file specified on the commandline ...
+ foreach my $filename ( @{$state->{args}} )
+ {
+ $filename = filecleanup($filename);
+
+ my $meta = $updater->getmeta($filename);
+
+ next unless ( $meta->{revision} );
+
+ # get all the commits that this file was in
+ # in dense format -- aka skip dead revisions
+ my $revisions = $updater->gethistorydense($filename);
+ my $lastseenin = $revisions->[0][2];
+
+ # populate the temporary index based on the latest commit were we saw
+ # the file -- but do it cheaply without checking out any files
+ # TODO: if we got a revision from the client, use that instead
+ # to look up the commithash in sqlite (still good to default to
+ # the current head as we do now)
+ system("git-read-tree", $lastseenin);
+ unless ($? == 0)
+ {
+ die "Error running git-read-tree $lastseenin $file_index $!";
+ }
+ $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
+
+ # do a checkout of the file
+ system('git-checkout-index', '-f', '-u', $filename);
+ unless ($? == 0) {
+ die "Error running git-checkout-index -f -u $filename : $!";
+ }
+
+ $log->info("Annotate $filename");
+
+ # Prepare a file with the commits from the linearized
+ # history that annotate should know about. This prevents
+ # git-jsannotate telling us about commits we are hiding
+ # from the client.
+
+ open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!";
+ for (my $i=0; $i < @$revisions; $i++)
+ {
+ print ANNOTATEHINTS $revisions->[$i][2];
+ if ($i+1 < @$revisions) { # have we got a parent?
+ print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
+ }
+ print ANNOTATEHINTS "\n";
+ }
+
+ print ANNOTATEHINTS "\n";
+ close ANNOTATEHINTS;
+
+ my $annotatecmd = 'git-annotate';
+ open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename)
+ or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!";
+ my $metadata = {};
+ print "E Annotations for $filename\n";
+ print "E ***************\n";
+ while ( <ANNOTATE> )
+ {
+ if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
+ {
+ my $commithash = $1;
+ my $data = $2;
+ unless ( defined ( $metadata->{$commithash} ) )
+ {
+ $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
+ $metadata->{$commithash}{author} =~ s/\s+.*//;
+ $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
+ $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
+ }
+ printf("M 1.%-5d (%-8s %10s): %s\n",
+ $metadata->{$commithash}{revision},
+ $metadata->{$commithash}{author},
+ $metadata->{$commithash}{modified},
+ $data
+ );
+ } else {
+ $log->warn("Error in annotate output! LINE: $_");
+ print "E Annotate error \n";
+ next;
+ }
+ }
+ close ANNOTATE;
+ }
+
+ # done; get out of the tempdir
+ chdir "/";
+
+ print "ok\n";
+
+}
+
+# This method takes the state->{arguments} array and produces two new arrays.
+# The first is $state->{args} which is everything before the '--' argument, and
+# the second is $state->{files} which is everything after it.
+sub argsplit
+{
+ return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
+
+ my $type = shift;
+
+ $state->{args} = [];
+ $state->{files} = [];
+ $state->{opt} = {};
+
+ if ( defined($type) )
+ {
+ my $opt = {};
+ $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
+ $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
+ $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
+ $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
+ $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
+ $opt = { k => 1, m => 1 } if ( $type eq "add" );
+ $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
+ $opt = { l => 0, b => 0, h => 0, R => 0, t => 0, N => 0, S => 0, r => 1, d => 1, s => 1, w => 1 } if ( $type eq "log" );
+
+
+ while ( scalar ( @{$state->{arguments}} ) > 0 )
+ {
+ my $arg = shift @{$state->{arguments}};
+
+ next if ( $arg eq "--" );
+ next unless ( $arg =~ /\S/ );
+
+ # if the argument looks like a switch
+ if ( $arg =~ /^-(\w)(.*)/ )
+ {
+ # if it's a switch that takes an argument
+ if ( $opt->{$1} )
+ {
+ # If this switch has already been provided
+ if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
+ {
+ $state->{opt}{$1} = [ $state->{opt}{$1} ];
+ if ( length($2) > 0 )
+ {
+ push @{$state->{opt}{$1}},$2;
+ } else {
+ push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
+ }
+ } else {
+ # if there's extra data in the arg, use that as the argument for the switch
+ if ( length($2) > 0 )
+ {
+ $state->{opt}{$1} = $2;
+ } else {
+ $state->{opt}{$1} = shift @{$state->{arguments}};
+ }
+ }
+ } else {
+ $state->{opt}{$1} = undef;
+ }
+ }
+ else
+ {
+ push @{$state->{args}}, $arg;
+ }
+ }
+ }
+ else
+ {
+ my $mode = 0;
+
+ foreach my $value ( @{$state->{arguments}} )
+ {
+ if ( $value eq "--" )
+ {
+ $mode++;
+ next;
+ }
+ push @{$state->{args}}, $value if ( $mode == 0 );
+ push @{$state->{files}}, $value if ( $mode == 1 );
+ }
+ }
+}
+
+# This method uses $state->{directory} to populate $state->{args} with a list of filenames
+sub argsfromdir
+{
+ my $updater = shift;
+
+ $state->{args} = [];
+
+ foreach my $file ( @{$updater->gethead} )
+ {
+ next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
+ next unless ( $file->{name} =~ s/^$state->{directory}// );
+ push @{$state->{args}}, $file->{name};
+ }
+}
+
+# This method cleans up the $state variable after a command that uses arguments has run
+sub statecleanup
+{
+ $state->{files} = [];
+ $state->{args} = [];
+ $state->{arguments} = [];
+ $state->{entries} = {};
+}
+
+sub revparse
+{
+ my $filename = shift;
+
+ return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
+
+ return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
+ return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
+
+ return undef;
+}
+
+# This method takes a file hash and does a CVS "file transfer" which transmits the
+# size of the file, and then the file contents.
+# If a second argument $targetfile is given, the file is instead written out to
+# a file by the name of $targetfile
+sub transmitfile
+{
+ my $filehash = shift;
+ my $targetfile = shift;
+
+ if ( defined ( $filehash ) and $filehash eq "deleted" )
+ {
+ $log->warn("filehash is 'deleted'");
+ return;
+ }
+
+ die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
+
+ my $type = `git-cat-file -t $filehash`;
+ chomp $type;
+
+ die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
+
+ my $size = `git-cat-file -s $filehash`;
+ chomp $size;
+
+ $log->debug("transmitfile($filehash) size=$size, type=$type");
+
+ if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
+ {
+ if ( defined ( $targetfile ) )
+ {
+ open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
+ print NEWFILE $_ while ( <$fh> );
+ close NEWFILE;
+ } else {
+ print "$size\n";
+ print while ( <$fh> );
+ }
+ close $fh or die ("Couldn't close filehandle for transmitfile()");
+ } else {
+ die("Couldn't execute git-cat-file");
+ }
+}
+
+# This method takes a file name, and returns ( $dirpart, $filepart ) which
+# refers to the directory porition and the file portion of the filename
+# respectively
+sub filenamesplit
+{
+ my $filename = shift;
+
+ my ( $filepart, $dirpart ) = ( $filename, "." );
+ ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
+ $dirpart .= "/";
+
+ return ( $filepart, $dirpart );
+}
+
+sub filecleanup
+{
+ my $filename = shift;
+
+ return undef unless(defined($filename));
+ if ( $filename =~ /^\// )
+ {
+ print "E absolute filenames '$filename' not supported by server\n";
+ return undef;
+ }
+
+ $filename =~ s/^\.\///g;
+ $filename = $state->{directory} . $filename;
+
+ return $filename;
+}
+
+package GITCVS::log;
+
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith <martyn@catalyst.net.nz>
+#### Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+GITCVS::log
+
+=head1 DESCRIPTION
+
+This module provides very crude logging with a similar interface to
+Log::Log4perl
+
+=head1 METHODS
+
+=cut
+
+=head2 new
+
+Creates a new log object, optionally you can specify a filename here to
+indicate the file to log to. If no log file is specified, you can specifiy one
+later with method setfile, or indicate you no longer want logging with method
+nofile.
+
+Until one of these methods is called, all log calls will buffer messages ready
+to write out.
+
+=cut
+sub new
+{
+ my $class = shift;
+ my $filename = shift;
+
+ my $self = {};
+
+ bless $self, $class;
+
+ if ( defined ( $filename ) )
+ {
+ open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
+ }
+
+ return $self;
+}
+
+=head2 setfile
+
+This methods takes a filename, and attempts to open that file as the log file.
+If successful, all buffered data is written out to the file, and any further
+logging is written directly to the file.
+
+=cut
+sub setfile
+{
+ my $self = shift;
+ my $filename = shift;
+
+ if ( defined ( $filename ) )
+ {
+ open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
+ }
+
+ return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
+
+ while ( my $line = shift @{$self->{buffer}} )
+ {
+ print {$self->{fh}} $line;
+ }
+}
+
+=head2 nofile
+
+This method indicates no logging is going to be used. It flushes any entries in
+the internal buffer, and sets a flag to ensure no further data is put there.
+
+=cut
+sub nofile
+{
+ my $self = shift;
+
+ $self->{nolog} = 1;
+
+ return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
+
+ $self->{buffer} = [];
+}
+
+=head2 _logopen
+
+Internal method. Returns true if the log file is open, false otherwise.
+
+=cut
+sub _logopen
+{
+ my $self = shift;
+
+ return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
+ return 0;
+}
+
+=head2 debug info warn fatal
+
+These four methods are wrappers to _log. They provide the actual interface for
+logging data.
+
+=cut
+sub debug { my $self = shift; $self->_log("debug", @_); }
+sub info { my $self = shift; $self->_log("info" , @_); }
+sub warn { my $self = shift; $self->_log("warn" , @_); }
+sub fatal { my $self = shift; $self->_log("fatal", @_); }
+
+=head2 _log
+
+This is an internal method called by the logging functions. It generates a
+timestamp and pushes the logged line either to file, or internal buffer.
+
+=cut
+sub _log
+{
+ my $self = shift;
+ my $level = shift;
+
+ return if ( $self->{nolog} );
+
+ my @time = localtime;
+ my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
+ $time[5] + 1900,
+ $time[4] + 1,
+ $time[3],
+ $time[2],
+ $time[1],
+ $time[0],
+ uc $level,
+ );
+
+ if ( $self->_logopen )
+ {
+ print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
+ } else {
+ push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
+ }
+}
+
+=head2 DESTROY
+
+This method simply closes the file handle if one is open
+
+=cut
+sub DESTROY
+{
+ my $self = shift;
+
+ if ( $self->_logopen )
+ {
+ close $self->{fh};
+ }
+}
+
+package GITCVS::updater;
+
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith <martyn@catalyst.net.nz>
+#### Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+
+use strict;
+use warnings;
+use DBI;
+
+=head1 METHODS
+
+=cut
+
+=head2 new
+
+=cut
+sub new
+{
+ my $class = shift;
+ my $config = shift;
+ my $module = shift;
+ my $log = shift;
+
+ die "Need to specify a git repository" unless ( defined($config) and -d $config );
+ die "Need to specify a module" unless ( defined($module) );
+
+ $class = ref($class) || $class;
+
+ my $self = {};
+
+ bless $self, $class;
+
+ $self->{dbdir} = $config . "/";
+ die "Database dir '$self->{dbdir}' isn't a directory" unless ( defined($self->{dbdir}) and -d $self->{dbdir} );
+
+ $self->{module} = $module;
+ $self->{file} = $self->{dbdir} . "/gitcvs.$module.sqlite";
+
+ $self->{git_path} = $config . "/";
+
+ $self->{log} = $log;
+
+ die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
+
+ $self->{dbh} = DBI->connect("dbi:SQLite:dbname=" . $self->{file},"","");
+
+ $self->{tables} = {};
+ foreach my $table ( $self->{dbh}->tables )
+ {
+ $table =~ s/^"//;
+ $table =~ s/"$//;
+ $self->{tables}{$table} = 1;
+ }
+
+ # Construct the revision table if required
+ unless ( $self->{tables}{revision} )
+ {
+ $self->{dbh}->do("
+ CREATE TABLE revision (
+ name TEXT NOT NULL,
+ revision INTEGER NOT NULL,
+ filehash TEXT NOT NULL,
+ commithash TEXT NOT NULL,
+ author TEXT NOT NULL,
+ modified TEXT NOT NULL,
+ mode TEXT NOT NULL
+ )
+ ");
+ }
+
+ # Construct the revision table if required
+ unless ( $self->{tables}{head} )
+ {
+ $self->{dbh}->do("
+ CREATE TABLE head (
+ name TEXT NOT NULL,
+ revision INTEGER NOT NULL,
+ filehash TEXT NOT NULL,
+ commithash TEXT NOT NULL,
+ author TEXT NOT NULL,
+ modified TEXT NOT NULL,
+ mode TEXT NOT NULL
+ )
+ ");
+ }
+
+ # Construct the properties table if required
+ unless ( $self->{tables}{properties} )
+ {
+ $self->{dbh}->do("
+ CREATE TABLE properties (
+ key TEXT NOT NULL PRIMARY KEY,
+ value TEXT
+ )
+ ");
+ }
+
+ # Construct the commitmsgs table if required
+ unless ( $self->{tables}{commitmsgs} )
+ {
+ $self->{dbh}->do("
+ CREATE TABLE commitmsgs (
+ key TEXT NOT NULL PRIMARY KEY,
+ value TEXT
+ )
+ ");
+ }
+
+ return $self;
+}
+
+=head2 update
+
+=cut
+sub update
+{
+ my $self = shift;
+
+ # first lets get the commit list
+ $ENV{GIT_DIR} = $self->{git_path};
+
+ # prepare database queries
+ my $db_insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
+ my $db_insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
+ my $db_delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
+ my $db_insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
+
+ my $commitinfo = `git-cat-file commit $self->{module} 2>&1`;
+ unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
+ {
+ die("Invalid module '$self->{module}'");
+ }
+
+
+ my $git_log;
+ my $lastcommit = $self->_get_prop("last_commit");
+
+ # Start exclusive lock here...
+ $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
+
+ # TODO: log processing is memory bound
+ # if we can parse into a 2nd file that is in reverse order
+ # we can probably do something really efficient
+ my @git_log_params = ('--parents', '--topo-order');
+
+ if (defined $lastcommit) {
+ push @git_log_params, "$lastcommit..$self->{module}";
+ } else {
+ push @git_log_params, $self->{module};
+ }
+ open(GITLOG, '-|', 'git-log', @git_log_params) or die "Cannot call git-log: $!";
+
+ my @commits;
+
+ my %commit = ();
+
+ while ( <GITLOG> )
+ {
+ chomp;
+ if (m/^commit\s+(.*)$/) {
+ # on ^commit lines put the just seen commit in the stack
+ # and prime things for the next one
+ if (keys %commit) {
+ my %copy = %commit;
+ unshift @commits, \%copy;
+ %commit = ();
+ }
+ my @parents = split(m/\s+/, $1);
+ $commit{hash} = shift @parents;
+ $commit{parents} = \@parents;
+ } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
+ # on rfc822-like lines seen before we see any message,
+ # lowercase the entry and put it in the hash as key-value
+ $commit{lc($1)} = $2;
+ } else {
+ # message lines - skip initial empty line
+ # and trim whitespace
+ if (!exists($commit{message}) && m/^\s*$/) {
+ # define it to mark the end of headers
+ $commit{message} = '';
+ next;
+ }
+ s/^\s+//; s/\s+$//; # trim ws
+ $commit{message} .= $_ . "\n";
+ }
+ }
+ close GITLOG;
+
+ unshift @commits, \%commit if ( keys %commit );
+
+ # Now all the commits are in the @commits bucket
+ # ordered by time DESC. for each commit that needs processing,
+ # determine whether it's following the last head we've seen or if
+ # it's on its own branch, grab a file list, and add whatever's changed
+ # NOTE: $lastcommit refers to the last commit from previous run
+ # $lastpicked is the last commit we picked in this run
+ my $lastpicked;
+ my $head = {};
+ if (defined $lastcommit) {
+ $lastpicked = $lastcommit;
+ }
+
+ my $committotal = scalar(@commits);
+ my $commitcount = 0;
+
+ # Load the head table into $head (for cached lookups during the update process)
+ foreach my $file ( @{$self->gethead()} )
+ {
+ $head->{$file->{name}} = $file;
+ }
+
+ foreach my $commit ( @commits )
+ {
+ $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
+ if (defined $lastpicked)
+ {
+ if (!in_array($lastpicked, @{$commit->{parents}}))
+ {
+ # skip, we'll see this delta
+ # as part of a merge later
+ # warn "skipping off-track $commit->{hash}\n";
+ next;
+ } elsif (@{$commit->{parents}} > 1) {
+ # it is a merge commit, for each parent that is
+ # not $lastpicked, see if we can get a log
+ # from the merge-base to that parent to put it
+ # in the message as a merge summary.
+ my @parents = @{$commit->{parents}};
+ foreach my $parent (@parents) {
+ # git-merge-base can potentially (but rarely) throw
+ # several candidate merge bases. let's assume
+ # that the first one is the best one.
+ if ($parent eq $lastpicked) {
+ next;
+ }
+ open my $p, 'git-merge-base '. $lastpicked . ' '
+ . $parent . '|';
+ my @output = (<$p>);
+ close $p;
+ my $base = join('', @output);
+ chomp $base;
+ if ($base) {
+ my @merged;
+ # print "want to log between $base $parent \n";
+ open(GITLOG, '-|', 'git-log', "$base..$parent")
+ or die "Cannot call git-log: $!";
+ my $mergedhash;
+ while (<GITLOG>) {
+ chomp;
+ if (!defined $mergedhash) {
+ if (m/^commit\s+(.+)$/) {
+ $mergedhash = $1;
+ } else {
+ next;
+ }
+ } else {
+ # grab the first line that looks non-rfc822
+ # aka has content after leading space
+ if (m/^\s+(\S.*)$/) {
+ my $title = $1;
+ $title = substr($title,0,100); # truncate
+ unshift @merged, "$mergedhash $title";
+ undef $mergedhash;
+ }
+ }
+ }
+ close GITLOG;
+ if (@merged) {
+ $commit->{mergemsg} = $commit->{message};
+ $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
+ foreach my $summary (@merged) {
+ $commit->{mergemsg} .= "\t$summary\n";
+ }
+ $commit->{mergemsg} .= "\n\n";
+ # print "Message for $commit->{hash} \n$commit->{mergemsg}";
+ }
+ }
+ }
+ }
+ }
+
+ # convert the date to CVS-happy format
+ $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
+
+ if ( defined ( $lastpicked ) )
+ {
+ my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
+ while ( <FILELIST> )
+ {
+ unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)\s+(.*)$/o )
+ {
+ die("Couldn't process git-diff-tree line : $_");
+ }
+
+ # $log->debug("File mode=$1, hash=$2, change=$3, name=$4");
+
+ my $git_perms = "";
+ $git_perms .= "r" if ( $1 & 4 );
+ $git_perms .= "w" if ( $1 & 2 );
+ $git_perms .= "x" if ( $1 & 1 );
+ $git_perms = "rw" if ( $git_perms eq "" );
+
+ if ( $3 eq "D" )
+ {
+ #$log->debug("DELETE $4");
+ $head->{$4} = {
+ name => $4,
+ revision => $head->{$4}{revision} + 1,
+ filehash => "deleted",
+ commithash => $commit->{hash},
+ modified => $commit->{date},
+ author => $commit->{author},
+ mode => $git_perms,
+ };
+ $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ }
+ elsif ( $3 eq "M" )
+ {
+ #$log->debug("MODIFIED $4");
+ $head->{$4} = {
+ name => $4,
+ revision => $head->{$4}{revision} + 1,
+ filehash => $2,
+ commithash => $commit->{hash},
+ modified => $commit->{date},
+ author => $commit->{author},
+ mode => $git_perms,
+ };
+ $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ }
+ elsif ( $3 eq "A" )
+ {
+ #$log->debug("ADDED $4");
+ $head->{$4} = {
+ name => $4,
+ revision => 1,
+ filehash => $2,
+ commithash => $commit->{hash},
+ modified => $commit->{date},
+ author => $commit->{author},
+ mode => $git_perms,
+ };
+ $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ }
+ else
+ {
+ $log->warn("UNKNOWN FILE CHANGE mode=$1, hash=$2, change=$3, name=$4");
+ die;
+ }
+ }
+ close FILELIST;
+ } else {
+ # this is used to detect files removed from the repo
+ my $seen_files = {};
+
+ my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
+ while ( <FILELIST> )
+ {
+ unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\s+(.*)$/o )
+ {
+ die("Couldn't process git-ls-tree line : $_");
+ }
+
+ my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+
+ $seen_files->{$git_filename} = 1;
+
+ my ( $oldhash, $oldrevision, $oldmode ) = (
+ $head->{$git_filename}{filehash},
+ $head->{$git_filename}{revision},
+ $head->{$git_filename}{mode}
+ );
+
+ if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
+ {
+ $git_perms = "";
+ $git_perms .= "r" if ( $1 & 4 );
+ $git_perms .= "w" if ( $1 & 2 );
+ $git_perms .= "x" if ( $1 & 1 );
+ } else {
+ $git_perms = "rw";
+ }
+
+ # unless the file exists with the same hash, we need to update it ...
+ unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
+ {
+ my $newrevision = ( $oldrevision or 0 ) + 1;
+
+ $head->{$git_filename} = {
+ name => $git_filename,
+ revision => $newrevision,
+ filehash => $git_hash,
+ commithash => $commit->{hash},
+ modified => $commit->{date},
+ author => $commit->{author},
+ mode => $git_perms,
+ };
+
+
+ $db_insert_rev->execute($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ }
+ }
+ close FILELIST;
+
+ # Detect deleted files
+ foreach my $file ( keys %$head )
+ {
+ unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
+ {
+ $head->{$file}{revision}++;
+ $head->{$file}{filehash} = "deleted";
+ $head->{$file}{commithash} = $commit->{hash};
+ $head->{$file}{modified} = $commit->{date};
+ $head->{$file}{author} = $commit->{author};
+
+ $db_insert_rev->execute($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
+ }
+ }
+ # END : "Detect deleted files"
+ }
+
+
+ if (exists $commit->{mergemsg})
+ {
+ $db_insert_mergelog->execute($commit->{hash}, $commit->{mergemsg});
+ }
+
+ $lastpicked = $commit->{hash};
+
+ $self->_set_prop("last_commit", $commit->{hash});
+ }
+
+ $db_delete_head->execute();
+ foreach my $file ( keys %$head )
+ {
+ $db_insert_head->execute(
+ $file,
+ $head->{$file}{revision},
+ $head->{$file}{filehash},
+ $head->{$file}{commithash},
+ $head->{$file}{modified},
+ $head->{$file}{author},
+ $head->{$file}{mode},
+ );
+ }
+ # invalidate the gethead cache
+ $self->{gethead_cache} = undef;
+
+
+ # Ending exclusive lock here
+ $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
+}
+
+sub _headrev
+{
+ my $self = shift;
+ my $filename = shift;
+
+ my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
+ $db_query->execute($filename);
+ my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
+
+ return ( $hash, $revision, $mode );
+}
+
+sub _get_prop
+{
+ my $self = shift;
+ my $key = shift;
+
+ my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
+ $db_query->execute($key);
+ my ( $value ) = $db_query->fetchrow_array;
+
+ return $value;
+}
+
+sub _set_prop
+{
+ my $self = shift;
+ my $key = shift;
+ my $value = shift;
+
+ my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
+ $db_query->execute($value, $key);
+
+ unless ( $db_query->rows )
+ {
+ $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
+ $db_query->execute($key, $value);
+ }
+
+ return $value;
+}
+
+=head2 gethead
+
+=cut
+
+sub gethead
+{
+ my $self = shift;
+
+ return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
+
+ my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head",{},1);
+ $db_query->execute();
+
+ my $tree = [];
+ while ( my $file = $db_query->fetchrow_hashref )
+ {
+ push @$tree, $file;
+ }
+
+ $self->{gethead_cache} = $tree;
+
+ return $tree;
+}
+
+=head2 getlog
+
+=cut
+
+sub getlog
+{
+ my $self = shift;
+ my $filename = shift;
+
+ my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
+ $db_query->execute($filename);
+
+ my $tree = [];
+ while ( my $file = $db_query->fetchrow_hashref )
+ {
+ push @$tree, $file;
+ }
+
+ return $tree;
+}
+
+=head2 getmeta
+
+This function takes a filename (with path) argument and returns a hashref of
+metadata for that file.
+
+=cut
+
+sub getmeta
+{
+ my $self = shift;
+ my $filename = shift;
+ my $revision = shift;
+
+ my $db_query;
+ if ( defined($revision) and $revision =~ /^\d+$/ )
+ {
+ $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
+ $db_query->execute($filename, $revision);
+ }
+ elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
+ {
+ $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
+ $db_query->execute($filename, $revision);
+ } else {
+ $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
+ $db_query->execute($filename);
+ }
+
+ return $db_query->fetchrow_hashref;
+}
+
+=head2 commitmessage
+
+this function takes a commithash and returns the commit message for that commit
+
+=cut
+sub commitmessage
+{
+ my $self = shift;
+ my $commithash = shift;
+
+ die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
+
+ my $db_query;
+ $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
+ $db_query->execute($commithash);
+
+ my ( $message ) = $db_query->fetchrow_array;
+
+ if ( defined ( $message ) )
+ {
+ $message .= " " if ( $message =~ /\n$/ );
+ return $message;
+ }
+
+ my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
+ shift @lines while ( $lines[0] =~ /\S/ );
+ $message = join("",@lines);
+ $message .= " " if ( $message =~ /\n$/ );
+ return $message;
+}
+
+=head2 gethistory
+
+This function takes a filename (with path) argument and returns an arrayofarrays
+containing revision,filehash,commithash ordered by revision descending
+
+=cut
+sub gethistory
+{
+ my $self = shift;
+ my $filename = shift;
+
+ my $db_query;
+ $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
+ $db_query->execute($filename);
+
+ return $db_query->fetchall_arrayref;
+}
+
+=head2 gethistorydense
+
+This function takes a filename (with path) argument and returns an arrayofarrays
+containing revision,filehash,commithash ordered by revision descending.
+
+This version of gethistory skips deleted entries -- so it is useful for annotate.
+The 'dense' part is a reference to a '--dense' option available for git-rev-list
+and other git tools that depend on it.
+
+=cut
+sub gethistorydense
+{
+ my $self = shift;
+ my $filename = shift;
+
+ my $db_query;
+ $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
+ $db_query->execute($filename);
+
+ return $db_query->fetchall_arrayref;
+}
+
+=head2 in_array()
+
+from Array::PAT - mimics the in_array() function
+found in PHP. Yuck but works for small arrays.
+
+=cut
+sub in_array
+{
+ my ($check, @array) = @_;
+ my $retval = 0;
+ foreach my $test (@array){
+ if($check eq $test){
+ $retval = 1;
+ }
+ }
+ return $retval;
+}
+
+=head2 safe_pipe_capture
+
+an alterative to `command` that allows input to be passed as an array
+to work around shell problems with weird characters in arguments
+
+=cut
+sub safe_pipe_capture {
+
+ my @output;
+
+ if (my $pid = open my $child, '-|') {
+ @output = (<$child>);
+ close $child or die join(' ',@_).": $! $?";
+ } else {
+ exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
+ }
+ return wantarray ? @output : join('',@output);
+}
+
+
+1;
diff --git a/git-fetch.sh b/git-fetch.sh
index b4325d9d98..0346d4a45c 100755
--- a/git-fetch.sh
+++ b/git-fetch.sh
@@ -164,6 +164,7 @@ fast_forward_local () {
;;
*,$local)
echo >&2 "* $1: fast forward to $3"
+ echo >&2 " from $local to $2"
git-update-ref "$1" "$2" "$local"
;;
*)
@@ -320,7 +321,7 @@ fetch_main () {
( : subshell because we muck with IFS
IFS=" $LF"
(
- git-fetch-pack $exec $keep "$remote" $rref || echo failed "$remote"
+ git-fetch-pack $exec $keep --thin "$remote" $rref || echo failed "$remote"
) |
while read sha1 remote_name
do
@@ -368,20 +369,25 @@ fetch_main "$reflist"
# automated tag following
case "$no_tags$tags" in
'')
- taglist=$(IFS=" " &&
- git-ls-remote $upload_pack --tags "$remote" |
- sed -ne 's|^\([0-9a-f]*\)[ ]\(refs/tags/.*\)^{}$|\1 \2|p' |
- while read sha1 name
- do
- test -f "$GIT_DIR/$name" && continue
- git-check-ref-format "$name" || {
- echo >&2 "warning: tag ${name} ignored"
- continue
- }
- git-cat-file -t "$sha1" >/dev/null 2>&1 || continue
- echo >&2 "Auto-following $name"
- echo ".${name}:${name}"
- done)
+ case "$reflist" in
+ *:refs/*)
+ # effective only when we are following remote branch
+ # using local tracking branch.
+ taglist=$(IFS=" " &&
+ git-ls-remote $upload_pack --tags "$remote" |
+ sed -ne 's|^\([0-9a-f]*\)[ ]\(refs/tags/.*\)^{}$|\1 \2|p' |
+ while read sha1 name
+ do
+ test -f "$GIT_DIR/$name" && continue
+ git-check-ref-format "$name" || {
+ echo >&2 "warning: tag ${name} ignored"
+ continue
+ }
+ git-cat-file -t "$sha1" >/dev/null 2>&1 || continue
+ echo >&2 "Auto-following $name"
+ echo ".${name}:${name}"
+ done)
+ esac
case "$taglist" in
'') ;;
?*)
diff --git a/git-fmt-merge-msg.perl b/git-fmt-merge-msg.perl
index c13af4884a..dae383f231 100755
--- a/git-fmt-merge-msg.perl
+++ b/git-fmt-merge-msg.perl
@@ -28,28 +28,13 @@ sub andjoin {
}
sub repoconfig {
- my $val;
- eval {
- my $pid = open(my $fh, '-|');
- if (!$pid) {
- exec('git-repo-config', '--get', 'merge.summary');
- }
- ($val) = <$fh>;
- close $fh;
- };
+ my ($val) = qx{git-repo-config --get merge.summary};
return $val;
}
sub current_branch {
- my $fh;
- my $pid = open($fh, '-|');
- die "$!" unless defined $pid;
- if (!$pid) {
- exec('git-symbolic-ref', 'HEAD') or die "$!";
- }
- my ($bra) = <$fh>;
+ my ($bra) = qx{git-symbolic-ref HEAD};
chomp($bra);
- close $fh or die "$!";
$bra =~ s|^refs/heads/||;
if ($bra ne 'master') {
$bra = " into $bra";
@@ -61,18 +46,12 @@ sub current_branch {
sub shortlog {
my ($tip) = @_;
- my ($fh, @result);
- my $pid = open($fh, '-|');
- die "$!" unless defined $pid;
- if (!$pid) {
- exec('git-log', '--topo-order',
- '--pretty=oneline', $tip, '^HEAD') or die "$!";
- }
- while (<$fh>) {
+ my @result;
+ foreach ( qx{git-log --topo-order --pretty=oneline $tip ^HEAD} ) {
s/^[0-9a-f]{40}\s+//;
push @result, $_;
}
- close $fh or die "$!";
+ die "git-log failed\n" if $?;
return @result;
}
diff --git a/git-format-patch.sh b/git-format-patch.sh
index eb75de4601..2bd26395ec 100755
--- a/git-format-patch.sh
+++ b/git-format-patch.sh
@@ -174,7 +174,7 @@ titleScript='
process_one () {
perl -w -e '
my ($keep_subject, $num, $signoff, $commsg) = @ARGV;
-my ($signoff_pattern, $done_header, $done_subject, $signoff_seen,
+my ($signoff_pattern, $done_header, $done_subject, $done_separator, $signoff_seen,
$last_was_signoff);
if ($signoff) {
@@ -228,6 +228,11 @@ while (<FH>) {
$done_subject = 1;
next;
}
+ unless ($done_separator) {
+ print "\n";
+ $done_separator = 1;
+ next if (/^$/);
+ }
$last_was_signoff = 0;
if (/Signed-off-by:/i) {
diff --git a/git-merge.sh b/git-merge.sh
index 4609fe54d5..7be9e81f1f 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -134,7 +134,7 @@ case "$#,$common,$no_commit" in
echo "Updating from $head to $1."
git-update-index --refresh 2>/dev/null
new_head=$(git-rev-parse --verify "$1^0") &&
- git-read-tree -u -m $head "$new_head" &&
+ git-read-tree -u -v -m $head "$new_head" &&
finish "$new_head" "Fast forward"
dropsave
exit 0
@@ -150,7 +150,7 @@ case "$#,$common,$no_commit" in
echo "Trying really trivial in-index merge..."
git-update-index --refresh 2>/dev/null
- if git-read-tree --trivial -m -u $common $head "$1" &&
+ if git-read-tree --trivial -m -u -v $common $head "$1" &&
result_tree=$(git-write-tree)
then
echo "Wonderful."
diff --git a/git-push.sh b/git-push.sh
index 706db9933e..73dcf067cb 100755
--- a/git-push.sh
+++ b/git-push.sh
@@ -8,6 +8,7 @@ USAGE='[--all] [--tags] [--force] <repository> [<refspec>...]'
has_all=
has_force=
has_exec=
+has_thin=
remote=
do_tags=
@@ -22,6 +23,8 @@ do
has_force=--force ;;
--exec=*)
has_exec="$1" ;;
+ --thin)
+ has_thin="$1" ;;
-*)
usage ;;
*)
@@ -72,6 +75,7 @@ set x "$remote" "$@"; shift
test "$has_all" && set x "$has_all" "$@" && shift
test "$has_force" && set x "$has_force" "$@" && shift
test "$has_exec" && set x "$has_exec" "$@" && shift
+test "$has_thin" && set x "$has_thin" "$@" && shift
case "$remote" in
http://* | https://*)
diff --git a/git-rm.sh b/git-rm.sh
new file mode 100755
index 0000000000..fda4541c76
--- /dev/null
+++ b/git-rm.sh
@@ -0,0 +1,70 @@
+#!/bin/sh
+
+USAGE='[-f] [-n] [-v] [--] <file>...'
+SUBDIRECTORY_OK='Yes'
+. git-sh-setup
+
+remove_files=
+show_only=
+verbose=
+while : ; do
+ case "$1" in
+ -f)
+ remove_files=true
+ ;;
+ -n)
+ show_only=true
+ ;;
+ -v)
+ verbose=--verbose
+ ;;
+ --)
+ shift; break
+ ;;
+ -*)
+ usage
+ ;;
+ *)
+ break
+ ;;
+ esac
+ shift
+done
+
+# This is typo-proofing. If some paths match and some do not, we want
+# to do nothing.
+case "$#" in
+0) ;;
+*)
+ git-ls-files --error-unmatch -- "$@" >/dev/null || {
+ echo >&2 "Maybe you misspelled it?"
+ exit 1
+ }
+ ;;
+esac
+
+if test -f "$GIT_DIR/info/exclude"
+then
+ git-ls-files -z \
+ --exclude-from="$GIT_DIR/info/exclude" \
+ --exclude-per-directory=.gitignore -- "$@"
+else
+ git-ls-files -z \
+ --exclude-per-directory=.gitignore -- "$@"
+fi |
+case "$show_only,$remove_files" in
+true,*)
+ xargs -0 echo
+ ;;
+*,true)
+ xargs -0 sh -c "
+ while [ \$# -gt 0 ]; do
+ file=\$1; shift
+ rm -- \"\$file\" && git-update-index --remove $verbose \"\$file\"
+ done
+ " inline
+ ;;
+*)
+ git-update-index --force-remove $verbose -z --stdin
+ ;;
+esac
diff --git a/git-svnimport.perl b/git-svnimport.perl
index ee2940f480..639aa41861 100755
--- a/git-svnimport.perl
+++ b/git-svnimport.perl
@@ -13,6 +13,7 @@
use strict;
use warnings;
use Getopt::Std;
+use File::Copy;
use File::Spec;
use File::Temp qw(tempfile);
use File::Path qw(mkpath);
@@ -29,19 +30,21 @@ die "Need SVN:Core 1.2.1 or better" if $SVN::Core::VERSION lt "1.2.1";
$SIG{'PIPE'}="IGNORE";
$ENV{'TZ'}="UTC";
-our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,$opt_b,$opt_r,$opt_s,$opt_l,$opt_d,$opt_D);
+our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,
+ $opt_b,$opt_r,$opt_I,$opt_A,$opt_s,$opt_l,$opt_d,$opt_D);
sub usage() {
print STDERR <<END;
Usage: ${\basename $0} # fetch/update GIT from SVN
[-o branch-for-HEAD] [-h] [-v] [-l max_rev]
[-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname]
- [-d|-D] [-i] [-u] [-r] [-s start_chg] [-m] [-M regex] [SVN_URL]
+ [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg]
+ [-m] [-M regex] [-A author_file] [SVN_URL]
END
exit(1);
}
-getopts("b:C:dDhil:mM:o:rs:t:T:uv") or usage();
+getopts("A:b:C:dDhiI:l:mM:o:rs:t:T:uv") or usage();
usage if $opt_h;
my $tag_name = $opt_t || "tags";
@@ -66,6 +69,25 @@ if ($opt_M) {
push (@mergerx, qr/$opt_M/);
}
+# Absolutize filename now, since we will have chdir'ed by the time we
+# get around to opening it.
+$opt_A = File::Spec->rel2abs($opt_A) if $opt_A;
+
+our %users = ();
+our $users_file = undef;
+sub read_users($) {
+ $users_file = File::Spec->rel2abs(@_);
+ die "Cannot open $users_file\n" unless -f $users_file;
+ open(my $authors,$users_file);
+ while(<$authors>) {
+ chomp;
+ next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+ (my $user,my $name,my $email) = ($1,$2,$3);
+ $users{$user} = [$name,$email];
+ }
+ close($authors);
+}
+
select(STDERR); $|=1; select(STDOUT);
@@ -112,16 +134,40 @@ sub file {
DIR => File::Spec->tmpdir(), UNLINK => 1);
print "... $rev $path ...\n" if $opt_v;
- my $pool = SVN::Pool->new();
- eval { $self->{'svn'}->get_file($path,$rev,$fh,$pool); };
- $pool->clear;
+ my (undef, $properties);
+ eval { (undef, $properties)
+ = $self->{'svn'}->get_file($path,$rev,$fh); };
if($@) {
return undef if $@ =~ /Attempted to get checksum/;
die $@;
}
+ my $mode;
+ if (exists $properties->{'svn:executable'}) {
+ $mode = '0755';
+ } else {
+ $mode = '0644';
+ }
close ($fh);
- return $name;
+ return ($name, $mode);
+}
+
+sub ignore {
+ my($self,$path,$rev) = @_;
+
+ print "... $rev $path ...\n" if $opt_v;
+ my (undef,undef,$properties)
+ = $self->{'svn'}->get_dir($path,$rev,undef);
+ if (exists $properties->{'svn:ignore'}) {
+ my ($fh, $name) = tempfile('gitsvn.XXXXXX',
+ DIR => File::Spec->tmpdir(),
+ UNLINK => 1);
+ print $fh $properties->{'svn:ignore'};
+ close($fh);
+ return $name;
+ } else {
+ return undef;
+ }
}
package main;
@@ -263,6 +309,14 @@ EOM
-d $git_dir
or die "Could not create git subdir ($git_dir).\n";
+my $default_authors = "$git_dir/svn-authors";
+if ($opt_A) {
+ read_users($opt_A);
+ copy($opt_A,$default_authors) or die "Copy failed: $!";
+} else {
+ read_users($default_authors) if -f $default_authors;
+}
+
open BRANCHES,">>", "$git_dir/svn2git";
sub node_kind($$$) {
@@ -296,7 +350,7 @@ sub get_file($$$) {
my $svnpath = revert_split_path($branch,$path);
# now get it
- my $name;
+ my ($name,$mode);
if($opt_d) {
my($req,$res);
@@ -316,8 +370,9 @@ sub get_file($$$) {
return undef if $res->code == 301; # directory?
die $res->status_line." at $url\n";
}
+ $mode = '0644'; # can't obtain mode via direct http request?
} else {
- $name = $svn->file("$svnpath",$rev);
+ ($name,$mode) = $svn->file("$svnpath",$rev);
return undef unless defined $name;
}
@@ -331,10 +386,37 @@ sub get_file($$$) {
chomp $sha;
close $F;
unlink $name;
- my $mode = "0644"; # SV does not seem to store any file modes
return [$mode, $sha, $path];
}
+sub get_ignore($$$$$) {
+ my($new,$old,$rev,$branch,$path) = @_;
+
+ return unless $opt_I;
+ my $svnpath = revert_split_path($branch,$path);
+ my $name = $svn->ignore("$svnpath",$rev);
+ if ($path eq '/') {
+ $path = $opt_I;
+ } else {
+ $path = File::Spec->catfile($path,$opt_I);
+ }
+ if (defined $name) {
+ my $pid = open(my $F, '-|');
+ die $! unless defined $pid;
+ if (!$pid) {
+ exec("git-hash-object", "-w", $name)
+ or die "Cannot create object: $!\n";
+ }
+ my $sha = <$F>;
+ chomp $sha;
+ close $F;
+ unlink $name;
+ push(@$new,['0644',$sha,$path]);
+ } else {
+ push(@$old,$path);
+ }
+}
+
sub split_path($$) {
my($rev,$path) = @_;
my $branch;
@@ -431,6 +513,10 @@ sub commit {
if (not defined $author) {
$author_name = $author_email = "unknown";
+ } elsif (defined $users_file) {
+ die "User $author is not listed in $users_file\n"
+ unless exists $users{$author};
+ ($author_name,$author_email) = @{$users{$author}};
} elsif ($author =~ /^(.*?)\s+<(.*)>$/) {
($author_name, $author_email) = ($1, $2);
} else {
@@ -540,6 +626,9 @@ sub commit {
my $opath = $action->[3];
print STDERR "$revision: $branch: could not fetch '$opath'\n";
}
+ } elsif ($node_kind eq $SVN::Node::dir) {
+ get_ignore(\@new, \@old, $revision,
+ $branch,$path);
}
} elsif ($action->[0] eq "D") {
push(@old,$path);
@@ -548,6 +637,9 @@ sub commit {
if ($node_kind eq $SVN::Node::file) {
my $f = get_file($revision,$branch,$path);
push(@new,$f) if $f;
+ } elsif ($node_kind eq $SVN::Node::dir) {
+ get_ignore(\@new, \@old, $revision,
+ $branch,$path);
}
} else {
die "$revision: unknown action '".$action->[0]."' for $path\n";
diff --git a/git.c b/git.c
index 4616df6e62..993cd0d490 100644
--- a/git.c
+++ b/git.c
@@ -230,62 +230,141 @@ static void show_man_page(char *git_cmd)
execlp("man", "man", page, NULL);
}
+static int cmd_version(int argc, char **argv, char **envp)
+{
+ printf("git version %s\n", GIT_VERSION);
+ return 0;
+}
+
+static int cmd_help(int argc, char **argv, char **envp)
+{
+ char *help_cmd = argv[1];
+ if (!help_cmd)
+ cmd_usage(git_exec_path(), NULL);
+ show_man_page(help_cmd);
+ return 0;
+}
+
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
+
+static void handle_internal_command(int argc, char **argv, char **envp)
+{
+ const char *cmd = argv[0];
+ static struct cmd_struct {
+ const char *cmd;
+ int (*fn)(int, char **, char **);
+ } commands[] = {
+ { "version", cmd_version },
+ { "help", cmd_help },
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(commands); i++) {
+ struct cmd_struct *p = commands+i;
+ if (strcmp(p->cmd, cmd))
+ continue;
+ exit(p->fn(argc, argv, envp));
+ }
+}
+
int main(int argc, char **argv, char **envp)
{
+ char *cmd = argv[0];
+ char *slash = strrchr(cmd, '/');
char git_command[PATH_MAX + 1];
- char wd[PATH_MAX + 1];
- int i, show_help = 0;
- const char *exec_path;
+ const char *exec_path = NULL;
+
+ /*
+ * Take the basename of argv[0] as the command
+ * name, and the dirname as the default exec_path
+ * if it's an absolute path and we don't have
+ * anything better.
+ */
+ if (slash) {
+ *slash++ = 0;
+ if (*cmd == '/')
+ exec_path = cmd;
+ cmd = slash;
+ }
- getcwd(wd, PATH_MAX);
+ /*
+ * "git-xxxx" is the same as "git xxxx", but we obviously:
+ *
+ * - cannot take flags in between the "git" and the "xxxx".
+ * - cannot execute it externally (since it would just do
+ * the same thing over again)
+ *
+ * So we just directly call the internal command handler, and
+ * die if that one cannot handle it.
+ */
+ if (!strncmp(cmd, "git-", 4)) {
+ cmd += 4;
+ argv[0] = cmd;
+ handle_internal_command(argc, argv, envp);
+ die("cannot handle %s internally", cmd);
+ }
- for (i = 1; i < argc; i++) {
- char *arg = argv[i];
+ /* Default command: "help" */
+ cmd = "help";
- if (!strcmp(arg, "help")) {
- show_help = 1;
- continue;
- }
+ /* Look for flags.. */
+ while (argc > 1) {
+ cmd = *++argv;
+ argc--;
- if (strncmp(arg, "--", 2))
+ if (strncmp(cmd, "--", 2))
break;
- arg += 2;
+ cmd += 2;
+
+ /*
+ * For legacy reasons, the "version" and "help"
+ * commands can be written with "--" prepended
+ * to make them look like flags.
+ */
+ if (!strcmp(cmd, "help"))
+ break;
+ if (!strcmp(cmd, "version"))
+ break;
- if (!strncmp(arg, "exec-path", 9)) {
- arg += 9;
- if (*arg == '=') {
- exec_path = arg + 1;
- git_set_exec_path(exec_path);
- } else {
- puts(git_exec_path());
- exit(0);
+ /*
+ * Check remaining flags (which by now must be
+ * "--exec-path", but maybe we will accept
+ * other arguments some day)
+ */
+ if (!strncmp(cmd, "exec-path", 9)) {
+ cmd += 9;
+ if (*cmd == '=') {
+ git_set_exec_path(cmd + 1);
+ continue;
}
- }
- else if (!strcmp(arg, "version")) {
- printf("git version %s\n", GIT_VERSION);
+ puts(git_exec_path());
exit(0);
}
- else if (!strcmp(arg, "help"))
- show_help = 1;
- else if (!show_help)
- cmd_usage(NULL, NULL);
- }
-
- if (i >= argc || show_help) {
- if (i >= argc)
- cmd_usage(git_exec_path(), NULL);
-
- show_man_page(argv[i]);
+ cmd_usage(NULL, NULL);
}
-
+ argv[0] = cmd;
+
+ /*
+ * We search for git commands in the following order:
+ * - git_exec_path()
+ * - the path of the "git" command if we could find it
+ * in $0
+ * - the regular PATH.
+ */
+ if (exec_path)
+ prepend_to_path(exec_path, strlen(exec_path));
exec_path = git_exec_path();
prepend_to_path(exec_path, strlen(exec_path));
- execv_git_cmd(argv + i);
+ /* See if it's an internal command */
+ handle_internal_command(argc, argv, envp);
+
+ /* .. then try the external ones */
+ execv_git_cmd(argv);
if (errno == ENOENT)
- cmd_usage(exec_path, "'%s' is not a git-command", argv[i]);
+ cmd_usage(exec_path, "'%s' is not a git-command", cmd);
fprintf(stderr, "Failed to run command '%s': %s\n",
git_command, strerror(errno));
diff --git a/http-fetch.c b/http-fetch.c
index ce3df5f35c..8fd9de081f 100644
--- a/http-fetch.c
+++ b/http-fetch.c
@@ -130,7 +130,7 @@ static void start_object_request(struct object_request *obj_req)
if (obj_req->local < 0) {
obj_req->state = ABORTED;
- error("Couldn't create temporary file %s for %s: %s\n",
+ error("Couldn't create temporary file %s for %s: %s",
obj_req->tmpfile, obj_req->filename, strerror(errno));
return;
}
@@ -830,9 +830,9 @@ static int fetch_object(struct alt_base *repo, unsigned char *sha1)
obj_req->errorstr, obj_req->curl_result,
obj_req->http_code, hex);
} else if (obj_req->zret != Z_STREAM_END) {
- ret = error("File %s (%s) corrupt\n", hex, obj_req->url);
+ ret = error("File %s (%s) corrupt", hex, obj_req->url);
} else if (memcmp(obj_req->sha1, obj_req->real_sha1, 20)) {
- ret = error("File %s has bad hash\n", hex);
+ ret = error("File %s has bad hash", hex);
} else if (obj_req->rename < 0) {
ret = error("unable to write sha1 filename %s",
obj_req->filename);
@@ -854,7 +854,7 @@ int fetch(unsigned char *sha1)
fetch_alternates(alt->base);
altbase = altbase->next;
}
- return error("Unable to find %s under %s\n", sha1_to_hex(sha1),
+ return error("Unable to find %s under %s", sha1_to_hex(sha1),
alt->base);
}
diff --git a/ls-files.c b/ls-files.c
index 90b289f03d..df25c8c012 100644
--- a/ls-files.c
+++ b/ls-files.c
@@ -279,8 +279,11 @@ static void read_directory(const char *path, const char *base, int baselen)
continue;
len = strlen(de->d_name);
memcpy(fullname + baselen, de->d_name, len+1);
- if (excluded(fullname) != show_ignored)
- continue;
+ if (excluded(fullname) != show_ignored) {
+ if (!show_ignored || DTYPE(de) != DT_DIR) {
+ continue;
+ }
+ }
switch (DTYPE(de)) {
struct stat st;
diff --git a/pack-objects.c b/pack-objects.c
index e3946db5ca..21ee572f48 100644
--- a/pack-objects.c
+++ b/pack-objects.c
@@ -3,7 +3,9 @@
#include "delta.h"
#include "pack.h"
#include "csum-file.h"
+#include "diff.h"
#include <sys/time.h>
+#include <signal.h>
static const char pack_usage[] = "git-pack-objects [-q] [--no-reuse-delta] [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list";
@@ -26,6 +28,13 @@ struct object_entry {
struct object_entry *delta_sibling; /* other deltified objects who
* uses the same base as me
*/
+ int preferred_base; /* we do not pack this, but is encouraged to
+ * be used as the base objectto delta huge
+ * objects against.
+ */
+ int based_on_preferred; /* current delta candidate is a preferred
+ * one, or delta against a preferred one.
+ */
};
/*
@@ -48,10 +57,11 @@ static int local = 0;
static int incremental = 0;
static struct object_entry **sorted_by_sha, **sorted_by_type;
static struct object_entry *objects = NULL;
-static int nr_objects = 0, nr_alloc = 0;
+static int nr_objects = 0, nr_alloc = 0, nr_result = 0;
static const char *base_name;
static unsigned char pack_file_sha1[20];
static int progress = 1;
+static volatile int progress_update = 0;
/*
* The object names in objects array are hashed with this hashtable,
@@ -89,7 +99,7 @@ static int reused_delta = 0;
static int pack_revindex_ix(struct packed_git *p)
{
- unsigned int ui = (unsigned int) p;
+ unsigned long ui = (unsigned long)(long)p;
int i;
ui = ui ^ (ui >> 16); /* defeat structure alignment */
@@ -229,7 +239,8 @@ static int encode_header(enum object_type type, unsigned long size, unsigned cha
return n;
}
-static unsigned long write_object(struct sha1file *f, struct object_entry *entry)
+static unsigned long write_object(struct sha1file *f,
+ struct object_entry *entry)
{
unsigned long size;
char type[10];
@@ -239,6 +250,9 @@ static unsigned long write_object(struct sha1file *f, struct object_entry *entry
enum object_type obj_type;
int to_reuse = 0;
+ if (entry->preferred_base)
+ return 0;
+
obj_type = entry->type;
if (! entry->in_pack)
to_reuse = 0; /* can't reuse what we don't have */
@@ -322,28 +336,51 @@ static void write_pack_file(void)
struct sha1file *f;
unsigned long offset;
struct pack_header hdr;
+ unsigned last_percent = 999;
+ int do_progress = 0;
if (!base_name)
f = sha1fd(1, "<stdout>");
- else
- f = sha1create("%s-%s.%s", base_name, sha1_to_hex(object_list_sha1), "pack");
+ else {
+ f = sha1create("%s-%s.%s", base_name,
+ sha1_to_hex(object_list_sha1), "pack");
+ do_progress = progress;
+ }
+ if (do_progress)
+ fprintf(stderr, "Writing %d objects.\n", nr_result);
+
hdr.hdr_signature = htonl(PACK_SIGNATURE);
hdr.hdr_version = htonl(PACK_VERSION);
- hdr.hdr_entries = htonl(nr_objects);
+ hdr.hdr_entries = htonl(nr_result);
sha1write(f, &hdr, sizeof(hdr));
offset = sizeof(hdr);
- for (i = 0; i < nr_objects; i++)
+ if (!nr_result)
+ goto done;
+ for (i = 0; i < nr_objects; i++) {
offset = write_one(f, objects + i, offset);
-
+ if (do_progress) {
+ unsigned percent = written * 100 / nr_result;
+ if (progress_update || percent != last_percent) {
+ fprintf(stderr, "%4u%% (%u/%u) done\r",
+ percent, written, nr_result);
+ progress_update = 0;
+ last_percent = percent;
+ }
+ }
+ }
+ if (do_progress)
+ fputc('\n', stderr);
+ done:
sha1close(f, pack_file_sha1, 1);
}
static void write_index_file(void)
{
int i;
- struct sha1file *f = sha1create("%s-%s.%s", base_name, sha1_to_hex(object_list_sha1), "idx");
+ struct sha1file *f = sha1create("%s-%s.%s", base_name,
+ sha1_to_hex(object_list_sha1), "idx");
struct object_entry **list = sorted_by_sha;
- struct object_entry **last = list + nr_objects;
+ struct object_entry **last = list + nr_result;
unsigned int array[256];
/*
@@ -368,7 +405,7 @@ static void write_index_file(void)
* Write the actual SHA1 entries..
*/
list = sorted_by_sha;
- for (i = 0; i < nr_objects; i++) {
+ for (i = 0; i < nr_result; i++) {
struct object_entry *entry = *list++;
unsigned int offset = htonl(entry->offset);
sha1write(f, &offset, 4);
@@ -378,27 +415,139 @@ static void write_index_file(void)
sha1close(f, NULL, 1);
}
-static int add_object_entry(unsigned char *sha1, unsigned int hash)
+static int locate_object_entry_hash(const unsigned char *sha1)
+{
+ int i;
+ unsigned int ui;
+ memcpy(&ui, sha1, sizeof(unsigned int));
+ i = ui % object_ix_hashsz;
+ while (0 < object_ix[i]) {
+ if (!memcmp(sha1, objects[object_ix[i]-1].sha1, 20))
+ return i;
+ if (++i == object_ix_hashsz)
+ i = 0;
+ }
+ return -1 - i;
+}
+
+static struct object_entry *locate_object_entry(const unsigned char *sha1)
+{
+ int i;
+
+ if (!object_ix_hashsz)
+ return NULL;
+
+ i = locate_object_entry_hash(sha1);
+ if (0 <= i)
+ return &objects[object_ix[i]-1];
+ return NULL;
+}
+
+static void rehash_objects(void)
+{
+ int i;
+ struct object_entry *oe;
+
+ object_ix_hashsz = nr_objects * 3;
+ if (object_ix_hashsz < 1024)
+ object_ix_hashsz = 1024;
+ object_ix = xrealloc(object_ix, sizeof(int) * object_ix_hashsz);
+ object_ix = memset(object_ix, 0, sizeof(int) * object_ix_hashsz);
+ for (i = 0, oe = objects; i < nr_objects; i++, oe++) {
+ int ix = locate_object_entry_hash(oe->sha1);
+ if (0 <= ix)
+ continue;
+ ix = -1 - ix;
+ object_ix[ix] = i + 1;
+ }
+}
+
+struct name_path {
+ struct name_path *up;
+ const char *elem;
+ int len;
+};
+
+#define DIRBITS 12
+
+static unsigned name_hash(struct name_path *path, const char *name)
+{
+ struct name_path *p = path;
+ const char *n = name + strlen(name);
+ unsigned hash = 0, name_hash = 0, name_done = 0;
+
+ if (n != name && n[-1] == '\n')
+ n--;
+ while (name <= --n) {
+ unsigned char c = *n;
+ if (c == '/' && !name_done) {
+ name_hash = hash;
+ name_done = 1;
+ hash = 0;
+ }
+ hash = hash * 11 + c;
+ }
+ if (!name_done) {
+ name_hash = hash;
+ hash = 0;
+ }
+ for (p = path; p; p = p->up) {
+ hash = hash * 11 + '/';
+ n = p->elem + p->len;
+ while (p->elem <= --n) {
+ unsigned char c = *n;
+ hash = hash * 11 + c;
+ }
+ }
+ /*
+ * Make sure "Makefile" and "t/Makefile" are hashed separately
+ * but close enough.
+ */
+ hash = (name_hash<<DIRBITS) | (hash & ((1U<<DIRBITS )-1));
+
+ if (0) { /* debug */
+ n = name + strlen(name);
+ if (n != name && n[-1] == '\n')
+ n--;
+ while (name <= --n)
+ fputc(*n, stderr);
+ for (p = path; p; p = p->up) {
+ fputc('/', stderr);
+ n = p->elem + p->len;
+ while (p->elem <= --n)
+ fputc(*n, stderr);
+ }
+ fprintf(stderr, "\t%08x\n", hash);
+ }
+ return hash;
+}
+
+static int add_object_entry(const unsigned char *sha1, unsigned hash, int exclude)
{
unsigned int idx = nr_objects;
struct object_entry *entry;
struct packed_git *p;
unsigned int found_offset = 0;
struct packed_git *found_pack = NULL;
-
- for (p = packed_git; p; p = p->next) {
- struct pack_entry e;
- if (find_pack_entry_one(sha1, &e, p)) {
- if (incremental)
- return 0;
- if (local && !p->pack_local)
- return 0;
- if (!found_pack) {
- found_offset = e.offset;
- found_pack = e.p;
+ int ix, status = 0;
+
+ if (!exclude) {
+ for (p = packed_git; p; p = p->next) {
+ struct pack_entry e;
+ if (find_pack_entry_one(sha1, &e, p)) {
+ if (incremental)
+ return 0;
+ if (local && !p->pack_local)
+ return 0;
+ if (!found_pack) {
+ found_offset = e.offset;
+ found_pack = e.p;
+ }
}
}
}
+ if ((entry = locate_object_entry(sha1)) != NULL)
+ goto already_added;
if (idx >= nr_alloc) {
unsigned int needed = (idx + 1024) * 3 / 2;
@@ -406,45 +555,94 @@ static int add_object_entry(unsigned char *sha1, unsigned int hash)
nr_alloc = needed;
}
entry = objects + idx;
+ nr_objects = idx + 1;
memset(entry, 0, sizeof(*entry));
memcpy(entry->sha1, sha1, 20);
entry->hash = hash;
- if (found_pack) {
- entry->in_pack = found_pack;
- entry->in_pack_offset = found_offset;
+
+ if (object_ix_hashsz * 3 <= nr_objects * 4)
+ rehash_objects();
+ else {
+ ix = locate_object_entry_hash(entry->sha1);
+ if (0 <= ix)
+ die("internal error in object hashing.");
+ object_ix[-1 - ix] = idx + 1;
}
- nr_objects = idx+1;
- return 1;
+ status = 1;
+
+ already_added:
+ if (progress_update) {
+ fprintf(stderr, "Counting objects...%d\r", nr_objects);
+ progress_update = 0;
+ }
+ if (exclude)
+ entry->preferred_base = 1;
+ else {
+ if (found_pack) {
+ entry->in_pack = found_pack;
+ entry->in_pack_offset = found_offset;
+ }
+ }
+ return status;
}
-static int locate_object_entry_hash(unsigned char *sha1)
+static void add_pbase_tree(struct tree_desc *tree, struct name_path *up)
{
- int i;
- unsigned int ui;
- memcpy(&ui, sha1, sizeof(unsigned int));
- i = ui % object_ix_hashsz;
- while (0 < object_ix[i]) {
- if (!memcmp(sha1, objects[object_ix[i]-1].sha1, 20))
- return i;
- if (++i == object_ix_hashsz)
- i = 0;
+ while (tree->size) {
+ const unsigned char *sha1;
+ const char *name;
+ unsigned mode, hash;
+ unsigned long size;
+ char type[20];
+
+ sha1 = tree_entry_extract(tree, &name, &mode);
+ update_tree_entry(tree);
+ if (!has_sha1_file(sha1))
+ continue;
+ if (sha1_object_info(sha1, type, &size))
+ continue;
+
+ hash = name_hash(up, name);
+ if (!add_object_entry(sha1, hash, 1))
+ continue;
+
+ if (!strcmp(type, "tree")) {
+ struct tree_desc sub;
+ void *elem;
+ struct name_path me;
+
+ elem = read_sha1_file(sha1, type, &sub.size);
+ sub.buf = elem;
+ if (sub.buf) {
+ me.up = up;
+ me.elem = name;
+ me.len = strlen(name);
+ add_pbase_tree(&sub, &me);
+ free(elem);
+ }
+ }
}
- return -1 - i;
}
-static struct object_entry *locate_object_entry(unsigned char *sha1)
+static void add_preferred_base(unsigned char *sha1)
{
- int i = locate_object_entry_hash(sha1);
- if (0 <= i)
- return &objects[object_ix[i]-1];
- return NULL;
+ struct tree_desc tree;
+ void *elem;
+
+ elem = read_object_with_reference(sha1, "tree", &tree.size, NULL);
+ tree.buf = elem;
+ if (!tree.buf)
+ return;
+ if (add_object_entry(sha1, name_hash(NULL, ""), 1))
+ add_pbase_tree(&tree, NULL);
+ free(elem);
}
static void check_object(struct object_entry *entry)
{
char type[20];
- if (entry->in_pack) {
+ if (entry->in_pack && !entry->preferred_base) {
unsigned char base[20];
unsigned long size;
struct object_entry *base_entry;
@@ -463,7 +661,8 @@ static void check_object(struct object_entry *entry)
*/
if (!no_reuse_delta &&
entry->in_pack_type == OBJ_DELTA &&
- (base_entry = locate_object_entry(base))) {
+ (base_entry = locate_object_entry(base)) &&
+ (!base_entry->preferred_base)) {
/* Depth value does not matter - find_deltas()
* will never consider reused delta as the
@@ -501,25 +700,6 @@ static void check_object(struct object_entry *entry)
sha1_to_hex(entry->sha1), type);
}
-static void hash_objects(void)
-{
- int i;
- struct object_entry *oe;
-
- object_ix_hashsz = nr_objects * 2;
- object_ix = xcalloc(sizeof(int), object_ix_hashsz);
- for (i = 0, oe = objects; i < nr_objects; i++, oe++) {
- int ix = locate_object_entry_hash(oe->sha1);
- if (0 <= ix) {
- error("the same object '%s' added twice",
- sha1_to_hex(oe->sha1));
- continue;
- }
- ix = -1 - ix;
- object_ix[ix] = i + 1;
- }
-}
-
static unsigned int check_delta_limit(struct object_entry *me, unsigned int n)
{
struct object_entry *child = me->delta_child;
@@ -538,14 +718,26 @@ static void get_object_details(void)
int i;
struct object_entry *entry;
- hash_objects();
prepare_pack_ix();
for (i = 0, entry = objects; i < nr_objects; i++, entry++)
check_object(entry);
- for (i = 0, entry = objects; i < nr_objects; i++, entry++)
- if (!entry->delta && entry->delta_child)
- entry->delta_limit =
- check_delta_limit(entry, 1);
+
+ if (nr_objects == nr_result) {
+ /*
+ * Depth of objects that depend on the entry -- this
+ * is subtracted from depth-max to break too deep
+ * delta chain because of delta data reusing.
+ * However, we loosen this restriction when we know we
+ * are creating a thin pack -- it will have to be
+ * expanded on the other end anyway, so do not
+ * artificially cut the delta chain and let it go as
+ * deep as it wants.
+ */
+ for (i = 0, entry = objects; i < nr_objects; i++, entry++)
+ if (!entry->delta && entry->delta_child)
+ entry->delta_limit =
+ check_delta_limit(entry, 1);
+ }
}
typedef int (*entry_sort_t)(const struct object_entry *, const struct object_entry *);
@@ -576,6 +768,24 @@ static int sha1_sort(const struct object_entry *a, const struct object_entry *b)
return memcmp(a->sha1, b->sha1, 20);
}
+static struct object_entry **create_final_object_list(void)
+{
+ struct object_entry **list;
+ int i, j;
+
+ for (i = nr_result = 0; i < nr_objects; i++)
+ if (!objects[i].preferred_base)
+ nr_result++;
+ list = xmalloc(nr_result * sizeof(struct object_entry *));
+ for (i = j = 0; i < nr_objects; i++) {
+ if (!objects[i].preferred_base)
+ list[j++] = objects + i;
+ }
+ current_sort = sha1_sort;
+ qsort(list, nr_result, sizeof(struct object_entry *), sort_comparator);
+ return list;
+}
+
static int type_size_sort(const struct object_entry *a, const struct object_entry *b)
{
if (a->type < b->type)
@@ -586,6 +796,10 @@ static int type_size_sort(const struct object_entry *a, const struct object_entr
return -1;
if (a->hash > b->hash)
return 1;
+ if (a->preferred_base < b->preferred_base)
+ return -1;
+ if (a->preferred_base > b->preferred_base)
+ return 1;
if (a->size < b->size)
return -1;
if (a->size > b->size)
@@ -610,6 +824,8 @@ static int try_delta(struct unpacked *cur, struct unpacked *old, unsigned max_de
{
struct object_entry *cur_entry = cur->entry;
struct object_entry *old_entry = old->entry;
+ int old_preferred = (old_entry->preferred_base ||
+ old_entry->based_on_preferred);
unsigned long size, oldsize, delta_size, sizediff;
long max_size;
void *delta_buf;
@@ -618,9 +834,15 @@ static int try_delta(struct unpacked *cur, struct unpacked *old, unsigned max_de
if (cur_entry->type != old_entry->type)
return -1;
- /* If the current object is at edge, take the depth the objects
- * that depend on the current object into account -- otherwise
- * they would become too deep.
+ /* We do not compute delta to *create* objects we are not
+ * going to pack.
+ */
+ if (cur_entry->preferred_base)
+ return -1;
+
+ /* If the current object is at pack edge, take the depth the
+ * objects that depend on the current object into account --
+ * otherwise they would become too deep.
*/
if (cur_entry->delta_child) {
if (max_depth <= cur_entry->delta_limit)
@@ -645,8 +867,27 @@ static int try_delta(struct unpacked *cur, struct unpacked *old, unsigned max_de
* delete).
*/
max_size = size / 2 - 20;
- if (cur_entry->delta)
- max_size = cur_entry->delta_size-1;
+ if (cur_entry->delta) {
+ if (cur_entry->based_on_preferred) {
+ if (old_preferred)
+ max_size = cur_entry->delta_size-1;
+ else
+ /* trying with non-preferred one when we
+ * already have a delta based on preferred
+ * one is pointless.
+ */
+ return -1;
+ }
+ else if (!old_preferred)
+ max_size = cur_entry->delta_size-1;
+ else
+ /* otherwise... even if delta with a
+ * preferred one produces a bigger result than
+ * what we currently have, which is based on a
+ * non-preferred one, it is OK.
+ */
+ ;
+ }
if (sizediff >= max_size)
return -1;
delta_buf = diff_delta(old->data, oldsize,
@@ -656,21 +897,30 @@ static int try_delta(struct unpacked *cur, struct unpacked *old, unsigned max_de
cur_entry->delta = old_entry;
cur_entry->delta_size = delta_size;
cur_entry->depth = old_entry->depth + 1;
+ cur_entry->based_on_preferred = old_preferred;
free(delta_buf);
return 0;
}
+static void progress_interval(int signum)
+{
+ signal(SIGALRM, progress_interval);
+ progress_update = 1;
+}
+
static void find_deltas(struct object_entry **list, int window, int depth)
{
int i, idx;
unsigned int array_size = window * sizeof(struct unpacked);
struct unpacked *array = xmalloc(array_size);
- int eye_candy;
+ unsigned processed = 0;
+ unsigned last_percent = 999;
memset(array, 0, array_size);
i = nr_objects;
idx = 0;
- eye_candy = i - (nr_objects / 20);
+ if (progress)
+ fprintf(stderr, "Deltifying %d objects.\n", nr_result);
while (--i >= 0) {
struct object_entry *entry = list[i];
@@ -679,9 +929,17 @@ static void find_deltas(struct object_entry **list, int window, int depth)
char type[10];
int j;
- if (progress && i <= eye_candy) {
- eye_candy -= nr_objects / 20;
- fputc('.', stderr);
+ if (!entry->preferred_base)
+ processed++;
+
+ if (progress) {
+ unsigned percent = processed * 100 / nr_result;
+ if (percent != last_percent || progress_update) {
+ fprintf(stderr, "%4u%% (%u/%u) done\r",
+ percent, processed, nr_result);
+ progress_update = 0;
+ last_percent = percent;
+ }
}
if (entry->delta)
@@ -713,6 +971,9 @@ static void find_deltas(struct object_entry **list, int window, int depth)
idx = 0;
}
+ if (progress)
+ fputc('\n', stderr);
+
for (i = 0; i < window; ++i)
free(array[i].data);
free(array);
@@ -720,18 +981,10 @@ static void find_deltas(struct object_entry **list, int window, int depth)
static void prepare_pack(int window, int depth)
{
- if (progress)
- fprintf(stderr, "Packing %d objects", nr_objects);
get_object_details();
- if (progress)
- fputc('.', stderr);
-
sorted_by_type = create_sorted_list(type_size_sort);
if (window && depth)
find_deltas(sorted_by_type, window+1, depth);
- if (progress)
- fputc('\n', stderr);
- write_pack_file();
}
static int reuse_cached_pack(unsigned char *sha1, int pack_to_stdout)
@@ -795,10 +1048,6 @@ int main(int argc, char **argv)
int window = 10, depth = 10, pack_to_stdout = 0;
struct object_entry **list;
int i;
- struct timeval prev_tv;
- int eye_candy = 0;
- int eye_candy_incr = 500;
-
setup_git_directory();
@@ -855,61 +1104,60 @@ int main(int argc, char **argv)
usage(pack_usage);
prepare_packed_git();
+
if (progress) {
+ struct itimerval v;
+ v.it_interval.tv_sec = 1;
+ v.it_interval.tv_usec = 0;
+ v.it_value = v.it_interval;
+ signal(SIGALRM, progress_interval);
+ setitimer(ITIMER_REAL, &v, NULL);
fprintf(stderr, "Generating pack...\n");
- gettimeofday(&prev_tv, NULL);
}
+
while (fgets(line, sizeof(line), stdin) != NULL) {
- unsigned int hash;
- char *p;
unsigned char sha1[20];
- if (progress && (eye_candy <= nr_objects)) {
- fprintf(stderr, "Counting objects...%d\r", nr_objects);
- if (eye_candy && (50 <= eye_candy_incr)) {
- struct timeval tv;
- int time_diff;
- gettimeofday(&tv, NULL);
- time_diff = (tv.tv_sec - prev_tv.tv_sec);
- time_diff <<= 10;
- time_diff += (tv.tv_usec - prev_tv.tv_usec);
- if ((1 << 9) < time_diff)
- eye_candy_incr += 50;
- else if (50 < eye_candy_incr)
- eye_candy_incr -= 50;
- }
- eye_candy += eye_candy_incr;
+ if (line[0] == '-') {
+ if (get_sha1_hex(line+1, sha1))
+ die("expected edge sha1, got garbage:\n %s",
+ line+1);
+ add_preferred_base(sha1);
+ continue;
}
if (get_sha1_hex(line, sha1))
die("expected sha1, got garbage:\n %s", line);
- hash = 0;
- p = line+40;
- while (*p) {
- unsigned char c = *p++;
- if (isspace(c))
- continue;
- hash = hash * 11 + c;
- }
- add_object_entry(sha1, hash);
+ add_object_entry(sha1, name_hash(NULL, line+41), 0);
}
if (progress)
fprintf(stderr, "Done counting %d objects.\n", nr_objects);
- if (non_empty && !nr_objects)
+ sorted_by_sha = create_final_object_list();
+ if (non_empty && !nr_result)
return 0;
- sorted_by_sha = create_sorted_list(sha1_sort);
SHA1_Init(&ctx);
list = sorted_by_sha;
- for (i = 0; i < nr_objects; i++) {
+ for (i = 0; i < nr_result; i++) {
struct object_entry *entry = *list++;
SHA1_Update(&ctx, entry->sha1, 20);
}
SHA1_Final(object_list_sha1, &ctx);
+ if (progress && (nr_objects != nr_result))
+ fprintf(stderr, "Result has %d objects.\n", nr_result);
if (reuse_cached_pack(object_list_sha1, pack_to_stdout))
;
else {
- prepare_pack(window, depth);
+ if (nr_result)
+ prepare_pack(window, depth);
+ if (progress && pack_to_stdout) {
+ /* the other end usually displays progress itself */
+ struct itimerval v = {{0,},};
+ setitimer(ITIMER_REAL, &v, NULL);
+ signal(SIGALRM, SIG_IGN );
+ progress_update = 0;
+ }
+ write_pack_file();
if (!pack_to_stdout) {
write_index_file();
puts(sha1_to_hex(object_list_sha1));
@@ -917,6 +1165,6 @@ int main(int argc, char **argv)
}
if (progress)
fprintf(stderr, "Total %d, written %d (delta %d), reused %d (delta %d)\n",
- nr_objects, written, written_delta, reused, reused_delta);
+ nr_result, written, written_delta, reused, reused_delta);
return 0;
}
diff --git a/pack-redundant.c b/pack-redundant.c
index 1869b38b72..cd81f5a66e 100644
--- a/pack-redundant.c
+++ b/pack-redundant.c
@@ -45,7 +45,7 @@ static inline void llist_item_put(struct llist_item *item)
free_nodes = item;
}
-static inline struct llist_item *llist_item_get()
+static inline struct llist_item *llist_item_get(void)
{
struct llist_item *new;
if ( free_nodes ) {
@@ -275,7 +275,7 @@ static void cmp_two_packs(struct pack_list *p1, struct pack_list *p2)
}
}
-void pll_free(struct pll *l)
+static void pll_free(struct pll *l)
{
struct pll *old;
struct pack_list *opl;
diff --git a/read-tree.c b/read-tree.c
index 52f06e312a..f39fe5ca65 100644
--- a/read-tree.c
+++ b/read-tree.c
@@ -9,6 +9,8 @@
#include "object.h"
#include "tree.h"
+#include <sys/time.h>
+#include <signal.h>
static int merge = 0;
static int update = 0;
@@ -16,6 +18,8 @@ static int index_only = 0;
static int nontrivial_merge = 0;
static int trivial_merges_only = 0;
static int aggressive = 0;
+static int verbose_update = 0;
+static volatile int progress_update = 0;
static int head_idx = -1;
static int merge_size = 0;
@@ -267,6 +271,12 @@ static void unlink_entry(char *name)
}
}
+static void progress_interval(int signum)
+{
+ signal(SIGALRM, progress_interval);
+ progress_update = 1;
+}
+
static void check_updates(struct cache_entry **src, int nr)
{
static struct checkout state = {
@@ -276,8 +286,49 @@ static void check_updates(struct cache_entry **src, int nr)
.refresh_cache = 1,
};
unsigned short mask = htons(CE_UPDATE);
+ unsigned last_percent = 200, cnt = 0, total = 0;
+
+ if (update && verbose_update) {
+ struct itimerval v;
+
+ for (total = cnt = 0; cnt < nr; cnt++) {
+ struct cache_entry *ce = src[cnt];
+ if (!ce->ce_mode || ce->ce_flags & mask)
+ total++;
+ }
+
+ /* Don't bother doing this for very small updates */
+ if (total < 250)
+ total = 0;
+
+ if (total) {
+ v.it_interval.tv_sec = 1;
+ v.it_interval.tv_usec = 0;
+ v.it_value = v.it_interval;
+ signal(SIGALRM, progress_interval);
+ setitimer(ITIMER_REAL, &v, NULL);
+ fprintf(stderr, "Checking files out...\n");
+ progress_update = 1;
+ }
+ cnt = 0;
+ }
+
while (nr--) {
struct cache_entry *ce = *src++;
+
+ if (total) {
+ if (!ce->ce_mode || ce->ce_flags & mask) {
+ unsigned percent;
+ cnt++;
+ percent = (cnt * 100) / total;
+ if (percent != last_percent ||
+ progress_update) {
+ fprintf(stderr, "%4u%% (%u/%u) done\r",
+ percent, cnt, total);
+ last_percent = percent;
+ }
+ }
+ }
if (!ce->ce_mode) {
if (update)
unlink_entry(ce->name);
@@ -289,6 +340,10 @@ static void check_updates(struct cache_entry **src, int nr)
checkout_entry(ce, &state);
}
}
+ if (total) {
+ fputc('\n', stderr);
+ signal(SIGALRM, SIG_IGN);
+ }
}
static int unpack_trees(merge_fn_t fn)
@@ -564,7 +619,7 @@ static int twoway_merge(struct cache_entry **src)
struct cache_entry *oldtree = src[1], *newtree = src[2];
if (merge_size != 2)
- return error("Cannot do a twoway merge of %d trees\n",
+ return error("Cannot do a twoway merge of %d trees",
merge_size);
if (current) {
@@ -616,7 +671,7 @@ static int oneway_merge(struct cache_entry **src)
struct cache_entry *a = src[1];
if (merge_size != 1)
- return error("Cannot do a oneway merge of %d trees\n",
+ return error("Cannot do a oneway merge of %d trees",
merge_size);
if (!a)
@@ -680,6 +735,11 @@ int main(int argc, char **argv)
continue;
}
+ if (!strcmp(arg, "-v")) {
+ verbose_update = 1;
+ continue;
+ }
+
/* "-i" means "index only", meaning that a merge will
* not even look at the working tree.
*/
diff --git a/receive-pack.c b/receive-pack.c
index eae31e370d..2a3db16d68 100644
--- a/receive-pack.c
+++ b/receive-pack.c
@@ -92,7 +92,7 @@ static int run_update_hook(const char *refname,
case -ERR_RUN_COMMAND_WAITPID_WRONG_PID:
return error("waitpid is confused");
case -ERR_RUN_COMMAND_WAITPID_SIGNAL:
- return error("%s died of signal\n", update_hook);
+ return error("%s died of signal", update_hook);
case -ERR_RUN_COMMAND_WAITPID_NOEXIT:
return error("%s died strangely", update_hook);
default:
@@ -158,7 +158,7 @@ static int update(struct command *cmd)
if (run_update_hook(name, old_hex, new_hex)) {
unlink(lock_name);
cmd->error_string = "hook declined";
- return error("hook declined to update %s\n", name);
+ return error("hook declined to update %s", name);
}
else if (rename(lock_name, name) < 0) {
unlink(lock_name);
diff --git a/refs.c b/refs.c
index d01fc3984a..826ae7ade7 100644
--- a/refs.c
+++ b/refs.c
@@ -268,7 +268,7 @@ static int write_ref_file(const char *filename,
char term = '\n';
if (write(fd, hex, 40) < 40 ||
write(fd, &term, 1) < 1) {
- error("Couldn't write %s\n", filename);
+ error("Couldn't write %s", filename);
close(fd);
return -1;
}
diff --git a/rev-list.c b/rev-list.c
index f2d1105cae..2e80930b2c 100644
--- a/rev-list.c
+++ b/rev-list.c
@@ -6,9 +6,10 @@
#include "blob.h"
#include "epoch.h"
#include "diff.h"
+#include "revision.h"
+
+/* bits #0 and #1 in revision.h */
-#define SEEN (1u << 0)
-#define INTERESTING (1u << 1)
#define COUNTED (1u << 2)
#define SHOWN (1u << 3)
#define TREECHANGE (1u << 4)
@@ -30,7 +31,7 @@ static const char rev_list_usage[] =
" --date-order\n"
" formatting output:\n"
" --parents\n"
-" --objects\n"
+" --objects | --objects-edge\n"
" --unpacked\n"
" --header | --pretty\n"
" --abbrev=nr | --no-abbrev\n"
@@ -38,29 +39,19 @@ static const char rev_list_usage[] =
" --bisect"
;
-static int dense = 1;
-static int unpacked = 0;
+struct rev_info revs;
+
static int bisect_list = 0;
-static int tag_objects = 0;
-static int tree_objects = 0;
-static int blob_objects = 0;
static int verbose_header = 0;
static int abbrev = DEFAULT_ABBREV;
static int show_parents = 0;
static int hdr_termination = 0;
static const char *commit_prefix = "";
-static unsigned long max_age = -1;
-static unsigned long min_age = -1;
-static int max_count = -1;
static enum cmit_fmt commit_format = CMIT_FMT_RAW;
static int merge_order = 0;
static int show_breaks = 0;
static int stop_traversal = 0;
-static int topo_order = 0;
-static int lifo = 1;
static int no_merges = 0;
-static const char **paths = NULL;
-static int remove_empty_trees = 0;
static void show_commit(struct commit *commit)
{
@@ -137,15 +128,15 @@ static int filter_commit(struct commit * commit)
return STOP;
if (commit->object.flags & (UNINTERESTING|SHOWN))
return CONTINUE;
- if (min_age != -1 && (commit->date > min_age))
+ if (revs.min_age != -1 && (commit->date > revs.min_age))
return CONTINUE;
- if (max_age != -1 && (commit->date < max_age)) {
+ if (revs.max_age != -1 && (commit->date < revs.max_age)) {
stop_traversal=1;
return CONTINUE;
}
if (no_merges && (commit->parents && commit->parents->next))
return CONTINUE;
- if (paths && dense) {
+ if (revs.paths && revs.dense) {
if (!(commit->object.flags & TREECHANGE))
return CONTINUE;
rewrite_parents(commit);
@@ -165,7 +156,7 @@ static int process_commit(struct commit * commit)
return CONTINUE;
}
- if (max_count != -1 && !max_count--)
+ if (revs.max_count != -1 && !revs.max_count--)
return STOP;
show_commit(commit);
@@ -173,90 +164,88 @@ static int process_commit(struct commit * commit)
return CONTINUE;
}
-static struct object_list **add_object(struct object *obj, struct object_list **p, const char *name)
-{
- struct object_list *entry = xmalloc(sizeof(*entry));
- entry->item = obj;
- entry->next = *p;
- entry->name = name;
- *p = entry;
- return &entry->next;
-}
-
-static struct object_list **process_blob(struct blob *blob, struct object_list **p, const char *name)
+static struct object_list **process_blob(struct blob *blob,
+ struct object_list **p,
+ struct name_path *path,
+ const char *name)
{
struct object *obj = &blob->object;
- if (!blob_objects)
+ if (!revs.blob_objects)
return p;
if (obj->flags & (UNINTERESTING | SEEN))
return p;
obj->flags |= SEEN;
- return add_object(obj, p, name);
+ return add_object(obj, p, path, name);
}
-static struct object_list **process_tree(struct tree *tree, struct object_list **p, const char *name)
+static struct object_list **process_tree(struct tree *tree,
+ struct object_list **p,
+ struct name_path *path,
+ const char *name)
{
struct object *obj = &tree->object;
struct tree_entry_list *entry;
+ struct name_path me;
- if (!tree_objects)
+ if (!revs.tree_objects)
return p;
if (obj->flags & (UNINTERESTING | SEEN))
return p;
if (parse_tree(tree) < 0)
die("bad tree object %s", sha1_to_hex(obj->sha1));
obj->flags |= SEEN;
- p = add_object(obj, p, name);
+ p = add_object(obj, p, path, name);
+ me.up = path;
+ me.elem = name;
+ me.elem_len = strlen(name);
entry = tree->entries;
tree->entries = NULL;
while (entry) {
struct tree_entry_list *next = entry->next;
if (entry->directory)
- p = process_tree(entry->item.tree, p, entry->name);
+ p = process_tree(entry->item.tree, p, &me, entry->name);
else
- p = process_blob(entry->item.blob, p, entry->name);
+ p = process_blob(entry->item.blob, p, &me, entry->name);
free(entry);
entry = next;
}
return p;
}
-static struct object_list *pending_objects = NULL;
-
static void show_commit_list(struct commit_list *list)
{
struct object_list *objects = NULL, **p = &objects, *pending;
while (list) {
struct commit *commit = pop_most_recent_commit(&list, SEEN);
- p = process_tree(commit->tree, p, "");
+ p = process_tree(commit->tree, p, NULL, "");
if (process_commit(commit) == STOP)
break;
}
- for (pending = pending_objects; pending; pending = pending->next) {
+ for (pending = revs.pending_objects; pending; pending = pending->next) {
struct object *obj = pending->item;
const char *name = pending->name;
if (obj->flags & (UNINTERESTING | SEEN))
continue;
if (obj->type == tag_type) {
obj->flags |= SEEN;
- p = add_object(obj, p, name);
+ p = add_object(obj, p, NULL, name);
continue;
}
if (obj->type == tree_type) {
- p = process_tree((struct tree *)obj, p, name);
+ p = process_tree((struct tree *)obj, p, NULL, name);
continue;
}
if (obj->type == blob_type) {
- p = process_blob((struct blob *)obj, p, name);
+ p = process_blob((struct blob *)obj, p, NULL, name);
continue;
}
die("unknown pending object %s (%s)", sha1_to_hex(obj->sha1), name);
}
while (objects) {
- /* An object with name "foo\n0000000000000000000000000000000000000000"
- * can be used confuse downstream git-pack-objects very badly.
+ /* An object with name "foo\n0000000..." can be used to
+ * confuse downstream git-pack-objects very badly.
*/
const char *ep = strchr(objects->name, '\n');
if (ep) {
@@ -270,75 +259,6 @@ static void show_commit_list(struct commit_list *list)
}
}
-static void mark_blob_uninteresting(struct blob *blob)
-{
- if (!blob_objects)
- return;
- if (blob->object.flags & UNINTERESTING)
- return;
- blob->object.flags |= UNINTERESTING;
-}
-
-static void mark_tree_uninteresting(struct tree *tree)
-{
- struct object *obj = &tree->object;
- struct tree_entry_list *entry;
-
- if (!tree_objects)
- return;
- if (obj->flags & UNINTERESTING)
- return;
- obj->flags |= UNINTERESTING;
- if (!has_sha1_file(obj->sha1))
- return;
- if (parse_tree(tree) < 0)
- die("bad tree %s", sha1_to_hex(obj->sha1));
- entry = tree->entries;
- tree->entries = NULL;
- while (entry) {
- struct tree_entry_list *next = entry->next;
- if (entry->directory)
- mark_tree_uninteresting(entry->item.tree);
- else
- mark_blob_uninteresting(entry->item.blob);
- free(entry);
- entry = next;
- }
-}
-
-static void mark_parents_uninteresting(struct commit *commit)
-{
- struct commit_list *parents = commit->parents;
-
- while (parents) {
- struct commit *commit = parents->item;
- commit->object.flags |= UNINTERESTING;
-
- /*
- * Normally we haven't parsed the parent
- * yet, so we won't have a parent of a parent
- * here. However, it may turn out that we've
- * reached this commit some other way (where it
- * wasn't uninteresting), in which case we need
- * to mark its parents recursively too..
- */
- if (commit->parents)
- mark_parents_uninteresting(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;
- parents = parents->next;
- }
-}
-
static int everybody_uninteresting(struct commit_list *orig)
{
struct commit_list *list = orig;
@@ -369,7 +289,7 @@ static int count_distance(struct commit_list *entry)
if (commit->object.flags & (UNINTERESTING | COUNTED))
break;
- if (!paths || (commit->object.flags & TREECHANGE))
+ if (!revs.paths || (commit->object.flags & TREECHANGE))
nr++;
commit->object.flags |= COUNTED;
p = commit->parents;
@@ -403,7 +323,7 @@ static struct commit_list *find_bisection(struct commit_list *list)
nr = 0;
p = list;
while (p) {
- if (!paths || (p->item->object.flags & TREECHANGE))
+ if (!revs.paths || (p->item->object.flags & TREECHANGE))
nr++;
p = p->next;
}
@@ -413,7 +333,7 @@ static struct commit_list *find_bisection(struct commit_list *list)
for (p = list; p; p = p->next) {
int distance;
- if (paths && !(p->item->object.flags & TREECHANGE))
+ if (revs.paths && !(p->item->object.flags & TREECHANGE))
continue;
distance = count_distance(p);
@@ -430,16 +350,32 @@ static struct commit_list *find_bisection(struct commit_list *list)
return best;
}
+static void mark_edge_parents_uninteresting(struct commit *commit)
+{
+ struct commit_list *parents;
+
+ for (parents = commit->parents; parents; parents = parents->next) {
+ struct commit *parent = parents->item;
+ if (!(parent->object.flags & UNINTERESTING))
+ continue;
+ mark_tree_uninteresting(parent->tree);
+ if (revs.edge_hint && !(parent->object.flags & SHOWN)) {
+ parent->object.flags |= SHOWN;
+ printf("-%s\n", sha1_to_hex(parent->object.sha1));
+ }
+ }
+}
+
static void mark_edges_uninteresting(struct commit_list *list)
{
for ( ; list; list = list->next) {
- struct commit_list *parents = list->item->parents;
+ struct commit *commit = list->item;
- for ( ; parents; parents = parents->next) {
- struct commit *commit = parents->item;
- if (commit->object.flags & UNINTERESTING)
- mark_tree_uninteresting(commit->tree);
+ if (commit->object.flags & UNINTERESTING) {
+ mark_tree_uninteresting(commit->tree);
+ continue;
}
+ mark_edge_parents_uninteresting(commit);
}
}
@@ -553,7 +489,7 @@ static void try_to_simplify_commit(struct commit *commit)
return;
case TREE_NEW:
- if (remove_empty_trees && same_tree_as_empty(p->tree)) {
+ if (revs.remove_empty_trees && same_tree_as_empty(p->tree)) {
*pp = parent->next;
continue;
}
@@ -604,7 +540,7 @@ static void add_parents_to_list(struct commit *commit, struct commit_list **list
* simplify the commit history and find the parent
* that has no differences in the path set if one exists.
*/
- if (paths)
+ if (revs.paths)
try_to_simplify_commit(commit);
parent = commit->parents;
@@ -633,9 +569,9 @@ static struct commit_list *limit_list(struct commit_list *list)
list = list->next;
free(entry);
- if (max_age != -1 && (commit->date < max_age))
+ if (revs.max_age != -1 && (commit->date < revs.max_age))
obj->flags |= UNINTERESTING;
- if (unpacked && has_sha1_pack(obj->sha1))
+ if (revs.unpacked && has_sha1_pack(obj->sha1))
obj->flags |= UNINTERESTING;
add_parents_to_list(commit, &list);
if (obj->flags & UNINTERESTING) {
@@ -644,155 +580,40 @@ static struct commit_list *limit_list(struct commit_list *list)
break;
continue;
}
- if (min_age != -1 && (commit->date > min_age))
+ if (revs.min_age != -1 && (commit->date > revs.min_age))
continue;
p = &commit_list_insert(commit, p)->next;
}
- if (tree_objects)
+ if (revs.tree_objects)
mark_edges_uninteresting(newlist);
if (bisect_list)
newlist = find_bisection(newlist);
return newlist;
}
-static void add_pending_object(struct object *obj, const char *name)
-{
- add_object(obj, &pending_objects, name);
-}
-
-static struct commit *get_commit_reference(const char *name, const unsigned char *sha1, unsigned int flags)
-{
- struct object *object;
-
- object = parse_object(sha1);
- if (!object)
- die("bad object %s", name);
-
- /*
- * Tag object? Look what it points to..
- */
- while (object->type == tag_type) {
- struct tag *tag = (struct tag *) object;
- object->flags |= flags;
- if (tag_objects && !(object->flags & UNINTERESTING))
- add_pending_object(object, tag->tag);
- object = parse_object(tag->tagged->sha1);
- if (!object)
- die("bad object %s", sha1_to_hex(tag->tagged->sha1));
- }
-
- /*
- * Commit object? Just return it, we'll do all the complex
- * reachability crud.
- */
- if (object->type == commit_type) {
- struct commit *commit = (struct commit *)object;
- object->flags |= flags;
- if (parse_commit(commit) < 0)
- die("unable to parse commit %s", name);
- if (flags & UNINTERESTING)
- mark_parents_uninteresting(commit);
- return commit;
- }
-
- /*
- * Tree object? Either mark it uniniteresting, or add it
- * to the list of objects to look at later..
- */
- if (object->type == tree_type) {
- struct tree *tree = (struct tree *)object;
- if (!tree_objects)
- return NULL;
- if (flags & UNINTERESTING) {
- mark_tree_uninteresting(tree);
- return NULL;
- }
- add_pending_object(object, "");
- return NULL;
- }
-
- /*
- * Blob object? You know the drill by now..
- */
- if (object->type == blob_type) {
- struct blob *blob = (struct blob *)object;
- if (!blob_objects)
- return NULL;
- if (flags & UNINTERESTING) {
- mark_blob_uninteresting(blob);
- return NULL;
- }
- add_pending_object(object, "");
- return NULL;
- }
- die("%s is unknown object", name);
-}
-
-static void handle_one_commit(struct commit *com, struct commit_list **lst)
-{
- if (!com || com->object.flags & SEEN)
- return;
- com->object.flags |= SEEN;
- commit_list_insert(com, lst);
-}
-
-/* for_each_ref() callback does not allow user data -- Yuck. */
-static struct commit_list **global_lst;
-
-static int include_one_commit(const char *path, const unsigned char *sha1)
-{
- struct commit *com = get_commit_reference(path, sha1, 0);
- handle_one_commit(com, global_lst);
- return 0;
-}
-
-static void handle_all(struct commit_list **lst)
-{
- global_lst = lst;
- for_each_ref(include_one_commit);
- global_lst = NULL;
-}
-
int main(int argc, const char **argv)
{
- const char *prefix = setup_git_directory();
- struct commit_list *list = NULL;
- int i, limited = 0;
+ struct commit_list *list;
+ int i;
+
+ argc = setup_revisions(argc, argv, &revs);
for (i = 1 ; i < argc; i++) {
- int flags;
const char *arg = argv[i];
- char *dotdot;
- struct commit *commit;
- unsigned char sha1[20];
/* accept -<digit>, like traditilnal "head" */
if ((*arg == '-') && isdigit(arg[1])) {
- max_count = atoi(arg + 1);
+ revs.max_count = atoi(arg + 1);
continue;
}
if (!strcmp(arg, "-n")) {
if (++i >= argc)
die("-n requires an argument");
- max_count = atoi(argv[i]);
+ revs.max_count = atoi(argv[i]);
continue;
}
if (!strncmp(arg,"-n",2)) {
- max_count = atoi(arg + 2);
- continue;
- }
- if (!strncmp(arg, "--max-count=", 12)) {
- max_count = atoi(arg + 12);
- continue;
- }
- if (!strncmp(arg, "--max-age=", 10)) {
- max_age = atoi(arg + 10);
- limited = 1;
- continue;
- }
- if (!strncmp(arg, "--min-age=", 10)) {
- min_age = atoi(arg + 10);
- limited = 1;
+ revs.max_count = atoi(arg + 2);
continue;
}
if (!strcmp(arg, "--header")) {
@@ -833,21 +654,6 @@ int main(int argc, const char **argv)
bisect_list = 1;
continue;
}
- if (!strcmp(arg, "--all")) {
- handle_all(&list);
- continue;
- }
- if (!strcmp(arg, "--objects")) {
- tag_objects = 1;
- tree_objects = 1;
- blob_objects = 1;
- continue;
- }
- if (!strcmp(arg, "--unpacked")) {
- unpacked = 1;
- limited = 1;
- continue;
- }
if (!strcmp(arg, "--merge-order")) {
merge_order = 1;
continue;
@@ -856,100 +662,33 @@ int main(int argc, const char **argv)
show_breaks = 1;
continue;
}
- if (!strcmp(arg, "--topo-order")) {
- topo_order = 1;
- lifo = 1;
- limited = 1;
- continue;
- }
- if (!strcmp(arg, "--date-order")) {
- topo_order = 1;
- lifo = 0;
- limited = 1;
- continue;
- }
- if (!strcmp(arg, "--dense")) {
- dense = 1;
- continue;
- }
- if (!strcmp(arg, "--sparse")) {
- dense = 0;
- continue;
- }
- if (!strcmp(arg, "--remove-empty")) {
- remove_empty_trees = 1;
- continue;
- }
- if (!strcmp(arg, "--")) {
- i++;
- break;
- }
+ usage(rev_list_usage);
- if (show_breaks && !merge_order)
- usage(rev_list_usage);
-
- flags = 0;
- dotdot = strstr(arg, "..");
- if (dotdot) {
- unsigned char from_sha1[20];
- char *next = dotdot + 2;
- *dotdot = 0;
- if (!*next)
- next = "HEAD";
- if (!get_sha1(arg, from_sha1) && !get_sha1(next, sha1)) {
- struct commit *exclude;
- struct commit *include;
-
- exclude = get_commit_reference(arg, from_sha1, UNINTERESTING);
- include = get_commit_reference(next, sha1, 0);
- if (!exclude || !include)
- die("Invalid revision range %s..%s", arg, next);
- limited = 1;
- handle_one_commit(exclude, &list);
- handle_one_commit(include, &list);
- continue;
- }
- *dotdot = '.';
- }
- if (*arg == '^') {
- flags = UNINTERESTING;
- arg++;
- limited = 1;
- }
- if (get_sha1(arg, sha1) < 0) {
- struct stat st;
- if (lstat(arg, &st) < 0)
- die("'%s': %s", arg, strerror(errno));
- break;
- }
- commit = get_commit_reference(arg, sha1, flags);
- handle_one_commit(commit, &list);
}
+ list = revs.commits;
+
if (!list &&
- (!(tag_objects||tree_objects||blob_objects) && !pending_objects))
+ (!(revs.tag_objects||revs.tree_objects||revs.blob_objects) && !revs.pending_objects))
usage(rev_list_usage);
- paths = get_pathspec(prefix, argv + i);
- if (paths) {
- limited = 1;
- diff_tree_setup_paths(paths);
- }
+ if (revs.paths)
+ diff_tree_setup_paths(revs.paths);
save_commit_buffer = verbose_header;
track_object_refs = 0;
if (!merge_order) {
sort_by_date(&list);
- if (list && !limited && max_count == 1 &&
- !tag_objects && !tree_objects && !blob_objects) {
+ if (list && !revs.limited && revs.max_count == 1 &&
+ !revs.tag_objects && !revs.tree_objects && !revs.blob_objects) {
show_commit(list->item);
return 0;
}
- if (limited)
+ if (revs.limited)
list = limit_list(list);
- if (topo_order)
- sort_in_topological_order(&list, lifo);
+ if (revs.topo_order)
+ sort_in_topological_order(&list, revs.lifo);
show_commit_list(list);
} else {
#ifndef NO_OPENSSL
diff --git a/rev-parse.c b/rev-parse.c
index a5fb93c3ca..610eacb35a 100644
--- a/rev-parse.c
+++ b/rev-parse.c
@@ -43,6 +43,7 @@ static int is_rev_argument(const char *arg)
"--min-age=",
"--no-merges",
"--objects",
+ "--objects-edge",
"--parents",
"--pretty",
"--show-breaks",
diff --git a/revision.c b/revision.c
new file mode 100644
index 0000000000..67ff4de2d1
--- /dev/null
+++ b/revision.c
@@ -0,0 +1,383 @@
+#include "cache.h"
+#include "tag.h"
+#include "blob.h"
+#include "tree.h"
+#include "commit.h"
+#include "refs.h"
+#include "revision.h"
+
+static char *path_name(struct name_path *path, const char *name)
+{
+ struct name_path *p;
+ char *n, *m;
+ int nlen = strlen(name);
+ int len = nlen + 1;
+
+ for (p = path; p; p = p->up) {
+ if (p->elem_len)
+ len += p->elem_len + 1;
+ }
+ n = xmalloc(len);
+ m = n + len - (nlen + 1);
+ strcpy(m, name);
+ for (p = path; p; p = p->up) {
+ if (p->elem_len) {
+ m -= p->elem_len + 1;
+ memcpy(m, p->elem, p->elem_len);
+ m[p->elem_len] = '/';
+ }
+ }
+ return n;
+}
+
+struct object_list **add_object(struct object *obj,
+ struct object_list **p,
+ struct name_path *path,
+ const char *name)
+{
+ struct object_list *entry = xmalloc(sizeof(*entry));
+ entry->item = obj;
+ entry->next = *p;
+ entry->name = path_name(path, name);
+ *p = entry;
+ return &entry->next;
+}
+
+static void mark_blob_uninteresting(struct blob *blob)
+{
+ if (blob->object.flags & UNINTERESTING)
+ return;
+ blob->object.flags |= UNINTERESTING;
+}
+
+void mark_tree_uninteresting(struct tree *tree)
+{
+ struct object *obj = &tree->object;
+ struct tree_entry_list *entry;
+
+ if (obj->flags & UNINTERESTING)
+ return;
+ obj->flags |= UNINTERESTING;
+ if (!has_sha1_file(obj->sha1))
+ return;
+ if (parse_tree(tree) < 0)
+ die("bad tree %s", sha1_to_hex(obj->sha1));
+ entry = tree->entries;
+ tree->entries = NULL;
+ while (entry) {
+ struct tree_entry_list *next = entry->next;
+ if (entry->directory)
+ mark_tree_uninteresting(entry->item.tree);
+ else
+ mark_blob_uninteresting(entry->item.blob);
+ free(entry);
+ entry = next;
+ }
+}
+
+void mark_parents_uninteresting(struct commit *commit)
+{
+ struct commit_list *parents = commit->parents;
+
+ while (parents) {
+ struct commit *commit = parents->item;
+ commit->object.flags |= UNINTERESTING;
+
+ /*
+ * Normally we haven't parsed the parent
+ * yet, so we won't have a parent of a parent
+ * here. However, it may turn out that we've
+ * reached this commit some other way (where it
+ * wasn't uninteresting), in which case we need
+ * to mark its parents recursively too..
+ */
+ if (commit->parents)
+ mark_parents_uninteresting(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;
+ parents = parents->next;
+ }
+}
+
+static void add_pending_object(struct rev_info *revs, struct object *obj, const char *name)
+{
+ add_object(obj, &revs->pending_objects, NULL, name);
+}
+
+static struct commit *get_commit_reference(struct rev_info *revs, const char *name, const unsigned char *sha1, unsigned int flags)
+{
+ struct object *object;
+
+ object = parse_object(sha1);
+ if (!object)
+ die("bad object %s", name);
+
+ /*
+ * Tag object? Look what it points to..
+ */
+ while (object->type == tag_type) {
+ struct tag *tag = (struct tag *) object;
+ object->flags |= flags;
+ if (revs->tag_objects && !(object->flags & UNINTERESTING))
+ add_pending_object(revs, object, tag->tag);
+ object = parse_object(tag->tagged->sha1);
+ if (!object)
+ die("bad object %s", sha1_to_hex(tag->tagged->sha1));
+ }
+
+ /*
+ * Commit object? Just return it, we'll do all the complex
+ * reachability crud.
+ */
+ if (object->type == commit_type) {
+ struct commit *commit = (struct commit *)object;
+ object->flags |= flags;
+ if (parse_commit(commit) < 0)
+ die("unable to parse commit %s", name);
+ if (flags & UNINTERESTING) {
+ mark_parents_uninteresting(commit);
+ revs->limited = 1;
+ }
+ return commit;
+ }
+
+ /*
+ * Tree object? Either mark it uniniteresting, or add it
+ * to the list of objects to look at later..
+ */
+ if (object->type == tree_type) {
+ struct tree *tree = (struct tree *)object;
+ if (!revs->tree_objects)
+ return NULL;
+ if (flags & UNINTERESTING) {
+ mark_tree_uninteresting(tree);
+ return NULL;
+ }
+ add_pending_object(revs, object, "");
+ return NULL;
+ }
+
+ /*
+ * Blob object? You know the drill by now..
+ */
+ if (object->type == blob_type) {
+ struct blob *blob = (struct blob *)object;
+ if (!revs->blob_objects)
+ return NULL;
+ if (flags & UNINTERESTING) {
+ mark_blob_uninteresting(blob);
+ return NULL;
+ }
+ add_pending_object(revs, object, "");
+ return NULL;
+ }
+ die("%s is unknown object", name);
+}
+
+static void add_one_commit(struct commit *commit, struct rev_info *revs)
+{
+ if (!commit || (commit->object.flags & SEEN))
+ return;
+ commit->object.flags |= SEEN;
+ commit_list_insert(commit, &revs->commits);
+}
+
+static int all_flags;
+static struct rev_info *all_revs;
+
+static int handle_one_ref(const char *path, const unsigned char *sha1)
+{
+ struct commit *commit = get_commit_reference(all_revs, path, sha1, all_flags);
+ add_one_commit(commit, all_revs);
+ return 0;
+}
+
+static void handle_all(struct rev_info *revs, unsigned flags)
+{
+ all_revs = revs;
+ all_flags = flags;
+ for_each_ref(handle_one_ref);
+}
+
+/*
+ * Parse revision information, filling in the "rev_info" structure,
+ * and removing the used arguments from the argument list.
+ *
+ * Returns the number of arguments left ("new argc").
+ */
+int setup_revisions(int argc, const char **argv, struct rev_info *revs)
+{
+ int i, flags, seen_dashdash;
+ const char *def = NULL;
+ const char **unrecognized = argv+1;
+ int left = 1;
+
+ memset(revs, 0, sizeof(*revs));
+ revs->lifo = 1;
+ revs->dense = 1;
+ revs->prefix = setup_git_directory();
+ revs->max_age = -1;
+ revs->min_age = -1;
+ revs->max_count = -1;
+
+ /* First, search for "--" */
+ seen_dashdash = 0;
+ for (i = 1; i < argc; i++) {
+ const char *arg = argv[i];
+ if (strcmp(arg, "--"))
+ continue;
+ argv[i] = NULL;
+ argc = i;
+ revs->paths = get_pathspec(revs->prefix, argv + i + 1);
+ seen_dashdash = 1;
+ break;
+ }
+
+ flags = 0;
+ for (i = 1; i < argc; i++) {
+ struct commit *commit;
+ const char *arg = argv[i];
+ unsigned char sha1[20];
+ char *dotdot;
+ int local_flags;
+
+ if (*arg == '-') {
+ if (!strncmp(arg, "--max-count=", 12)) {
+ revs->max_count = atoi(arg + 12);
+ continue;
+ }
+ if (!strncmp(arg, "--max-age=", 10)) {
+ revs->max_age = atoi(arg + 10);
+ revs->limited = 1;
+ continue;
+ }
+ if (!strncmp(arg, "--min-age=", 10)) {
+ revs->min_age = atoi(arg + 10);
+ revs->limited = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--all")) {
+ handle_all(revs, flags);
+ continue;
+ }
+ if (!strcmp(arg, "--not")) {
+ flags ^= UNINTERESTING;
+ continue;
+ }
+ if (!strcmp(arg, "--default")) {
+ if (++i >= argc)
+ die("bad --default argument");
+ def = argv[i];
+ continue;
+ }
+ if (!strcmp(arg, "--topo-order")) {
+ revs->topo_order = 1;
+ revs->limited = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--date-order")) {
+ revs->lifo = 0;
+ revs->topo_order = 1;
+ revs->limited = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--dense")) {
+ revs->dense = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--sparse")) {
+ revs->dense = 0;
+ continue;
+ }
+ if (!strcmp(arg, "--remove-empty")) {
+ revs->remove_empty_trees = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--objects")) {
+ revs->tag_objects = 1;
+ revs->tree_objects = 1;
+ revs->blob_objects = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--objects-edge")) {
+ revs->tag_objects = 1;
+ revs->tree_objects = 1;
+ revs->blob_objects = 1;
+ revs->edge_hint = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--unpacked")) {
+ revs->unpacked = 1;
+ revs->limited = 1;
+ continue;
+ }
+ *unrecognized++ = arg;
+ left++;
+ continue;
+ }
+ dotdot = strstr(arg, "..");
+ if (dotdot) {
+ unsigned char from_sha1[20];
+ char *next = dotdot + 2;
+ *dotdot = 0;
+ if (!*next)
+ next = "HEAD";
+ if (!get_sha1(arg, from_sha1) && !get_sha1(next, sha1)) {
+ struct commit *exclude;
+ struct commit *include;
+
+ exclude = get_commit_reference(revs, arg, from_sha1, flags ^ UNINTERESTING);
+ include = get_commit_reference(revs, next, sha1, flags);
+ if (!exclude || !include)
+ die("Invalid revision range %s..%s", arg, next);
+ add_one_commit(exclude, revs);
+ add_one_commit(include, revs);
+ continue;
+ }
+ *dotdot = '.';
+ }
+ local_flags = 0;
+ if (*arg == '^') {
+ local_flags = UNINTERESTING;
+ arg++;
+ }
+ if (get_sha1(arg, sha1) < 0) {
+ struct stat st;
+ int j;
+
+ if (seen_dashdash || local_flags)
+ die("bad revision '%s'", arg);
+
+ /* If we didn't have a "--", all filenames must exist */
+ for (j = i; j < argc; j++) {
+ if (lstat(argv[j], &st) < 0)
+ die("'%s': %s", arg, strerror(errno));
+ }
+ revs->paths = get_pathspec(revs->prefix, argv + i);
+ break;
+ }
+ commit = get_commit_reference(revs, arg, sha1, flags ^ local_flags);
+ add_one_commit(commit, revs);
+ }
+ if (def && !revs->commits) {
+ unsigned char sha1[20];
+ struct commit *commit;
+ if (get_sha1(def, sha1) < 0)
+ die("bad default revision '%s'", def);
+ commit = get_commit_reference(revs, def, sha1, 0);
+ add_one_commit(commit, revs);
+ }
+ if (revs->paths)
+ revs->limited = 1;
+ *unrecognized = NULL;
+ return left;
+}
diff --git a/revision.h b/revision.h
new file mode 100644
index 0000000000..a22f198515
--- /dev/null
+++ b/revision.h
@@ -0,0 +1,50 @@
+#ifndef REVISION_H
+#define REVISION_H
+
+#define SEEN (1u<<0)
+#define UNINTERESTING (1u<<1)
+
+struct rev_info {
+ /* Starting list */
+ struct commit_list *commits;
+ struct object_list *pending_objects;
+
+ /* Basic information */
+ const char *prefix;
+ const char **paths;
+
+ /* Traversal flags */
+ unsigned int dense:1,
+ remove_empty_trees:1,
+ lifo:1,
+ topo_order:1,
+ tag_objects:1,
+ tree_objects:1,
+ blob_objects:1,
+ edge_hint:1,
+ limited:1,
+ unpacked:1;
+
+ /* special limits */
+ int max_count;
+ unsigned long max_age;
+ unsigned long min_age;
+};
+
+/* revision.c */
+extern int setup_revisions(int argc, const char **argv, struct rev_info *revs);
+extern void mark_parents_uninteresting(struct commit *commit);
+extern void mark_tree_uninteresting(struct tree *tree);
+
+struct name_path {
+ struct name_path *up;
+ int elem_len;
+ const char *elem;
+};
+
+extern struct object_list **add_object(struct object *obj,
+ struct object_list **p,
+ struct name_path *path,
+ const char *name);
+
+#endif
diff --git a/send-pack.c b/send-pack.c
index 990be3f1a3..f558386143 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -12,6 +12,7 @@ static const char *exec = "git-receive-pack";
static int verbose = 0;
static int send_all = 0;
static int force_update = 0;
+static int use_thin_pack = 0;
static int is_zero_sha1(const unsigned char *sha1)
{
@@ -37,26 +38,47 @@ static void exec_pack_objects(void)
static void exec_rev_list(struct ref *refs)
{
+ struct ref *ref;
static char *args[1000];
- int i = 0;
+ int i = 0, j;
args[i++] = "rev-list"; /* 0 */
- args[i++] = "--objects"; /* 1 */
- while (refs) {
- char *buf = malloc(100);
- if (i > 900)
+ if (use_thin_pack) /* 1 */
+ args[i++] = "--objects-edge";
+ else
+ args[i++] = "--objects";
+
+ /* First send the ones we care about most */
+ for (ref = refs; ref; ref = ref->next) {
+ if (900 < i)
die("git-rev-list environment overflow");
- if (!is_zero_sha1(refs->old_sha1) &&
- has_sha1_file(refs->old_sha1)) {
+ if (!is_zero_sha1(ref->new_sha1)) {
+ char *buf = malloc(100);
args[i++] = buf;
- snprintf(buf, 50, "^%s", sha1_to_hex(refs->old_sha1));
+ snprintf(buf, 50, "%s", sha1_to_hex(ref->new_sha1));
buf += 50;
+ if (!is_zero_sha1(ref->old_sha1) &&
+ has_sha1_file(ref->old_sha1)) {
+ args[i++] = buf;
+ snprintf(buf, 50, "^%s",
+ sha1_to_hex(ref->old_sha1));
+ }
}
- if (!is_zero_sha1(refs->new_sha1)) {
+ }
+
+ /* Then a handful of the remainder
+ * NEEDSWORK: we would be better off if used the newer ones first.
+ */
+ for (ref = refs, j = i + 16;
+ i < 900 && i < j && ref;
+ ref = ref->next) {
+ if (is_zero_sha1(ref->new_sha1) &&
+ !is_zero_sha1(ref->old_sha1) &&
+ has_sha1_file(ref->old_sha1)) {
+ char *buf = malloc(42);
args[i++] = buf;
- snprintf(buf, 50, "%s", sha1_to_hex(refs->new_sha1));
+ snprintf(buf, 42, "^%s", sha1_to_hex(ref->old_sha1));
}
- refs = refs->next;
}
args[i] = NULL;
execv_git_cmd(args);
@@ -361,6 +383,10 @@ int main(int argc, char **argv)
verbose = 1;
continue;
}
+ if (!strcmp(arg, "--thin")) {
+ use_thin_pack = 1;
+ continue;
+ }
usage(send_pack_usage);
}
if (!dest) {
diff --git a/sha1_file.c b/sha1_file.c
index 9cab99ae7c..a80d849f15 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -247,6 +247,7 @@ static void link_alt_odb_entries(const char *alt, const char *ep, int sep,
for ( ; cp < ep && *cp != sep; cp++)
;
if (last != cp) {
+ struct stat st;
struct alternate_object_database *alt;
/* 43 = 40-byte + 2 '/' + terminating NUL */
int pfxlen = cp - last;
@@ -269,9 +270,19 @@ static void link_alt_odb_entries(const char *alt, const char *ep, int sep,
}
else
memcpy(ent->base, last, pfxlen);
+
ent->name = ent->base + pfxlen + 1;
- ent->base[pfxlen] = ent->base[pfxlen + 3] = '/';
- ent->base[entlen-1] = 0;
+ ent->base[pfxlen + 3] = '/';
+ ent->base[pfxlen] = ent->base[entlen-1] = 0;
+
+ /* Detect cases where alternate disappeared */
+ if (stat(ent->base, &st) || !S_ISDIR(st.st_mode)) {
+ error("object directory %s does not exist; "
+ "check .git/objects/info/alternates.",
+ ent->base);
+ goto bad;
+ }
+ ent->base[pfxlen] = '/';
/* Prevent the common mistake of listing the same
* thing twice, or object directory itself.
@@ -552,7 +563,9 @@ static void prepare_packed_git_one(char *objdir, int local)
len = strlen(path);
dir = opendir(path);
if (!dir) {
- fprintf(stderr, "unable to open object pack directory: %s: %s\n", path, strerror(errno));
+ if (errno != ENOENT)
+ error("unable to open object pack directory: %s: %s",
+ path, strerror(errno));
return;
}
path[len++] = '/';
@@ -1500,7 +1513,8 @@ int write_sha1_from_fd(const unsigned char *sha1, int fd, char *buffer,
local = mkstemp(tmpfile);
if (local < 0)
- return error("Couldn't open %s for %s\n", tmpfile, sha1_to_hex(sha1));
+ return error("Couldn't open %s for %s",
+ tmpfile, sha1_to_hex(sha1));
memset(&stream, 0, sizeof(stream));
@@ -1548,7 +1562,7 @@ int write_sha1_from_fd(const unsigned char *sha1, int fd, char *buffer,
}
if (memcmp(sha1, real_sha1, 20)) {
unlink(tmpfile);
- return error("File %s has bad hash\n", sha1_to_hex(sha1));
+ return error("File %s has bad hash", sha1_to_hex(sha1));
}
return move_temp_to_file(tmpfile, sha1_file_name(sha1));
diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh
new file mode 100755
index 0000000000..cabfadd56d
--- /dev/null
+++ b/t/t3600-rm.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Carl D. Worth
+#
+
+test_description='Test of the various options to git-rm.'
+
+. ./test-lib.sh
+
+# Setup some files to be removed, some with funny characters
+touch -- foo bar baz 'space embedded' 'tab embedded' 'newline
+embedded' -q
+git-add -- foo bar baz 'space embedded' 'tab embedded' 'newline
+embedded' -q
+git-commit -m "add files"
+
+test_expect_success \
+ 'Pre-check that foo exists and is in index before git-rm foo' \
+ '[ -f foo ] && git-ls-files --error-unmatch foo'
+
+test_expect_success \
+ 'Test that git-rm foo succeeds' \
+ 'git-rm foo'
+
+test_expect_success \
+ 'Post-check that foo exists but is not in index after git-rm foo' \
+ '[ -f foo ] && ! git-ls-files --error-unmatch foo'
+
+test_expect_success \
+ 'Pre-check that bar exists and is in index before "git-rm -f bar"' \
+ '[ -f bar ] && git-ls-files --error-unmatch bar'
+
+test_expect_success \
+ 'Test that "git-rm -f bar" succeeds' \
+ 'git-rm -f bar'
+
+test_expect_success \
+ 'Post-check that bar does not exist and is not in index after "git-rm -f bar"' \
+ '! [ -f bar ] && ! git-ls-files --error-unmatch bar'
+
+test_expect_success \
+ 'Test that "git-rm -- -q" succeeds (remove a file that looks like an option)' \
+ 'git-rm -- -q'
+
+test_expect_success \
+ "Test that \"git-rm -f\" succeeds with embedded space, tab, or newline characters." \
+ "git-rm -f 'space embedded' 'tab embedded' 'newline
+embedded'"
+
+chmod u-w .
+test_expect_failure \
+ 'Test that "git-rm -f" fails if its rm fails' \
+ 'git-rm -f baz'
+chmod u+w .
+
+test_expect_success \
+ 'When the rm in "git-rm -f" fails, it should not remove the file from the index' \
+ 'git-ls-files --error-unmatch baz'
+
+test_done
diff --git a/templates/hooks--applypatch-msg b/templates/hooks--applypatch-msg
index bda3c86be7..02de1ef84c 100644
--- a/templates/hooks--applypatch-msg
+++ b/templates/hooks--applypatch-msg
@@ -9,6 +9,7 @@
#
# To enable this hook, make this file executable.
+. git-sh-setup
test -x "$GIT_DIR/hooks/commit-msg" &&
exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
:
diff --git a/templates/hooks--pre-applypatch b/templates/hooks--pre-applypatch
index a54751600e..5f56ce8053 100644
--- a/templates/hooks--pre-applypatch
+++ b/templates/hooks--pre-applypatch
@@ -8,6 +8,7 @@
#
# To enable this hook, make this file executable.
+. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
:
diff --git a/upload-pack.c b/upload-pack.c
index 3606529f61..635abb371d 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -14,6 +14,7 @@ static const char upload_pack_usage[] = "git-upload-pack [--strict] [--timeout=n
#define MAX_HAS 256
#define MAX_NEEDS 256
static int nr_has = 0, nr_needs = 0, multi_ack = 0, nr_our_refs = 0;
+static int use_thin_pack = 0;
static unsigned char has_sha1[MAX_HAS][20];
static unsigned char needs_sha1[MAX_NEEDS][20];
static unsigned int timeout = 0;
@@ -49,8 +50,10 @@ static void create_pack_file(void)
char *buf;
char **p;
- if (create_full_pack)
+ if (create_full_pack) {
args = 10;
+ use_thin_pack = 0; /* no point doing it */
+ }
else
args = nr_has + nr_needs + 5;
argv = xmalloc(args * sizeof(char *));
@@ -62,7 +65,7 @@ static void create_pack_file(void)
close(fd[0]);
close(fd[1]);
*p++ = "rev-list";
- *p++ = "--objects";
+ *p++ = use_thin_pack ? "--objects-edge" : "--objects";
if (create_full_pack || MAX_NEEDS <= nr_needs)
*p++ = "--all";
else {
@@ -192,6 +195,8 @@ static int receive_needs(void)
"expected to get sha, not '%s'", line);
if (strstr(line+45, "multi_ack"))
multi_ack = 1;
+ if (strstr(line+45, "thin-pack"))
+ use_thin_pack = 1;
/* We have sent all our refs already, and the other end
* should have chosen out of them; otherwise they are
@@ -213,7 +218,7 @@ static int receive_needs(void)
static int send_ref(const char *refname, const unsigned char *sha1)
{
- static char *capabilities = "multi_ack";
+ static char *capabilities = "multi_ack thin-pack";
struct object *o = parse_object(sha1);
if (!o)