*.o
*.py[co]
config.mak
+git-blame
{0#</citerefentry>}
endif::backend-docbook[]
+ifdef::backend-docbook[]
+# "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this.
+[listingblock]
+<example><title>{title}</title>
+<literallayout>
+|
+</literallayout>
+{title#}</example>
+endif::backend-docbook[]
+
ifdef::backend-xhtml11[]
[gitlink-inlinemacro]
<a href="{target}.html">{target}{0?({0})}</a>
(i.e. you are listing the files explicitly), it does not
add `subdir/git-foo.sh` to the index.
+See Also
+--------
+gitlink:git-rm[1]
Author
------
SYNOPSIS
--------
[verse]
-`git-archimport` [-h] [-v] [-o] [-a] [-f] [-T] [-D depth] [-t tempdir]
+'git-archimport' [-h] [-v] [-o] [-a] [-f] [-T] [-D depth] [-t tempdir]
<archive/branch> [ <archive/branch> ]
DESCRIPTION
SYNOPSIS
--------
-git-fetch-pack [-q] [-k] [--exec=<git-upload-pack>] [<host>:]<directory> [<refs>...]
+'git-fetch-pack' [-q] [-k] [--exec=<git-upload-pack>] [<host>:]<directory> [<refs>...]
DESCRIPTION
-----------
SYNOPSIS
--------
-'git-pack-redundant [ --verbose ] [ --alt-odb ] < --all | .pack filename ... >'
+'git-pack-redundant' [ --verbose ] [ --alt-odb ] < --all | .pack filename ... >
DESCRIPTION
-----------
shell expand the asterisk (i.e. you are listing the files
explicitly), it does not remove `subdir/git-foo.sh`.
+See Also
+--------
+gitlink:git-add[1]
Author
------
SYNOPSIS
--------
-'git-shell -c <command> <argument>'
+'git-shell' -c <command> <argument>
DESCRIPTION
-----------
SYNOPSIS
--------
-'git-log --pretty=short | git shortlog'
+git-log --pretty=short | 'git-shortlog'
DESCRIPTION
-----------
SYNOPSIS
--------
[verse]
-git-show-branch [--all] [--heads] [--tags] [--topo-order] [--current]
- [--more=<n> | --list | --independent | --merge-base]
- [--no-name | --sha1-name] [<rev> | <glob>]...
+'git-show-branch' [--all] [--heads] [--tags] [--topo-order] [--current]
+ [--more=<n> | --list | --independent | --merge-base]
+ [--no-name | --sha1-name] [<rev> | <glob>]...
DESCRIPTION
-----------
SYNOPSIS
--------
-`git-update-ref` <ref> <newvalue> [<oldvalue>]
+'git-update-ref' <ref> <newvalue> [<oldvalue>]
DESCRIPTION
-----------
SYNOPSIS
--------
-git-var [ -l | <variable> ]
+'git-var' [ -l | <variable> ]
DESCRIPTION
-----------
#include <assert.h>
#include <time.h>
#include <sys/time.h>
+#include <math.h>
#include "cache.h"
#include "refs.h"
#define DEBUG 0
-struct commit **blame_lines;
-int num_blame_lines;
+static const char blame_usage[] = "[-c] [-l] [--] file [commit]\n"
+ " -c, --compability Use the same output mode as git-annotate (Default: off)\n"
+ " -l, --long Show long commit SHA1 (Default: off)\n"
+ " -h, --help This message";
+
+static struct commit **blame_lines;
+static int num_blame_lines;
+static char* blame_contents;
+static int blame_len;
struct util_info {
int *line_map;
die("write failed: %s", strerror(errno));
close(fd);
- sprintf(diff_cmd, "diff -u0 %s %s", tmp_path1, tmp_path2);
+ sprintf(diff_cmd, "diff -u -U 0 %s %s", tmp_path1, tmp_path2);
fin = popen(diff_cmd, "r");
if (!fin)
die("popen failed: %s", strerror(errno));
alloc_line_map(commit);
util = commit->object.util;
- num_blame_lines = util->num_lines;
- for (i = 0; i < num_blame_lines; i++)
+ for (i = 0; i < util->num_lines; i++)
util->line_map[i] = i;
}
util = commit->object.util;
num_blame_lines = util->num_lines;
blame_lines = xmalloc(sizeof(struct commit *) * num_blame_lines);
+ blame_contents = util->buf;
+ blame_len = util->size;
+
for (i = 0; i < num_blame_lines; i++)
blame_lines[i] = NULL;
} while ((commit = get_revision(rev)) != NULL);
}
+struct commit_info
+{
+ char* author;
+ char* author_mail;
+ unsigned long author_time;
+ char* author_tz;
+};
+
+static void get_commit_info(struct commit* commit, struct commit_info* ret)
+{
+ int len;
+ char* tmp;
+ static char author_buf[1024];
+
+ tmp = strstr(commit->buffer, "\nauthor ") + 8;
+ len = index(tmp, '\n') - tmp;
+ ret->author = author_buf;
+ memcpy(ret->author, tmp, len);
+
+ tmp = ret->author;
+ tmp += len;
+ *tmp = 0;
+ while(*tmp != ' ')
+ tmp--;
+ ret->author_tz = tmp+1;
+
+ *tmp = 0;
+ while(*tmp != ' ')
+ tmp--;
+ ret->author_time = strtoul(tmp, NULL, 10);
+
+ *tmp = 0;
+ while(*tmp != ' ')
+ tmp--;
+ ret->author_mail = tmp + 1;
+
+ *tmp = 0;
+}
+
+static const char* format_time(unsigned long time, const char* tz_str)
+{
+ static char time_buf[128];
+ time_t t = time;
+ int minutes, tz;
+ struct tm *tm;
+
+ tz = atoi(tz_str);
+ minutes = tz < 0 ? -tz : tz;
+ minutes = (minutes / 100)*60 + (minutes % 100);
+ minutes = tz < 0 ? -minutes : minutes;
+ t = time + minutes * 60;
+ tm = gmtime(&t);
+
+ strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S ", tm);
+ strcat(time_buf, tz_str);
+ return time_buf;
+}
+
int main(int argc, const char **argv)
{
int i;
struct commit *initial = NULL;
unsigned char sha1[20];
- const char* filename;
+
+ const char *filename = NULL, *commit = NULL;
+ char filename_buf[256];
+ int sha1_len = 8;
+ int compability = 0;
+ int options = 1;
+
int num_args;
const char* args[10];
struct rev_info rev;
- setup_git_directory();
+ struct commit_info ci;
+ const char *buf;
+ int max_digits;
- if (argc != 3)
- die("Usage: blame commit-ish file");
+ const char* prefix = setup_git_directory();
+ for(i = 1; i < argc; i++) {
+ if(options) {
+ if(!strcmp(argv[i], "-h") ||
+ !strcmp(argv[i], "--help"))
+ usage(blame_usage);
+ else if(!strcmp(argv[i], "-l") ||
+ !strcmp(argv[i], "--long")) {
+ sha1_len = 40;
+ continue;
+ } else if(!strcmp(argv[i], "-c") ||
+ !strcmp(argv[i], "--compability")) {
+ compability = 1;
+ continue;
+ } else if(!strcmp(argv[i], "--")) {
+ options = 0;
+ continue;
+ } else if(argv[i][0] == '-')
+ usage(blame_usage);
+ else
+ options = 0;
+ }
- filename = argv[2];
+ if(!options) {
+ if(!filename)
+ filename = argv[i];
+ else if(!commit)
+ commit = argv[i];
+ else
+ usage(blame_usage);
+ }
+ }
+
+ if(!filename)
+ usage(blame_usage);
+ if(!commit)
+ commit = "HEAD";
+
+ if(prefix)
+ sprintf(filename_buf, "%s%s", prefix, filename);
+ else
+ strcpy(filename_buf, filename);
+ filename = filename_buf;
{
- struct commit* commit;
- if (get_sha1(argv[1], sha1))
- die("get_sha1 failed");
- commit = lookup_commit_reference(sha1);
+ struct commit* c;
+ if (get_sha1(commit, sha1))
+ die("get_sha1 failed, commit '%s' not found", commit);
+ c = lookup_commit_reference(sha1);
- if (fill_util_info(commit, filename)) {
- printf("%s not found in %s\n", filename, argv[1]);
+ if (fill_util_info(c, filename)) {
+ printf("%s not found in %s\n", filename, commit);
return 1;
}
}
args[num_args++] = NULL;
args[num_args++] = "--topo-order";
args[num_args++] = "--remove-empty";
- args[num_args++] = argv[1];
+ args[num_args++] = commit;
args[num_args++] = "--";
args[num_args++] = filename;
args[num_args] = NULL;
prepare_revision_walk(&rev);
process_commits(&rev, filename, &initial);
+ buf = blame_contents;
+ for (max_digits = 1, i = 10; i <= num_blame_lines + 1; max_digits++)
+ i *= 10;
+
for (i = 0; i < num_blame_lines; i++) {
struct commit *c = blame_lines[i];
if (!c)
c = initial;
- printf("%d %.8s\n", i, sha1_to_hex(c->object.sha1));
-// printf("%d %s\n", i, find_unique_abbrev(blame_lines[i]->object.sha1, 6));
+ get_commit_info(c, &ci);
+ fwrite(sha1_to_hex(c->object.sha1), sha1_len, 1, stdout);
+ if(compability)
+ printf("\t(%10s\t%10s\t%d)", ci.author,
+ format_time(ci.author_time, ci.author_tz), i+1);
+ else
+ printf(" (%-15.15s %10s %*d) ", ci.author,
+ format_time(ci.author_time, ci.author_tz),
+ max_digits, i+1);
+
+ if(i == num_blame_lines - 1) {
+ fwrite(buf, blame_len - (buf - blame_contents),
+ 1, stdout);
+ if(blame_contents[blame_len-1] != '\n')
+ putc('\n', stdout);
+ } else {
+ char* next_buf = index(buf, '\n') + 1;
+ fwrite(buf, next_buf - buf, 1, stdout);
+ buf = next_buf;
+ }
}
if (DEBUG) {
--- /dev/null
+## Build and install stuff
+
+EMACS = emacs
+
+ELC = git.elc vc-git.elc
+INSTALL = install
+INSTALL_ELC = $(INSTALL) -m 644
+prefix = $(HOME)
+emacsdir = $(prefix)/share/emacs/site-lisp
+
+all: $(ELC)
+
+install: all
+ $(INSTALL) -d $(emacsdir)
+ $(INSTALL_ELC) $(ELC) $(emacsdir)
+
+%.elc: %.el
+ $(EMACS) --batch --eval '(byte-compile-file "$<")'
+
+clean:; rm -f $(ELC)
}
}
push @revqueue, $head;
-init_claim( defined $starting_rev ? $starting_rev : 'dirty');
+init_claim( defined $starting_rev ? $head : '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: $!";
return () unless defined $rev && defined $filename;
my $blob = git_ls_tree($rev, $filename);
+ die "Failed to find a blob for $filename in rev $rev\n" if !defined $blob;
my $catfile = open_pipe("git","cat-file", "blob", $blob)
or die "Failed to git-cat-file blob $blob (rev $rev, file $filename): " . $!;
my ($mode, $type, $blob, $tfilename);
while(<$lstree>) {
+ chomp;
($mode, $type, $blob, $tfilename) = split(/\s+/, $_, 4);
last if ($tfilename eq $filename);
}
close($lstree);
- return $blob if $filename eq $filename;
+ return $blob if ($tfilename eq $filename);
die "git-ls-tree failed to find blob for $filename";
}
return $_[0];
}
my ($timestamp, $timezone) = split(' ', $_[0]);
- return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($timestamp));
+ my $minutes = abs($timezone);
+ $minutes = int($minutes / 100) * 60 + ($minutes % 100);
+ if ($timezone < 0) {
+ $minutes = -$minutes;
+ }
+ my $t = $timestamp + $minutes * 60;
+ return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($t));
}
# Copied from git-send-email.perl - We need a Git.pm module..
struct commit *get_revision(struct rev_info *revs)
{
struct commit_list *list = revs->commits;
- struct commit *commit;
if (!list)
return NULL;
/* Check the max_count ... */
- commit = list->item;
switch (revs->max_count) {
case -1:
break;
}
do {
- commit = pop_most_recent_commit(&revs->commits, SEEN);
+ struct commit *commit = revs->commits->item;
+
if (commit->object.flags & (UNINTERESTING|SHOWN))
- continue;
+ goto next;
if (revs->min_age != -1 && (commit->date > revs->min_age))
- continue;
+ goto next;
if (revs->max_age != -1 && (commit->date < revs->max_age))
return NULL;
if (revs->no_merges && commit->parents && commit->parents->next)
- continue;
+ goto next;
if (revs->paths && revs->dense) {
if (!(commit->object.flags & TREECHANGE))
- continue;
+ goto next;
rewrite_parents(commit);
}
+ /* More to go? */
+ if (revs->max_count)
+ pop_most_recent_commit(&revs->commits, SEEN);
commit->object.flags |= SHOWN;
return commit;
+next:
+ pop_most_recent_commit(&revs->commits, SEEN);
} while (revs->commits);
return NULL;
}
--- /dev/null
+# This file isn't used as a test script directly, instead it is
+# sourced from t8001-annotate.sh and t8001-blame.sh.
+
+check_count () {
+ head=
+ case "$1" in -h) head="$2"; shift; shift ;; esac
+ $PROG file $head | perl -e '
+ my %expect = (@ARGV);
+ my %count = ();
+ while (<STDIN>) {
+ if (/^[0-9a-f]+\t\(([^\t]+)\t/) {
+ my $author = $1;
+ for ($author) { s/^\s*//; s/\s*$//; }
+ if (exists $expect{$author}) {
+ $count{$author}++;
+ }
+ }
+ }
+ my $bad = 0;
+ while (my ($author, $count) = each %count) {
+ my $ok;
+ if ($expect{$author} != $count) {
+ $bad = 1;
+ $ok = "bad";
+ }
+ else {
+ $ok = "good";
+ }
+ print STDERR "Author $author (expected $expect{$author}, attributed $count) $ok\n";
+ }
+ exit($bad);
+ ' "$@"
+}
+
+test_expect_success \
+ 'prepare reference tree' \
+ 'echo "1A quick brown fox jumps over the" >file &&
+ echo "lazy dog" >>file &&
+ git add file
+ GIT_AUTHOR_NAME="A" git commit -a -m "Initial."'
+
+test_expect_success \
+ 'check all lines blamed on A' \
+ 'check_count A 2'
+
+test_expect_success \
+ 'Setup new lines blamed on B' \
+ 'echo "2A quick brown fox jumps over the" >>file &&
+ echo "lazy dog" >> file &&
+ GIT_AUTHOR_NAME="B" git commit -a -m "Second."'
+
+test_expect_success \
+ 'Two lines blamed on A, two on B' \
+ 'check_count A 2 B 2'
+
+test_expect_success \
+ 'merge-setup part 1' \
+ 'git checkout -b branch1 master &&
+ echo "3A slow green fox jumps into the" >> file &&
+ echo "well." >> file &&
+ GIT_AUTHOR_NAME="B1" git commit -a -m "Branch1-1"'
+
+test_expect_success \
+ 'Two lines blamed on A, two on B, two on B1' \
+ 'check_count A 2 B 2 B1 2'
+
+test_expect_success \
+ 'merge-setup part 2' \
+ 'git checkout -b branch2 master &&
+ sed -e "s/2A quick brown/4A quick brown lazy dog/" < file > file.new &&
+ mv file.new file &&
+ GIT_AUTHOR_NAME="B2" git commit -a -m "Branch2-1"'
+
+test_expect_success \
+ 'Two lines blamed on A, one on B, one on B2' \
+ 'check_count A 2 B 1 B2 1'
+
+test_expect_success \
+ 'merge-setup part 3' \
+ 'git pull . branch1'
+
+test_expect_success \
+ 'Two lines blamed on A, one on B, two on B1, one on B2' \
+ 'check_count A 2 B 1 B1 2 B2 1'
+
+test_expect_success \
+ 'Annotating an old revision works' \
+ 'check_count -h master A 2 B 2'
+
+test_expect_success \
+ 'Annotating an old revision works' \
+ 'check_count -h master^ A 2'
+
+test_expect_success \
+ 'merge-setup part 4' \
+ 'echo "evil merge." >>file &&
+ EDITOR=: git commit -a --amend'
+
+test_expect_success \
+ 'Two lines blamed on A, one on B, two on B1, one on B2, one on A U Thor' \
+ 'check_count A 2 B 1 B1 2 B2 1 "A U Thor" 1'
+
test_description='git-annotate'
. ./test-lib.sh
-test_expect_success \
- 'prepare reference tree' \
- 'echo "1A quick brown fox jumps over the" >file &&
- echo "lazy dog" >>file &&
- git add file
- GIT_AUTHOR_NAME="A" git commit -a -m "Initial."'
-
-test_expect_success \
- 'check all lines blamed on A' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "A") == 2 ]'
-
-test_expect_success \
- 'Setup new lines blamed on B' \
- 'echo "2A quick brown fox jumps over the" >>file &&
- echo "lazy dog" >> file &&
- GIT_AUTHOR_NAME="B" git commit -a -m "Second."'
-
-test_expect_success \
- 'Two lines blamed on A' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "A") == 2 ]'
-
-test_expect_success \
- 'Two lines blamed on B' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "B") == 2 ]'
-
-test_expect_success \
- 'merge-setup part 1' \
- 'git checkout -b branch1 master &&
- echo "3A slow green fox jumps into the" >> file &&
- echo "well." >> file &&
- GIT_AUTHOR_NAME="B1" git commit -a -m "Branch1-1"'
-
-test_expect_success \
- 'Two lines blamed on A' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^A$") == 2 ]'
-
-test_expect_success \
- 'Two lines blamed on B' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B$") == 2 ]'
-
-test_expect_success \
- 'Two lines blamed on B1' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B1$") == 2 ]'
-
-test_expect_success \
- 'merge-setup part 2' \
- 'git checkout -b branch2 master &&
- sed -e "s/2A quick brown/4A quick brown lazy dog/" < file > file.new &&
- mv file.new file &&
- GIT_AUTHOR_NAME="B2" git commit -a -m "Branch2-1"'
-
-test_expect_success \
- 'Two lines blamed on A' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^A$") == 2 ]'
-
-test_expect_success \
- 'One line blamed on B' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B$") == 1 ]'
-
-test_expect_success \
- 'One line blamed on B2' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B2$") == 1 ]'
-
-
-test_expect_success \
- 'merge-setup part 3' \
- 'git pull . branch1'
-
-test_expect_success \
- 'Two lines blamed on A' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^A$") == 2 ]'
-
-test_expect_success \
- 'One line blamed on B' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B$") == 1 ]'
-
-test_expect_success \
- 'Two lines blamed on B1' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B1$") == 2 ]'
-
-test_expect_success \
- 'One line blamed on B2' \
- '[ $(git annotate file | awk "{print \$3}" | grep -c "^B2$") == 1 ]'
+PROG='git annotate'
+. ../annotate-tests.sh
test_done
--- /dev/null
+#!/bin/sh
+
+test_description='git-blame'
+. ./test-lib.sh
+
+PROG='git blame -c'
+. ../annotate-tests.sh
+
+test_done